Skip to content

Vue源码详细解析(四)--link函数 #4

@Ma63d

Description

@Ma63d

link

compile结束后就到了link阶段。前文说了所有的link函数都是被linkAndCapture包裹着执行的。那就先看看linkAndCapture:

// link函数的执行过程会生成新的Directive实例,push到_directives数组中 // 而这些_directives并没有建立对应的watcher,watcher也没有收集依赖, // 一切都还处于初始阶段,因此capture阶段需要找到这些新添加的directive, // 依次执行_bind,在_bind里会进行watcher生成,执行指令的bind和update,完成响应式构建 function linkAndCapture (linker, vm) { // 先记录下数组里原先有多少元素,他们都是已经执行过_bind的,我们只_bind新添加的directive var originalDirCount = vm._directives.length linker() // slice出新添加的指令们 var dirs = vm._directives.slice(originalDirCount) // 对指令进行优先级排序,使得后面指令的bind过程是按优先级从高到低进行的 dirs.sort(directiveComparator) for (var i = 0, l = dirs.length; i < l; i++) { dirs[i]._bind() } return dirs }

linkAndCapture的作用很清晰:排序然后遍历执行_bind()。注释很清楚了。我们直接看link阶段。我们之前说了几种complie方法,但是他们的link都很相近,基本就是使用指令描述对象创建指令就完毕了。为了缓解你的好奇心,我们还是举个例子:看看compileDirective生成的link长啥样:

// makeNodeLinkFn就是compileDirective最后执行并且return出去返回值的函数 // 它让link函数闭包住编译阶段生成好的指令描述对象(他们还不是Directive实例,虽然变量名叫做directives) function makeNodeLinkFn (directives) { return function nodeLinkFn (vm, el, host, scope, frag) { // reverse apply because it's sorted low to high var i = directives.length while (i--) { vm._bindDir(directives[i], el, host, scope, frag) } } } // 这就是vm._bindDir Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) { this._directives.push( new Directive(descriptor, this, node, host, scope, frag) ) }

我们可以看到,这么一段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函数。

