最近部门分享,有同学提到了 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。
最终,如果你对此有任何主意,欢迎留言评论!