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
Watcher.prototype.beforeGet=function(){Dep.target=this}Watcher.prototype.get=function(){this.beforeGet()// v-for情况下,this.scope有值,是对应的数组元素,其继承自this.vmvarscope=this.scope||this.vmvarvaluetry{// 执行getter,这一步很精妙,表面上看是求出指令的初始值,// 其实也完成了初始的依赖收集操作,即:让当前的Watcher订阅到对应的依赖(Dep)// 比如a+b这样的expression实际是依赖两个a和b变量,this.getter的求值过程中// 会依次触发a 和 b的getter,在observer/index.js:defineReactive函数中,我们定义好了他们的getter// 他们的getter会将Dep.target也就是当前Watcher加入到自己的subs(订阅者数组)里value=this.getter.call(scope,scope)}catch(e){// 输出相关warn信息}// "touch" every property so they are all tracked as// dependencies for deep watching// deep指令的处理,类似于我在文章开头写的那个遍历所有属性的touch函数,大家请跳过此处if(this.deep){traverse(value)}if(this.preProcess){value=this.preProcess(value)}if(this.filters){// 若有过滤器则对value执行过滤器,请跳过value=scope._applyFilters(value,null,this.filters,false)}if(this.postProcess){value=this.postProcess(value)}this.afterGet()returnvalue}// 新一轮的依赖收集后,依赖被收集到this.newDepIds和this.newDeps里// this.deps存储的上一轮的的依赖此时将会被遍历, 找出其中不再依赖的dep,将自己从dep的subs列表中清除// 不再订阅那些不依赖的depWatcher.prototype.afterGet=function(){Dep.target=nullvari=this.deps.lengthwhile(i--){vardep=this.deps[i]if(!this.newDepIds.has(dep.id)){dep.removeSub(this)}}// 清除订阅完成,this.depIds和this.newDepIds交换后清空this.newDepIdsvartmp=this.depIdsthis.depIds=this.newDepIdsthis.newDepIds=tmpthis.newDepIds.clear()// 同上,清空数组tmp=this.depsthis.deps=this.newDepsthis.newDeps=tmpthis.newDeps.length=0}
// bind指令的指令构造对象exportdefault{
...
update(value){varattr=this.argconstel=this.elconstinterp=this.descriptor.interpif(this.modifiers.camel){// 将绑定的attribute名字转回驼峰命名,svg的属性绑定时可能会用到attr=camelize(attr)}// 对于value|checked|selected等attribute,不仅仅要setAttribute把dom上的attribute值修改了// 还要在el上修改el['value']/el['checked']等值为对应的值if(!interp&&attrWithPropsRE.test(attr)&&//attrWithPropsRE为/^(?:value|checked|selected|muted)$/attrinel){varattrValue=attr==='value'
? value==null// IE9 will set input.value to "null" for null...
? ''
: value
: valueif(el[attr]!==attrValue){el[attr]=attrValue}}// set model props// vue支持设置checkbox/radio/option等的true-value,false-value,value等设置,// 如<input type="radio" v-model="pick" v-bind:value="a">// 如果bind的是此类属性,那么则把value放到元素的对应的指定属性上,供v-model提取varmodelProp=modelProps[attr]if(!interp&&modelProp){el[modelProp]=value// update v-model if presentvarmodel=el.__v_modelif(model){// 如果这个元素绑定了一个model,那么就提示model,这个input组件value有更新model.listener()}}// do not set value attribute for textareaif(attr==='value'&&el.tagName==='TEXTAREA'){el.removeAttribute(attr)return}// update attribute// 如果是只接受true false 的"枚举型"的属性if(enumeratedAttrRE.test(attr)){// enumeratedAttrRE为/^(?:draggable|contenteditable|spellcheck)$/el.setAttribute(attr,value ? 'true' : 'false')}elseif(value!=null&&value!==false){if(attr==='class'){// handle edge case #1960:// class interpolation should not overwrite Vue transition classif(el.__v_trans){value+=' '+el.__v_trans.id+'-transition'}setClass(el,value)}elseif(xlinkRE.test(attr)){// /^xlink:/el.setAttributeNS(xlinkNS,attr,value===true ? '' : value)}else{//核心就是这里了el.setAttribute(attr,value===true ? '' : value)}}else{el.removeAttribute(attr)}}}
update中要处理的边界情况较多,但是核心还是比较简单的:el.setAttribute(attr, value === true ? '' : value),就是这么一句。
link
compile结束后就到了link阶段。前文说了所有的link函数都是被linkAndCapture包裹着执行的。那就先看看linkAndCapture:
linkAndCapture的作用很清晰:排序然后遍历执行_bind()。注释很清楚了。我们直接看link阶段。我们之前说了几种complie方法,但是他们的link都很相近,基本就是使用指令描述对象创建指令就完毕了。为了缓解你的好奇心,我们还是举个例子:看看compileDirective生成的link长啥样:
我们可以看到,这么一段link函数是很灵活的,他的5个参数
(vm, el, host, scope, frag)
对应着vm实例、dom分发的宿主环境(slot中的相关内容,大家先忽略)、v-for情况下的数组作用域scope、document fragment(包含el的那个fragment)。只要你传给我合适的参数,我就可以还给你一段响应式的dom。我们之前说的大数据量的v-for情况下,新dom(el)+ link+具体的数据(scope)实现就是基于此。回到link函数本身,其功能就是将指令描述符new为Directive实例,存放至this._directives数组。而Directive构造函数就是把传入的参数、指令构造函数的属性赋值到this上而已,整个构造函数就是this.xxx = xxx的模式,所以我们就不说它了。
关键在于linkAndCapture函数中在指令生成、排序之后执行了指令的_bind函数。
这个函数其实也很简单,主要先执行指令的bind方法(注意和_bind区分)。每个指令的bind和update方法都不相同,他们都是定义在各个指令自己的定义对象(def)上的,在_bind代码的开头将他们拷贝到实例上:extend(this, def)。然后就是new了watcher,然后将watcher计算得到的value update到界面上(
this.update(wtacher.value)
),此处用到的update即刚刚说的指令构造对象上的update。那我们先看看bind做了什么,每个指令的bind都是不一样的,大家可以随便找一个指令定义对象看看他的bind方法。如Vue官网所说:只调用一次,在指令第一次绑定到元素上时调用,bind方法大都很简单,例如v-on的bind阶段几乎什么都不做。我们此处随便举两个简单的例子吧:v-bind和v-text:
两个指令的bind函数都足够简单,v-text甚至只是根据当前是文本节点还是元素节点预先为update阶段设置好修改
data
还是textContent
。指令的bind阶段完成后_bind方法继续执行到创建Watcher。那我们又再去看看Watcher构造函数:
代码不难,首先我们又看到了熟悉的dep相关的属性,他们就是用来存放我们一开始在observe章节讲到的dep。在此处存放dep主要是依赖的属性值变动之后,我们需要清除原来的依赖,不再监听他的变化。
接下来代码对表达式执行parseExpression(expOrFn, this.twoWay),twoWay一般为false,我们先忽略他去看看parseExpression做了什么:
先计算你传入的表达式的get函数,isSimplePath(exp)用于判断你传入的表达式是否是“简单表达式”(见代码注释),因为Vue支持你在v-on等指令里写
v-on:click="a/=2"
等等这样的指令,也就是写一个statement,这样就明显不是"简单表达式"了。如果是简单表达式那很简单,直接makeGetterFn('scope.' + exp),比如v-bind:id="myId"
,就会得到function(scope){return scope.myId},这就是表达式的getter了。如果是非简单表达式比如a && b() || c()
那就会得到function(scope){return scope.a && scope.b() || scope.c()},相比上述结果就是在每个变量前增加了一个“scope.”
,这个操作是用正则表达式提取变量部分加上“scope.”
后完成的。后续的setter对应于twoWay指令中要将数据写回vm的情况,在此不表(此处分析path的过程就是@勾三股四大神那篇非常出名的博客里path解析状态机涉及的部分)。现在我们明白vue是怎么把一个表达式字符串变成一个可以计算的函数了。回到之前的Watcher构造函数代码,这个get函数存放在了this.getter属性上,然后进行了this.get(),开始进行我们期待已久的依赖收集部分和表达式求值部分!
这部分代码的原理,我在observe数据部分其实就已经完整的剧透了,watcher在计算getter之前先把自己公开放置到Dep.target上,然后执行getter,getter会依次触发各个响应式数据的getter,大家把这个watcher加入到自己的dep.subs数组中。完成依赖订阅,同时getter计算结束,也得到了表达式的值。
wait,watcher加入到dep.subs数组的过程中好像还有其他操作。我们回过头看看:响应式数据的getter被触发的函数里写了用
dep.depend()
来收集依赖:依赖收集的过程中,首先是判断是否已经处理过这个依赖:newDepIds中是否有这个dep的id了。然后再在depIds里判断。如果连depIds里都没有,说明之前就没有收集过这个依赖,依赖的订阅者里面也没有我这个Watcher。那么赶紧订阅这个依赖dep.addSub(this)。这个过程保证了这一轮的依赖都会被newDepIds准确记录,并且如果有此前没有订阅过的依赖,那么我需要订阅他。
因为并不只是这样的初始状态会用watcher.get去计算表达式的值。每一次我这个watcher被notify有数据变动时,也会去get一次,订阅新的依赖,依赖也会被收集到this.newDepIds里,收集完成后,我需要对比哪些旧依赖没有在this.newDepIds里,这些不再需要订阅的依赖,我需要把我从它的subs数组中移除,避免他更新后错误的notify我。
watcher构造完毕,成功收集依赖,并计算得到表达式的值。回到指令的_bind函数,最后一步:
this.update(watcher.value)
。这里执行的是指令构造对象的update方法。我们举个例子,看看v-bind函数的update[为便于理解,有改动]:
update中要处理的边界情况较多,但是核心还是比较简单的:
el.setAttribute(attr, value === true ? '' : value)
,就是这么一句。好了,现在整个link过程就完毕了,所有的指令都已建立了对应的watcher,而watcher也已订阅了数据变动。在_compile函数最后
replace(original, el)
后,就直接append到页面里了。将我们预定设计的内容呈现到dom里了。那最后我们来讲一讲如果数据有更新的话,是如何更新到dom里的。虽然具体的dom操作是执行指令的update函数,刚刚的这个例子也已经举例介绍了v-bind指令的update过程。但是在update前,Vue引入了批处理机制,来提升dom操作性能。所以我们来看看数据变动,依赖触发notify之后发生的事情。
The text was updated successfully, but these errors were encountered: