认识
一、前言
Vue.js 3.0
源码学习的目标: 精准理解 Vue.js 3.0
的高层设计思想, 在 Vue.js 3.0
框架设计权衡层面有自己深入的思考。从高层的设计角度探讨框架需要关注的问题,从而更好的理解一些具体的实现为何要做出这样的选择。
1.1 权衡
框架设计里到处体现了权衡的艺术 作为框架的设计者, 一定要对框架的定位和方向有全局的把控, 这样才能做好后续的模块设计和拆分;作为框架的学习者, 学习框架的时候, 应该从全局的角度对框架的设计拥有清晰的认知,否则很容易被细节困住,看不清全貌。
1.2 命令式和声明式
命令式 框架的一大特点是关注过程, 通过一行行代码描述了做事的过程, 这符合我们的逻辑直觉;声明式 更加关注结果, 我们的代码提供的只是一个结果, 具体实现我们并不关心。
因此, Vue.js 3.0
内部的实现一定是命令式的, 而暴露给用户的却更加声明式。
1.3 性能与可维护性的权衡
命令式 和 声明式 各有优缺点, 在框架设计的层面, 则体现在性能与可维护性之间的权衡。 声明式代码的性能不优于命令式代码的性能, 。命令式代码可以做到极致的性能优化。比如说:
div.textContent = 'hello world 修改';
通过 命令式代码, 我们明确的知道了哪些发生了变更,只要做必要的修改就行了。但是,对于 声明式 代码而言, 为了实现最优的更新性能, 需要找到前后的差异并只更新变化的地方,但是最终完成这次更新的代码仍然是:
div.textContent = 'hello world 修改';
由上可知, 声明式 代码会比 命令式 代码多出找到差异的性能消耗。既然在性能层面, 命令式 代码是更好的选择, 为什么 Vue.js 3.0
要选择声明式*的设计方案呢?
答: 声明式代码的可维护性比命令式更强, 在命令式代码中, 我们需要维护实现目标的整个过程, 包括要手动完成 DOM
元素的创建、更新、删除等工作。而声明式 代码展示的就是我们想要的结果, 看上去更加的直观。所以,Vue.js 3.0
在采用声明式提升可维护性的同时, 做到了 性能损失最小化。
1.4 虚拟 DOM 和真实 DOM 的权衡
声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,因此, 如果我们能够最小化找出差异的性能消耗, 就可以让声明式代码的性能无限接近命令式代码的性能。 而虚拟 DOM 就是为了 最小化 找出差异这一步的性能消耗而出现的。
因此, 虚拟 DOM 的更新技术的性能理论上不可能比原生 JavaScript
操作 DOM
更好
二、初始化
Vue.js 3.0
入口逻辑为:
import App from "./Ap.vue";
import { createApp } from 'vue';
const app = createApp(App);
app.mount('#app');
createApp
主要逻辑如下:
export const createApp = ((...args)=>{
const app = ensureRenderer().createApp(...args);
const { mount } = app;
app.mount = ()=>{
……
}
return app;
})
所以, Vue.js 3.0
通过 createApp
开启初始化工作流。通过 mount
开始挂载流程。createApp()
用于创建一个应用实例, 第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props
。createApp
实际调用 ensureRenderer
先获取 baseCreateRenderer
渲染器函数, 并执行。 baseCreateRenderer
返回一个对象, 有 render
、hydrate
、createApp
方法。其中, createApp
执行的是 createApp
方法, 返回 App
实例。App
实例中提供了 config
上下文、 use
方法、mixin
方法、 component
方法、directive
方法、mount
方法、unmount
方法、provide
方法等。随后, 将原来的 mount
方法保存起来, 重写 mount
方法, 主要逻辑如下:
-
获取
root DOM
-
将
root DOM
innerHTML
赋值给app.component.template
-
清空
root DOM
innerHTML
-
调用原来的
mount
方法 开始进入挂载流程
三、挂载
Vue.js 3.0
通过 app.mount(container)
来进行挂载, mount
逻辑如下:
-
获取容器元素
-
清空容器内容
-
根据根组件模版创建根组件
VNode
-
调用
render
开始进入渲染器流程:render
主要作用为: 接收虚拟DOM
, 转化为真实DOM
, 挂载到container
-
根据传入的根组件
VNode
,VNode
不为null
, 进入patch
流程: 只要VNode
转换为真实VNode
, 都需要调用patch
-
根组件
VNode
类型为Component
, 会进入processComponent
流程 -
在
processComponent
中, 初始化流程会调用mountComponent
, 进入mountComponent
-
在
mountComponent
中, 调用createComponentInstance
创建组件实例 -
在
mountComponent
中, 调用setupComponent
进行组件实例安装:Vue2.0
组件实例安装, 调用this._init
实例属性,方法初始化,数据响应式,生命周期的钩子、属性声明,响应式等等。Vue3.0
调用setupComponent
进行组件实例安装, 初始化props
, 初始化slots
,setup
数据状态处理。-
调用
initProps
初始化props
-
调用
initSlots
初始化slots
-
调用
setupStatefulComponent
执行setup
函数, 执行逻辑如下:-
创建
setup
上下文, 上下文中有attrs
、slots
、emit
、expose
-
获取
setup
函数, 传入props
和setup
上下文, 并执行 -
处理
setup
函数返回值, 如果返回值为函数, 将覆盖instance.render = 返回值
, 如果为对象, 作为数据将暴露给模版使用 -
调用
finishComponentSetup
得到组件实例render
函数, 如果组件实例已经存在render
函数, 不会编译解析Template
。优先级为render > template
。如果组件实例不存在render
函数, 调用compile
函数, 编译解析Template
, 得到render
函数。 -
在
finishComponentSetup
中最后调用applyOptions
兼容Vue.js 2.0
选项式API
。其中,data
会通过reactive
来实现响应式
-
-
-
在
mountComponent
中, 调用setupRenderEffect
安装渲染函数副作用:Vue2
中的更新机制为new Watcher(updateComponent)
,Vue3
的更新机制为ReactiveEffect(componentUpdateFn, ()=> queueJob(update))
。-
定义副作用函数: 执行组件
render
函数, 返回组件子节点VNode
, 子节点VNode
作为subTree
调用patch
继续递归执行。执行render
函数的期间, 如果有响应式数据, 会访问响应式数据, 会收集当前副作用函数。当响应式数据发生变化时, 会触发当前副作用函数重新执行。 -
基于副作用函数创建
effect
实例, 并以调度器的方式, 将当前的更新函数加入到异步任务队列, 等所有数据都更新完成再一次统一执行异步任务队列里面的更新函数,进行VNode
的更新。
-
-
最后调用
mountElement
将VNode
转化为真实DOM
挂载到容器上