Chen Tim 已發佈 2019-10-24

Vue 的生命週期

學 Vue 的時候,一定會接觸到的其中一個概念就是生命週期。
它讓我們在 Vue 實例創建到銷毀的過程中,
能獲取到每個階段的不同狀態,
讓我們來介紹一下生命週期吧!

以下分階段介紹 Vue 的生命週期:

Vue 生命週期 beforeCreate/created
(圖片擷取自 Vue 官網)

1. beforeCreate:

完成實例初始化,事件生命週期等。

2. created:

這時候 data, computed, methods, watch… 中的值都已被創建出來。
但 Vue 實例尚未掛載到 DOM 上。
$el 也尚未被創建。

接著,

Vue 生命週期 beforeMount 之前
(圖片擷取自 Vue 官網)

這時候會檢查 el 是否存在,
如果 el 不存在的話,就必須手動執行 $mount()
才會繼續後續的 template 編譯。
因為 el 所表示的就是 Vue 創建後,所要掛載的位置。
如果要掛載的位置不存在的話,自然就沒有後續渲染出畫面的必要了。

再來會繼續判斷,是否有 template ?

  1. 如果有 template 的話,就會把 template 字符串轉為 render 方法。
  2. 沒有的話,就會直接拿 el.outerHTML 當成 template,一樣會轉為 render 方法。

但如果有 render 方法,就會跳過上面的內容。

也就是說,其實不論如何,最後都會執行 render()
而這裡指的 render 方法 是什麼呢?
它的功用主要是拿來渲染出 VNode(虛擬 DOM)。

Vue 生命週期 beforeMount/mounted
(圖片擷取自 Vue 官網)

3. beforeMount:

DOM 綁定之前,即將開始進行掛載。

4. Mounted:

完成創建 $el, 掛載 DOM 和渲染完成,
完成雙向綁定。
可以在 mounted 鉤子上進行對 DOM 的操作。

Vue 生命週期 mounted 之後
(圖片擷取自 Vue 官網)

5. beforeUpdate:

可以在更新之前,訪問現有的 DOM,
手動移除監聽器也是可能會在此使用的情境。

6. update:

完成 DOM 的渲染與更新。
可以操作 DOM 元素。

7. beforeDestory:

在此實體被銷毀前時叫用,這時實體還是擁有完整的功能。

8. destoryed:

實體銷毀。
所有綁定被解除、事件偵聽被移除。

掛載?

大致上說明完了。Vue 常使用到的生命週期。
每次看完之後,我都很好奇,
所謂的「掛載」到底是什麼?
能不能用文字試著去說明,於是深入了一點去了解,
new Vue 以來,到底發生了什麼事?

結論

先給結論好了。
所謂的掛載,我認為是將 VNode 渲染出來後,
並同步更新 DOM 完畢的這個過程。
也可以說,掛載完後,
Vue 的雙向綁定就算完成了。


來試著用 Vue 的原始碼來理解

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)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    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)
    }
  }

下面是 Vue 在初始化時的原碼,
一開始就會先初始化生命週期、data、props、component、watcher 等。

可以發現,

在 beforeCreate 前,初始化的是事件、生命週期、Render 函數。
在 created 前,初始化的是 data, computed, prop, methods 等。(initState(vm) 中)

完全如同上面圖片所示。
這也是為什麼我們在 created 之後,才可以取得 data 中的值。

接著如果 vm.$options.el 存在的話,就會執行 $mount()
而相關程式碼如下:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function  
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

先把原本的 $mount 存進變數裡,
然後寫了一個新的 function 覆蓋 $mount

而這新的 function 主要就是先判斷了是否存在 render 方法,
若不存在則判斷是否有 template,
若有的話,會透過 compileToFunctions 將 template 轉為 render 方法,
最後再存進 options 裡。

最後在執行一次 mount(前面先預存的原本的 $mount


所以 $mount 又做了啥呢?

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  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
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        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
}

有兩個比較關鍵的地方為

updateComponent = () => {
   vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

我們可以理解為,在 mounted 之前,
Vue 創建了一個 Watcher 去監聽 Vue 實例數據的變化(第一個帶入的參數 vm)。
監測到變化後,會執行 updateComponent

updateComponent 這個 function
會透過 vm._render() 生成虛擬 DOM,
然後 update() 才更新真實 DOM。

我自己理解為:
render() 所做的是,生成相對應的 VNode
update() 所做的是把 VNode 渲染成真实的 DOM

最後就會執行 mounted。


說到這邊,其實還是有很多沒說明完的。
例如:vm._render() 實際上到底做了什麼?是怎樣生成 VNode…等。
但我想至少對於整個 Vue 的運作過程,
有了更深一層的理解。

看見了在 beforeCreate 時,做了哪些事。
發現了在 created 才能訪問 data 的直接證據。
好像理解了所謂 ” 掛載 “ 的意思。(至少可以自己用中文說出來)

參考資料:

https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/new-vue.html
https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/mounted.html
https://segmentfault.com/a/1190000014640577

關於筆者

暱稱:Chen Tim

文章列表 文章列表