狂賀! Vue 2.0 終於正式發佈!

關於 Vue 2.0 的新特性,作者也在官方 Blog - Vue 2.0 is Here! (中文翻譯) 一文中敘述地相當詳細,這裡就不多說。

如果你也與我一樣是從 V1 就開始接觸的開發者,一定都知道 VueJS 最核心的一部分是 Component,而 Component 是由實體 (Vue Instance) 來實作。 在這篇文章,我們就來談談 Vue 1.x 與 2.x 元件實體的差異。


Vue 2.0 元件實體註冊

tree of components

像上面這樣的網站,我們可以將它抽象化為一棵「元件樹」,而每個元件樹都會有個根節點,或稱為根實體 (root Vue instance)。

那麼,每個 Vue 元件樹的根實體其實是透過 Vue 這個建構函式所產生:

var vm = new Vue({
  // options
});

將 Vue 元件與實體 DOM 結合的方式有兩種,一種是直接寫在 el option 內:

var vm = new Vue({
  el: '#app'
});

而另一種方式則是透過 $mount 來指定節點:

var vm = new Vue({
  // options without 'el'
});

vm.$mount('#app');

這部分跟 Vue 1.x 的註冊是完全一樣的,但是需要注意的是,在 vue 1.x 允許開發者以 <body><html> 作為根實體的掛載點, 但到了 VueJS 2.0 後,只能透過 獨立的節點掛載 ,如: div 等。 否則會產生錯誤,警告訊息如下:

“Do not mount Vue to <html> or <body> - mount to normal elements instead.“

換成用獨立的 DOM 節點,如 <div id="app"></div>,就可以正常運作了。


Vue 2.0 元件實體的生命週期

Vue 2.0 Lifecycle Diagram 圖片來源: Vue 2.0 Guide: Instance Lifecycle Hooks

基本上 Vue 2.0 實體生命週期中,大部分的階段都與 Vue 1.x 是一樣的,最大的不同在於 lifecycle hook 名稱的改變,以及在元件被掛載 mounted 之後,還新增了 beforeUpdate 以及 updated 這兩組偵測更新的 hook。

vue 1.x 的 init 變成 beforeCreatebeforeCompiled 改為 beforeMount。 而原本的 compliedready 則是統一收斂成 mounted

另外需要注意的是,若元件本身是透過 server-side rendering 的話,除了 beforeCreate 以及 created 以外的所有 hook 都不會被呼叫 (參考資料)

有關元件 V-DOM 的重新渲染與更新後面再提,其他部分則與 Vue 1.x 大同小異。


Vue 2.0 元件與模板的編譯 - Render Function

在大部分情況下,透過元件的 template 屬性,或是直接寫在 HTML 中就已經足夠操作你的元件了。 不過若是你想完全透過 JavaScript 來操作你的元件,那麼可以使用 render 這個 function 直接來寫底層的 virtual-DOM 來取代 template 屬性。 VueJS 2.0 的 virtual DOM 機制,是採用 snabbdom 這個 virtual DOM 的 library 來實作的。

可以使用 createElement 這個 function 來建立你的元件內容:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // An HTML tag name, component options, or function
  // returning one of these. Required.
  'div',
  // {Object}
  // A data object corresponding to the attributes
  // you would use in a template. Optional.
  {
    // (see details in the next section below)
  },
  // {String | Array}
  // Children VNodes. Optional.
  [
    createElement('h1', 'hello world')
    createElement(MyComponent, {
      props: {
        someProp: 'foo'
      }
    }),
    'bar'
  ]
)

官方也提供了一個完整的 render function 範例:

var getChildrenTextContent = function (children) {
  return children.map(function (node) {
    return node.children
      ? getChildrenTextContent(node.children)
      : node.text
  }).join('')
}
Vue.component('anchored-heading', {
  render: function (createElement) {
    // create kebabCase id
    var headingId = getChildrenTextContent(this.$slots.default)
      .toLowerCase()
      .replace(/\W+/g, '-')
      .replace(/(^\-|\-$)/g, '')

    return createElement(
      'h' + this.level,
      [
        createElement('a', {
          attrs: {
            name: headingId,
            href: '#' + headingId
          }
        }, this.$slots.default)
      ]
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

當然,你可能跟我一樣覺得一層又一層的 createElement 看了總是讓人厭煩,你也可以透過這個 Plugin: babel-plugin-transform-vue-jsx,來做 JSX 語法的轉換,如果你曾是 react 應用程式的開發者,應該對 JSX 語法不陌生。 寫起來會像這樣:

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

在預設情況中,VueJS 2.0 會將 template 內的 HTML 透過 parse 轉換成 AST,再自動轉換優化成 render function 去建立 virtual DOM。 在建立 virtual DOM 之後,透過 observe 機制與資料進行綁定,再 compile 成實體的 DOM 並渲染至網頁上:

參考資料與圖片來源: Next Vue.js 2.0 by kazupon

前面說過,VueJS 2.0 會將 template 內的 HTML 自動編譯成 render function,下面這是官方文件以 Vue.compile 提供的 demo:

<!-- template -->
<div>
  <h1>I'm a template!</h1>
  <p v-if="message">
    {{ message }}
  </p>
  <p v-else>
    No message.
  </p>
</div>
// render:
function anonymous() {
  with(this){return _h('div',[_m(0),(message)?_h('p',[_s(message)]):_h('p',["No message."])])}
}
// staticRenderFns:
_m(0): function anonymous() {
  with(this){return _h('h1',["I'm a template!"])}
}


Vue 2.0 元件的追蹤變化

最後,我們來看看元件內狀態的追蹤變化。有寫過 VueJS 1.x 的朋友應該知道,元件實體內有個 option 叫 data, 這個 data 物件就是用來存放元件內狀態/資料的地方。

與 Vue 1.x 相同的地方是,data 物件透過 Object.defineProperty() 來為元件內各屬性設定 「getter」與「setter」。 就在 data 屬性被存取修改時,會透過 getter/setter 來通知物件內屬性的變化,當先前設定好的 setter 被呼叫的時候,會去觸發 watcher 重新計算,也就會導致 DOM 的更新與重新渲染。

與 Vue 1.x 不同的是,Vue 1.x 是透過 directive 來重新渲染 DOM 內容:

而 Vue 2.0 在通知 watcher 更新時,會去呼叫前面介紹的 「render function」與更新後的 data 去做更新後再次渲染,概念與 1.x 大致相同。 但更新 DOM 的手法不同,減少了不必要的比對,也因此大幅度提升了效能。