Vue源碼閱讀 - 文件結構與運行機制

vue已經是目前國內前端web端三分天下之一,同時也做爲本人主要技術棧之一,在平常使用中知其然也好奇着因此然,另外最近的社區涌現了一大票vue源碼閱讀類的文章,在下借這個機會從你們的文章和討論中汲取了一些養分,同時對一些閱讀源碼時的想法進行總結,出產一些文章,做爲本身思考的輸出,本人水平有限,歡迎留言討論~前端

目標Vue版本:2.5.17-beta.0vue

vue源碼註釋:github.com/SHERlocked9…node

聲明:文章中源碼的語法都使用 Flow,而且源碼根據須要都有刪節(爲了避免被迷糊 @_@),若是要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~react

感興趣的同窗能夠加文末的微信羣,一塊兒討論吧~webpack

0. 前備知識

  • Flow
  • ES6語法
  • 經常使用的設計模式
  • 柯里化等函數式編程思想

這裏推介幾篇前備文章:JS 靜態類型檢查工具 FlowECMAScript 6 入門 - 阮一峯JS中的柯里化JS 觀察者模式JS 利用高階函數實現函數緩存(備忘模式)git

1. 文件結構

文件結構在vue的CONTRIBUTING.md中有介紹,這邊直接翻譯過來:es6

├── scripts ------------------------------- 包含與構建相關的腳本和配置文件
│   ├── alias.js -------------------------- 源碼中使用到的模塊導入別名
│   ├── config.js ------------------------- 項目的構建配置
├── build --------------------------------- 構建相關的文件,通常狀況下咱們不須要動
├── dist ---------------------------------- 構建後文件的輸出目錄
├── examples ------------------------------ 存放一些使用Vue開發的應用案例
├── flow ---------------------------------- JS靜態類型檢查工具 [Flow](https://flowtype.org/) 的類型聲明
├── package.json
├── test ---------------------------------- 測試文件
├── src ----------------------------------- 源碼目錄
│   ├── compiler -------------------------- 編譯器代碼,用來將 template 編譯爲 render 函數
│   │   ├── parser ------------------------ 存放將模板字符串轉換成元素抽象語法樹的代碼
│   │   ├── codegen ----------------------- 存放從抽象語法樹(AST)生成render函數的代碼
│   │   ├── optimizer.js ------------------ 分析靜態樹,優化vdom渲染
│   ├── core ------------------------------ 存放通用的,平臺無關的運行時代碼
│   │   ├── observer ---------------------- 響應式實現,包含數據觀測的核心代碼
│   │   ├── vdom -------------------------- 虛擬DOM的 creation 和 patching 的代碼
│   │   ├── instance ---------------------- Vue構造函數與原型相關代碼
│   │   ├── global-api -------------------- 給Vue構造函數掛載全局方法(靜態方法)或屬性的代碼
│   │   ├── components -------------------- 包含抽象出來的通用組件,目前只有keep-alive
│   ├── server ---------------------------- 服務端渲染(server-side rendering)的相關代碼
│   ├── platforms ------------------------- 不一樣平臺特有的相關代碼
│   │   ├── weex -------------------------- weex平臺支持
│   │   ├── web --------------------------- web平臺支持
│   │   │   ├── entry-runtime.js ---------------- 運行時構建的入口
│   │   │   ├── entry-runtime-with-compiler.js -- 獨立構建版本的入口
│   │   │   ├── entry-compiler.js --------------- vue-template-compiler 包的入口文件
│   │   │   ├── entry-server-renderer.js -------- vue-server-renderer 包的入口文件
│   ├── sfc ------------------------------- 包含單文件組件(.vue文件)的解析邏輯,用於vue-template-compiler包
│   ├── shared ---------------------------- 整個代碼庫通用的代碼
複製代碼

幾個重要的目錄:github

  • compiler: 編譯,用來將template轉化爲render函數
  • core: Vue的核心代碼,包括響應式實現、虛擬DOM、Vue實例方法的掛載、全局方法、抽象出來的通用組件等
  • platform: 不一樣平臺的入口文件,主要是 web 平臺和 weex 平臺的,不一樣平臺有其特殊的構建過程,固然咱們的重點是 web 平臺
  • server: 服務端渲染(SSR)的相關代碼,SSR 主要把組件直接渲染爲 HTML 並由 Server 端直接提供給 Client 端
  • sfc: 主要是 .vue 文件解析的邏輯
  • shared: 一些通用的工具方法,有一些是爲了增長代碼可讀性而設置的

其中在platform下src/platforms/web/entry-runtime.js文件做爲運行時構建的入口,ESM方式輸出 dist/vue.runtime.esm.js,CJS方式輸出 dist/vue.runtime.common.js,UMD方式輸出 dist/vue.runtime.js,不包含模板 template 到 render 函數的編譯器 src/platforms/web/entry-runtime-with-compiler.js文件做爲運行時構建的入口,ESM方式輸出 dist/vue.esm.js,CJS方式輸出 dist/vue.common.js,UMD方式輸出 dist/vue.js,包含compilerweb

2. 入口文件

任何前端項目均可以從 package.json 文件看起,先來看看它的 script.dev 就是咱們運行 npm run dev 的時候它的命令行:算法

"scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
}
複製代碼

這裏的 rollup 是一個相似於 webpack 的JS模塊打包器,事實上 Vue - v1.0.10 版本以前用的仍是 webpack ,其後改爲了 rollup ,若是想知道爲何換成 rollup ,能夠看看 尤雨溪本人的回答,總的來講就是爲了打出來的包體積小一點,初始化速度快一點。

能夠看到這裏 rollup 去運行 scripts/config.js 文件,而且給了個參數 TARGET:web-full-dev,那來看看 scripts/config.js 裏面是啥

// scripts/config.js

const builds = {
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),  // 入口文件
    dest: resolve('dist/vue.js'),                          // 輸出文件
    format: 'umd',                                         // 參看下面的編譯方式說明
    env: 'development',                                    // 環境
    alias: { he: './entity-decoder' },                     // 別名
    banner                                        // 每一個包前面的註釋-版本/做者/日期.etc
  },
}
複製代碼

format 編譯方式說明:

  • es: ES Modules,使用ES6的模板語法輸出
  • cjs: CommonJs Module,遵循CommonJs Module規範的文件輸出
  • amd: AMD Module,遵循AMD Module規範的文件輸出
  • umd: 支持外鏈規範的文件輸出,此文件能夠直接使用script標籤

這裏的 web-full-dev 就是對應剛剛咱們在命令行裏傳入的命令,那麼 rollup 就會按下面的 entry 入口文件開始去打包,還有其餘不少命令和其餘各類輸出方式和格式能夠自行查看一下源碼。

所以本文主要的關注點在包含 compiler 編譯器的 src/platforms/web/entry-runtime-with-compiler.js 文件,在生產和開發環境中咱們使用 vue-loader 來進行 template 的編譯從而不須要帶 compiler 的包,可是爲了更好的理解原理和流程仍是推介從帶 compiler 的入口文件看起。

先看看這個文件,這裏導入了個 Vue ,看看它從哪來的

// src/platforms/web/entry-runtime-with-compiler.js

import Vue from './runtime/index'
複製代碼

繼續看

// src/platforms/web/runtime/index.js

import Vue from 'core/index'
複製代碼

keep moving

// src/core/index.js

import Vue from './instance/index'
複製代碼

keep moving*2

// src/core/instance/index.js

/* 這裏就是vue的構造函數了,不用ES6的Class語法是由於mixin模塊劃分的方便 */
function Vue(options) {
  this._init(options)         // 初始化方法,位於 initMixin 中
}

// 下面的mixin往Vue.prototype上各類掛載
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
複製代碼

當咱們 new Vue( ) 的時候,實際上調用的就是這個構造函數,能夠從這裏開始看了。

3. 運行機制

這裏我用xmind粗略的畫了一張運行機制圖,基本上後面的分析都在這張圖上面的某些部分了

本文 Vue 實例都是用 vm 來表示

上面這個圖能夠分爲多個部分細加閱讀,具體的實現咱們在後面的文章中詳細討論,這裏先貼一部分源碼嚐嚐鮮

3.1 初始化 _init( )

當咱們在 main.js 裏 new Vue( ) 後,Vue 會調用構造函數的 _init( ) 方法,這個方法是位於 core/instance/index.js 的 initMixin( ) 方法中定義的

// src/core/instance/index.js

/* 這裏就是Vue的構造函數 */
function Vue(options) {
  this._init(options)              // 初始化方法,位於 initMixin 中
}

// 下面的mixin往Vue.prototype上各類掛載,這是在加載的時候已經掛載好的
initMixin(Vue)                     // 給Vue.prototype添加:_init函數,...
stateMixin(Vue)                    // 給Vue.prototype添加:$data屬性, $props屬性, $set函數, $delete函數, $watch函數,...
eventsMixin(Vue)                   // 給Vue.prototype添加:$on函數, $once函數, $off函數, $emit函數, $watch方法,...
lifecycleMixin(Vue)                // 給Vue.prototype添加: _update方法, $forceUpdate函數, $destroy函數,...
renderMixin(Vue)                   // 給Vue.prototype添加: $nextTick函數, _render函數,...

export default Vue
複製代碼

咱們能夠看看 init( ) 這個方法到底進行了哪些初始化:

// src/core/instance/index.js

