了解Vue计算属性的实现原理
computed的作用
在vue的开发中,我们不免会使用到计算属性,使用计算属性,vue会帮我们收集所有的该计算属性所依赖的所有data属性的依赖,当data属性改变时,便会重新获取computed属性,这样我们就不用关注计算属性所依赖的data属性的改变,而手动修改computed属性,这是vue强大之处之一。那么我们不免会产生疑问,computed属性为啥能随着data属性的改变而跟着改变的?
带着这个疑问,我们来解析下vue的源码,看看它是如何实现computed的依赖收集。
整体流程
computed的依赖收集是借助vue的watcher来实现的,我们称之为computed watcher,每一个计算属性会对应一个computed watcher对象,
该watcher对象包含了getter属性和get方法,getter属性就是计算属性对应的函数,get方法是用来更新计算属性(通过调用getter属性),并会把该computed watcher添加到计算属性依赖的所有data属性的订阅器列表中,这样当任何计算属性依赖的data属性改变的时候,就会调用该computed watcher的update方法,把该watcher标记为dirty,然后更新dom的dom watcher更新dom时,会触发dirty的computed
watcher调用evaluate去计算最新的值,以便更新dom。
所以computed的实现是需要两个watcher来实现的,一个用来收集依赖,一个用来更新dom,并且两种watcher是有关联的。后续我们把更新DOM的watcher称为domWatcher,另一种叫computedWatcher
initComputed
该方法是用来初始化computed属性的,它会遍历computed属性,然后做两件事:
1、为每个计算属性生成一个computedWathcer,后续计算属性依赖的data属性会把这个computedWatcher添加到自己订阅器列表中,以此来实现依赖收集。
2、挟持每个计算属性的get和set方法,set方法没有意义,主要是get方法,后面会提到。
function initComputed (vm, computed) {
varwatchers = vm._computedWatchers = Object.create(null);
//遍历所有的computed属性
for (varkey in computed) {
varuserDef = computed[key];
//每个计算属性对应的函数或者其get方法(computed属性可以设置get方法)
vargetter = typeof userDef === 'function' ? userDef : userDef.get;
// ....
if(!isSSR) {
//为每个计算属性生成一个Wathcer
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
if (!(keyin vm)) {
//defineComputed的作用就是挟持每个计算属性的get和set方法
defineComputed(vm, key, userDef);
} else {
// ....
}
}
}
defineComputed
如上面所述,definedComputed是挟持计算属性get和set方法,当然set方法对于计算属性是没什么作用,所以这里我们重点关注get方法,我们这里只需要知道get方法是触发依赖收集的关键,并且它把两种watcher进行了关联。
function defineComputed (
target,
key,
userDef
) {
varshouldCache = !isServerRendering();
//下面这段代码就是定义get和set方法了
if (typeofuserDef === 'function') {
sharedPropertyDefinition.get = shouldCache
?createComputedGetter(key)
:userDef;
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
?shouldCache && userDef.cache !== false
?createComputedGetter(key)
:userDef.get
: noop;
sharedPropertyDefinition.set = userDef.set
?userDef.set
: noop;
}
//...
//这里进行挟持
Object.defineProperty(target, key, sharedPropertyDefinition);
}
createComputedGetter
createComputedGetter有两个作用:
1、收集依赖
当domWatcher获取计算属性的时候,会触发该方法,然后computedWatcher会调用evaluate方法,最终会调用computedWatcher的get方法(下面会分析),来完成依赖的收集
2、关联两种watcher
通过第一步完成依赖收集后,computedWatcher能知道依赖的data属性的改变,从而计算出最新的计算属性值,那么它是怎么让另外一个watcher,即domWatcher知道的呢,其实就是通过调用computedWatcher.depend方法把两种watcher关联起来的,这个方法会把Dep.target(就是domWatcher)放入到计算属性依赖的所有data属性的订阅器列表中。
通过这两个作用,当计算属性依赖的data属性有改变的时候,就会调用domWatcher的update方法,它会获取计算属性的值,因此会触发computedGetter方法,使得computedWatcher调用evaluate来计算最新的值,以便domWatcher更新dom。
function createComputedGetter (key) {
returnfunction computedGetter () {
//取出initComputed创建的watcher
varwatcher = this._computedWatchers && this._computedWatchers[key];
if(watcher) {
//这个dirty的作用一个是避免重复计算,比如我们的模板中两次引用了这个计算属性,那么我们只需要计算一次就够了,一个是当计算属性依赖的data属性改变,会把这个计算属性对应的watcher给设置为dirty=true,然后
if(watcher.dirty) {
//这个会计算计算属性的值,并且会调用watcher的get方法,完成依赖收集
watcher.evaluate();
}
//Dep.target指向的是模板中计算属性对应节点的domWatcher
//这个语句的意思就是把domWatcher放入到当前computedWatcher的所有依赖中,这样计算属性依赖的data值一改,
//就会触发domWatcher的update方法,它会获取计算属性的值从而触发这个computedGetter,然后computedWatcher会通过调用evaluate方法获取最新值,
//然后交给domWatcher更新到dom
if(Dep.target) {
watcher.depend(); //关联了两种watcher
}
returnwatcher.value
}
}
}
Computed Watcher
watcher是实现computed依赖的关键,它的第二个参数getter属性即是计算属性对应的方法或get方法。
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm =vm;
// ...
// watcher的第二个参数,即是我们计算属性对应的方法或get方法,用于算出计算属性的值
if (typeofexpOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if(!this.getter) {
this.getter = function () {};
}
}
//不会立即计算
this.value= this.lazy
?undefined
:this.get();
};
那么只要调用getter方法,那么它就会触发计算属性所有依赖的data的get方法,我们看下get方法
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get:function reactiveGetter () {
varvalue = getter ? getter.call(obj) : val;
//Dep.target保存的是当前正在处理的Watcher,这里其实就是computedWatcher
if(Dep.target) {
//这句代码其实就是将Dep.target放入到该data属性的订阅器列表当中
dep.depend();
//...
}
returnvalue
},
...
})
如上所述,其实就是把Dep.taget(当前的watcher)放入到该data属性的订阅器列表当中,那么这个时候,Dep.target指向哪个Watcher呢?我们看下watcher的get方法
Watcher.prototype.get = function get () {
//这句语句会把Dep.target执行本watcher
pushTarget(this);
var value;
var vm =this.vm;
try {
//调用getter,会触发上述讲的get,而get方法就会把Dep.target即本watcher放入到计算属性所依赖的data属性的订阅器列表中
//这样依赖的data属性有改变就会调用该watcher的update方法
value =this.getter.call(vm, vm);
} catch (e){
//...
} finally {
//...
popTarget(); //将Dep.target指回上次的watcher,这里就是计算属性对应的domWatcher
this.cleanupDeps();
}
returnvalue
};
可以看到get方法开始运行时,把Dep.target指向计算属性对应的computedWatcher,然后调用watcher的getter方法,触发这个计算属性对应的data属性的get方法,就会把Dep.target指向的watcher加入到这些依赖的data的订阅器列表当中,以此完成依赖收集。
这样当我们的计算属性依赖的data属性改变的时候,就会调用订阅器的notify方法,它会遍历订阅器列表,其中就包含了该计算属性对应的computedWatcher和domWatcher,调用computedWatcher的update方法会把computedWatcher置为dirty,调用domWathcer的update方法会触发computedGetter,它会再次调用computedWatcher的evaluate计算出最新的值交给domWatcher去更新dom。
Watcher.prototype.update = function update () {
if(this.lazy) {
//computed专属的watcher走这里
this.dirty = true;
} else if(this.sync) {
// run方法会调用get方法,get方法会重新计算计算属性的值
//但这个时候get方法不会再收集依赖了,vue会去重
this.run();
} else {
queueWatcher(this);
}
};
Watcher.prototype.run = function run () {
if(this.active) {
//调用get方法,重新计算计算属性的值
var value= this.get();
//值改变了、Array或Object类型watch配置了deep属性为true的
if (
value!== this.value ||
isObject(value) ||
this.deep
) {
varoldValue = this.value;
this.value = value;
if(this.user) {
//watch监听走此处
try {
this.cb.call(this.vm, value, oldValue);
}catch (e) {
handleError(e, this.vm, ("callback for watcher \"" +(this.expression) + "\""));
}
} else{
//data数据改变,会触发更新函数cb,从而更新dom
this.cb.call(this.vm, value, oldValue);
}
}
}
};
总结
遍历computed,为每个计算属性新建一个computedWatcher对象,并将该computedWatcher的getter属性赋值为计算属性对应的方法或者get方法。(大家应该知道计算属性不但可以是一个函数,还可以是一个包含get方法和set方法的对象吧)
使用Object.defineProperty挟持计算属性的get方法,当模版获取计算属性的值的时候,触发get方法,它会调用第一步创建的computedWatcher的evaluate方法,而evaluate方法就会调用watcher的get方法
computedWatcher的get方法会将Dep.target指向该computedWatcher,并调用getter方法,getter方法会触发该计算属性依赖的所有data属性的get方法,从而把Dep.target指向的computedWatcher添加到data属性的订阅器列表中。同时,computedWatcher保存了依赖的data属性的订阅器(deps属性保存)。
同时调用computedWatcher的depend方法,它会把Dep.taget指向的domWatcher放入到计算属性依赖的data属性的订阅器列表中,如此计算属性依赖的data属性改变了,就会触发domWatcher和computedWatcher的update方法,computedWatcher赋值获取计算属性的最新值,domWatcher负责更新dom。
网友评论