Vue2 更新流程源码解析

502 阅读2分钟

本篇文章,我将详细介绍Vue的更新策略。

首先,让我们来思考一下,对于Vue的更新环节,我们应该从哪里入手开始阅读呢?仔细想一想,Vue中由数据影响视图,是不是当我们修改了响应式的数据时,Vue就会启动更新机制呢?而在前面的几节中,我们也得知了数据的响应式,得益于我们对属性描述符 get,set 操作的拦截,更新环节也从这里开始进入讲解。

defineReactive

// src/core/observer/index.js
export function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  ...
  Object.defineProperty(obj, key, {
    ...
    set: function reactiveSetter (newVal) {
      ...
      dep.notify()
    }
  })
}

dep.notify

读源码可知,在defineReactive中的set操作中,调用了depnotify方法,而更新流程由此开始。

class Dep {
  subs: Array<Watcher>
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

watcher.update

当调用了notify,我们首先拷贝与当前数据相关的所有watcher,然后调用它们的update方法。

class Watcher {
  update () {
    if (this.lazy) {
      // computed缓存相关
      this.dirty = true
    } else if (this.sync) {
      // 如果设置了watcher的sync为true,直接同步执行
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

queueWatcher

正常的情况下,我们会调用queueWatcher方法

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 只处理还没有入队列的watcher
  if (has[id] == null) {
    // 入队
    has[id] = true
    // flushing表示正常冲刷,也就是正在更新的意思
    if (!flushing) {
      // 没有在更新时,直接推入队列即可
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      // index为正在更新的watcher的id
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      // 用splice的方式将该watcher推入队列,需要注意的是,如果当前更新的watcher的id比该watcher的要小,意味着该watcher将会进入下一次的更新队列中
      queue.splice(i + 1, 0, watcher)
    }
    // 如果没有开启队列
    if (!waiting) {
      // 开启队列
      waiting = true
      // 调用nextTick方法,将flushSchedulerQueue传入,这里的nextTick等同于我们使用的this.$nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher做了两件事儿,一个是将当前的watcher传入队列,第二个就是打开了更新队列,但是需要注意的,此时并没有开始更新。

nextTick

我们传入了一个flushSchedulerQueue函数给nextTick,接下来,我们来看看这之中又发生了什么。

export function nextTick(cb, ctx) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick主要实现了两个功能,一个是向callbacks数组存入该flushSchedulerQueue回调函数,第二个是添加异步任务到事件循环队列之中。添加的关键就在于timerFunc函数。

timerFunc

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

timerFunc就是一个更新策略的选择,也可以说是对异步任务的兼容操作。具体实现的优先级为Promise > MutationObserver > setImmediate > setTimeout,关键点在于,我们总会以一个异步的操作去执行flushCallbacks函数,也是从这里开始,后面的任务执行都是异步下进行的啦。

flushCallbacks

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

该函数的操作很简单,执行callbacks中的所有函数,其中的函数又是什么呢,其实就是我们前面存入的flushSchedulerQueue

flushSchedulerQueue

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // 需要提前进行排序,有三点原因:第一是需要从父组件到子组件更新,第二是用户级watcher先于系统的watcher,第三是当组件在父组件的watcher还在进行run操作时被销毁了,则会跳过不进行更新。
  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // watcher的run方法才是更新的关键
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  // 重置更新队列
  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

watcher.run / get

class Watcher {
  run () {
    if (this.active) {
      // 调用了watcher的get方法
      const value = this.get()
      // 以下的内容主要是对于用户级watcher的处理,不在本节的考虑范畴中
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  get () {
    // 结合之前的内容,代表以下内容是需要进行响应式的
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用watcher.getter
      value = this.getter.call(vm, vm)
    } catch (e) {
      // 错误处理
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        // 递归的遍历并且转换它
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

从上面可以看出,最终当我们调用了传入的getter方法,就会更新所有内容了。那么又有一个问题来了,我们说了这么多,但是并没有提到,watcher是哪里开始调用的,其实这个也很简单,在我们了解到整个Vue到机制之后,我们可以很快速的得出一个结论:在Vue挂载的时候,我们会进行数据响应式处理。挂载这个词是不是非常的熟悉,我们在前几章中,有提到过这个词的哦,不记得的小伙伴可以回去看一看。

$mount

// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent

挂载的实质就是调用了mountComponent的方法

export function mountComponent (vm, el, hydrating) {
  vm.$el = el
  // 我们在之前已经进行了render的添加处理,如果此时还是没有render,那么就代表出错了
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  // 调用钩子
  callHook(vm, 'beforeMount')

  // updateComponent的实质就是Vue的更新函数
  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  // 传入的第二个参数就是前文所说的watcher.getter
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

以上可得,在组件挂载时,我们会进行new Watcher的操作,执行并传入更新函数updateComponent,这就是watcher.getter,而在后续的数据更新时,会再次调用它,完成组件更新操作,而在updateComponent中,vm._render()实际上更多的是对template的处理,返回一个当前的虚拟DOM,而vm._update()则进行的是Vue的patch操作,进行节点的更新。

至此,本篇的更新流程都已讲述完毕,下一节我会从updateComponent出发,讲述一下patch函数究竟做了什么事儿。