Vue.prototype._init = function(options?: Object) {
  const vm: Component = this

  initLifecycle(vm)                     // 初始化生命週期 src/core/instance/lifecycle.js
  initEvents(vm)                        // 初始化事件 src/core/instance/events.js
  initRender(vm)                        // 初始化render src/core/instance/render.js
  callHook(vm, 'beforeCreate')          // 調用beforeCreate鉤子
  initInjections(vm)                    // 初始化注入值 before data/props src/core/instance/inject.js
  initState(vm)                         // 掛載 data/props/methods/watcher/computed
  initProvide(vm)                       // 初始化Provide after data/props
  callHook(vm, 'created')               // 調用created鉤子

  if (vm.$options.el) {                    // $options能夠認爲是咱們傳給 `new Vue(options)` 的options
    vm.$mount(vm.$options.el)              // $mount方法
  }
}
複製代碼

這裏 _init() 方法中會對當前 vm 實例進行一系列初始化設置,比較重要的是初始化 State 的方法 initState(vm) 的時候進行 data/props 的響應式化,這就是傳說中的經過 Object.defineProperty() 方法對須要響應式化的對象設置 getter/setter,以此爲基礎進行依賴蒐集(Dependency Collection),達到數據變化驅動視圖變化的目的。

最後檢測 vm.$options 上面有沒有 el 屬性,若是有的話使用 vm.$mount 方法掛載 vm,造成數據層和視圖層的聯繫。這也是若是沒有提供 el 選項就須要本身手動 vm.$mount('#app') 的緣由。

咱們看到 created 鉤子是在掛載 $mount 以前調用的,因此咱們在 created 鉤子觸發以前是沒法操做 DOM 的,這是由於尚未渲染到 DOM 上。

3.2 掛載 $mount( )

掛載方法 vm.$mount( ) 在多個地方有定義,是根據不一樣打包方式和平臺有關的,src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js,咱們的關注點在第一個文件,但在 entry-runtime-with-compiler.js 文件中會首先把 runtime/index.js 中的 $mount 方法保存下來,並在最後用 call 運行:

// src/platform/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount    // 把原來的$mount保存下來,位於 src/platform/web/runtime/index.js
Vue.prototype.$mount = function( el?: string | Element, // 掛載的元素 hydrating?: boolean // 服務端渲染相關參數 ): Component {
  el = el && query(el)
  
  const options = this.$options
  if (!options.render) {                // 若是沒有定義render方法
    let template = options.template
    
    // 把獲取到的template經過編譯的手段轉化爲render函數
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {...}, this)
      options.render = render
    }
  }
  return mount.call(this, el, hydrating)      // 執行原來的$mount
}
複製代碼

在 Vue 2.0 版本中,全部 Vue 的組件的渲染最終都須要 render 方法,不管咱們是用單文件 .vue 方式開發組件,仍是寫了 el 或者 template 屬性,最終都會轉換成 render 方法。這裏的 compileToFunctions 就是把 template 編譯爲 render 的方法,後面會介紹。

// src/platform/weex/runtime/index.js

Vue.prototype.$mount = function ( el?: string | Element, // 掛載的元素 hydrating?: boolean // 服務端渲染相關參數 ): Component {
  el = el && inBrowser ? query(el) : undefined        // query就是document.querySelector方法
  return mountComponent(this, el, hydrating)          // 位於core/instance/lifecycle.js
}
複製代碼

這裏的 el 一開始若是不是DOM元素的話會被 query 方法換成DOM元素再被傳給 mountComponent 方法,咱們繼續看 mountComponent 的定義:

// src/core/instance/lifecycle.js

export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')            // 調用beforeMount鉤子

  // 渲染watcher,當數據更改,updateComponent做爲Watcher對象的getter函數,用來依賴收集,並渲染視圖
  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  // 渲染watcher, Watcher 在這裏起到兩個做用,一個是初始化的時候會執行回調函數
  // ,另外一個是當 vm 實例中的監測的數據發生變化的時候執行回調函數
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')            // 調用beforeUpdate鉤子
      }
    }
  }, true /* isRenderWatcher */)

  // 這裏注意 vm.$vnode 表示 Vue 實例的父虛擬 Node,因此它爲 Null 則表示當前是根 Vue 的實例
  if (vm.$vnode == null) {
    vm._isMounted = true               // 表示這個實例已經掛載
    callHook(vm, 'mounted')            // 調用mounted鉤子
  }
  return vm
}
複製代碼

mountComponent 方法裏實例化了一個渲染 Watcher,而且傳入了一個 updateComponent ,這個方法:() => { vm._update(vm._render(), hydrating) } 首先使用 _render 方法生成 VNode,再調用 _update 方法更新DOM。能夠看看視圖更新部分的介紹

這裏調用了幾個鉤子,他們的時機能夠關注一下。

3.3 編譯 compile( )

若是在須要轉換 render 的場景下,好比咱們寫的 template ,將會被 compiler 轉換爲 render 函數,這其中會有幾個步驟組成:

