Vue源码详解之link函数

# Vue源码详解之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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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)
    )
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// 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)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 + ';')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
上次更新: 2022/7/6 上午11:51:19