Vue源码解析之 派发更新

304 阅读5分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 9 篇,关注专栏

前言

我们在 依赖收集 文章中了解到,响应式数据会进行依赖收集,收集的目的就是当数据修改时,会进行依赖派发更新,该部分逻辑在 setter 函数中触发,我们主要关注 dep.notify(),该方法会通知所有订阅者,也是本节的关键。

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // ...
    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()
    }
  })
}

派发更新过程

当响应的数据修改时,会触发 setter 逻辑,最终会调用 dep.notify(), 它定义在 src/core/observer/dep.js

class Dep {
  // ...
  notify () {
  // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

subs 为每个 Watcher 实例的数组,它会调用每个 Watcher 的 update 方法,它定义在 src/core/observer/watcher.js,我们主要关注 queueWatcher 方法

class Watcher {
  // ...
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // 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.
      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)
    }
  }
} 

queueWatcher 方法定义在 src/core/observer/scheduler.js 。该方法引入队列的概念,另外需要注意是使用 has 对象保证同一个 Watcher 只会添加一次,最后会执行 nextTick(flushSchedulerQueue)

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
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)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue 方法定义在 src/core/observer/scheduler.js ,该方法会遍历 queue ,拿到对应的 watcher,它会先执行 watcher.before() 触发 beforeUpdate 钩子函数,然后执行 watcher.run()

let flushing = false
let index = 0
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  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.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 执行 beforeUpdate 钩子函数 new Watcher 实例中
      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()

  resetSchedulerState()

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

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

另外还需注意的是,在遍历时都会对 queue.length 求值,因为在 watcher.run() 时,很可能用户会再次添加新的 watcher,这样会再次执行到 queueWatcher。当 flushing 为 true 时,会执行 else 逻辑。

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)
    }
    // ...
  }
}

接着会执行 resetSchedulerState() ,该方法定义在 src/core/observer/scheduler.js ,该方法主要是重置变量初始值,把 watcher 队列的清空。

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

之后我们继续分析 watcher.run() ,它定义在 src/core/observer/watcher.js

class Watcher {
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    const value = this.get()
    // 判断条件
    // 新值 与 旧值 不同
    // 新值 为一个 对象
    // deep watcher
    // watch: {
    //     c: {  
    //          handler: function (val, oldVal) { /* ... */ },  
    //          deep: true  
    //     }
    // }
    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) ||
      this.deep
    ) {
      // 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)
      }
    }
  }
}

可以看出 run 方法实际是调用 getAndInvoke 方法,并传入 watcher 回调函数。getAndInvoke 方法先通过 this.get() 拿到当前的值,根据判断,如满足新旧值不同、新值为对象类型、deep watcher 时会执行 watcher 的回调,该回调执行时会把新值 value 和 旧值 oldValue 传入,这也是当我们自定义添加 watcher 的时候能在回调函数的参数中能拿到新旧值的原因。

而对于渲染 watcher,它在执行 this.get() 方法求值时,会执行 getter 方法,该方法在 new Watcher 实例时,作为第二个回调参数传入,实际执行 updateComponent 方法:

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

该过程根据前文分析,是将 VNode patch 为真实 DOM。执行完该逻辑后,页面数据也随之发生变化,组件更新过程也就执行完毕。

// 执行完 updateComponent 后,页面 旧数据 --> 新数据
hello world  --> hello vue

注意点

对于渲染 watcher , getAndInvoke 方法中的回调 cb 就是个空函数,而对于 user watcher 即 new Vue实例中的 watch 属性,cb 为传入的函数。

下面案例场景,会造成死循环:

// 该案例会报错 仅调试
const app = new Vue({
    el: '#app',
    data() {
        return {
            msg: false,
        }
    },
    watch: {
        msg(newValue, oldValue) {
            // 重新赋值
            this.msg = Math.random()
        }
    },
    methods: {
        handleClick() {
            this.msg = true
        }
    }
})

// user watcher 的 cb 回调函数为
msg(newValue, oldValue) {
    this.msg = Math.random()
}

我们上面讲解到数据的更新会执行 flushSchedulerQueue 方法,该方法会遍历 watcher 队列,案例的队列包含 渲染 watcher 和 user watcher 两个 watcher,之后会执行每个 Watcher run 函数的 getAndInvoke 方法。

对于 user watcher ,会执行判断中的逻辑,最终会执行 cb.call(this.vm, value, oldValue),等同于执行 watch 属性中的 msg 函数,之后又会对 msg 数据进行重新赋值。我们知道修改数据会派发更新,队列中又会插入新的 watcher ,这样就会造成一个死循环导致报错。

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) ||
      this.deep
    ) {
      // 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.call(this.vm, value, oldValue) 执行 等同于 执行 watch 属性中 msg 函数
// 这也是为什么我们自定义添加 watcher 时能在回调函数的参数中能拿到新旧值的原因
msg(newValue, oldValue) {
    // 重新赋值
    this.msg = Math.random()
}

// 报错
[Vue warn]: You may have an infinite update loop in watcher with expression "msg"

总结

  • 派发更新实际上就是当数据发生变化时,触发 setter 逻辑,把在依赖过程中订阅的的所有 watcher 都触发它们的 update 过程
  • 派发更新 update 过程实际执行了 queueWatcher 方法,接着执行 nextTick(flushSchedulerQueue)flushSchedulerQueue 核心逻辑会调用 watcherrun 方法,该方法会执行 getAndInvoke 函数。最终会执行 this.get() 逻辑,触发 updateComponent 方法,该方法会把 VNode patch 为真实 DOM,数据更新完毕。综上,该过程为整个派发更新 update 过程。

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick