VUE源码学习第十一篇--响应式原理(订阅)

一、总述

     前一章节,通过对prop,data所定义的属性建立观察类和发布器,但此时的发布类Dep中sub空空如也,如何实现watcher的注册,并在属性发生变化时,实现更新。本章节将继续介绍订阅Watcher。

在介绍前,我们先思考下,用户定义的prop,data在哪些方法或者表达式是需要实现响应式变化的?

首先用户自定义的watch,computed是需要的,另外我们的视图也会用到大量的属性表达式(如前面的实例{{item.id}}),也是需要的。method中虽然用到了这些数据,但是都是及时调用,所以不需要的。

事实上,源码中也是在这三个场景创建watcher,如果阅读过我前面的博客,在initWatcher,initComputer(第五篇),以及mount(第六篇)简单的介绍过,做过一些知识的铺垫。

这三种watcher分别为user watcher,compute watcher,以及render watcher,本篇就结合前面的知识,详细介绍这几种watcher实现原理。

二、user watcher

我们以下面的的watch为运行实例:

 data:{
      msg:'this is msg',
      items:[
      {id:1},
      {id:2},
      {id:3}
      ]
    }
watch: {
    msg: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    ....
}

回顾下第五篇initWatcher的创建watcher过程,如下:

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    ...
    //创建watcher对象,进行监听
    const watcher = new Watcher(vm, expOrFn, cb, options)
    ...
  }

其中主要入参,vm,即vue对象;expOrFn,待观察的表达式,即本例中属性msg对象;cb,回调函数,即本例中msg属性的对应的方法。watcher的定义位于src/core/observer/watcher.js。

  export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,//组件对象
    expOrFn: string | Function,//待观察的表达式
    cb: Function,//回调函数,更新的时候调用
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    //1、初始化变量
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    //2、解析表达式,获取getter方法
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        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
        )
      }
    }
    //3、依赖收集
    if (this.computed) {
      //对于计算属性watcher,此时没有立即进行依赖收集,在执行render函数时收集
      this.value = undefined
      this.dep = new Dep()
    } else {
      //对于普通watcher,调用get方法,进行依赖收集
      this.value = this.get()
    }
  }
...
}

1、构造方法

我们先看下其构造方法。主要过程包括:

(1)、初始化变量。主要变量如下:

deep,表示是否要进行深度监听,即属性嵌套的变量进行监听。

computed,表示是否是计算属性。

sync,表示更新时,是否需要同步执行。

deps,newDeps,该watcher对应的维护的发布器数组。

(2)、解析表达式,获取getter方法,如果expOrFn是function类型,则直接设置为getter方法,否则调用parsePath方法返回属性对象作为getter方法,本例中则使用后一种,执行结果为vm[msg]。

export function parsePath (path: string): any {
  ...
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

(3)、依赖收集,这部分处理的很巧妙,对于非计算属性,直接调用了get方法(对于计算属性的watcher我们等会在表)。继续看下get方法。

2、get方法

 get () {
    //1、将当前的watcher压栈
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //2、核心代码,依赖收集
      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
      //3、后置处理
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

(1)将自身的watcher对象压入栈,设置全局的变量Dep.target为当前的watcher对象。

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

(2)执行getter方法,触发该属性的get劫持,我们回顾前一章节定义的劫持方法。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //依赖收集
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }
...

全局变量Dep.target表示的就是当前的watcher对象,非空,继续调用Dep类的depend方法

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

最终又回调了watcher中的addDep方法,

addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      //加入到watcher的dep数组
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        //加入到dep的sub数组
        dep.addSub(this)
      }
    }
  }

该方法,首先将dep保存到watcher的newDeps数组中,然后调用Dep的addSub,将watcher对象加入到sub数组中。

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

整个调用过程比较绕,这样做的目的,就是为了建立dep和watcher间的双向链表。

(3)后置处理,包括deep处理,清除当前的watcher对象等。

整个过程完成后,msg属性的Dep对象的sub中就添加了watcher对象。msg的依赖模型如下:

三、compute watcher

以下面的计算属性computeMsg为运行实例:

<div id="app">
   {{computeMsg}}
  ...
</div>

var vm = new Vue({
data:{
      msg:'this is msg',
      items:[
      {id:1},
      {id:2},
      {id:3}
      ]
    }
watch: {
    msg: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
computed:{
    computeMsg:function(){
      return "this is computed msg:"+this.msg
    }
  }
    ....
}

   该实例中,创建computeMsg计算属性,并在模板(template)中使用。computeMsg计算属性表达式中调用了msg属性。

我们来回顾下第五章节的initComputed方法。

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
  //1、循环计算属性
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    ...

    if (!isSSR) {
      // create internal watcher for the computed property.
      //2、为每个属性创建watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
     
    ...
  //3、劫持数据变化,创建监听方法
    defineComputed(vm, key, userDef)
  }
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
 const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    ....
  }
  //4、并对计算属性的getter和setter进行劫持
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
}