入口位於剛剛 src/platform/web/entry-runtime-with-compiler.js 的 compileToFunctions 方法:

// src/platforms/web/compiler/index.js

const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
複製代碼

繼續看這裏的 createCompiler 方法:

// src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
複製代碼

這裏能夠看到有三個重要的過程 parseoptimizegenerate,以後生成了 render 方法代碼。

  • parse:會用正則等方式解析 template 模板中的指令、class、style等數據,造成抽象語法樹 AST
  • optimize:優化AST,生成模板AST樹,檢測不須要進行DOM改變的靜態子樹,減小 patch 的壓力
  • generate:把 AST 生成 render 方法的代碼

3.4 響應式化 observe( )

Vue做爲一個MVVM框架,咱們知道它的 Model 層和 View 層之間的橋樑 ViewModel 是作到數據驅動的關鍵,Vue的響應式是經過 Object.defineProperty 來實現,給被響應式化的對象設置 getter/setter ,當 render 函數被渲染的時候會觸發讀取響應式化對象的 getter 進行依賴收集,而在修改響應式化對象的時候會觸發設置 settersetter 方法會 notify 它以前收集到的每個 watcher 來告訴他們本身的值更新了,從而觸發 watcherupdatepatch 更新視圖。

響應式化的入口位於 src/core/instance/init.js 的 initState 中:

// src/core/instance/state.js

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)
  }
}
複製代碼

它很是規律的定義了幾個方法來初始化 propsmethodsdatacomputedwathcer,這裏只看 initData 方法,來窺一豹

// src/core/instance/state.js

function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
                    ? getData(data, vm)
                    : data || {}
  
  observe(data, true /* asRootData */) // 給data作響應式處理
}
複製代碼

首先判斷了下 data 是否是函數,是則取返回值不是則取自身,以後有一個 observe 方法對 data 進行處理,看看這個方法

// src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}
複製代碼

這個方法主要用 data 去實例化一個 Observer 對象實例,Observer 是一個 Class,Observer 的構造函數使用 defineReactive 方法給對象的鍵響應式化,它給對象的屬性遞歸添加 getter/setter,用於依賴收集和 notify 更新,這個方法大概是這樣的

// src/core/observer/index.js

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 進行依賴收集 */
            return val;
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            notify();                // 觸發更新
        }
    });
}
複製代碼

3.5 視圖更新 patch( )

當使用 defineReactive 方法將對象響應式化後,當 render 函數被渲染的時候,會讀取響應化對象的 getter 從而觸發 getter 進行 watcher 依賴的收集,而在修改響應化對象的值的時候,會觸發 setter 通知 notify 以前收集的依賴,通知本身已被修改,請按需從新渲染視圖。被通知的 watcher 調用 update 方法去更新視圖,位於上面介紹過的傳遞給 new Watcher( )updateComponent 方法中,這個方法會調用 update 方法去 patch 更新視圖。

// src/core/instance/lifecycle.js

let updateComponent
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

// 渲染watcher, Watcher 在這裏起到兩個做用,一個是初始化的時候會執行回調函數
// ,另外一個是當 vm 實例中的監測的數據發生變化的時候執行回調函數
new Watcher(vm, updateComponent, noop, {...}, true /* isRenderWatcher */)
複製代碼

這個 _render 方法生成虛擬 Node, _update 方法中的會將新的 VNode 與舊的 VNode 一塊兒傳入 patch

// src/core/instance/lifecycle.js

Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { // 調用此方法去更新視圖
  const vm: Component = this
  const prevVnode = vm._vnode
  vm._vnode = vnode

  if (!prevVnode) {
    // 初始化
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    //更新
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}
複製代碼

_update 調用 __patch__ 方法,它主要是對新老 VNode 進行比較 patchVnode,通過 diff 算法得出它們的差別直接,最後這些差別的對應 DOM 進行更新。

到這裏基本上一個主要的流程就介紹完了,咱們大概瞭解了一個 Vue 從一個構造函數的實例化開始是如何運轉的,後面會展開來討論一下各個部分的內容,在下才疏學淺,未免紕漏,歡迎你們討論~


本文是系列文章,隨後會更新後面的部分,共同進步~

  1. Vue源碼閱讀 - 文件結構與運行機制
  2. Vue源碼閱讀 - 依賴收集原理
  3. Vue源碼閱讀 - 批量異步更新與nextTick原理

網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~

參考:

  1. Vue2.1.7源碼學習
  2. Vue.js 技術揭祕
  3. 剖析 Vue.js 內部運行機制
  4. Vue.js 文檔
  5. 【大型乾貨】手拉手帶你過一遍vue部分源碼
  6. MDN - Object.defineProperty()

PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~

另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~

相關文章
相關標籤/搜索