Vue原理解(一): Vue 是什麼?

Vue, 現階段受熱指數上升比較塊的前端架構。有必要從源碼的角度,對它的功能的實現原理一窺究竟。看源碼一般主要看兩樣東西, 從宏觀角度是它的設計思想和實現原理; 微觀上則是編程技巧。 這裏我們側重點是實現原理上。

vue 是什麼?
我們在使用vue時,初始化操作都會使用new Vue({…}),不難發現 vue 其實是一個類。 不過ES6普及的今天,vue 的定義任是普通構造函數。 爲什麼不用 ES6的class 呢? 稍後會介紹。 首先來看看vue 被定義的地方:

function  Vue(options) {
    ...
    this._init(options)
}

這裏省略掉了,flow的類型檢查及一些邊界情況的源碼及講解。比如省略號這裏邊界情況是使用必須是new Vue() 的形式,否則會報錯。
其實vue 源碼就像一顆樹, 在看之前最好先確定看什麼功能,避開那些分叉邏輯。
我們接下來的目標就是從new Vue()開始,走完一整條從初始化,數據,模板到真實Dom的這整個流程

當執行new Vue時, 內部會執行一個方法 this._init(options), 將初始化的參數傳入。
*這裏在vue的內部, 使用 _ 符號開頭定義私有變量, 使用使符號定義供用戶使用的變量* 。且用戶定義的變量不能以_或開頭,以防止內部衝突。
我們接着看:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'

function Vue(options) {
  ...
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

現在我們講解下上面提到的爲什麼不採用ES6的class來定義。 因爲這樣可以方便的把vue的功能拆分到不同的目錄中去維護, 將 vue 的構造函數傳入到以下方法內(這裏通過注入方式給Vue擴展API):

  • initMixin(Vue): 定義 _init 方法。
  • stateMixin(Vue): 定義數據相關的方法$set, $delete, $watch 方法。
  • eventsMixin(Vue): 定義事件相關的方法$on, $once, $off, $emit。
  • lifecycleMixin(Vue): 定義_update, 及生命週期相關的 forceUpdateforceUpdate和destroy。
  • renderMixin(Vue): 定義$nextTick, _render 將render函數轉爲vnode。

這些方法都在各自的文件內維護,從而讓代碼結構更加清晰易懂可維護。 如 this._init 方法被定義在:

export function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    ...當執行new Vue時,進行一系列初始化並掛載
  }
}

再這些 xxxMixin 完成後, 接着會定義一些全局的API:

export function initGlobalAPI(Vue) {
  Vue.set方法
  Vue.delete方法
  Vue.nextTick方法
  
  ...
  
  內置組件:
  keep-alive
  transition
  transition-group
  
  ...
  
  initUse(Vue):Vue.use方法
  initMixin(Vue):Vue.mixin方法
  initExtend(Vue):Vue.extend方法
  initAssetRegisters(Vue):Vue.component,Vue.directive,Vue.filter方法
}	

這裏有部分 apixxxMixin 定義的原型方法功能是類似的,如 this.$setVue.set 他們都是使用set 這樣一個內部定義的方法。

這裏提一下 vue 的架構設計,它的架構是分層式的。 最頂層是一個ES5 的構造函數, 在上層在原型上會定義一些==_init==, $watch, _render 等這樣的方法, 再上層會在構造函數自定義全局的一些API, 如 set, nextTick, use 等(以上這些事不區分平臺的核心代碼), 接着是跨平臺和服務端渲染及編譯器。 這些屬性方法都定義好了以後,最後導出一個完整的構造函數給到用戶使用。 new Vue 就是開啓的鑰匙。

上面我們從比較微觀的角度近距離的觀察了vue, 現在我們從宏觀角度來了解他內部的代碼結構是如何組建起來的。 目錄如下:

|-- dist  打包後的vue版本
|-- flow  類型檢測,3.0換了typeScript
|-- script  構建不同版本vue的相關配置
|-- src  源碼
    |-- compiler  編譯器
    |-- core  不區分平臺的核心代碼
        |-- components  通用的抽象組件
        |-- global-api  全局API
        |-- instance  實例的構造函數和原型方法
        |-- observer  數據響應式
        |-- util  常用的工具方法
        |-- vdom  虛擬dom相關
    |-- platforms  不同平臺不同實現
    |-- server  服務端渲染
    |-- sfc  .vue單文件組件解析
    |-- shared  全局通用工具方法
|-- test 測試
  • flow: javascript 是弱類型語言, 使用 flow 以定義類型和檢測類型,增加代碼的健壯性。
  • src/compiler: 將template 模板編譯爲 render 函數。
  • src/core: 與平臺無關通用的邏輯, 可以運用在任何javaScript 環境下: 如 web, Node.js weex 嵌入原生應用中。
  • src/platforms: 針對web 平臺和 weex 平臺分別的實現, 並提供統一的 API供調用。
  • src/observer: vue 檢測數據變化,改變視圖的代碼實現。
  • src/vdom: 將render 函數轉爲 vnode 從而 patch 爲真實 dom 以及diff 算法的代碼實現。
  • dist: 存放着針對不同使用方式的不同vue版本

Vue 版本

vue 使用的是rollup構建的, 具體怎麼構建不重要,總之會構建出很多不同版本vue。 按使用方式的不同,可分爲以下三類:

  • UMD: 通過 script 標籤直接在瀏覽器中使用。
  • CommonJS: 使用比較舊的打包工具使用, 如 webpack1
  • ES Module: 配合現代打包工具使用, 如 webpack2 及以上。

而每個使用方式內又分爲了完整版和運行時版本, 這裏主要以 ES Module 爲例, 有了官方腳手架其它兩類應該沒多少人用了。 在介紹兩個版本區別之前,我們先插入一個小廣告。 即: 在vue的內部是隻認render 函數的, 我們自定義一個render函數:

new Vue({
  data: {
    msg: 'hello Vue!'
  },
  render(h) {
    return h('span', this.msg);
  }
}).$mount('#app');

爲什麼只認render 函數, 我們在寫代碼的時候好似並沒有些過render函數,而是使用template 模板。 這是因爲有 vue-loader, 它會將我們在template內定義的內容編譯爲render 函數,而這個編譯就是區分完整版和運行時版本的關鍵所在,完整版自帶這個編譯器, 而運行時版本就沒有。如下代碼在運行時版本環境下就會報錯:

new Vue({
  data: {
    msg: 'hello Vue!'  
  },
  template: `<div>{{msg}}</div>`
})

vue-cli 默認是使用運行時版本的, 更改或覆蓋腳手架內的默認配置,將其更改爲完整版即可通過編譯: ‘vue$’: ‘vue/dist/vue.esm.js’, 推薦還是使用運行時版本。

下面帶着一個問題結束本章的內容。

  • 請問runtimeruntime-only 這兩個版本的區別是?
    解答:
  • 最明顯的就是大小寫區別,帶編譯器會比不帶的版本大6kb
  • 編譯的時機不同, 編譯器是運行時編譯,性能會有一定的損耗;運行時版本是藉助loader 做的離線編譯,運行性能更高。

下一篇 Vue原理解析(二): 初始化時beforeCreate之前做了什麼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章