Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Ma63d opened this issue Feb 27, 2017 · 0 comments
Open

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

Ma63d opened this issue Feb 27, 2017 · 0 comments

Comments

@Ma63d
Copy link
Owner

Ma63d commented Feb 27, 2017

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之后发生的事情。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant