当前位置: 首页 > Web前端 > vue.js

Vue2响应式原理解析(二):计算属性大揭秘

时间:2023-04-01 01:07:59 vue.js

大家好~上一篇Vue2响应式原理解析(一):从设计出发,讲了Vue2如何抽象设计响应式,数据如何实现响应式,包括依赖收集和双向依赖记录的设计思路和关键代码。在这篇文章中,我们就来看看康康Vue中一个非常强大的响应式功能:计算属性。主要从功能需求的角度分析计算属性的实现和关键代码,希望能给大家带来其他文章看不到的东西。请先看下面的内容,再看就好了~计算属性在Vue的文档中有提到。当然,这个问题的另一种解决方案是使用方法中定义的方法,但是计算属性有一个非常强大的特性:缓存。这意味着如果计算属性依赖的数据没有变化,再次访问计算属性时不会重新计算,直接返回缓存的结果,这对于计算复杂的场景非常实用。计算属性的实现如何和我们前面提到的响应式设计结合起来呢~?这里我们先看一张图:这张图描述的是当你声明一个计算属性时(这里举个栗子是声明fullName计算属性),Vue将其转换成图中右侧getter+watcher的结构来实现计算属性的所有功能。如果看起来有点混乱,请不要担心,让我们一一揭示计算属性是如何实现和工作的。实现细节首先出现在src/core/instance/state.js文件中。有一个initComputed函数,就是初始化计算属性的地方。我们看一下代码的关键部分:functioninitComputed(vm:Component,computed:Object){//vm对象中添加了_computedWatchers,用于存储计算属性对应的watchersconstwatchers=vm._computedWatchers=Object.create(null)//...for(constkeyincomputed){//计算属性支持setter,为了简单说明重点,我们只关注计算属性声明为函数的情况。constgetter=typeofuserDef==='函数'?userDef:userDef.get//...//服务端渲染的情况也先忽略if(!isSSR){//这里注意为每个计算属性生成一个watcher,计算属性的函数作为吸气剂传入。watchers[key]=newWatcher(vm,getter||noop,noop,computedWatcherOptions)}if(!(keyinvm)){//这是定义在vm对象上的计算属性的描述符defineComputed(vm,key,userDef)}//...}}这是计算属性的主要实现过程。首先我们把视角抬高一点,只关注关键流程,不要纠结太多细节,后面再说。关键过程如上图所示:Vue为每个计算属性生成一个watcher,并在vm对象上声明一个与计算属性同名的访问描述符,后面会一起使用。这里需要注意的是Watcher构造函数传入的computedWatcherOptions。这个对象有一个lazy:true属性。稍后你就会知道为什么要使用它。下面从计算属性的使用开始,详细介绍计算属性的实现细节和本质。首先我们来到上面提到的defineComputed函数,依然是去除神马服务端渲染逻辑的一些干扰,只看主要实现细节的代码:导出函数defineComputed(target:any,key:string,userDef:Object|Function){//不考虑服务端渲染constshouldCache=!isServerRendering()if(typeofuserDef==='function'){//注意这里调用createComputedGetter生成描述符gettersharedPropertyDefinition.get=应该缓存吗?createComputedGetter(key):createGetterInvoker(userDef)//...}//...//生成一个与虚拟机上的计算属性同名的访问描述符Object.defineProperty(target,key,sharedPropertyDefinition)}在defineComputed中我们看到最终的描述符get是由createComputedGetter生成的。这个功能是关键。在继续之前,让我们回顾一下计算属性的使用场景和缓存的应用。通常我们定义好计算属性后,就会在模板中使用它。当界面第一次显示时,计算属性会计算值,除非计算属性的依赖发生变化(例如:依赖数据对象的属性被重新赋值),否则后续刷新不会导致计算属性要重新计算,但是会直接返回上次缓存的值。从这里可以看出,读取模板中计算属性的值,实际上就是在vm上调用计算属性描述符的get。理清场景后,我们把createComputedGetter分为两部分,重点关注缓存相关的前半部分代码:this._computedWatchers&&this._computedWatchers[key]if(watcher){if(watcher.dirty){watcher.evaluate()}//...returnwatcher.value}}}上面代码中的watcher.dirty是表示是否计算属性的当前值需要重新计算。如果不需要重新计算,直接返回watcher.value。这就是缓存的作用。所以这里我们回忆一下,computed属性在初始化watcher的时候传递了一个lazy:true,在Watcher的构造函数中有这样的逻辑:exportdefaultclassWatcher{//...Function,cb:Function,options?:?Object,isRenderWatcher?:boolean){//...this.lazy=!!options.lazy//...this.dirty=this.lazy//如果是是脏的,需要求值,初始化时为真//...//如果是计算属性,初始化观察者不会求值this.value=this.lazy?undefined:this.get()}}这意味着:如果它是一个计算属性,它不会在watcher初始化时被评估,而只会被标记为脏——缓存无效。然后watcher.dirty(第一次显示界面)在第一次调用上面计算属性的get时,会调用watcher.evaluate():evaluate(){this.value=this.get()this.dirty=false}在evaluate中,执行get()进行评估,并将缓存标记为有效。回想一下,当依赖dep被设置时,watcher.update()会被执行:update(){if(this.lazy){//如果是计算属性,这里会被标记为dirty。this.dirty=true}//...}因此,如果依赖发生变化,缓存就会失效,重新计算计算属性~以上就是计算属性缓存的设计实现细节。我尽量只提取关键代码,把关键的东西解释清楚。这里需要注意的是,Vue将计算属性的场景抽象成了一个lazywatcher。lazywatcher只在需要的时候计算值,并且有缓存功能!所以抽象能力值得学习~依赖转移我们回过头来看看createComputedGetter的后半部分代码:(watcher){//前面的代码是判断缓存是否应该更新//...//注意这里传递的是依赖if(Dep.target){watcher.depend()}returnwatcher.value}}}前半部分缓存已经讲过了,现在我们注意到这里有一个依赖转移的逻辑,什么意思呢?这里我们还是以场景为例。例如,在计算属性的声明中,您可以引用另一个计算属性!这很好,因为在初始化观察者时不会评估计算属性,也就是惰性观察者。例如下面的代码:constvm=newVue({el:'#demo',data:{a:1,b:2},computed:{c:function(){returna+b},d:function(){returnc*2}}})这里用这个简单的例子来说明为什么需要依赖转移:计算属性c依赖a,b依赖data,计算属性d依赖c。那么问题来了,当a和b发生变化时,c就会变脏,当然d也需要变脏,否则d会有缓存,不会被重新计算。那么d是如何得到通知的呢?关键代码就是上面的watcher.depend()。首先,当d取值时,会调用d自己的watcher.get()。这时d的watcher会被设置为Dep.target;然后,当c的watcher.get()被执行时,c的watcher也会被设置为Dep。目标。这里注意pushTarget是在设置Dep.target的时候调用的。这个函数会调用targetStack数组,将当前记录的Dep.target压入数组中:exportfunctionpopTarget(){targetStack.pop()Dep.target=targetStack[targetStack.length-1]}//之前说过targetStack数组会在以后的文章中分析,这里又回来了。执行完这些之后,c的watcher首先与a、b的dep建立依赖关系,然后对watcher.evaluate()进行求值。求值完成后,c执行自己的watcher.get()。注意popTarget()是在get()方法的最后执行的,也就是说c弹出自己的watcher,当前的Dep.target又变了。成为了d的守望者!当c被评估时,如果Dep.target仍然存在,将执行watcher.depend()以传递依赖项。我们来看看watcher.depend()做了什么:depend(){leti=this.deps.lengthwhile(i--){this.deps[i].depend()}}代码的意思是很明确,就是让c当前依赖的deps也和Dep.target建立依赖(也就是d起来)。这样,当a和b发生变化时,d也会变脏。通过上面场景的描述,你应该明白为什么需要依赖转移了~以上是Vue的计算属性的详细介绍和我的解读。不清楚的请参考第一篇文章。在这一点上,Vue2的响应性大致相同。后面会再写一篇文章讲听力属性,然后再回到设计上整体巩固一下。如果说的不对或者有其他意见欢迎留言讨论wow~欢迎star关注我的JS博客:小生笔笔JavaScript