Skip to content

Vue 3.0 源码解读

导读

发展历程

  • Vue.js 1.x --(引入虚拟DOM)→ Vue.js 2.x
  • Vue.js 2.x 痛点,Vue 3.0 诞生

    • 源码自身的可维护性:Vue 2.x 数据量大后的渲染/更新性能问题
    • 兼容性:Vue 2.x 想舍弃但为了兼容仍旧保留的鸡肋API,如 Mixin 等
    • 更好的编程体验
    • 更好的 TypeScript 支持
    • 更好的逻辑复用实践

源码优化

  • 目的:让源码更易于开发和维护
  • 手段:使用 Monorepo 和 TypeScript 管理和开发源码,提升可维护性。
  • 优化实践 Monorepo

    • Vue 2.x 源码结构

      • compiler:模板编译相关代码

        • parser
        • directives
        • codegen
      • core:平台无关的通用运行时代码

        • components 内置组件:如 keep-alive
        • global-api:如 extend.js、mixin.js、use.js、assets.js
        • instance:如 render-helpers、events.js、init.js、lifecycle.js、proxy.js、render.js、state.js、inject.js
        • observer:如 array.js、dep.js、scheduler.js、traverse.js、watcher.js
        • utils:如 env.js、error.js、next-tick.js、options.js、props.js
        • vdom:如 create-component.js、create-element.js、create-functional-component.js、patch.js、vnode.js
      • platforms:平台专有代码

        • web:compiler.js、runtime.js等
        • weex
      • server:服务端渲染相关代码

        • bundle-renderer
        • template-renderer
        • optimizing-compiler
        • webpack-plugin
        • create-basic-renderer.js
        • create-renderer.js
        • render.js
        • render-context.js
        • render-stream.js
      • sfc:.vue 单文件解析相关代码

        • parser.js
      • Shared:共享工具代码

        • constants.js
        • utils.js
    • Vue 3.0 源码结构

      • reactivity: 响应式系统
      • runtime-core:与平台无关的运行时核心
      • runtime-dom:针对浏览器的运行时
      • compiler-core:与平台无关的编译核心
      • compiler-dom:针对浏览器的编译模块
      • compiler-ssr:针对服务端渲染的编译模块
      • compiler-sfc:针对.vue单文件解析
      • shared:多个包之间共享的内容
      • server-renderer:用于服务端渲染
      • vue:完整版本,包括运行时和编译器
    • 好处

      • 相比于 Vue 2.x 的代码组织,monorepo 把这些代码模块拆分到不同 packages 中,每个 package 有各自的 API、类型定义和测试
      • 拆分更细化,职责划分更明确,模块之间的依赖关系更清新
      • 开发人员更容易阅读、理解和变更所有源码,提升可维护性
      • package 可以独立于 Vue.js 使用;按需使用,体积更小。
  • 优化实践 TypeScript

    • Vue2.x 使用 Flow

      • Flow 是 Facebook 出品的 Javascript 静态类型检查工具,能 以很小成本 对已有的 Javascript 代码迁入,非常灵活。
      • 但 Flow 对一些 复杂场景的类型检查,支持不够好
    • Vue3.0 使用 TypeScript

      • 可以在编码期间完成 类型检查,避免因类型问题导致的错误
      • 有利于定义接口类型,利于IDE对变量类型的推导
      • TypeScript 生态更完善,并且保持一定频率的更新

