Vue源码详细解析(四)--link函数
Ma63d opened this issue · comments
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之后发生的事情。