//getter方法劫持
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      //依赖收集,将订阅类添加到
      watcher.depend()
      //返回值
      return watcher.evaluate()
    }
  }
}

在该方法中,循环所定义的计算属性,为每个计算属性创建watcher,并设置getter方法,监听数据变化(计算属性setter方法用的较少)。watcher的主要入参:vm,即vue对象;getter,计算属性的表达式,即本例中computeMsg对应的表达式。继续看下watcher的构造方法。

1、构造函数

constructor (
    vm: Component,//组件对象
    expOrFn: string | Function,//待观察的表达式
    cb: Function,//回调函数
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    ...
    // parse expression for getter
    //1、对于计算属性,表达式即为getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        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
        )
      }
    }
    
    if (this.computed) {
      //2、对于计算属性,创建了dep,此时没有立即进行依赖收集,
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }

watcher的入参,vm表示vue对象,getter为待观察的表达式,即计算属性函数。

与user watcher相比,有两个不同的地方。

(1)、由于expOrFn 是计算属性的表达式,类型是function,所以走一个分支,将getter设置为计算属性表达式。

(2)、并没有调用get方法进行依赖收集,只是创建了dep对象,该对象保存依赖该计算属性的watcher。

计算属性的依赖关系不同于普通的属性,它即依赖于其表达式包含的普通属性,比如说本例中的属性msg,同时又被其他调用者依赖,如本例中调用computeMsg的模板表达式({{computeMsg}})。本例的依赖模型如下:

那么问题来了,既然这些依赖关系没有在定义的时候进行收集,那是什么时候做的呢?答案就是在模板调用的时候。

计算属性的设计初衷是简化模板的表达式,避免太多的逻辑导致模板的复杂。所以,只有在模板调用的情况下才会触发依赖,如果只定义不调用,进行依赖就会造成浪费。

下面我们来介绍与模板render相关的watcher(render watcher),并看下如何完成红色标注的依赖收集。

四、render watcher

我们先回顾下第六章节介绍挂载时的mountComponent方法。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ....
   //2、定义updateComponent,vm._render将render表达式转化为vnode,vm._update将vnode渲染成实际的dom节点
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
   //3、首次渲染,并监听数据变化,并实现dom的更新
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ....
}

当时我们说这个watcher有两个作用,1、实现首次dom渲染,并完成依赖收集。2、监听模板所包含表达式的变化,实现update。

该watcher是数据"render"模板的"桥梁",称之为render watcher,其核心部分体现在入参expression(第二个参数,即updateComponent)。我们看下该watcher的初始化的主要过程:

constructor (
    vm: Component,//组件对象
    expOrFn  : string | Function,//待观察的表达式
    cb: Function,//回调函数
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
   ....
    if (typeof expOrFn === 'function') {//1、设置getter方法为表达式
      this.getter = expOrFn
    } else {
      .....
    }
    
    if (this.computed) {
     ....
    } else {
      //2、执行get方法,进行依赖收集
      this.value = this.get()
    }
  }

1、expOrFn为function,所以设置getter方法为表达式,即为updateComponent方法。

2、由于不是计算属性,与user watcher一样,执行get方法,实际就是执行updateComponent方法。

updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

该方法包含vm._render(),vm._update两个执行阶段,我们在第六章重点分析过,我们不再做详细介绍。今天我们重点回答前面提出的问题,如何实现依赖关系的收集。

以上面的计算属性为例,在模板中,我们使用了"{{computeMsg}}"。

<div id="app">
   {{computeMsg}}
  ...
</div>

该模板经过编译(参见前面的编译部分)后,该部分的render表达式"_s(computeMsg)",在执行vm._render时,就会触发computeMsg所设置的getter方法。

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      //1、依赖收集,将订阅类添加到dep中
      watcher.depend()
      //2、返回值
      return watcher.evaluate()
    }
  }
}

1、调用watcher.depend,将当前的render watcher对象添加到compteMsg的dep中,完成依赖关系的收集。

 depend () {
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }

注意这里的Dep.target指的是render watcher。依赖模型如下:

2、调用watcher.evaluate()。

  evaluate () {
    //如果有更新,则重新计算,否则返回缓存值
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

这是this指的是compute watcher。其get方法就是属性表达式。

function(){
      return "this is computed msg:"+this.msg
    }

由于表达式中包含了msg属性,在执行过程中,又触发msg的get监听方法,将该compute watcher 添加到msg的dep中,完成被依赖关系的收集。最终的依赖模型如下:

至此,computeMsg的依赖和被依赖关系收集完成。大家也可以尝试分析下模板中调用普通属性的依赖关系收集过程。

五、派发更新

目前msg属性的dep发布器中收集了两个依赖的watcher对象,当我们重新设置msg值,会发生什呢?

当重新设置msg值,就会触发set方法。

Object.defineProperty(obj, key, {
    ...
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //核心部分,通知更新
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })

调用其发布器的notify方法。

//通知相关的watcher类更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

循环属性关联的watcher类,由前面可知,此时dep的sub集合中有user watcher和computed watcher两个对象。分别调用其update方法。

 update () {
    /* istanbul ignore else */
    if (this.computed) {//计算属性watcher处理
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      //当没有订阅计算属性时,不需要计算,仅仅设置dirty为true,表示下次重新计算
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {//同步处理,立即执行
      this.run()
    } else {//异步处理,进入堆栈
      queueWatcher(this)
    }
  }

update分为三个分支处理,我们分别对三种情况进行分析。

(1)、计算属性处理。

(2)、同步处理,立即执行。

(3)、异步处理,加入到堆栈中。

1、计算属性

对于计算属性的watcher对象,判断是否有订阅过该计算属性,如果没有(即该计算属性的dep的sub集合是否为空),则设置dirty为true,下次将重新计算。

如本例中,computeMsg是被render watcher依赖,所以会进入else分支,执行getAndInvoke方法。

 this.getAndInvoke(() => {
          this.dep.notify()
        })

getAndInvoke (cb: Function) {
    //获取最新的值
    const value = this.get()
    if (
      //
      value !== this.value ||//值发生变化
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||//object对象
      this.deep//深度watcher
    ) {
      // set new value
      //更新值
      const oldValue = this.value
      this.value = value
      this.dirty = false
      //执行回调函数
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

该方法核心部分是执行传入的回调cb方法,即this.dep.notify(),此时递归调用render watcher的update,又回到了update方法。

2、同步处理

如果不是计算属性(本例中的user watcher,render watcher),并设置了同步处理,则调用run方法。如:

 run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

run方法调用getAndInvoke,该方法核心部分是:

(1)执行get方法获取value,对于render watcher ,执行updateComponent方法,重新生成Vnode,并patch,实现dom的更新(下一章节将详细说明)。

(2)回调cb方法,如本例中的msg的watch表达式(注意:render watcher的cb是noon)

3、异步处理

对于非同步,则调用queueWatcher将watcher压入堆栈中。在将这个方法之前,我们先看如果更新queue中的watcher,即flushSchedulerQueue方法,在src/core/observer/scheduler.js中

function flushSchedulerQueue () {
  //1、设置标识位flush为true,标识正在刷新中
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  //2、将queue数组从小到大排序
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  //3、循环队栈queue,执行watcher.run方法,实现更新。
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  //4、重置相关的状态
  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

我们看下核心的步骤:

(1)设置标识位flush,表示queue正在处理中。

(2)根据queue中的watcher的id,从小到大进行排序(也就是创建的先后)

(3)循环queque中的watcher,执行run方法,实现更新。

(4)queue中的执行完成后,则重置相关的状态,包括flush,wait等,等待下一次执行。

整个处理过程还是比较清晰的,这里注意一点,在第3步中,queue是可能动态变化的。

现在回过头来,继续看queueWatcher,是如何将watcher加入到queue中,又是如何触发flushSchedulerQueue执行的。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {//对于同一个watcher,不会重复加入到queue,避免多次触发
    has[id] = true
    //1、将watcher加入队列中
    if (!flushing) {//尚未刷新,则加入队栈,待执行
      queue.push(watcher)
    } else {//2、正在刷新中,则动态的插入到到对应位置。
      // 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)
    }
    // queue the flush
    //3、通过nexttick执行queue中的watcher
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

将watcher加入queue队栈中,分两种情况,

(1) flushSchedulerQueue未执行,则将watcher加入到queue即可,待下一次执行。

(2)flushSchedulerQueue执行中,则从后往前查找对应的位置,然后插入到queue。

(3)通过nextTick,调用flushSchedulerQueue,实现queue中watcher的更新。vue的DOM是异步更新的,nextTick确保了在DOM更新后再执行,在这里可以认为下一个事件循环的"tick"。nextTick机制实现了"批量"的更新,效率更高。

这里要注意,对于同一个watcher,不能重复的加入到queue中,避免多次触发。

六、总结

本章节的逻辑还是比较复杂。我们将各个方法间的调用关系总结下,便于大家理解。

发布了33 篇原创文章 · 获赞 95 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/tcy83/article/details/90543287