性能优化

  • 源码体积优化

    • 移除一些冷门的 Feature,比如 filter、inline-template等
    • 引入 tree-shaking 的技术

      • 依赖ES2015模块语法静态结构(即import/export),通过 编译阶段 的静态分析,找到没有引入的模块并打上标记;压缩阶段,利用uglify-js、terser 等压缩工具删除没用的代码。
      • 如果项目中没有引用 Transition、KeepAlive 等组件,那么打包就会移除,从而减少项目引入Vue.js 体积
  • 数据劫持优化

    • 思路:渲染DOM时,访问了数据,对访问进行劫持;通过劫持数据的访问和更新,来实现DOM更新功能。
    • vue 1.x

    x

    • vue 2.x & vue 3.0

    x

    • vue 2.x 使用 Object.defineProperty()缺陷

      • 必须预先知道要劫持的 key 是什么
      • 不能监测到对象的添加和删除,只能通过 \(set、\)delete 实例方法实现
      • 对于深层嵌套对象,只能递归遍历对象;存在性能问题
    • vue 3.0 使用 new Proxy()

      • 对于增加和删除都能监测
      • Proxy API 不能监听深层次对象变化,那么Vue.js 3.0 处理方式是在getter中去递归响应式;即只能在真正访问到的内部对象才会变成响应式(惰性监测)

编译优化

  • Vue 2.x

    • 全流程图

    x

    • template -- compile → render function 阶段可以借助 vue-loader 在 webpack 编译阶段离线完成,不必优化
    • 思路:重点优化相对耗时的 patch 阶段;vue 2.x 数据更新触发重新渲染的粒度是组件级的;diff 时,对于静态节点的遍历都是不必要的。
    • 渲染管线图

    x

  • vue 3.0

    • patch 优化:通过编译阶段对静态模版的分析,编译生成 Block Tree
    • Block Tree:是一个将模板 基于动态节点指令 切割的嵌套 Block,每个 Block 内部节点结构是 固定的,每个 Block 只需维护一个 Array 来追踪包含的动态节点。
    • 借助 Block Tree,实现了巨大性能突破,从整体规模相关到只与动态内容规模相关。
    • 只把绑定数据的动态节点加入嵌套区块数据,每个区块以数组追踪。
  • 此外

    • Slot 编译优化
    • 事件侦听函数缓存
    • 运行时重写 diff 算法

语法优化

  • 优化逻辑组织

    • Vue 2.x/Options API 思想

      • 编写组件本质就是在写一个 「包含了描述组件options 的对象」
      • 优点

        • 写法符合直觉思维,新手友好。
        • Options API 按照 methods、computed、data、props 等不同选项进行分类;
        • 当组件小时,这种分类方式一目了然。
      • 缺点

        • 但在大型组件中,一个组件可能有多个 逻辑关注点
        • 每个「关注点」都有自己的 options,如果需要修改一个逻辑关注点,需要在单文件中不断上下切换与寻找
    • Vue 3.0/Composition API

      • 将逻辑关注点相关的代码,放在一个函数中;
      • 这样修改一个功能时,不需要在文件中跳来跳去。
    • 对比图

    x - 优化逻辑复用

    • vue 2.x 用 mixins 去复用逻辑

      • 但当大量 mixins 被使用,会造成「命名冲突」和「数据来源不明确」:
      • 每个 mixin 都可以定义自己的 props、data、methods,且相互之间无感知,容易定义相同变量,导致「命名冲突」。
      • 对组件而言,如果模板中使用不在当前组件中定义的变量,那么就难以知道变量在哪里定义,即「数据来源不明确」。
    • vue 3 的 Composition API 很好解决上述问题。

  • 更好的类型支持

    • Composition API 都是函数,在函数调用时,所有的类型就自然被推导出来;
    • 不像 Options API,所有东西都使用 this
  • Tree-shaking 友好

    • Vue 3.0/Composition API tree-shaking 友好,让代码更容易压缩

大规模启用 RFC

  • RFC (Request For Comments),使每个版本改动可控
  • 为新功能进入框架提供一个一致且受控的路径
  • 了解每一 feature 采纳或废弃的前因后果

Vue.js 核心组件实现

组件渲染

组件更新

Composition API

编译过程及优化思想

实用特性及原理

内置组件及实现原理

官方生态的实现原理

未整理

