You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functioninitComputed(vm,computed){// $flow-disable-linevarwatchers=vm._computedWatchers=Object.create(null);// computed properties are just getters during SSRvarisSSR=isServerRendering();for(varkeyincomputed){varuserDef=computed[key];vargetter=typeofuserDef==='function' ? userDef : userDef.get;if(process.env.NODE_ENV!=='production'&&getter==null){warn(("Getter is missing for computed property \""+key+"\"."),vm);}if(!isSSR){// create internal watcher for the computed property.watchers[key]=newWatcher(vm,getter||noop,noop,computedWatcherOptions);}// component-defined computed properties are already defined on the// component prototype. We only need to define computed properties defined// at instantiation here.if(!(keyinvm)){defineComputed(vm,key,userDef);}elseif(process.env.NODE_ENV!=='production'){if(keyinvm.$data){warn(("The computed property \""+key+"\" is already defined in data."),vm);}elseif(vm.$options.props&&keyinvm.$options.props){warn(("The computed property \""+key+"\" is already defined as a prop."),vm);}}}}
Watcher.prototype.get=functionget(){pushTarget(this);varvalue;varvm=this.vm;try{value=this.getter.call(vm,vm);}catch(e){if(this.user){handleError(e,vm,("getter for watcher \""+(this.expression)+"\""));}else{throwe}}finally{// "touch" every property so they are all tracked as// dependencies for deep watchingif(this.deep){traverse(value);}popTarget();this.cleanupDeps();}returnvalue};``这里我们看到了熟悉的`pushTarget`函数,不过这次不是清除了,而是真的把`this`作为一个参数传进去,那么结果就是`Dep.target === this`。忘记这一块的童鞋,我直接把`pushTarget`代码再贴一遍:```jsDep.target=nullconsttargetStack=[]exportfunctionpushTarget(_target: ?Watcher){if(Dep.target)targetStack.push(Dep.target)Dep.target=_target}
notify(){// stabilize the subscriber list firstconstsubs=this.subs.slice()for(leti=0,l=subs.length;i<l;i++){subs[i].update()}}
他会调用 watcher.update 来更新 value ,这样当我们给 msg 设置了一个新的值,watcher.value 就会自动被更新。因为性能问题,watcher.update 函数默认是异步更新的,我们看看代码:
update(){/* istanbul ignore else */if(this.computed){// A computed property watcher has two modes: lazy and activated.// It initializes as lazy by default, and only becomes activated when// it is depended on by at least one subscriber, which is typically// another computed property or a component's render function.if(this.dep.subs.length===0){// In lazy mode, we don't want to perform computations until necessary,// so we simply mark the watcher as dirty. The actual computation is// performed just-in-time in this.evaluate() when the computed property// is accessed.this.dirty=true}else{// In activated mode, we want to proactively perform the computation// but only notify our subscribers when the value has indeed changed.this.getAndInvoke(()=>{this.dep.notify()})}}elseif(this.sync){this.run()}else{queueWatcher(this)}}
再回到我们最开始的 initComputed 函数,前面那么多内容我们弄懂了 new Watcher 的工作原理,这个函数还有最后一段代码:
if(!(keyinvm)){defineComputed(vm,key,userDef);}
defineComputed 函数的作用是在 this 上做一个 upperMsg 的代理,因此我们可以通过 this.upperMsg 来访问。 defineComputed 代码如下:
exportfunctiondefineComputed(target: any,key: string,userDef: Object|Function){constshouldCache=!isServerRendering()if(typeofuserDef==='function'){sharedPropertyDefinition.get=shouldCache
? createComputedGetter(key)
: userDefsharedPropertyDefinition.set=noop}else{sharedPropertyDefinition.get=userDef.get
? shouldCache&&userDef.cache!==false
? createComputedGetter(key)
: userDef.get
: noopsharedPropertyDefinition.set=userDef.set
? userDef.set
: noop}if(process.env.NODE_ENV!=='production'&&sharedPropertyDefinition.set===noop){sharedPropertyDefinition.set=function(){warn(`Computed property "${key}" was assigned to but it has no setter.`,this)}}Object.defineProperty(target,key,sharedPropertyDefinition)}
从
computed
说起为了弄懂
Watcher
我们需要选择一个切入点,这次我们选择从computed
为切入点来讲解。这个是大家非常常用的功能,而且他能比较好的解释我们是如何检测到状态变化并获取最新值的。我们先假设我们有如下组件:我们有
data.msg
和computed.upperMsg
两个自定义的数据。显然,upperMsg
依赖于msg
,当msg
更新的时候,upperMsg
也会更新。根据上一章的讲解,我们知道通过Observer
我们可以监控msg
的读写,那么如何和upperMsg
关联起来呢?Watcher 就是把这两者连接起来的关键,我们来看看
initWatcher
的代码如何工作的。完整代码如下:core/observer/watcher.js
为了方便起见,我们把在开发环境下的一些友好警告删除,并删除一些不影响我们逻辑的代码,再看看代码:
我们一行一行的来看代码,为了方便起见,我们把在开发环境下的一些友好警告跳过,也跳过一些不影响我们逻辑和理解代码意思的几行。
首先是开头两行代码:
这两行代码定义了两个变量,
watchers
是空的对象,显然是用来存储接下来创建的watchers
,isSSR
表示是否是服务器端渲染,因为如果是在服务器端渲染,就没有必要进行监听了,我们暂且不考虑服务器端的内容。接下来是一个
for
循环,会遍历computed
对象,循环体的第一段代码如下:这里的
getter
就是我们的upperMsg
函数,不过他处理了我们通过getter
来定义的情况。有了getter
之后,就会对我们定义的每一个key创建一个Watcher
。这里是我们要讲解的重点。我们暂且跳入watcher
的构造函数中看看,在文件core/observer/watcher
中。深入 Watcher 类
完整的构造函数代码如下:
代码虽然有些长,但是大部分代码都是一些属性的初始化,其中比较重要的几个是:
lazy
如果设置为true
则在第一次get
的时候才计算值,初始化的时候并不计算。默认值为true
deps
,newDeps
,depIds
,newDepIds
记录依赖,这是我们要讲的重点expOrFn
我们的表达式本身除了这些属性的设置之外,只有最后一行代码:
注意这个设计
this.value
, Vue 的设计上,Watcher
不止会监听Observer
,而且他会直接把值计算出来放在this.value
上。虽然这里因为lazy
没有直接计算,但是取值的时候肯定要计算的,所以我们直接看看getter
的代码:当我们取
upperMsg
的值的时候,全局的Dep.target
就变成了upperMsg
对应的watcher
实例了。接下来就可以直接取值了:这样,我们执行了
upperMsg
函数,取到了msg
的大写字符串。而在getter
函数中,我们有这样的代码this.msg
会读取msg
的值,因此,他会跳入defineReactive
中的getter
函数。再回顾下我们在
defineReactive
中的代码:此时的
value
肯定是msg
的值,重点是if
函数,因为Dep.target
就是我们为upperMsg
创建的watcher
实例,所以此时会执行dep.depend()
函数,这个函数如下:代码就一行,因为
Dep.target
就是watcher
,所以这行代码等价于watcher.addDep(dep)
.让我们看看addDep
函数:当执行
addDep
的时候会把dep
存起来,不过这里会有之前初始化的两个数组deps
和newDeps
,以及depIds
和newDepIds
两个set
。其实大家一看就能明白,这里明显是用来去重的,特别是其中的depIds
和newDepIds
是一个Set
。但是这个去重的逻辑有些复杂,因为包含了两个
if
,分别对depIds
和newDepIds
进行去重。那么为什么要进行两次去重呢? 举个栗子说明,我们首先假设我们有这样一个计算属性:这里进行了两次
this.msg
取值,那么显然会触发两次getter
函数,而getter
中的dep.depend()
调用并没有判断任何重复条件,所以为了计算一个doubleMsg
会两次进入Watcher.prototype.addDep
函数。而第二次进入的时候,由于newDepIds
已经记录了dep
实例的id,因此会直接忽略。那么为什么第二次进入的时候dep
和第一次是同一个呢?因为dep
是在getter/setter
外面的闭包中的,对当前msg
来说是唯一的。我们弄懂了
newDepIds
是怎么去重的,那么里面的那个if
中使用了depIds
去重,又是怎么回事呢?我们首先看看哪里用到了newDepIds
,其实是在Watcher.protototype.cleanupDeps
函数中,而这个函数是在Watcher.prototype.get
中调用的,我们看看get
的代码中的finally
是怎么写的:也就是在
get
取到值后,就调用this.cleanupDeps
,这个函数会把newDepIds
的值赋给depIds
,然后把newDepIds
清空。当Vue对
doubleMsg
进行求值的时候,会调用两次this.msg
,求值结束后,会进行this.cleanupDeps
操作。这样求值结束之后,我们的依赖就存在于depIds
而不是newDepIds
中。知道了这一点之后就比较好理解了。newDepIds
只是在对doubleMsg
进行求值的过程中,避免对msg
的多次依赖。当求值结束之后,newDepIds
就空了吗,而依赖被记录在depIds
中。如果我们在第一次对doubleMsg
求值之后,再次进行求值会怎么样呢? 比如我们这样:在
$mount
结束后对this.msg
进行赋值,那么就会触发watcher.update
方法,而这里面会进行再次进行this.msg
求值。此时,newDepIds
为空,而depIds
有值,因此不会被重复记录依赖。所以总结下来就是:
newDepIds
可以在upperMsg
的一次求值过程中,避免对msg
的重复依赖depIds
可以在由于msg
更新而导致再次对doubleMsg
求值的时候,避免对msg
的重复依赖搞懂了去重代码之后,最主要的一行代码就是
dep.addSub(this)
。也就是会把watcher
添加到dep.subs
中。到目前为止,我们能做到 一旦 调用
this.upperMsg
读取值,就会触发依赖收集。那么当msg
被更新的时候,watcher.value
又是怎么知道而更新的呢?还是先看defineReactive
中的setter
定义:其中最重要的是最后一行代码
dep.notify
而这行代码就会去通知所有的watcher
,notify
代码如下:他会调用
watcher.update
来更新value
,这样当我们给msg
设置了一个新的值,watcher.value
就会自动被更新。因为性能问题,watcher.update
函数默认是异步更新的,我们看看代码:里面有很多注释,前几行是处理当有其他的值依赖我们的
upperMsg
的情况的,我们下面会讲到,这里暂且跳过。直接看最后几行代码:如果是
sync
模式,那么直接调用run
来更新value
。默认情况是异步的,所以会进入queueWatcher(this)
方法,会把run
的运行推迟到nextTick
才运行。这也是我们为什么更新了msg
之后立刻读取upperMsg
其实内容并没有被更新的原因。因为把所有的更新都集中到nextTick
进行,所以Vue
会有比较好的性能。queueWatcher 其实比较简单,他会用一个队列记录所有的操作,然后在nextTick
的时候统一调用一次。这里就不做过多介绍了,我们会有单独的一章来介绍。到这里我们已经弄懂了
upperMsg
是如何依赖msg
的,我画了一个图来梳理他们之间的关系:解释一下这个图,其中蓝色的线是引用关系(除了 Observer 和 dep 中间那条线,因为那条线其实是闭包而不是引用),红色的线是依赖的触发流程。
this.msg = xxx
来修改msg
的值,他被observer
监听,因此 observer 可以知道这个更新的发生dep
记录了依赖,他会调用dep.notify
来通知那些订阅者update
方法watcher.update
方法,经过几次调用后最终会在nextTick
的时候更新this.value
的值回到 initComputed
再回到我们最开始的
initComputed
函数,前面那么多内容我们弄懂了new Watcher
的工作原理,这个函数还有最后一段代码:defineComputed
函数的作用是在this
上做一个upperMsg
的代理,因此我们可以通过this.upperMsg
来访问。defineComputed
代码如下:他会通过
Object.defineProperty
设置this.upperMsg
,依然是通过getter/setter
来定义的,this.upperMsg
的读写会被代理到我们在options
中定于的upperMsg
上。不过你可能会发现,在我写的demo中,defineComputed
方法根本没执行,那么this.upperMsg
是哪来的呢?这是因为我的项目中使用了Vue.router
他会用Vue.extend
来创建一个自定义的类,而Vue.extend
为了避免在创建实例的时候重复创建upperMsg
,就把他提前创建好了。到此我们通过对
data
和computed
的解读,彻底弄懂了响应式的工作原理。至于props
因为涉及到VDOM,这里暂时先不展开了,但是他的响应式部分实现和data
是一样的。下一章:Vue2.x源码解析系列六:模板渲染之render和watcher
The text was updated successfully, but these errors were encountered: