Vue - The Good Parts: 生命周期

avatar
@滴滴出行

前言

我们知道 Vue 实例都有着相同的生命周期,而且你会发现随着对 Vue 的深入使用,也总是离不开对生命周期的使用。

下面我们就一起来分析下 Vue 中生命周期都有哪些,有什么样的作用,以及我们可以从中学到些什么。

正文分析

What

生命周期是什么呢?按照 Vue 官网的描述 cn.vuejs.org/v2/guide/in…

image2021-6-28_15-58-46.png

大概可以理解为:从 Vue 实例的创建到更新、销毁的完整的一个过程,在这个过程中 Vue 会执行一些对应的钩子函数,进而达到了用户更强大的自定义功能的能力。

完整的生命周期,Vue 文档中也给出了 cn.vuejs.org/v2/guide/in…

lifecycle.png

上边图示的生命周期钩子的含义分别为(这些也是最为重要的生命周期钩子):

  • beforeCreate 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
  • created 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。
  • beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted 实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。
  • beforeUpdate 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
  • updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
  • beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

当然,Vue 中还存在其他的一些生命周期钩子:activated、deactivated、errorCaptured,这里我们就不做详细介绍了。

How

那这些生命周期钩子是在什么时机调用的,我们一起来看下。

我们为了简化,以最简单的示例来看:

var vm = new Vue({
  data: {
    msg: 'Hello World!'
  },
  render(h) {
    return h('div', this.msg)
  }
})
vm.$mount('#app')

首先来看下,初始化的两个钩子 beforeCreatecreated。文件在 github.com/vuejs/vue/b… 核心在 _init 中(Vue 初始化的时候 会调用 _init):

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++
 
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }
 
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期
    initLifecycle(vm)
    // 初始化事件
    initEvents(vm)
    // 初始化 render
    initRender(vm)
    // 1. 调用 beforeCreate 钩子
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    // 2. 调用 created 钩子
    callHook(vm, 'created')
 
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
 
    if (vm.$options.el) {
      // 后边看,标记下
      vm.$mount(vm.$options.el)
    }
  }
}

beforeCreate

在 beforeCreate 之前,初始化了生命周期、事件、render:

// https://github.com/vuejs/vue/blob/v2.6.14/src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options
 
  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
 
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
 
  vm.$children = []
  vm.$refs = {}
 
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
 
// https://github.com/vuejs/vue/blob/v2.6.14/src/core/instance/events.js
export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}
 
// https://github.com/vuejs/vue/blob/v2.6.14/src/core/instance/render.js
export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
 
  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data
 
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

可以看出基本上就是初始化一些对应的模块需要用到的一些变量,处理一些初始值的case,可以先大概了解下,也约等于知道了在 beforeCreate 中可以访问哪些属性(可以访问不代表有效或者叫有正确的值)。

created

调用完 beforeCreate 钩子之后,做了三个初始化的操作:Injections、State、Provide。

Inject 和 Provide 是相对应的,也需要一起使用,相关文档可以参考 cn.vuejs.org/v2/api/#pro…

// https://github.com/vuejs/vue/blob/v2.6.14/src/core/instance/inject.js
export function initInjections (vm: Component) {
  // 层层查找注入项,从 vm 的 _provided 值上取
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 刻意为之的 非响应式
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}
 
// https://github.com/vuejs/vue/blob/v2.6.14/src/core/instance/state.js
// 初始化和状态相关的逻辑
// 主要包含了熟知的:props methods data computed watch
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
 
 
// https://github.com/vuejs/vue/blob/v2.6.14/src/core/instance/inject.js
export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    // 直接 call 即可,得到了 provided 的值,挂载在 _provided 上
    // 和上边 inject 的逻辑对应上了
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

上边有一个很重要的初始化状态相关的逻辑,依次初始化了 props、methods、data、computed 以及 watch,这些在 Vue 中是相当重要的组成部分,这个初始化的顺序也决定了我们可以在对应的配置项中可以访问的内容。

我们也一起来重点看(调整了顺序)下 github.com/vuejs/vue/b…

// 初始化 props
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  // 只有根实例的 props 才会被转为 响应式对象 其他实例不会
  // 因为非根的实例的 props 都是父组件传递下去的,理论上都已经是响应式对象了
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      // 注意这里 转为响应式的 KV
      // defineReactive 见 响应式 文章
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
 