Directive.prototype._bind = function () { var name = this.name var descriptor = this.descriptor // remove attribute if ( // 只要不是cloak指令那就从dom的attribute里移除 // 是cloak指令但是已经编译和link完成了的话,那也还是可以移除的 (name !== 'cloak' || this.vm._isCompiled) && this.el && this.el.removeAttribute ) { var attr = descriptor.attr || ('v-' + name) this.el.removeAttribute(attr) } // copy def properties // 不采用原型链继承,而是直接extend定义对象到this上,来扩展Directive实例 var def = descriptor.def if (typeof def === 'function') { this.update = def } else { extend(this, def) } // setup directive params // 获取指令的参数, 对于一些指令, 指令的元素上可能存在其他的attr来作为指令运行的参数 // 比如v-for指令,那么元素上的attr: track-by="..." 就是参数 // 比如组件指令,那么元素上可能写了transition-mode="out-in", 诸如此类 this._setupParams() // initial bind if (this.bind) { this.bind() } this._bound = true if (this.literal) { this.update && this.update(descriptor.raw) } else if ( // 下面这些判断是因为许多指令比如slot component之类的并不是响应式的, // 他们只需要在bind里处理好dom的分发和编译/link即可然后他们的使命就结束了,生成watcher和收集依赖等步骤根本没有 // 所以根本不用执行下面的处理 (this.expression || this.modifiers) && (this.update || this.twoWay) && !this._checkStatement() ) { // wrapped updater for context var dir = this if (this.update) { // 处理一下原本的update函数,加入lock判断 this._update = function (val, oldVal) { if (!dir._locked) { dir.update(val, oldVal) } } } else { this._update = noop } // 绑定好 预处理 和 后处理 函数的this,因为他们即将作为属性放入一个参数对象当中,不绑定的话this会变 var preProcess = this._preProcess ? bind(this._preProcess, this) : null var postProcess = this._postProcess ? bind(this._postProcess, this) : null var watcher = this._watcher = new Watcher( this.vm, this.expression, this._update, // callback { filters: this.filters, twoWay: this.twoWay,//twoWay指令和deep指令请参见官网自定义指令章节 deep: this.deep, //twoWay指令和deep指令请参见官网自定义指令章节 preProcess: preProcess, postProcess: postProcess, scope: this._scope } ) // v-model with inital inline value need to sync back to // model instead of update to DOM on init. They would // set the afterBind hook to indicate that. if (this.afterBind) { this.afterBind() } else if (this.update) { this.update(watcher.value) } } }

这个函数其实也很简单,主要先执行指令的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:

// v-bind指令的指令定义对象 [有删节] export default { ... bind () { var attr = this.arg var tag = this.el.tagName // handle interpolation bindings const descriptor = this.descriptor const tokens = descriptor.interp if (tokens) { // handle interpolations with one-time tokens if (descriptor.hasOneTime) { // 对于单次插值的情况 // 在tokensToExp内部使用$eval将表达式'a '+val+' c'转换为'"a " + "text" + " c"',以此结果为新表达式 // $eval过程中未设置Dep.target,因而不会订阅任何依赖, // 而后续Watcher.get在计算这个新的纯字符串表达式过程中虽然设置了target但必然不会触发任何getter,也不会订阅任何依赖 // 单次插值由此完成 this.expression = tokensToExp(tokens, this._scope || this.vm) } } }, .... } // v-text指令的执行定义对象 export default { bind () { this.attr = this.el.nodeType === 3 ? 'data' : 'textContent' }, update (value) { this.el[this.attr] = _toString(value) } }

两个指令的bind函数都足够简单,v-text甚至只是根据当前是文本节点还是元素节点预先为update阶段设置好修改data还是textContent

指令的bind阶段完成后_bind方法继续执行到创建Watcher。那我们又再去看看Watcher构造函数:

export default function Watcher (vm, expOrFn, cb, options) { // mix in options if (options) { extend(this, options) } var isFn = typeof expOrFn === 'function' this.vm = vm vm._watchers.push(this) this.expression = expOrFn // 把回调放在this上, 在完成了一轮的数据变动之后,在批处理最后阶段执行cb, cb一般是dom操作 this.cb = cb this.id = ++uid // uid for batching this.active = true // lazy watcher主要应用在计算属性里,我在注释版源码里进行了解释,这里大家先跳过 this.dirty = this.lazy // for lazy watchers // 用deps存储当前的依赖,而新一轮的依赖收集过程中收集到的依赖则会放到newDeps中 // 之所以要用一个新的数组存放新的依赖是因为当依赖变动之后, // 比如由依赖a和b变成依赖a和c // 那么需要把原先的依赖订阅清除掉,也就是从b的subs数组中移除当前watcher,因为我已经不想监听b的变动 // 所以我需要比对deps和newDeps,找出那些不再依赖的dep,然后dep.removeSub(当前watcher),这一步在afterGet中完成 this.deps = [] this.newDeps = [] // 这两个set是用来提升比对过程的效率,不用set的话,判断deps中的一个dep是否在newDeps中的复杂度是O(n) // 改用set来判断的话,就是O(1) this.depIds = new Set() this.newDepIds = new Set() this.prevError = null // for async error stacks // parse expression for getter/setter if (isFn) { // 对于计算属性而言就会进入这里,我们先忽略 this.getter = expOrFn this.setter = undefined } else { // 把expression解析为一个对象,对象的get/set属性存放了获取/设置的函数 // 比如hello解析的get函数为function(scope) {return scope.hello;} var res = parseExpression(expOrFn, this.twoWay) this.getter = res.get // 比如scope.a = {b: {c: 0}} 而expression为a.b.c // 执行res.set(scope, 123)能使scope.a变成{b: {c: 123}} this.setter = res.set } // 执行get(),既拿到表达式的值,又完成第一轮的依赖收集,使得watcher订阅到相关的依赖 // 如果是lazy则不在此处计算初值 this.value = this.lazy ? undefined : this.get() // state for avoiding false triggers for deep and Array // watchers during vm._digest() this.queued = this.shallow = false }

代码不难,首先我们又看到了熟悉的dep相关的属性,他们就是用来存放我们一开始在observe章节讲到的dep。在此处存放dep主要是依赖的属性值变动之后,我们需要清除原来的依赖,不再监听他的变化。

接下来代码对表达式执行parseExpression(expOrFn, this.twoWay),twoWay一般为false,我们先忽略他去看看parseExpression做了什么:

export function parseExpression (exp, needSet) { exp = exp.trim() // try cache // 缓存机制 var hit = expressionCache.get(exp) if (hit) { if (needSet && !hit.set) { hit.set = compileSetter(hit.exp) } return hit } var res = { exp: exp } res.get = isSimplePath(exp) && exp.indexOf('[') < 0 // optimized super simple getter ? makeGetterFn('scope.' + exp) // dynamic getter // 如果不是简单Path, 也就是语句了,那么就要对这个字符串做一些额外的处理了, // 主要是在变量前加上'scope.' : compileGetter(exp) if (needSet) { res.set = compileSetter(exp) } expressionCache.put(exp, res) return res } const pathTestRE = // pathTestRE太长了,其就是就是检测是否是a或者a['xxx']或者a.xx.xx.xx这种表达式  const literalValueRE = /^(?:true|false|null|undefined|Infinity|NaN)$/ function isSimplePath (exp) { // 检查是否是 a['b'] 或者 a.b.c 这样的 // 或者是true false null 这种字面量 // 再或者就是Math.max这样, // 对于a=true和a/=2和hello()这种就不是simple path return pathTestRE.test(exp) && // don't treat literal values as paths !literalValueRE.test(exp) && // Math constants e.g. Math.PI, Math.E etc. exp.slice(0, 5) !== 'Math.' } function makeGetterFn (body) { return new Function('scope', 'return ' + body + ';') }

先计算你传入的表达式的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(),开始进行我们期待已久的依赖收集部分和表达式求值部分!

Watcher.prototype.beforeGet = function () { Dep.target = this } Watcher.prototype.get = function () { this.beforeGet() // v-for情况下,this.scope有值,是对应的数组元素,其继承自this.vm var scope = this.scope || this.vm var value try { // 执行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() return value } // 新一轮的依赖收集后,依赖被收集到this.newDepIds和this.newDeps里 // this.deps存储的上一轮的的依赖此时将会被遍历, 找出其中不再依赖的dep,将自己从dep的subs列表中清除 // 不再订阅那些不依赖的dep Watcher.prototype.afterGet = function () { Dep.target = null var i = this.deps.length while (i--) { var dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // 清除订阅完成,this.depIds和this.newDepIds交换后清空this.newDepIds var tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() // 同上,清空数组 tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }

这部分代码的原理,我在observe数据部分其实就已经完整的剧透了,watcher在计算getter之前先把自己公开放置到Dep.target上,然后执行getter,getter会依次触发各个响应式数据的getter,大家把这个watcher加入到自己的dep.subs数组中。完成依赖订阅,同时getter计算结束,也得到了表达式的值。

wait,watcher加入到dep.subs数组的过程中好像还有其他操作。我们回过头看看:响应式数据的getter被触发的函数里写了用dep.depend()来收集依赖:

Dep.prototype.depend = function () { Dep.target.addDep(this) } // 实际执行的是watcher.addDep Watcher.prototype.addDep = function (dep) { var id = dep.id // 如果newDepIds里已经有了这个Dep的id, 说明这一轮的依赖收集过程已经完成过这个依赖的处理了 // 比如a + b + a这样的表达式,第二个a在get时就没必要在收集一次了 if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { // 如果连depIds里都没有,说明之前就没有收集过这个依赖,依赖的订阅者里面没有我这个Watcher, // 所以加进去 // 一般发生在有新依赖时,第一次依赖收集时当然会总是进入这里 dep.addSub(this) } } } 

依赖收集的过程中,首先是判断是否已经处理过这个依赖: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[为便于理解,有改动]:

// bind指令的指令构造对象 export default { ... update (value) { var attr = this.arg const el = this.el const interp = this.descriptor.interp if (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)$/ attr in el ) { var attrValue = attr === 'value' ? value == null // IE9 will set input.value to "null" for null... ? '' : value : value if (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提取 var modelProp = modelProps[attr] if (!interp && modelProp) { el[modelProp] = value // update v-model if present var model = el.__v_model if (model) { // 如果这个元素绑定了一个model,那么就提示model,这个input组件value有更新 model.listener() } } // do not set value attribute for textarea if (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') } else if (value != null && value !== false) { if (attr === 'class') { // handle edge case #1960: // class interpolation should not overwrite Vue transition class if (el.__v_trans) { value += ' ' + el.__v_trans.id + '-transition' } setClass(el, value) } else if (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过程就完毕了,所有的指令都已建立了对应的watcher,而watcher也已订阅了数据变动。在_compile函数最后replace(original, el)后,就直接append到页面里了。将我们预定设计的内容呈现到dom里了。

那最后我们来讲一讲如果数据有更新的话,是如何更新到dom里的。虽然具体的dom操作是执行指令的update函数,刚刚的这个例子也已经举例介绍了v-bind指令的update过程。但是在update前,Vue引入了批处理机制,来提升dom操作性能。所以我们来看看数据变动,依赖触发notify之后发生的事情。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions