[笔记]|[笔记] Vue3.0源码解析

Vue 3.0 源码解析
源码优化

  • 目的是让代码更易于开发和维护。源码的优化主要体现在使用monorepo 和 TypeScript 管理和开发源码,这样做的目标是提升自身代码可维护性
  • Vue.js 2.x 的源码托管在 src 目录,然后依据功能拆分出了 compiler(模板编译的相关代码)、core(与平台无关的通用运行时代码)、platforms(平台专有代码)、server(服务端渲染的相关代码)、sfc(.vue 单文件解析相关代码)、shared(共享工具代码) 等目录。
  • Vue.js 3.0 ,整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到 packages 目录下面不同的子目录中,monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。
性能优化
  • 源码体积优化,JavaScript 包体积越小,意味着网络传输时间越短,JavaScript 引擎解析包的速度也越快
  • Vue.js 3.0 在源码体积的减少方面:移除一些冷门的 feature(比如 filter、inline-template 等),引入 tree-shaking 的技术,减少打包体积
  • 数据劫持优化,Vue.js 1.x 和 Vue.js 2.x 内部都是通过 Object.defineProperty 这个 API 去劫持数据的 getter 和 setter,它必须预先知道要拦截的 key,它并不能检测对象属性的添加和删除(提供了 delete 实例方法)。
  • 嵌套层级比较深的对象,要劫持它内部深层次的对象变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的。响应式数据过于复杂,就会有相当大的性能负担。
  • Vue.js 3.0 使用了 Proxy API 做数据劫持,由于它劫持的是整个对象,那么自然对于对象的属性的增加和删除都能检测到。
  • Proxy API 并不能监听到内部深层次的对象变化,因此 Vue.js 3.0 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,减少非必要的递归,提升性能。
  • 编译优化,Vue2.x有许多非必要的diff 和遍历,导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。
  • Vue.js 3.0通过编译阶段对静态模板的分析,编译生成了 Block tree。Vue.js 3.0 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关。
  • Vue.js 3.0 在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法。
语法 API 优化
01 | 组件渲染:vnode 到真实 DOM 是如何转变的?
  • 组件的时候,它的内部是如何工作的吗?
  • 从编写组件开始,到最终真实的 DOM 又是怎样的一个转变过程呢?
应用程序初始化
在 Vue.js 内部,一个组件想要真正的渲染生成 DOM,还需要经历“创建 vnode - 渲染 vnode - 生成 DOM” 这几个步骤。
一个组件可以通过“模板加对象描述”的方式创建,组件创建好以后是如何被调用并初始化的呢?
在 Vue.js 3.0 中还导入了一个 createApp,其实这是个入口函数,它是 Vue.js 对外暴露的一个函数。
createApp 主要做了两件事情:创建 app 对象和重写 app.mount 方法。
  1. 创建 app 对象
    使用 ensureRenderer().createApp() 来创建 app 对象
const app = ensureRenderer().createApp(...args)

ensureRenderer() 用来创建一个渲染器对象,用 ensureRenderer() 来延时创建渲染器,这样做的好处是当用户只依赖响应式包的时候,就不会创建渲染器,因此可以通过 tree-shaking 的方式移除核心渲染逻辑相关的代码。
  1. 重写 app.mount 方法
    为什么要重写这个方法,而不把相关逻辑放在 app 对象的 mount 方法内部来实现呢?
    这是因为 Vue.js 不仅仅是为 Web 平台服务,它的目标是支持跨平台渲染,而 createApp 函数内部的 app.mount 方法是一个标准的可跨平台的组件渲染流程:标准的跨平台渲染流程是先创建 vnode,再渲染 vnode。
mount(rootContainer) { // 创建根组件的 vnode const vnode = createVNode(rootComponent, rootProps) // 利用渲染器渲染 vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy }

核心渲染流程:创建 vnode 和渲染 vnode
  1. 创建 vnode
    vnode 有什么优势呢?为什么一定要设计 vnode 这样的数据结构呢?
  • 首先是抽象,引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升。
  • 其次是跨平台,因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、Weex 平台、小程序平台的渲染都变得容易了很多。
Vue.js 内部是如何创建这些 vnode 的呢?
通过 createVNode 函数创建了根组件的 vnode :对 props 做标准化处理、对 vnode 的类型信息编码、创建 vnode 对象,标准化子节点 children 。
const vnode = createVNode(rootComponent, rootProps) function createVNode(type, props = null ,children = null) { if (props) { // 处理 props 相关逻辑,标准化 class 和 style }// 对 vnode 类型信息编码 const shapeFlag = isString(type) ? 1 /* ELEMENT */ : isSuspense(type) ? 128 /* SUSPENSE */ : isTeleport(type) ? 64 /* TELEPORT */ : isObject(type) ? 4 /* STATEFUL_COMPONENT */ : isFunction(type) ? 2 /* FUNCTIONAL_COMPONENT */ : 0 const vnode = { type, props, shapeFlag, // 一些其他属性 } // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型 normalizeChildren(vnode, children) return vnode }

  1. 渲染 vnode
02 | 组件更新:完整的 DOM diff 流程是怎样的?(上)
03 | 组件更新:完整的 DOM diff 流程是怎样的?(下)
04 | Setup:组件渲染前的初始化过程是怎样的?

模板中引用到的变量 state 和 increment 包含在 setup 函数的返回对象中,那么它们是如何建立联系的呢?
在Vue.js 2.x 编写组件的时候,会在 props、data、methods、computed 等 options 中定义一些变量。在组件初始化阶段,Vue.js 内部会处理这些 options,即把定义的变量添加到了组件实例上。等模板编译成 render 函数的时候,内部通过 with(this){} 的语法去访问在组件实例中的变量。
模板中引用到的变量 state 和 increment 包含在 setup 函数的返回对象中,那么它们是如何建立联系的呢?
到了 Vue.js 3.0,既支持组件定义 setup 函数,而且在模板 render 的时候,又可以访问到 setup 函数返回的值,这是如何实现的?
Vue.js 2.x 使用 new Vue 来初始化一个组件的实例,到了 Vue.js 3.0,我们直接通过创建对象去创建组件的实例。这两种方式并无本质的区别,都是引用一个对象,在整个组件的生命周期中去维护组件的状态数据和上下文环境。
创建和设置组件实例
组件的渲染流程:创建 vnode 、渲染 vnode 和生成 DOM。
渲染 vnode 的过程主要就是在挂载组件,挂载组件的代码主要做了三件事情:创建组件实例、设置组件实例和设置并运行带副作用的渲染函数。
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => { // 创建组件实例 const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)) // 设置组件实例 setupComponent(instance) // 设置并运行带副作用的渲染函数 setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) }

使用createComponentInstance 方法,创建组件实例
function createComponentInstance (vnode, parent, suspense) { // 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。 const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext; const instance = { // 组件唯一 id uid: uid++, // 组件 vnode vnode, … }// 初始化渲染上下文 instance.ctx = { _: instance } // 初始化根组件指针 instance.root = parent ? parent.root : instance // 初始化派发事件方法 instance.emit = emit.bind(null, instance) return instance }

组件实例 instance 上定义了很多属性.
Vue.js 2.x 使用 new Vue 来初始化一个组件的实例,到了 Vue.js 3.0,我们直接通过创建对象去创建组件的实例。这两种方式并无本质的区别,都是引用一个对象,在整个组件的生命周期中去维护组件的状态数据和上下文环境。
组件实例的设置流程就是对setup 函数的处理。
function setupComponent (instance, isSSR = false) { const { props, children, shapeFlag } = instance.vnode // 判断是否是一个有状态的组件 const isStateful = shapeFlag & 4 // 初始化 props initProps(instance, props, isStateful, isSSR) // 初始化 插槽 initSlots(instance, children) // 设置有状态的组件实例 const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined return setupResult }

【[笔记]|[笔记] Vue3.0源码解析】setupStatefulComponent 函数,它主要做了三件事:创建渲染上下文代理、判断处理 setup 函数和完成组件实例设置。
05 | 响应式:响应式内部的实现原理是怎样的?(上)
06 | 响应式:响应式内部的实现原理是怎样的?(下)
07 | 计算属性:计算属性比普通函数好在哪里?
08 | 侦听器:侦听器的实现原理和使用场景是什么?(上)
09 | 侦听器:侦听器的实现原理和使用场景是什么?(下)
10 | 生命周期:各个生命周期的执行时机和应用场景是怎样的?
11 | 依赖注入:子孙组件如何共享数据?
12 | 模板解析:构造 AST 的完整流程是怎样的?(上)
13 | 模板解析:构造 AST 的完整流程是怎样的?(下)
14 | AST 转换:AST 节点内部做了哪些转换?(上)
15 | AST 转换:AST 节点内部做了哪些转换?(下)
16 | 生成代码:AST 如何生成可运行的代码?(上)
17 | 生成代码:AST 如何生成可运行的代码?(下)
18 | Props:Props 的初始化和更新流程是怎样的?
19 | 插槽:如何实现内容分发?
20 | 指令:指令完整的生命周期是怎样的?
21 | v-model:双向绑定到底是怎么实现的?
22 | Teleport 组件:如何脱离当前组件渲染子组件?
23 | KeepAlive 组件:如何让组件在内存中缓存和调度?
24 | Transition 组件:过渡动画的实现原理是怎样的?(上)
25 | Transition 组件:过渡动画的实现原理是怎样的?(下)
26 | Vue Router:如何实现一个前端路由?(上)
27 | Vue Router:如何实现一个前端路由?(下)

    推荐阅读