// 初始化 methods
function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    // 绑定上下文 这些方法的调用 上下文一定是 当前组件实例
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}
 
// 初始化 data
function initData (vm: Component) {
  let data = vm.$options.data
  // 如果 data 是 function 则调用 得到 data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // data 转换为响应式对象
  observe(data, true /* asRootData */)
}
 
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}
 
const computedWatcherOptions = { lazy: true }
 
// 初始化 computed
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // _computedWatchers 保存
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
 
  for (const key in computed) {
    const userDef = computed[key]
    // 取默认 getter 或者 用户自定义的
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
 
    if (!isSSR) {
      // create internal watcher for the computed property.
      // 创建 computed 所对应的watcher
      // computedWatcherOptions 的 lazy 为 true 默认不会执行 取值的操作
      // lazy 基本为 computed 定制的
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    // 注意这里的注释,如果在组件原型上已经定义的 computed 这里就不需要定义了
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      // 定义 computed,约等于是 vm 上定义一个 key 的值
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}
 
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  // 只需要关注 createComputedGetter
  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
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  // 利用 defineProperty 定义一个值
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
 
function createComputedGetter (key) {
  // 返回了一个 getter 函数 当访问 vm[key] 的时候 会触发 getter 进而 调用这个函数
  return function computedGetter () {
    // computedWatcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // dirty ,取值
      if (watcher.dirty) {
        // evaluate 会调用 watcher 的 getter,即 用户自定义的 computed 声明(get)
        watcher.evaluate()
      }
      // 添加依赖
      if (Dep.target) {
        watcher.depend()
      }
      // 返回值
      return watcher.value
    }
  }
}
 
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}
 
// 初始化 watch
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // handler 可以是数组
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      // 创建watcher
      createWatcher(vm, key, handler)
    }
  }
}
 
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 对象模式 参数兼容
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // vm 实例上的 方法
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 调用实例 $watch
  return vm.$watch(expOrFn, handler, options)
}
 
