Vue:多角度剖析计算属性的运行机制 #219

欢迎 star评论Vue:多角度剖析计算属性的运行机制 #219vue

大纲

计算属性的初始化过程

在建立Vue实例时调用this._init初始化。react

其中就有调用initState初始化git

export function initState (vm: Component) {
  // ...
  if (opts.computed) initComputed(vm, opts.computed)
 	// ...
}
复制代码

initState会初始化计算属性:调用initComputedgithub

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // ...
  for (const key in computed) {
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    // ...
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // ...
    }
  }
}
复制代码

遍历computedtypescript

先建立计算属性的watcher实例,留意computedWatcherOptions这个option决定了计算属性的watcher和普通watcher的不一样express

而后定义计算属性的属性的getter和setterapi

  • 再来看看watcher的建立
export default class Watcher {
  // ...

  constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) {
    // ...
    if (options) {
    	// ...
    	this.lazy = !!options.lazy
      // ...
    }
    this.dirty = this.lazy // for lazy watchers
      
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  // ...
}
复制代码
  1. watcher.lazy = true;
  2. watcher.dirty = true;
  3. watcher.getter = typeof userDef === 'function' ? userDef : userDef.get
  4. 不会在构造函数内调用watcher.get()`(非计算属性的watcher/lazy watcher会在建立watcher实例时调用)
  • 再来看计算属性defineProperty的定义
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed ( target: any, key: string, userDef: Object | Function ) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // ...
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

shouldCache,浏览器渲染都是 shouldCache = true数组

那么gtter就是由createComputedGetter方法建立浏览器

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
复制代码

以上就是计算属性的的初始化过程。缓存

计算属性被访问时的运行机制

如上,假设计算属性当前被调用

就是触发计算属性的getter,再次强调:计算属性的getter不是用户定义的回调,而是由createComputedGetter返回的函数(详细参考计算属性的初始化过程的最后一段代码)。 用户定义的回调则是在计算属性getter的逻辑中进行调用。

计算属性getter中主要由两个if控制流, 这个两个if组合起来就可能由四种可能, 对于第二个控制流的逻辑watcher.depend,若是有看到Vue的Dep的功能的话,能够推测这段代码是用于收集依赖, 结合以上能够以下推测:

序号 if (watcher.dirty) if (Dep.target) 功能
1 N N 返回旧值
2 N Y 收集依赖
3 Y N 更新计算属性值(watcher.value)
4 Y Y 收集依赖,并更新计算属性值(watcher.value)

目前掌握的信息有:

  1. 计算属性的getter是核心功能就是获取计算属性的值,而getter返回的是watcher.value,说明计算属性的值保存在watcher.value
  2. evaluate多是用于更新watcher.value;
  3. watcher.depend多是用于收集依赖,不清楚收集什么;

咱们先来看第一个控制流:

// watcher.dirty = true
if (watcher.dirty) {
  watcher.evaluate()
}
复制代码

根据计算属性的初始化过程中建立计算属性watcher实例时就能够看出,第一次调用watcher.dirty确定是true

但不论watcher.dirty是否是“真”,咱们都要去看看“evaluate ”时何方神圣,并且确定会有访问它的时候。

evaluate () {
  this.value = this.get()
  this.dirty = false
}
复制代码

显然,evaluate确实是用于更新计算属性值(watcher.value)的。

另外,你能够发如今this.value = this.get()执行完后,还执行了一句代码:this.dirty = false

而后你会发现一个逻辑:

  1. 初始化计算属性时,watcher.dirty = false;
  2. 执行evaluate更新后,watcher.dirty = true;
  3. watcher.dirty = true时不会去更新计算属性的值。

一切说明计算属性是懒加载的,在访问时根据状态值来判断使用缓存数据仍是从新计算。

再者,咱们还能够再总结一下dirty和lazy的信息:

对比普通的watcher实例建立:

构造函数中的逻辑

normal computed
this.value = this.get() this.value = undefined
this.lazy = false this.lazy = true
this.dirty = false this.dirty = true

综上,能够看出 lazy的意思

  • 实例化时调用get就是非lazy

  • 非实例化时调用get就是lazy

dirty(脏值)的意思

  • watcher.value仍是undefined(或者还不是当前真是)就是dirty
  • watcher.value已经存有当前计算的实际值就不是dirty

lazy属性只是一个说明性的标志位,主要用来代表当前watcher是惰性模式的。 而dirty则是对lazy的实现,做为状态为表示当前是否是脏值状态。

再来看看watcher.get()的调用,其内部的动做

import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
  // ...
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  // ...
}
复制代码

在get()函数开头的地方调用pushTarget函数,为了接下来的内容,有必要先说明下pushTarget和结尾处的popTarget,根据字面意思就知道是对什么进行入栈出栈。

你能够看到是该方法来自于dep,具体函数实现以下:

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
复制代码

显然,pushTarget和popTarget操做的对象是Watcher,存放在全局变量targetStack中。每次出栈入栈都会更新Dep.target的值,而它值由上可知是targetStack的栈顶元素。

如今就知道pushTarget(this)的意思是:将当前的watcher入栈,并设置Dep.Target为当前watcher。

而后就是执行:

value = this.getter.call(vm, vm)
复制代码

计算属性watcher的getter是什么?

watcher.getter = typeof userDef === 'function' ? userDef : userDef.get
复制代码

是用户定义的回调函数,计算属性的回调函数。 回顾这一节开头的结论:

用户定义的回调则是在计算属性getter的逻辑中进行调用。

到此,咱们就能够清晰知道:用户定义的getter是在计算属性的getter中的computedWatcher.evaluate()中的computedWatcher.value = computedWatcher.get()中调用!

调用完getter算是完事没有呢?没有,这里还有一层隐藏的逻辑!

咱们知道通常计算属性都依赖于$data的属性,而调用计算属性的回调函数就会访问这些属性,就会触发这些属性的getter。

这些基础属性的getter就是隐藏的逻辑,若是你有看过基础属性的数据劫持就知道他们的getter都是有收集依赖的逻辑。

这些基本属性的getter都是在数据劫持的时候定义的,咱们去看看会发生什么!

Object.defineProperty(obj, key, {
  // ...
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      // ...
    }
    return value
  },
  // ...
}
复制代码

记得刚刚调用了pushTarget吧,如今Dep.target已经不为空,而且Dep.target就是当前计算属性的watcher。

则会执行dep.depend(),dep是每一个$data属性关联的(经过闭包关联)。

dep是依赖收集器,收集watcher,用一个数组(dep.subs)存放watcher,

而执行dep.depend(),除了执行其余逻辑,里面还有一个关键逻辑就是将Dep.targetpush到当前属性关联的dep.subs,言外之意就是,计算属性的访问在条件适合的状况下是会让计算属性所依赖的属性收集它的wathcer,而这个收集操做的做用且听下回分解。

小结

  1. 计算watcher.value:computed-watcher.evaluate(),访问计算属性时,若当前计算属性是脏值状态则调用evaluate计算计算属性的真实值;
  2. 在计算计算属性真实值时,合乎条件下会触发它依赖的基础属性收集它的watcher。

计算属性的更新机制

如何通知变更

计算属性所依赖属性的dep收集computed-watcher的意义何在呢?

假如如今更新计算属性依赖的任一个属性,会发生什么?

更新依赖的属性,固然是触发对应属性的setter,首先来看看基础属性setter的定义。

Object.defineProperty(obj, key, {
  // ...
  set: function reactiveSetter (newVal) {
    // ...
    dep.notify()
  }
})
复制代码

首先是在setter里面调用dep.notify(),通知变更。dep固然就是与属性关联的依赖收集器,notfiy必然是去通知订阅者它们订阅的数据之一已经发生变更。

export default class Dep {
  // ...

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

在notify方法里面能够看出,遍历了当前收集里面全部(订阅者)watcher,而且调用了他们的update方法。

计算属性被访问时的运行机制已经知道,计算属性的watcher是会被它所依赖属性的dep收集的。所以,notify中的subs确定也包含了计算属性的watcher。

因此,计算属性所依赖属性变更是经过调用计算属性watcher的update方法通知计算属性的。

接下来,在深刻去看看watcher.update是怎么更新计算属性的。

export default class Watcher {
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}
复制代码

计算属性被访问时的运行机制中就知道,计算属性watcher是lazy的,因此,comuptedWatcher.update的对应逻辑就是下面这一句:

this.dirty = true
复制代码

再回想一下计算属性被访问时的运行机制中计算属性getter调用evalute()的控制流逻辑(if(watcher.dirty)),这下计算属性的访问和他的被动更新就造成闭环!

每次变化通知都是只更新脏值状态,真是计算仍是访问的时候再计算

计算属性如何被更新

从上面咱们就知道通知计算属性“变化”是不会直接引起计算属性的更新!

那么问题就来了,现实咱们看到的是:绑定的视图上的计算属性的值,只要它所依赖的属性值更新,会直接响应到视图上。

那就说明在通知完以后,当即访问了计算属性,引发了计算属性值的更新,而且更新了视图。

对于,不是绑定在视图上的计算属性很好理解,毕竟咱们也是在有须要的时候才会去访问他,至关于即时计算了(假如是脏值),所以不管是不是即时更新都无所谓,只要在访问时能够拿到最新的实际值就好。

可是对于视图却不同,要即时反映出来,因此确定是还有更新视图这一步的,咱们如今须要作的测试找出vue是怎么作的。

其实假如你有去看过vue数据劫持的逻辑就知道:在访问属性时,只要当前的Dep.target(订阅者的引用)不为空,与这个属性关联的dep就会收集这个订阅者

这个订阅者之一是“render-watcher”,它是视图对应的watcher,只要在视图上绑定了的属性都会收集这个render-watcher,因此每一个属性的dep.subs都有一个render-watcher。

没错,就是这个render-watcher完成了对计算属性的访问与视图的更新。

到这里咱们就能够小结一下计算属性对所依赖属性的响应机制: 所依赖属性更新,会通知该属性收集的全部watcher,调用update方法,其中就包含计算属性的watcher(computed-watcher),若是计算属性绑定在视图上,则还包含render-watcher,computed-watcher负责更新计算属性的脏值状态,render-watcher负责更新访问计算属性和更新视图。

可是这里又引出了一个问题!

假设如今计算属性就绑定在视图上,那么如今计算属性响应更新就须要两个watcher,分别是computed-watcher和render-watcher。

你细心点就会发现,要达到预期的效果,对这两个watcher.update()的调用顺序是有要求的!

必需要先调用computed-watcher.update()更新脏值状态,而后再调用render-watcher.update()去访问计算属性,才会去从新算计算属性的值,否者只会直接缓存的值watcher.value。

好比说有模板是

<span>{{ attr }}<span>
<span>{{ computed }}<span>
复制代码

attr的dep.subs中的watcher顺序就是

状况1:

[render-watcher, computed-watcher]
复制代码

反之就是

状况2:

[computed-watcher, render-watcher]
复制代码

咱们知道deo.notify的逻辑遍历调用subs里面的每一个watcher.update

假如这个遍历的顺序是按照subs数组的顺序来更新的话,状况1就会有问题

状况1

是先触发视图watcher的更新,他会更新视图上全部绑定的属性,不论属性有没有更新过

然而此时computed-watcher的属性dirty 仍是 false,这意味这着这个计算属性不会从新计算,而是使用已有的挂在watcher.value的旧值。

若是真是如此,以后在调用computred-watcher的update也没有意义了,除非从新调用render-watcher的update方法。

很明显,vue不可能那么蠢,确定会作控制更新顺序的逻辑

咱们看看notify方法的逻辑:

notify (key) {
  const subs = this.subs.slice()
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort((a, b) => a.id - b.id)
  }
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
复制代码

你能够看到控制流里面确实作了顺序控制

可是process.env.NODE_ENV !== 'production' && !config.async 的输出是false呢

很直观,在生成环境就进不了这个环境!

然而,现实表现出来的结果是,就算没有进入这个控制流里面,视图仍是正确更新了

更使人惊异的是:更新的遍历顺序确实是按着[render-watcher, computed-watcher]进行的

image

你能够看到是先遍历了render-watcher(render-watcher的id确定是最大的,越日后建立的watcher的id越大,计算属性是在渲染前建立,而render-watcher则是在渲染时)

可是若是你细心的话你能够发现,render-watcher更新回调是在遍历完全部的watcher以后才执行的(白色框)

image

咱们再来看看watcher.update的内部逻辑

update () {
  /* istanbul ignore else */
  console.log(
    'watcher.id:', this.id
  );
  if (this.lazy) {
    this.dirty = true
    console.log(`update with lazy`)
  } else if (this.sync) {
    console.log(`update with sync`)
    this.run()
  } else {
    console.log(`update with queueWatcher`)
    queueWatcher(this)
  }
  console.log(
    'update finish',
    this.lazy ? `this.dirty = ${this.dirty}` : ''
  )
}
复制代码

根据打印的信息,能够看到render-watcher进入了else的逻辑,调用queueWatcher(this)

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    console.log('queueWatcher:', queue)
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

根据函数名,能够知道是个watcher的队列

has是一个用于判断待处理watcher是否存在于队列中,而且在队中的每一个watcher处理完都会将当前has[watcher.id] = null

flushing这个变量是一个标记:是否正在处理队列

if (!flushing) {
  queue.push(watcher)
} else {
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}
复制代码

以上是不一样的将待处理watcher推入队列的方式。

而后接下来的逻辑,才是处理watcher队列

waittingflushing这两个标志标量大体相同,他们都会在watcher队列处理完以后重置为false

而不一样的是waitting在最开始就会置为true,而flushing则是在调用flushSchedulerQueue函数的时候才会置为true

nextTick(flushSchedulerQueue)
复制代码

这一句是关键,nextTick,能够理解为一个微任务,即会在主线程任务调用完毕以后才会执行回调,

此时回调便是flushSchedulerQueue

关于nextTick能够参考Vue:深刻nextTick的实现

这样就能够解析:

更使人惊异的是:更新的遍历顺序确实是按着[render-watcher, computed-watcher]进行的

可是若是你细心的话你能够发现,render-watcher更新回调是在遍历完全部的watcher以后才执行的(白色框)

小结

  • 经过遍历调用dep.subs里的watcher.update方法(其中就包含computed-watcher)来通知计算属性基础属性已经更新,在下次访问计算属性时就是作脏值检测,而后从新计算计算属性。绑定在视图上的计算属性的即时更新是经过调用render-watcher的update方法达到,它会访问计算属性,并更新整个视图。
  • 绑定在视图上的计算属性,它所依赖属性的dep.subs中,computed-watcher和render-watcher的顺序不会影响计算属性在视图上的正常更新,由于render-watcher的update方法的主体逻辑是放在微任务中执行,所以render-watcher.update()老是会在computed-watcher.update()以后执行。

计算属性如何收集依赖

计算属性的更新机制中咱们知道了计算属性所依赖属性的dep是会收集computed-watcher的,目的是为了通知计算属性当前依赖的属性已经发生变化。

那么计算属性为何要收集依赖?是如何收集依赖的?

“计算属性所依赖属性的dep具体怎么收集computed-watcher”并无展开详细说。如今咱们来详细看看这部分逻辑。那就必然要从第一次访问计算属性开始, 第一次访问必然会调用watcher.evaluate去算计算属性的值,那就是必然会调用computed-watcher.get(),而后在get方法里面去调用用户定义的回调函数,算计算属性的值,调用用户定义的回调函数就必然会访问计算属性所依赖属性,那就必然触发他们的getter,没错咱们就是要从这里开始看详细的逻辑,也是从这里开始收集依赖:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      // ...
    }
    return value
  },
  // ...
}
复制代码

计算属性依赖的属性经过dep.depend()收集computed-watcher,展开dep.depend()看看详细逻辑:

// # dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
复制代码

很显然如今的全局watcher就是computed-watcher,而this则是当前计算属性所依赖属性的dep(下面简称:prop-dep),继续展开computed-watcher.addDep(prop-dep)

// # watcher.js
addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}
复制代码

在dep收集watcher的以前(dep.addSub(this)),watcher也在收集dep。

`this.newDeps.push(dep)`
复制代码

watcher收集dep就是接下来咱们要说的点之一!

另外,上面的代码中还包含了以前没见过的三个变量this.newDepIdsthis.newDepsthis.depIds

先看看他们的声明:

export default class Watcher {
  // ...
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
	// ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // ...
  }
复制代码

depIdsnewDepIds都是Set的数据结构,结合if (!this.newDepIds.has(id))!this.depIds.has(id)就能够推断他们的功能是防止重复操做的。

到此,咱们知道了计算属性是如何收集依赖的!而且,从上面知道了所收集的依赖是不重复的。

可是,到这里尚未结束!

这个newDeps并非最终存放存放点,真实的dep存放点是deps,在上面声明你就能够看见它。

在调用computed-watcher.get()的过程当中还有一个比较关键的方法没有给出:

get () {
  // ...
  // 在最后调用
  this.cleanupDeps()
}
复制代码

形如其名,就是用来清除dep的,清除newDeps,而且转移newDeps到Deps上。

cleanupDeps () {
  let i = this.deps.length
  // 遍历deps,对比newDeps,看看哪些dep已经没有被当前的watcher收集
  // 若是没有,一样也解除dep对当前watcher的收集
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  // 转存newDepIds到depIds
  let tmp = this.depIds
  this.depIds = this.newDepIds
  this.newDepIds = tmp
  this.newDepIds.clear()
  
  // 转存newDeps到Deps
  tmp = this.deps
  this.deps = this.newDeps
  this.newDeps = tmp
  this.newDeps.length = 0
}
复制代码

下面是执行完computed-watcher.get()后的打印信息:

从上面的分析咱们能够知道:计算属性的watcher会在计算值(watcher.evalute())时,收集每一个它依赖属性的dep,并最后存放在watcher.deps

接下来再来探究计算属性为何要收集依赖

还记得计算属性的getter中的另外一个控制流,一直没有展开细说。

if (Dep.target) {
  watcher.depend()
}
复制代码

从这段代码能够知道,只有全局watcher(Dep.target)不为空,才会执行watcher.depend(),这就是要想的第一个问题:什么状况下全局watcher是不为空?

首先来确认下全局watcher的update机制:

  • pushTarget和popTarget是成对出现的;
  • 只有在watcher.get方法中才会入栈非空的watcher;
  • 在执行watcher.get的开头pushTarget(this),在结尾popTarget(),意味着在get方法调用完成后,全局watcher就变回调用get方法前的全局watcher。

还记得computed的getter的逻辑吧!

if (watcher.dirty) {
  watcher.evaluate()
}
if (Dep.target) {
  watcher.depend()
}
复制代码

在脏值状态下会执行watcher.evaluate(),执行完已经完成watcher.get()的调用,因此watcher.evaluate不会影响到下面的if (Dep.target)判断。

pushTarget和popTarget是成对出现的,显然只有在调用完pushTarget后,且未调用popTarget这个时间段内调用计算属性才会执行watcher.depend()。另外,只有watcher.get()才会入栈非空的watcher,因此咱们就能够再次缩小范围到:在调用watcher.get()的过程当中访问了计算属性

记得在计算属性被访问时的运行机制中有用表格对比过新建普通watcher和计算属性watcher实例的异同,其中普通watcher的建立就会在实例化的时候调用this.get()

此刻让我想到了render-watcher,它就是一个普通的watcher,并且render-watcher是会访问绑定在视图上的所用属性,并且它访问视图上属性的过程就是在get方法里面的getter的调用中。

get () {
  // 那么全局watcer就是render-watcher了
  pushTarget(this)
  // ...
  try {
    // 视图上的全部属性都在getter方法被访问,包括计算属性
    value = this.getter.call(vm, vm)
  } catch (e) {
    // ...
  } finally {
    // ...
    popTarget()
    this.cleanupDeps()
  }
  return value
}
复制代码

接下展开watcher.depend看看:

depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}
复制代码

已经很明了,上面已经说过this.deps是计算属性收集的dep(它所依赖的dep),而后如今遍历deps,调用dep.depend(),上面也一样已经说过dep.depend()的功能是收集全局watcher。

因此,watcher.depend()的功能就是让计算属性收集的deps去收集当前的全局watcher。 而如今的全局watcher就是render-watcher!

如今咱们知道watcher.depend的功能是让prop-dep去收集全局watcher,可是为何要这么作? 不放将问题细化到render-watcher的场景上。为何prop-watcher要去收集render-watcher?

首先,我要再次强调:一个绑定在视图上的计算属性要即时响应所依赖属性的更新,那么这些依赖属性的dep.subs就必须包含computed-watcherrender-watcher,前者是用来更新计算属性的脏值状态,后者用来访问计算属性,让计算属性从新计算。并更新视图。

计算属性所依赖属性的dep.subs中确定会包含computed-watcher,这一点不须要质疑,上面已经证实分析过!

可是,是否会包含render-watcher就不必定了!首先上面也有间接地提过,绑定在视图上的属性,它的dep会收集到render-watcher。那么,计算属性所依赖的属性,有可能存在一些是没有绑定在视图上,而是直接定义在data上而已,对于这些属性,它的dep.subs是确定没有render-watcher的了。没有render-watcher意味着没有更新视图的能力。那么怎么办?那固然就是去保证它!

watcher.depend()就起到了这个做用!它让计算属性所依赖的属性

对于这个推测

绑定在视图上的属性,它的dep会收集到render-watcher

咱们能够探讨一下。

要一个vue.$data属性的dep去收集dep.subs没有的watcher须要具有两个条件:

  • 访问这个属性;
  • 全局watcher(Dep.target)不为空;

而没有绑定在视图上的属性,在render-watcher.get()调用的过程当中就没有访问,没有访问就不会调用dep.depend()去收集render-watcher!

可能有人会问,在访问计算属性的时候不是有调用用户定义的回调吗?不就访问了这些依赖的属性?

是!确实是访问了,那个时候的Dep.target是computed-watcher。

ok,render-watcher这个场景也差很少了。咱们该抽离表象看本质!

首先想一想属性dep为何要收集依赖(订阅者),由于有函数依赖了这个属性,但愿这个属性在更新的时候通知订阅者。能够以此类比一下计算属性,计算属性的deps为何须要收集依赖(订阅者),是否是也是由于有函数依赖了计算属性,但愿计算属性在更新时通知订阅者,在想深一层:怎么样才算是计算属性更新?不就是它所依赖的属性发生变更吗?计算属性所依赖属性更新 = 计算属性更新,计算属性更新就要通知依赖他的订阅者!再想一想,计算属性所依赖属性更新就能够直接通知依赖计算属性的订阅者了,那么计算属性所依赖属性的dep直接收集依赖计算属性的订阅者就行了!这不就是watcher.depend()在作的事情吗?!

本质咱们知道了,可是怎么才能够实现依赖计算属性!

首先全局watcher不为空! 怎么才会让Dep.target不为空!只有一个方法:调用watcher.get(),在vue里面只有这个方法会入栈非空的watcher,另外咱们知道pushTarget和popTarget是成对出现的,即要在未调用popTarget前访问计算属性,怎么访问呢?pushTarget和popTarget分别在get方法的一头一尾,中间能够用户定义的只有一个地方!

get () {
  pushTarget(this)
  // ...
  value = this.getter.call(vm, vm)
  // ...
  popTarget()
}
复制代码

就是getter,getter是能够由用户定义的~

再来getter具体存储的是什么

export default class Watcher {
  // ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        // ...
      }
    }
    // ...
  }
复制代码

由上能够知道,一个有效的getter是有expOrFn决定,expOrFn若是是Function则getter就是用户传入的函数!若是是String则由parsePath进行构造:

// 返回一个访问vm属性(包含计算属性)的函数
export function parsePath (path: string): any {
  // 判断是不是一个有效的访问vm属性的路径
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
复制代码

由上可知,咱们有两种手段可让getter访问计算属性: 而且在此我不作说明,直接说结论,watch一个属性(包含计算属性),包括使用$watch都是会建立一个watcher实例的,并且是普通的watcher,即会在构造函数直接调用watcher.get()

  • 直接watch计算属性
const vm = new Vue({
  data: {
    name: 'isaac'
  },
  computed: {
    msg() { return this.name; }
  }
  watch: {
    msg(val) {
      console.log(`this is computed property ${val}`);
    }
  }
}).$mount('#app');
复制代码

这种方法就是在建立实例时传进了一个路径,这个路径就是msg,即expOrFn是String,而后由parsePath构造getter,从而访问到计算属性。

vm.$watch(function() {
  return this.msg;
}, function(val) {
  console.log(`this is computed property ${val}`);
});
复制代码

这种方法直接就传入一个函数,即expOrFn是Function,就是$watch的第一个参数!一样在getter中访问了计算属性。

上面两种都是在getter中访问了计算属性,从而让deps收集订阅者,计算属性的变更(固然并不是真的更新了值,只是进入脏值状态)就会通知依赖他的订阅者,调用watcher.update(),若是没有传入什么特殊的参数,就会调用watch的回调函数,若是在回调函数中有访问计算属性就会从新计算计算属性,更新状态为非脏值!

小结

  • 计算属性所依赖的属性的dep会收集computed-watcher,存放在prop-dep.subs中;
  • computed-watcher也会收集它所依赖的dep,存放在computed-watcher.deps中,为了确保计算属性得到通知依赖他的订阅者能够监听到他的变化,经过watcher.depend()来收集依赖它的订阅者。

总结

  • 计算属性在initState阶段初始化;
  • 计算属性也是会使用defineProperty进行计算属性劫持;
  • 每一个计算属性都会关联一个特殊的watcher(lazy)。存放在一个对象中,以计算属性的名字做为键值,挂载在vm.computedWatchers
  • 经过让计算属性所依赖属性的dep收集计算属性watcher的行为实现“依赖属性的变更通知计算属性”;
  • 计算属性的watcher是lazy的,不会在建立实例时计算自身的值(即不会调用watcher.get());
  • 计算属性是lazy的,调用计算属性的watcher.update不会直接计算值,只是更新标志位(this.dirty = true),直到计算属性被访问才会计算值;
  • dep(依赖收集器)会收集watcher(订阅者),watcher也会收集dep;
  • 计算属性经过watcher.value对其值进行缓存,不会每次访问都重新计算;
  • 计算属性经过watcher.depend()来收集依赖它的订阅者。
相关文章
相关标签/搜索