1、新特性

  • Performance: 性能提升 1.2 ~ 2 倍
  • Tree-shaking 支持: 按需加载,体积更小
  • CompositionAPI: 类似 React Hooks
  • 更好的 TS 支持
  • Custom Render API: 暴露自定义 API
  • Fragment, Teleport, Suspense 组件

2、如何做到提速

  • explorer
  • diff 方法优化

    • Vue 2.x 是全量对比
    • Vue 3.0 新增静态标记 (PatchFlag):
    • Vue3.0中,在模版编译时,编译器会在 动态标签 末尾加上 /* Text */ PatchFlag。
    • 每一个 Block 中的节点,就算很深,也是直接跟 Block 一层绑定的,可以直接跳转到动态节点而不需要逐个逐层遍历。
    export const enum PatchFlags {
      TEXT = 1, // 1 动态文本节点
      CLASS = 1 << 1, // 2 动态 class
      STYLE = 1 << 2, // 4 动态 style
      PROPS = 1 << 3, // 8 动态属性
      FULL_PROPS = 1 << 4, // 16 具有动态 key 属性,当 key 变化,需要进行完整比较 
      HYDRATE_EVENTS = 1 << 5, // 32 带有事件监听器的节点
      STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 fragment
      KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 fragment
      UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 fragment
      NEED_PATCH = 1 << 9, // 512 一个节点只进行非 props 比较
      DYNAMIC_SLOTS = 1 << 10, // 1024 动态
      // SPECIAL FLAGS
      HOISTED = -1,
      BAIL = -2
    }
    
    <div>Hello World!</div>
    <div>{{msg}}</div>
    

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (
        _openBlock(),
        _createBlock(_Fragment, null, [
          _createVNode("div", null, "Hello World!"),
          _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
        ],
        64 /* STABLE_FRAGMENT */
        )
      )
    }
    
    - hoistStatic 静态节点提升

    • Vue 2 的节点不管是否参与更新,每次更新都会重新 _createVNode
    • Vue 3 对于不参与更新的节点,只创建一次,缓存起来,之后的每次渲染复用缓存

    const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "Hello World!", -1 /* HOISTED */)
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock(_Fragment, null, [
        _hoisted_1,
        _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
      ], 64 /* STABLE_FRAGMENT */))
    }
    
    - cacheHandler 事件监听缓存

    • 默认情况下,onClick 会被视为动态绑定,所以每次都会 watch 它的变化;
    • 但因为是同一个函数,所以没有变化,直接缓存起来复用;
    • 这个节点可以被看作一个静态节点。
    <div>Hello World!</div>
    <div>{{msg}}</div>
    <button @click="handleClick">事件监听缓存</button>
    
    // 不开启缓存
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock(_Fragment, null, [
        _createVNode("div", null, "Hello World!"),
        _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
        _createVNode("button", { onClick: _ctx.handleClick }, "事件监听缓存", 8 /* PROPS */, ["onClick"])
      ], 64 /* STABLE_FRAGMENT */))
    }
    

    // 开启缓存
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createBlock(_Fragment, null, [
        _createVNode("div", null, "Hello World!"),
        _createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
        _createVNode("button", {
          onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.handleClick(...args)))
        }, "事件监听缓存")
      ], 64 /* STABLE_FRAGMENT */))
    }
    
    - SSR 渲染

    • 当有大量 静态内容 时,内容会被当作纯字符串推进一个 buffer 里。
    • 即使存在 动态绑,会通过模版差值嵌入其中,这个性能肯定比 React 转成 vDOM 再转化为 HTML 快很多。
  • StaticNode 静态节点

    • 已知在 SSR 中静态的节点会被转化为纯字符串。
    • 如果在 客户端,当静态节点嵌套足够多的时候,vue 3 编译器会用 _createStaticNode 方法生成字符串类型的 staticNode,直接innerHTML;不需要创建 vDOM 对象,然后根据对象渲染。