export function stateMixin (Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  // 给原型定义 $data $props 属性,
  // 这是一个技巧,给原型定义 getter 在所有的实例上都可以访问 且上下文是 实例
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
 
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
 
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // 创建 watcher 实例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 立即执行
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    // 返回值,一个 unwatch 的函数,用于取消 watcher 的监测
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

以上基本上就完成了从 beforeCreate 到 created 钩子的所有的逻辑,重点就是和 state 相关处理,通过分析,我们知道了 props、methods、data、computed 以及 watch 具体有怎样的逻辑实现,进而知道他们的作用。

beforeMount & mounted

回到 _init 的逻辑中,如果配置项存在 el,那么就会调用 vm.$mount(el)去挂载实例。

这个逻辑在 github.com/vuejs/vue/b… 这里

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

而这个 mountComponent 就来自于 github.com/vuejs/vue/b… 中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 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')
 
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`
 
      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)
 
      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
 
  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  // 传说中的 render watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
 
  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  // 在目前我们的场景中 所走的逻辑
  if (vm.$vnode == null) {
    // 设置标识
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

可以看到基本上,立即调用了 beforeMount 钩子。

后边就初始化了大名鼎鼎的 render watcher,每一个实例都会对应一个 render watcher,作用也很明显,用于监控是否应该 rerender 的。

接着基本上就调用了 mounted 的钩子。

那我们就详细看下这个 render watcher,传入的第二个参数,即 getter 就是 updateComponent,默认实例化 Watcher 的时候就会调用这个 getter。updateComponent 做了两个事情:

  1. 调用 vm._render(),得到 vnode 数据
  2. 调用 vm._update() 进行更新,也就是大家所理解的后续 vnode patch 过程就在这里边。

先来看 _render 的逻辑 github.com/vuejs/vue/b…

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)
 
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
 
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
 
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }
 
    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    // vm.$vnode 指向的是 parent vnode
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      // 又见利用 JS 单线程的变量
      currentRenderingInstance = vm
      // 调用 options 中的 render 函数 得到 vnode 数据
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      // 执行完 设置 null
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    // 这样 vnode 的完整 tree 一层层就构建好了
    vnode.parent = _parentVnode
    return vnode
  }
}

再来看 _update 的逻辑 github.com/vuejs/vue/b…

// 在 lifecycleMixin 中 定义的
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  // 上一次的 DOM 元素(根),我们这里就是挂载的容器元素
  const prevEl = vm.$el
  // 上一次调用 render() 得到的 vnode 数据
  const prevVnode = vm._vnode
  // 设置当前 active instance 且得到恢复上一个 active instance 的 函数
  const restoreActiveInstance = setActiveInstance(vm)
  // 设置 _vnode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    // 初次渲染 我们的逻辑,注意这里的 第一个参数是 vm.$el 我们实际的容器元素
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    // 其他时间 就是 两次 vnode 数据进行对比 patch 以更新 DOM
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  执行完 patch 就恢复 active instance
  restoreActiveInstance()
  // update __vue__ reference
  // 干掉引用关系 释放
  if (prevEl) {
    prevEl.__vue__ = null
  }
  // 设置新的
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  // 如果高阶组件,即 <template><child>xx</child></template> 这种case
  // parent 的 $el 一样需要更新
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

这里相关逻辑有个巧妙的设计:

export let activeInstance: any = null
 
export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

巧用闭包的特性,实现了一个类似于链表的感觉,处理完当前的,直接恢复到上一个 active Instance,也就是根据当前的这个 总是能够恢复(找到)上一个,但是利用闭包,他们之间并不需要存在实体的关联。

接下来就是重点的 __patch__ 逻辑 github.com/vuejs/vue/b…

Vue.prototype.__patch__ = inBrowser ? patch : noop

而 patch 就来自于 github.com/vuejs/vue/b…

import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
 
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
 
export const patch: Function = createPatchFunction({ nodeOps, modules })

重点就是这个 createPatchFunction 所返回的 patch,来自 github.com/vuejs/vue/b…

// 较长,这里简化了
export function createPatchFunction (backend) {
  // ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
 
    let isInitialPatch = false
    const insertedVnodeQueue = []
 
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      // 此时 我们的场景 isRealElement 为 true
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }
 
        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
 
        // create new node
        // 创建新元素
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
 
        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.componentInstance
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }
 
        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

上述逻辑此时需要我们重点关注:createElm

let creatingElmInVPre = 0
 
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }
 
  vnode.isRootInsert = !nested // for transition enter check
  // 创建组件 & 检查
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
 
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }
    // 创建实际的 DOM 元素
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)
 
    /* istanbul ignore if */
    if (__WEEX__) {
      // in Weex, the default insertion order is parent-first.
      // List items can be optimized to use children-first insertion
      // with append="tree".
      const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
      if (!appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
      createChildren(vnode, children, insertedVnodeQueue)
      if (appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
    } else {
      // 创建子元素
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      // 插入元素
      insert(parentElm, vnode.elm, refElm)
    }
 
    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    // 注释元素
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // 文本元素
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}
 
 
function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}
 
 
function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    // 循环创建子元素们
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    // 子元素 文本
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

到这里结束,可以看到元素已经可以正常渲染出来了,mount 阶段最核心的就是根据虚拟 DOM 数据,进行 patch 得到实际的 DOM 元素,然后插入到页面中。

这样就构成了完整的挂载。

beforeUpdate & updated

分析 mounted 中,我们知道有这样一段逻辑,也就是我们的渲染 watcher 部分

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        // 如果 mounted 了 还没销毁 就调用 beforeUpdate 钩子
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

我们知道在执行 updateComponent 的过程中,会收集依赖,看当前执行 render() 过程中依赖了哪些响应式数据,那么当数据变化的时候,会调用 Watcher 实例的 update 方法,这个在响应式文章有聊过,这里看下这个后续执行的事情:

// Watcher class
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

针对于我们的场景,就是调用 queueWatcher(this)。这个 queueWatcher 的大概逻辑 github.com/vuejs/vue/b…

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
 
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

可以认为基本上加入到一个 watcher 的队列中,利用 nextTick 集中在下一个 tick 执行这些 watcher,即 flushSchedulerQueue

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  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]
    // 如果 watcher 有 before 钩子 则执行他们
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 调用 run
    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)
  // 重点 调用 updated 钩子
  callUpdatedHooks(updatedQueue)
 
  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

针对于我们的场景,如果我们修改了依赖,例如

vm.msg = 'New Hello World!'

那么就会在下一个 tick 的时候,先执行 watcher 的 before:

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        // 如果 mounted 了 还没销毁 就调用 beforeUpdate 钩子
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

此时这个时机,就调用了 beforeUpdate 钩子。然后继续执行 watcher 的 run() 方法,会再次进行调用 watche 的 getter 进行新的一轮的求值,此时,也就意味着会重新调用 updateComponent,再次执行 render & update,此时 render() 得到的新的 vnode 数据其实发生了变化,接着重点就是这个 update 操作,也就是最终执行的 patch 操作:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  // 上一次调用 render() 得到的 vnode 数据
  const prevVnode = vm._vnode
  // 设置 _vnode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    // 此时就是单纯的上一次的 vnode 和新的 vnode 进行 patch 处理!!
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  // ...
}

所以再次回到了 patch 逻辑,此时的 case,会执行这样一段逻辑:

const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  // 重点 patch vnode
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

sameVnode 的逻辑比较简单

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

最核心的判断是 key 以及 tag 是否是相同的,此时我们的场景也是符合的,所以会进行下一个重点 patchVnode

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  if (oldVnode === vnode) {
    return
  }
 
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }
  // 元素直接复用
  const elm = vnode.elm = oldVnode.elm
 
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }
 
  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }
 
  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
 
  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  if (isUndef(vnode.text)) {
    // children 对比
    if (isDef(oldCh) && isDef(ch)) {
      // 然后重点是这个 updateChildren
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // 纯文本 case
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

一起来看看这个 updateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm
 
  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly
 
  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }
 
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 递归调用 patchVnode
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

基本按照一些策略(头和头、尾和尾、头和尾、头尾同步遍历),同层的 vnode 之间对比 check,详细文章可以参考 github.com/CommanderXL…

回到我们的核心,会继续递归调用了 patchVnode,因为只有 msg 的纯文本信息发生了变更,所以执行 setTextContent 更新元素文本内容即完成了所有的 patch 更新 DOM 的操作。

到这里,因为 msg 的更新,引起 DOM 更新,整个过程已经完成了。

再次回到 flushSchedulerQueue 中,有一个重点

function flushSchedulerQueue () {
  // ...
  // 重点 调用 updated 钩子
  callUpdatedHooks(updatedQueue)
  // ...
}
 
 
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    // 判断是 渲染 watcher 且已经 mounted 且没有 destroy
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

遍历队列中的 watcher,只要是渲染 watcher,那么就调用这个实例的 updated 钩子。

beforeDestroy & destroyed

销毁的逻辑,基本要从 $destroy 开始,相关代码 github.com/vuejs/vue/b…

// 在lifecycleMixin中
Vue.prototype.$destroy = function () {
  const vm: Component = this
  // 标识 防止重复销毁
  if (vm._isBeingDestroyed) {
    return
  }
  callHook(vm, 'beforeDestroy')
  // 做标识
  vm._isBeingDestroyed = true
  // remove self from parent
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // teardown watchers
  // render watcher 需要卸载监听
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  // 普通的 watch 和 computed 对应的 watchers
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // invoke destroy hooks on current rendered tree
  // 执行 patch 第二个参数 新的 vnode 为 null
  vm.__patch__(vm._vnode, null)
  // fire destroyed hook
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  // 事件
  vm.$off()
  // remove __vue__ reference
  // 取消相关引用 释放内存 以防内存泄漏
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

可以看到逻辑还是比较清晰的,有一个防重处理,大概做的核心事情:

  • 调用 beforeDestroy 钩子
  • render watcher 卸载监听
  • computed 以及 watch 相关的 watcher 卸载监听
  • __patch__
  • 调用 destroyed 钩子
  • 取消事件监听
  • 取消相关引用

看起来还是需要深入看下此时的 __patch__ 逻辑细节:

// 在 createPatchFunction 中的部分逻辑
return function patch (oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }
  // ...
}
 
 
function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}

可以看到在这种情况下,基本上就是遍历 vnode,然后依次调用对应 vnode 上的对应的销毁 hook。

此时也可以看出,针对于我们的场景,Vue 根实例销毁了,并不会移除 DOM 元素,这些元素还是会保持原样。

Why

这个在官网上其实也有涉及,生命周期钩子的最核心的作用就是让用户在不同时机可以自定义不同的行为。

这个问题,可以理解为:怎么样可以让开发者更方便、更优雅的定义自定义逻辑?

那反过来思考,都在一个函数中或者在一个地方去写自定义逻辑是不是也是可以的?当然可以,但是可能引发什么样的问题呢?

  • 可实现:为了达到用户感知不同逻辑或者周期的目标,这一个函数中应该会在Vue的内部执行过程中,多次调用,在函数中才能根据状态去控制自身逻辑
  • 职责问题:不够单一,里边混淆了各种逻辑
  • 可维护性:里边会针对于各种时机做各种判断

那针对于这些问题,采用定义好的生命周期钩子,开发者就可以很方便的在不同的状态(阶段)执行自己想要的逻辑,很好的组织了自定义代码,同时各个的职责很清晰。

额外的,这是一个完整的统一的生命周期,对于开发者理解实例的执行逻辑也是很重要的,实现自定义逻辑的时候,可以做到心中有图。

当然也是有成本的,需要理解这些生命周期,以及对应的生命周期可以做的事情。但是,如果没有的话,只会让这个成本变得更大。

复杂度本身是不会消失的,只能转移他们。

总结

相信你通过上边的分析大概知道了 Vue 中几个核心的生命周期钩子以及他们的实现上做了什么样的事情。可以看出还是比较复杂的,有很多细节我们目前都是没有涉及到的,尤其是和组件相关的,这个会留到组件的分析部分。

那通过以上的分析,我们可以学到哪些东西吗?可以有什么样的收获?

生命周期

生命周期,就是一个个有序的阶段,不同的阶段可以做不同的事情,这些阶段构成了一个完整的全局或者整体。视角的话,是站在全局来看的,是一种自顶向下拆分设计系统的一种很好的方法。

这些不同的阶段之间,区分又是明显的,每个阶段或者步骤都很简单、易懂。

当然,我们分析了 Vue 最核心的几个生命周期钩子,他们分别在什么时机调用的,做了哪些事情,相信你已经有了大概的了解了。

钩子

这里的重点其实是钩子,钩子本身就是一个模板方法的模式的应用,经常和插件化一起出现。钩子的最大好处就是提供了强大的用户自定义能力,且很好的实现了隔离和扩展。也基本上意味着,可以保持较好的可维护性。

钩子本身也是一种 DI依赖注入 的思想,由上层使用者决定注入什么样的内容去执行。

模块组织

上边的分析 instance 模块相关能力的时候,我们可以发现 Vue 中去扩展能力的方式是按照 Mixin 混入的这种方式组织的,github.com/vuejs/vue/b…

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
 
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
 
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
 
export default Vue

通过这种方式实现了模块的拆解,各个模块的职责比较清晰,共同混入各种能力组成了最终的完整功能。

在 init 模块中,也是从其他各个模块中引入了对应的 init 方法依此来执行初始化的功能

import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
 
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // ...
  }
}

通过这种组合的方式,去拆分我们的功能,这里边还是需要我们根据实际的场景,去好好分析模块之间的组织关系,进而合理拆分和组合他们。做到模块职责聚合,达到各司其职的目标。

computed & watch

Vue 文档中专门花了一个篇幅来讲计算属性 computed 和侦听器 watch,也讲明了为啥需要这两个特性,他们分别解决了什么场景的事情,也是基于响应式系统之上提供的十分好用的特性,详情可以看 cn.vuejs.org/v2/guide/co…

相信有过 Vue 实践的同学一定用了不少这两个特性,可以说在我们实际开发逻辑的过程中,给了我们很多的便利,而且相当直观,易于理解。

其他小Tips

  • 巧用闭包实现类似链表的效果,根据一个实例总能找到上一个
  • 在原型上定义 getter 和 setter 的作用
  • 事件通信,一个经典的事件通信实现,本质上就是一个发布订阅模式 github.com/vuejs/vue/b…
  • 如何 resolveConstructorOptions,考虑继承的情况 github.com/vuejs/vue/b…
  • 如何 mergeOptions github.com/vuejs/vue/b…
  • VDOM 是如何跨平台的(Web、Weex)、如何根据 vnode 数据去挂载真实 DOM 大概过程、以及根据 vnode 数据 patch 出更新,进而 nodeOps 有哪些API去实现跨平台 github.com/vuejs/vue/b…
  • 再次出现的队列,以及如何使用 nextTick
  • 多个地方的内存管理,及时释放内存,避免内存泄漏

滴滴前端技术团队的团队号已经上线,我们也同步了一定的招聘信息,我们也会持续增加更多职位,有兴趣的同学可以一起聊聊。