共计 5269 个字符,预计需要花费 14 分钟才能阅读完成。
最近部门分享,有同学提到了 Vue 响应式原理,大家在探讨时,发现一些同学对这一常识了解还不够深刻,不能造成一个闭环,为了帮忙大家了解这个问题,我从新过了一下 Vue 源码,并整顿了多张流程图,便于大家了解。
- Vue 初始化
- 模板渲染
- 组件渲染
本文 Vue 源码版本:2.6.11,为了便于了解,均有所删减。
本文将从以下两个方面进行摸索:
- 从 Vue 初始化,到首次渲染生成 DOM 的流程。
- 从 Vue 数据批改,到页面更新 DOM 的流程。
Vue 初始化
先从最简略的一段 Vue 代码开始:
<template>
<div>
{{message}}
</div>
</template>
<script>
new Vue({data() {
return {message: "hello world",};
},
});
</script>
这段代码很简略,最终会在页面上打印一个 hello world,它是如何实现的呢?
咱们从源头:new Vue 的中央开始剖析。
// 执行 new Vue 时会顺次执行以下办法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)
// 5. 调用 walk 办法,遍历 data 中的每一个属性,监听数据的变动。function walk(obj) {const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i]);
}
}
// 6. 执行 defineProperty 监听数据读取和设置。function defineReactive(obj, key, val) {
// 为每个属性创立 Dep(依赖收集的容器,后文会讲)const dep = new Dep();
// 绑定 get、set
Object.defineProperty(obj, key, {get() {
const value = val;
// 如果有 target 标识,则进行依赖收集
if (Dep.target) {dep.depend();
}
return value;
},
set(newVal) {
val = newVal;
// 批改数据时,告诉页面从新渲染
dep.notify();},
});
}
数据描述符绑定实现后,咱们就能失去以下的流程图:
图中咱们能够看到,Vue 初始化时,进行了数据的 get、set 绑定,并创立了一个 Dep 对象。
对于数据的 get、set 绑定咱们并不生疏,然而 Dep 对象什么呢?
Dep 对象用于依赖收集,它实现了一个公布订阅模式,实现了数据 Data 和渲染视图 Watcher 的订阅,咱们一起来分析一下。
class Dep {
// 依据 ts 类型提醒,咱们能够得出 Dep.target 是一个 Watcher 类型。static target: ?Watcher;
// subs 寄存收集到的 Watcher 对象汇合
subs: Array<Watcher>;
constructor() {this.subs = [];
}
addSub(sub: Watcher) {
// 收集所有应用到这个 data 的 Watcher 对象。this.subs.push(sub);
}
depend() {if (Dep.target) {
// 收集依赖,最终会调用下面的 addSub 办法
Dep.target.addDep(this);
}
}
notify() {const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update();}
}
}
依据对 Dep 的源码剖析,咱们失去了上面这张逻辑图:
理解 Data 和 Dep 之后,咱们来持续揭开 Watcher 的面纱。
class Watcher {constructor(vm: Component, expOrFn: string | Function) {
// 将 vm._render 办法赋值给 getter。// 这里的 expOrFn 其实就是 vm._render,后文会讲到。this.getter = expOrFn;
this.value = this.get();}
get() {
// 给 Dep.target 赋值为以后 Watcher 对象
Dep.target = this;
// this.getter 其实就是 vm._render
// vm._render 用来生成虚构 dom、执行 dom-diff、更新实在 dom。const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep: Dep) {
// 将以后的 Watcher 增加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 开启异步队列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 和初始化一样,会调用 get 办法,更新视图
const value = this.get();}
}
源码中咱们看到,Watcher 实现了渲染办法 _render
和 Dep 的关联,初始化 Watcher 的时候,打上 Dep.target 标识,而后调用 get 办法进行页面渲染。加上上文的 Data,目前 Data、Dep、Watcher 三者的关系如下:
咱们再拉通串一下整个流程:Vue 通过 defineProperty
实现了 Data 中所有数据的代理,当数据触发 get 查问时,会将以后的 Watcher 对象退出到依赖收集池 Dep 中,当数据 Data 变动时,会触发 set 告诉所有应用到这个 Data 的 Watcher 对象去 update 视图。
目前的整体流程如下:
上图的流程中 Data 和 Dep 都是 Vue 初始化时创立的,但当初咱们并不知道 Wacher 是从哪里创立的,带着这个问题,咱们接着往下摸索。
模板渲染
上文中,咱们剖析了初始化 Vue 过程中解决数据的局部,接下来,咱们剖析一下数据渲染的局部。
其实 new Vue 执行到最初,会调用 mount 办法,将 Vue 实例渲染成 dom。
// new Vue 执行流程。// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template),编译 Vue 中的 template 模板,生成 render 办法。// 4. Vue.prototype.$mount 调用下面的 render 办法挂载 dom。// 5. mountComponent
// 6. 创立 Watcher 实例
const updateComponent = () => {vm._update(vm._render());
};
// 联合上文,咱们就能得出,updateComponent 就是传入 Watcher 外部的 getter 办法。new Watcher(vm, updateComponent);
// 7. new Watcher 会执行 Watcher.get 办法
// 8. Watcher.get 会执行 this.getter.call(vm, vm),也就是执行 updateComponent 办法
// 9. updateComponent 会执行 vm._update(vm._render())
// 10. 调用 vm._render 生成虚构 dom
Vue.prototype._render = function (): VNode {
const vm: Component = this;
const {render} = vm.$options;
let vnode = render.call(vm._renderProxy, vm.$createElement);
return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚构 dom
Vue.prototype._update = function (vnode: VNode) {
const vm: Component = this;
if (!prevVnode) {
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
// 12. vm.__patch__ 办法就是做的 dom diff 比拟,而后更新 dom,这里就不开展了。
看完 Vue 模板渲染的过程,咱们能够失去如下的流程图:
到这里,咱们就晓得了 Watcher 其实是在 Vue 初始化的阶段创立的,属于生命周期中 beforeMount 的地位创立的,创立 Watcher 时会执行 render 办法,最终将 Vue 代码渲染成实在的 DOM。
咱们再将之前的流程整合一下,就能失去以下的流程:
上图剖析了 Vue 初始化到渲染 DOM 的整个过程,最初咱们再剖析一下,当数据变动时,Vue 又是怎么进行更新的?
其实,在上图也能看出,在 Data 变动时,会调用 Dep.notify 办法,随即调用 Watcher 外部的 update 办法,此办法会将所有应用到这个 Data 的 Watcher 退出一个队列,并开启一个异步队列进行更新,最终执行 _render
办法实现页面更新。
整体的流程如下:
好了,摸索到这里,Vue 的响应式原理,曾经被咱们剖析透彻了,如果你还没有明确,无妨再细品一下上图。
组件渲染
原本摸索到下面的流程图就完结了,但好奇的我又想到了一个问题 ????。
Vue 组件又是怎么渲染的呢?
带着这个问题,我持续查阅了源码。
// 从模板编译开始,当发现一个自定义组件时,会执行以下函数
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement
// 6. createComponent
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// $options._base 其实就是全局 Vue 构造函数,在初始化时 initGlobalAPI 中定义的:Vue.options._base = Vue
const baseCtor = context.$options._base;
// Ctor 就是 Vue 组件中 <script> 标签下 export 出的对象
if (isObject(Ctor)) {
// 将组件中 export 出的对象,继承自 Vue,失去一个构造函数
// 相当于 Vue.extend(YourComponent)
Ctor = baseCtor.extend(Ctor);
}
const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
return vnode;
}
// 7. 实现组件继承 Vue,并调用 Vue._init 办法,进行初始化
Vue.extend = function (extendOptions: Object): Function {
const Super = this;
const Sub = function VueComponent(options) {
// 调用 Vue.prototype._init,之后的流程就和首次加载保持一致
this._init(options);
};
// 原型继承,相当于:Component extends Vue
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
return Sub;
};
看完组件渲染的源码后,联合上文,重新整理了一张流程图,图中的蓝色局部就是渲染组件的过程。
好了,当初是真的完结了,最终的流程图就是下面的这一张图。
问个问题,当初你了解 Vue 响应式原理了吗?
如果仍感觉不好了解,我这里还筹备了一张带标注的简图 ????
思考与总结
本文从源码的角度,介绍了 Vue 响应式原理,来简略回顾一下吧。
- 从 new Vue 开始,首先通过 get、set 监听 Data 中的数据变动,同时创立 Dep 用来收集应用该 Data 的 Watcher。
- 编译模板,创立 Watcher,并将 Dep.target 标识为以后 Watcher。
- 编译模板时,如果应用到了 Data 中的数据,就会触发 Data 的 get 办法,而后调用 Dep.addSub 将 Watcher 收集起来。
- 数据更新时,会触发 Data 的 set 办法,而后调用 Dep.notify 告诉所有应用到该 Data 的 Watcher 去更新 DOM。
最初,如果你对此有任何想法,欢送留言评论!