构建流程大致框图
构建与数据更新要点
该顺序并不代表真正的构建流程,只是对重要内容的拆解
初始化与挂载
在使用 new Vue()
后 Vue 会调用 _init
函数进行初始化,其会
- 初始化 options参数
- 初始化生命周期
- 初始化 vm 状态,其中包含props/methods/data/computed与watch等
- 初始化完成后挂载实例
数据初始化过程中还会递归使用 Object.defineProperty (Vue2.X) / Proxy (Vue3)
设置 setter 与 getter 函数实现响应式与依赖收集
挂载后编译
挂载完成后为生成 render function ,vue 会将 template 进行编译,该过程大抵分为三阶段
parse
【阶段目标】生成 AST 语法树
parse 会使用正则表达式解析 template 模板中的指令、class、style 等数据,最终生成 AST 语法树
optimize
【阶段目标】增添优化标记
optimize 主要作用为标记 static 静态节点,该阶段主要是对后序页面更新后比对过程的优化
generate
【阶段目标】生成 render function
generate 是将 AST 语法树转换为 render function 字符串的过程
构建vDom
vDom 相当于一个缓冲层,其能够实现对真实 Dom 的最少操作从而优化性能,而且由于其本身是 JS 对象,在不同环境中拥有良好的跨平台性
转换过程
在获取到 render function 后其会被转换为 VNode 节点,虚拟 Dom 实质上就是由 VNode 组合成的树,在逻辑上是对真实 Dom的抽象
VNode
其归根结底是一个 JS 对象,使用对象的属性来描述当前节点的一些状态,最终使用 VNode 节点的形式模拟 Dom 树
// 简单范例
class VNode {
constructor (tag, data, children, text, elm) {
/*当前节点的标签名*/
this.tag = tag;
/*当前节点的一些数据信息,比如 props、attrs 等数据*/
this.data = data;
/*当前节点的子节点,是一个数组*/
this.children = children;
/*当前节点的文本*/
this.text = text;
/*当前虚拟节点对应的真实 dom 节点*/
this.elm = elm;
}
}
发布订阅者模式
发布者
发布者维护一个订阅者列表,当该发布者被引用时则将引用的对象置入订阅者列表中,每当该发布者改变时就会通知订阅者更新视图
class Dep {
constructor () {
/* 用来存放订阅者对象的数组 */
this.subs = [];
}
/* 在订阅者数组中添加一个订阅对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有订阅对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
订阅者(观察者)
订阅者在引用发布对象时会被收集进入发布对象的订阅者列表中
class Watcher {
constructor () {
/* 在new一个订阅者对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
}
/* 更新视图的方法 */
update () {
console.log("视图更新啦~");
}
}
Dep.target = null;
依赖收集
在完成上述逻辑后需要明确发布者如何得知订阅者引用了自己,这时就需要使用 getter 逻辑,使发布者在自身被引用时完成捕获逻辑
具体体现在应用方法上 Vue2.X 使用的是 Object.defineProperty
,Vue3使用的是 Proxy
function defineReactive (obj, key, val) {
/* new一个发布者对象对象 */
const dep = new Dep();
Object.defineProperty(obj, key, { // 此处以 Vue2.X 举例
enumerable: true,
configurable: true,
/* 数据被读取时触发get内逻辑 */
get: function reactiveGetter () {
/* 将Dep.target(即当前的订阅对象存入发布者的订阅者列表中) */
dep.addSub(Dep.target);
return val;
},
/* 数据被操作时触发set内逻辑 */
set: function reactiveSetter (newVal) {
if (newVal === val) return;
/* 在set时触发发布者的通知逻辑来通知所有的订阅者对象更新视图 */
dep.notify();
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
/* 此处完成数据的响应式处理 */
observer(this._data);
/* 新建一个订阅者对象,这时候 Dep.target 会指向这个订阅者对象 */
new Watcher();
/* 在这里模拟数据渲染的过程,其会触发 test 属性的 get 逻辑 */
console.log('render~', this._data.test);
}
}
注:该阶段体现在 Vue 构建过程中其实可以分成两部分,在数据初始化(init
)时对参数完成发布订阅者逻辑的创建,在渲染(render
)时收集部分依赖,最终完成响应式设计
数据更新比对
在页面中对数据进行操作时会触发发布者的通知逻辑从而使订阅者执行相应逻辑并生成新的 VNode ,新老 VNode 会进行一个 patch
过程比对得出差异,最终将差异更新至 Dom 完成视图的更新
patch
比对的核心为 diff 算法,diff 算法是通过同层的树节点进行比对,其时间复杂度只有 O(n)
图中相同色块的节点会进行比对,得出的差异最终会更新至视图
需要注意的是 Vue 与 React 得到 diff 算法虽说都是忽略跨级比较,只进行同级比较,但是
Vue 对比节点时,当节点元素类型相同但是 className 不同则认定它们为不同类型元素并删除重建,而 React 则会认为其为同类型节点只修改节点属性
- Vue 同级对比时采用从两端至中心的对比方式,而 React 采用从左向右依次对比,这样的区别是当最右节点移动至最左端时,React 会将前面的节点依次移动而 Vue 只会移动一个节点
- Vue 组件数据变动时只更新自己,React 组件数据变动会更新自己及所有子组件
碍于篇幅具体对比流程请自行搜索,这里不多赘述
更新渲染
在更新渲染阶段,Vue 使用了异步更新策略
为什么需要异步更新
Vue 的异步更新策略更多的是出于性能优化的考量
举一个栗子,页面某值循环执行1000次自增(模拟多 effect 场景),此时若是遵循同步更新逻辑则会触发1000次 setter=>Dep=>watcher=>update
流程,这就导致会有大量的性能损耗
而 Vue 使用的异步更新策略会创建一个更新栈,然后将更新压入栈中并进行去重处理(去重操作的目的就在于优化本例出现的多次执行情况),在当轮同步代码执行完成后开始执行更新栈中的更新工作,最终将改变反馈至视图中
异步更新流程
Vue 的异步更新机制借用了事件循环机制内容,其将更新标明 id 并放入微任务队列中,待当轮同步任务执行完成后就会开始执行微任务队列中所有更新任务,最终将更新渲染至视图
当微任务队列中被置入相同 id 的更新任务时,Vue 异步更新策略只会保留最后的更新任务,实现性能优化效果