第一篇留了两个问题:1.当计算属性所依赖的属性发生变化时,如何触发计算属性的更新。2、watch选项或者$watch方法的原理是什么?本文就来分析一下这两个问题,简单看一下自定义元素是如何渲染的。计算属性
{data:{message:'HelloVue.js!'},computed:{showMessage(){returnthis.message.toUpperCase()}}}在这个简单的例子中,首先,computed属性也将作为实例的属性挂载到vue实例上:for(varkeyincomputed){varuserDef=computed[key]vardef={enumerable:true,configurable:true}if(typeofuserDef==='function'){def.get=_.bind(userDef,this)def.set=noop}else{def.get=userDef.get?_.bind(userDef.get,this):noopdef.set=userDef.set?_.bind(userDef.set,this):noop}Object.defineProperty(this,key,def)}accessviathis.xxx计算属性时,会调用我们定义的计算选项中的函数。其次,在模板编译指令解析阶段,计算属性与普通属性没有区别。这个v-text指令将创建一个Directive实例。当这个Directive实例初始化的时候,它会以showMessage+'我是一个不重要的字符串'作为唯一标识创建一个Watcher实例,v-text指令的update方法会被这个Watcher实例收集起来,添加到它的cbs中大批。Watcher实例化时,会将自己赋值给Observer.target,然后showMessage+'我不重要的字符串'表达式求值,也会调用计算属性的函数showMessage()。这个函数调用之后,会引用它所依赖的所有属性,这里是message,会触发message的getter,从而将这个Watcher实例添加到message的依赖集合对象dep中,并且当message的值变化触发它的setter时,会遍历dep中收集的Watcher实例,触发Watcher的update方法,最后遍历cbs方法中添加的command的update,使得依赖的指令计算属性上的更新。值得注意的是,在这个版本中,计算属性没有被缓存,即使依赖值没有改变,重复引用计算属性的值也会重新执行我们定义的计算属性函数。listenerwatch选项声明的监听器最后也会调用$watch方法。我们在第一篇已经知道,$watch方法主要是创建一个Watcher实例://exp就是我们要监听的数据,比如:a.a.bexports.$watch=function(exp,cb,deep,immediate){varvm=thisvarkey=deep?exp+'**deep**':expvarwatcher=vm._userWatchers[key]varwrappedCb=function(val,oldVal){cb.call(vm,val,oldVal)}if(!watcher){watcher=vm._userWatchers[key]=newWatcher(vm,exp,wrappedCb,{deep:deep,user:true})}else{watcher.addCb(wrappedCb)}}我们现在对Watcher非常熟悉了。实例化的时候会把自己赋值给Observer.target,然后触发表达式的求值,也就是我们要检测Listen属性,触发它的gettter然后把Watcher收集到它的依赖集合对象dep中。只要收起来,就好办了。后续属性值发生变化后,会触发Watcher的更新,同时也会触发上面的回调。自定义组件的渲染>',data(){return{msg:'helloworld!'}}}}})在第一篇文章中,我们提到每个组件选项将被创建为一个继承vue的构造函数:然后到遍历这个自定义元素期间模板编译会为其添加一个v-component属性:tag=el.tagName.toLowerCase()component=tag.indexOf('-')>0&&options.components[tag]if(component){el.setAttribute(config.prefix+'component',tag)}所以后续也是通过指令对这个自定义组件进行处理,然后生成链接函数,组件是一种终端指令:然后又回到正常的指令编译过程,_bindDir方法会为v-component指令创建一个Directive实例,然后调用component指令的bind方法:{bind:function(){//el是我们的自定义元素my-com如果(!this.el.__vue__){//创建一个注释元素来替换自定义元素this.ref=document.createComment('v-component')_.replace(this.el,this.ref)//检查是否存在keep-alive选项this.keepAlive=this._checkParam('keep-alive')!=null//检查是否有ref指向这个组件this.refID=_.attr(this.el,'ref')if(this.keepAlive){this.cache={}}//resolve构造函数,即返回初始化时optionmerge阶段生成的构造函数,这里的expression为指令值my-componentthis.resolveCtor(this.expression)//创建子实例varchild=this.build()//插入子实例child.$before(this.ref)//设置refthis.setCurrent(child)}}}查看构建方法:{build:function(){//如果有缓存直接returnif(this.keepAlive){varcached=this.cache[this.ctorId]if(cached){returncached}}varvm=this.vmif(this.Ctor){varchild=vm.$addChild({el:this.el,_asComponent:true,_host:this._host},this.Ctor)//Ctor是这个组件的构造函数if(this.keepAlive){this.cache[this.ctorId]=child}returnchild}}}该方法用于创建子实例,调用$addChild方法,简化如下:exports.$addChild=function(opts,BaseCtor){varparent=this//父实例就是我们上面说的新建Vue的实例opts._parent=parent//根组件也是父实例的根组件opts._root=parent.$root//创建aninstanceofthecustomcomponentvarchild=newBaseCtor(opts)returnchild}以上两个方法主要是创建组件构造器的实例,因为组件构造器继承vue,所以前面newvue中所做的初始化工作会还要再过一遍,什么观察数据,遍历自定义组件及其所有子元素进行模板编译绑定指令等,因为我们传递了template选项,所以第一篇传递过来的方法_compile将在co之前被调用mpilemethodProcessthisfirst://这里将template模板字符串转成dom,原理很简单,创建一个documentfragment,然后创建一个div,然后将模板字符串设置为div的innserHTML,最后放theAllelementscanbeaddedtothedocumentfragmentel=transclude(el,options)//编译并链接其余部分compile(el,options)(this,el)最后,如果有keep-alive,缓存实例并返回在bind方法中对child.$before(this.ref):exports.$before=function(target,cb,withTransition){returninsert(this,target,cb,withTransition,before,transition.before)}functioninsert(虚拟机,target,cb,withTransition,op1,op2){//获取目标元素,这里是bind方法中创建的注解元素target=query(target)//该元素当前不在文档中vartargetIsDetached=!_.inDoc(target)//判断是否使用transition方式插入,如果元素不在文档中,则使用transition方式插入varop=withTransition===false||targetIsDetached?op1:op2//如果目标元素当前插入到文档中,并且组件还没有挂载,则需要触发附加的生命周期。varshouldCallHook=!targetIsDetached&&!vm._isAttached&&!_.inDoc(vm.$el)//插入文档op(vm.$el,target,vm,cb)if(shouldCallHook){vm._callHook('attached')}returnvm}op方法将调用transition.before方法将元素插入到文档中。transition插入的详细分析可以参考vue0.11版本源码阅读系列之六:transition原理to这里组件已经渲染完成,最后在bind方法中调用了setCurrent:{setCurrent:function(child){this.childVM=childvarrefID=child._refID||this.refIDif(refID){this.vm.$[refID]=child}}}如果我们设置一个引用,例如:
,那么我们可以通过this.$.myComponent访问子组件。keep-alive的工作原理也很简单,就是返回之前的实例,而不是创建一个新的实例,这样所有的状态依然保留。总结这个系列到这里基本就结束了。相信能看到这里的人不多,因为是第一次写这一系列的源码阅读。总的来说,有点乱。很多地方的重点不是很突出,可能描述的不是很详细,可读性可能不是很好。另外难免会有错误,欢迎大家指出。阅读源代码是每个开发者绕不过去的必由之路。无论是为了自我提升还是为了面试,我们都需要对自己一直在使用的东西有更深入的了解,这样才能更得心应手地使用它们。说起来也是有益的,另外,思考和学习别人优秀的编码思维,也能让自己变得更好。不得不说,阅读源码是一件相当枯燥和困难的事情,很容易让人戒掉。有很多地方如果不是很了解它的功能是看不懂的。当然,我们不必执着于这些地方,你不必阅读和理解所有的地方。更好的方法是带着问题阅读。比如我想了解某个地方的原理,那你看这部分代码就可以了。当你沉浸其中时,里面也有一些有趣的东西。话不多说,白搭~