响应式原理概述
大致流程,首先实例化Vue,调用_init
方法,初始化data、methods、watch、props、生命周期等,通过调用观察者函数observer
生成被代理的对象,对象上会有个__ob__
属性就是observer
的实例,也就是表示该对象被代理了,被代理对象中每个属性通过defineReactive$$1
方法来代理(如果属性值是对象或数组就递归向下执行),对象代理功能是使用Object.defineProperty
这个原生api来实现的。Object.defineProperty
分别定义get
和set
方法,get
方法当对象属性被引用时触发依赖收集,set
方法用来修改属性并执行更新方法。在$mount
方法中,会解析模板,引用对象属性触发依赖收集生成vNode,再渲染真实视图。后面在属性更新时,会触发set
方法,进行更新。
从源码角度分析
初始化实例
_init
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
_init
初始化Vue实例,_init
定义在原型链上,查看Vue.prototype._init
中的内容,其中initLifecycle
,确定组件的层级,父组件,根组件,定义属性来保存子组件、wather对象、记录生命周期执行状态属性等;initEvents
做一些承父级相关监听函数的操作;initRender
,初始化一些跟虚拟DOM相关属性和方法。
注入
initInjections
属性注入,配合文档,这里和props属性很相似,inject
格式应该是字符串数组或者对象,具体在resolveInject
函数中,如果是数组,每一项就对应provide
中的属性;如果是对象,再对对象的属性值类型做判断,如果是对象,则需要包含一个default
属性返回默认属性的值,反之,则该值就对应provide
中的属性。同时注入的属性会不断向上查询,直到找到包含该属性的provide或者到根组件没找到为止。最后生成的对象会注入到当前Vue实例中,注入过程中会使用修改toggleObserving
修改shouldObserve
状态为false,注入的对象不会递归向下做代理。initState
初始化data、props、methods、computed、watch等属性。这里也是Vue实现响应式原理地方,先放一下,先介绍注入。initProvide
为前面的initInjections
注入方法提供provide。
这里查看这几个函数的执行顺序
initInjections => initState => initProvide
首先initState
放在initInjections
后面,这个就是防止data中的属性被覆盖,而initProvide
放在最后执行,因为它是提供数据的,最外层不需要注入操作。而后面的子孙组件中的inject
通过向上查找祖先组件的方式,可以访问到最外层Provide
。
响应式实现
再回头看initState
方法,这其中最重要的就是对data
做对象代理。
执行observe
方法,observer
作为构造函数,data
作为参数,生成一个observer
对象,value
属性来保存data
,被代理对象使用__ob__
属性来保存这个observer
实例,dep
属性生成对该对象的订阅者,然后调用原型上的方法walk
,依次使用defineReactive$$1
对data
对象中的每个属性做代理操作。
defineReactive$$1
defineReactive$$1
首先生成一个Dep订阅者,然后判断属性是否可配置,再判断属性值如果是对象或数组会进一步向下构造观察者对象,接下来就是Object.defineProperty
方法,这也是让Vue能实现响应式原理的API,Object.defineProperty
原意是用来定义属性修饰符,包含属性的可修改性、可配置性、可枚举性,同时也可以定义get
和set
方法,分别在属性读取和修改时执行,这样利用这两个方法进行一些扩展,get
方法在执行时在获取属性值的基础上再进行依赖收集,set
方法则通知依赖该属性对应的dep
的所有watcher
观察者来更新视图。
简单介绍完defineReactive$$1
,再仔细看下这个函数中两个关键的东西:Dep
、Watcher
Dep
订阅者构造函数,每个属性会新建一个dep
订阅者,dep
中有个subs
属性,其中Dep.target
一个全局的变量会指向当前watcher
观察者,然后当某个视图中对这个属性被引用,就会向该属性的dep
订阅者中的subs
添加当前的watcher
观察者对象。当属性更新时,会调用对应dep中的notify
方法,通知subs
中所有的watcher
执行update
更新视图。
Watcher
观察者构造函数,上面已介绍部分,每个Vue实例中会存在_watcher
属性对应一个watcher
观察者,下面的依赖收集过程会详细介绍Watcher
。
这时候应该来一张图描述:对象代理,Dep,Watcher,依赖收集,虚拟DOM,渲染视图等。
依赖收集
接下来介绍依赖收集是什么时候发生的,回顾前面在注入、初始化数据等结束后,先执行created
回调函数,然后执行$mount
进行组件挂载,这里有意思的是作者定义了两次Vue.prototype.$mount
,开始我也觉得奇怪,当然后面定义的会覆盖前面,只是用了一行代码var mount = Vue.prototype.$mount;
在覆盖前把第一次的方法保存下来了,这时再看新的$mount
方法,主要是在解析编译模板等操作,最后生成一个render
的函数,末尾再执行前面的保存的老的$mount
方法,里面使用了mountComponent
,可以看到开始前会做一些检验判断render
是否存在,没有的话就赋值成一个默认方法(创建一个仅包含文本的虚拟DOM),并抛出警告。然后执行beforeMount
生命周期函数,然后会先定义一个updateComponent
方法,该方法的作用就是根据对象属性的引用触发依赖收集并生成vNode。
在看后面先new一个Watcher
对象,updateComponent
方法会作为第二个参数传递进去。接下来查看Watcher
是如何定义的。
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy
? undefined
: this.get();
};
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
初始化一系列属性和方法,当构造一个watcher
对象时,会调用原型上的get
方法,该方法首先调用pushTarget
会将Dep.target
变成当前的wathcer
,之后调用对象中的getter
方法,此时的getter
就是对应前面updateComponent
方法,updateComponent
真正被执行,从而会调用Vue实例上的_render
方法。
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentvNode = ref._parentvNode;
// ......省略分割......
var vNode;
// ......省略分割......
vNode.parent = _parentvNode;
return vNode
经历一系列地操作,完成了依赖收集和返回对应vNode
,即虚拟DOM,vNode有个重要的属性parent
父节点,如果为空就表示该节点是根节点,有了虚拟DOM以后,再经历_update
、__patch__
等主要是diff算法部分,除了第一次vNode还不存在的时候,更新时生成新的vNode后,不会整个拿来更新而是经过diff处理生成最小更新的单位提高性能,updateComponent
执行结束前会先调用popTarget
方法,让Dep.target
回到上一个的值。
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
为什么要用一个数组targetStack
来保存watcher
,这个跟父子组件挂载更新规则有关,父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted。这是挂载阶段父子组件生命周期执行顺序。因为子组件依赖收集发生在beforeMount
之后mounted
之前,因此在父组件如果存在子组件时,子组件构造一个新的watcher
对象,此时父组件依赖收集还未结束,pushTarget
操作会将父组件的watcher
push进targetStack
暂缓起来,子组件生成vNode后,popTarget
后Dep.target
又恢复成父组件的watcher
。
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var i = this.deps.length;
while (i--) {
var dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
var tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
};
至于cleanupDeps
方法,用来更新属性所对应dep
订阅者中的watcher
对象,该视图中不需要依赖该属性,则从对应的dep
中移除watcher
,watcher
中的newDepIds
每次都保存最新的dep
的id集合。举个栗子,比如v-if
控制的节点组件,showChild
值由true
变成false
时,由于子组件已经不需要渲染了,cleanupDeps
执行以后就会取消对childVal
的订阅。
<Parent>
<Child v-if="showChild">{{childVal}}</Child>
</parent>
视图更新
如上,updateComponent
执行完成后,视图就完成渲染了。当某个对象代理的数据更新时,set
方法执行,dep触发notify
方法通知所有watcher
观察者执行update
,生成新的vNode,重新进行依赖收集等,patch对新老vNode进行diff
,更新差异部分的视图。
Vue3都出来了,为什么还在写Vue2的总结?害,我觉得现在不整理出来以后更没机会了🐶 。嘿嘿,有错误的话也大家望指出喽。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。