Vue - The Good Parts: 生命週期

前言

咱們知道 Vue 實例都有着相同的生命週期,並且你會發現隨着對 Vue 的深刻使用,也老是離不開對生命週期的使用。html

下面咱們就一塊兒來分析下 Vue 中生命週期都有哪些,有什麼樣的做用,以及咱們能夠從中學到些什麼。前端

正文分析

What

生命週期是什麼呢?按照 Vue 官網的描述 cn.vuejs.org/v2/guide/in…vue

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

大概能夠理解爲:從 Vue 實例的建立到更新、銷燬的完整的一個過程,在這個過程當中 Vue 會執行一些對應的鉤子函數,進而達到了用戶更強大的自定義功能的能力。node

完整的生命週期,Vue 文檔中也給出了 cn.vuejs.org/v2/guide/in…react

lifecycle.png

上邊圖示的生命週期鉤子的含義分別爲(這些也是最爲重要的生命週期鉤子):git

  • 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,這裏咱們就不作詳細介紹了。github

How

那這些生命週期鉤子是在什麼時機調用的,咱們一塊兒來看下。web

咱們爲了簡化,以最簡單的示例來看:express

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):api

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
  • 多個地方的內存管理,及時釋放內存,避免內存泄漏

滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。

相關文章
相關標籤/搜索