Vue2.1.7源碼學習

 

Vue2.1.7源碼學習

本來文章的名字叫作《源碼解析》,不事後來想一想,仍是用「源碼學習」來的合適一點,在沒有完全掌握源碼中的每個字母以前,「解析」就有點標題黨了。建議在看這篇文章以前,最好打開2.1.7的源碼對照着看,這樣可能更容易理解。另外本人水平有限,文中有錯誤或不妥的地方望你們多多指正共同成長。html

補充:Vue 2.2 剛剛發佈,做爲一個系列文章的第一篇,本篇文章主要從Vue代碼的組織,Vue構造函數的還原,原型的設計,以及參數選項的處理和已經被寫爛了的數據綁定與如何使用 Virtual DOM 更新視圖入手。從總體的大方向觀察框架,這麼看來 V2.1.7 對於理解 V2.2 的代碼不會有太大的影響。該系列文章的後續文章,都會從最新的源碼入手,並對改動的地方作相應的提示。前端

好久以前寫過一篇文章:JavaScript實現MVVM之我就是想監測一個普通對象的變化,文章開頭提到了我寫博客的風格,仍是那句話,只寫努力讓小白,甚至是小學生都能看明白的文章。這難免會致使對於某些同窗來講這篇文章有些墨跡,因此你們根據本身的喜愛,能夠詳細的看,也能夠跳躍着看。vue

1、從瞭解一個開源項目入手

要看一個項目的源碼,不要一上來就看,先去了解一下項目自己的元數據和依賴,除此以外最好也瞭解一下 PR 規則,Issue Reporting 規則等等。特別是「前端」開源項目,咱們在看源碼以前第一個想到的應該是:package.json文件。node

package.json 文件中,咱們最應該關注的就是 scripts 字段和 devDependencies 以及 dependencies 字段,經過 scripts 字段咱們能夠知道項目中定義的腳本命令,經過 devDependenciesdependencies 字段咱們能夠了解項目的依賴狀況。webpack

瞭解了這些以後,若是有依賴咱們就 npm install 安裝依賴就ok了。git

除了 package.json 以外,咱們還要閱讀項目的貢獻規則文檔,瞭解如何開始,一個好的開源項目確定會包含這部份內容的,Vue也不例外:https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md,在這個文檔裏說明了一些行爲準則,PR指南,Issue Reporting 指南,Development Setup 以及 項目結構。經過閱讀這些內容,咱們能夠了解項目如何開始,如何開發以及目錄的說明,下面是對重要目錄和文件的簡單介紹,這些內容你均可以去本身閱讀獲取:github

├── build --------------------------------- 構建相關的文件,通常狀況下咱們不須要動
├── dist ---------------------------------- 構建後文件的輸出目錄
├── examples ------------------------------ 存放一些使用Vue開發的應用案例
├── flow ---------------------------------- 類型聲明,使用開源項目 [Flow](https://flowtype.org/)
├── package.json -------------------------- 不解釋
├── test ---------------------------------- 包含全部測試文件
├── src ----------------------------------- 這個是咱們最應該關注的目錄,包含了源碼
│   ├── entries --------------------------- 包含了不一樣的構建或包的入口文件
│   │   ├── web-runtime.js ---------------- 運行時構建的入口,輸出 dist/vue.common.js 文件,不包含模板(template)到render函數的編譯器,因此不支持 `template` 選項,咱們使用vue默認導出的就是這個運行時的版本。你們使用的時候要注意
│   │   ├── web-runtime-with-compiler.js -- 獨立構建版本的入口,輸出 dist/vue.js,它包含模板(template)到render函數的編譯器
│   │   ├── web-compiler.js --------------- vue-template-compiler 包的入口文件
│   │   ├── web-server-renderer.js -------- vue-server-renderer 包的入口文件
│   ├── compiler -------------------------- 編譯器代碼的存放目錄,將 template 編譯爲 render 函數
│   │   ├── parser ------------------------ 存放將模板字符串轉換成元素抽象語法樹的代碼
│   │   ├── codegen ----------------------- 存放從抽象語法樹(AST)生成render函數的代碼
│   │   ├── optimizer.js ------------------ 分析靜態樹,優化vdom渲染
│   ├── core ------------------------------ 存放通用的,平臺無關的代碼
│   │   ├── observer ---------------------- 反應系統,包含數據觀測的核心代碼
│   │   ├── vdom -------------------------- 包含虛擬DOM建立(creation)和打補丁(patching)的代碼
│   │   ├── instance ---------------------- 包含Vue構造函數設計相關的代碼
│   │   ├── global-api -------------------- 包含給Vue構造函數掛載全局方法(靜態方法)或屬性的代碼
│   │   ├── components -------------------- 包含抽象出來的通用組件
│   ├── server ---------------------------- 包含服務端渲染(server-side rendering)的相關代碼
│   ├── platforms ------------------------- 包含平臺特有的相關代碼
│   ├── sfc ------------------------------- 包含單文件組件(.vue文件)的解析邏輯,用於vue-template-compiler包
│   ├── shared ---------------------------- 包含整個代碼庫通用的代碼

大概瞭解了重要目錄和文件以後,咱們就能夠查看 Development Setup 中的經常使用命令部分,來了解如何開始這個項目了,咱們能夠看到這樣的介紹:web

# watch and auto re-build dist/vue.js
$ npm run dev

# watch and auto re-run unit tests in Chrome
$ npm run dev:test

如今,咱們只須要運行 npm run dev 便可監測文件變化並自動從新構建輸出 dist/vue.js,而後運行 npm run dev:test 來測試。不過爲了方便,我會在 examples 目錄新建一個例子,而後引用 dist/vue.js 這樣,咱們能夠直接拿這個例子一邊改Vue源碼一邊看本身寫的代碼想怎麼玩怎麼玩。算法

2、看源碼的小提示

在真正步入源碼世界以前,我想簡單說一說看源碼的技巧:npm

注重大致框架,從宏觀到微觀

當你看一個項目代碼的時候,最好是能找到一條主線,先把大致流程結構摸清楚,再深刻到細節,逐項擊破,拿Vue舉個栗子:假如你已經知道Vue中數據狀態改變後會採用virtual DOM的方式更新DOM,這個時候,若是你不瞭解virtual DOM,那麼聽我一句「暫且不要去研究內部具體實現,由於這會是你喪失主線」,而你僅僅須要知道virtual DOM分爲三個步驟:

1、createElement(): 用 JavaScript對象(虛擬樹) 描述 真實DOM對象(真實樹)
2、diff(oldNode, newNode) : 對比新舊兩個虛擬樹的區別,收集差別
3、patch() : 將差別應用到真實DOM樹

有的時候 第二步 可能與 第三步 合併成一步(Vue 中的patch就是這樣),除此以外,還好比 src/compiler/codegen 內的代碼,可能你不知道他寫了什麼,直接去看它會讓你很痛苦,可是你只須要知道 codegen 是用來將抽象語法樹(AST)生成render函數的就OK了,也就是生成相似下面這樣的代碼:

function anonymous() {
    with(this){return _c('p',{attrs:{"id":"app"}},[_v("\n      "+_s(a)+"\n      "),_c('my-com')])}
}

當咱們知道了一個東西存在,且知道它存在的目的,那麼咱們就很容易抓住這條主線,這個系列的第一篇文章就是圍繞大致主線展開的。瞭解大致以後,咱們就知道了每部份內容都是作什麼的,好比 codegen 是生成相似上面貼出的代碼所示的函數的,那麼再去看codegen下的代碼時,目的性就會更強,就更容易理解。

3、Vue 的構造函數是什麼樣的

balabala一大堆,開始來乾貨吧。咱們要作的第一件事就是搞清楚 Vue 構造函數究竟是什麼樣子的。

咱們知道,咱們要使用 new 操做符來調用 Vue,那麼也就是說 Vue 應該是一個構造函數,因此咱們第一件要作的事兒就是把構造函數先扒的一清二楚,如何尋找 Vue 構造函數呢?固然是從 entry 開始啦,還記的咱們運行 npm run dev 命令後,會輸出 dist/vue.js 嗎,那麼咱們就去看看 npm run dev 幹了什麼:

"dev": "TARGET=web-full-dev rollup -w -c build/config.js",

首先將 TARGET 得值設置爲 ‘web-full-dev’,而後,而後,而後若是你不瞭解 rollup 就應該簡單去看一下啦……,簡單的說就是一個JavaScript模塊打包器,你能夠把它簡單的理解爲和 webpack 同樣,只不過它有他的優點,好比 Tree-shaking (webpack2也有),但一樣,在某些場景它也有他的劣勢。。。廢話很少說,其中 -w 就是watch,-c 就是指定配置文件爲 build/config.js ,咱們打開這個配置文件看一看:

// 引入依賴,定義 banner
...

// builds 對象
const builds = {
    ...
    // Runtime+compiler development build (Browser)
    'web-full-dev': {
        entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
        dest: path.resolve(__dirname, '../dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
    },
    ...
}

// 生成配置的方法
function genConfig(opts){
    ...
}

if (process.env.TARGET) {
  module.exports = genConfig(builds[process.env.TARGET])
} else {
  exports.getBuild = name => genConfig(builds[name])
  exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name]))
}

上面的代碼是簡化過的,當咱們運行 npm run dev 的時候 process.env.TARGET 的值等於 ‘web-full-dev’,因此

module.exports = genConfig(builds[process.env.TARGET])

這句代碼至關於:

module.exports = genConfig({
    entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
    dest: path.resolve(__dirname, '../dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
})

最終,genConfig 函數返回一個 config 對象,這個config對象就是Rollup的配置對象。那麼咱們就不難看到,入口文件是:

src/entries/web-runtime-with-compiler.js

咱們打開這個文件,不要忘了咱們的主題,咱們在尋找Vue構造函數,因此當咱們看到這個文件的第一行代碼是:

import Vue from './web-runtime'

這個時候,你就應該知道,這個文件暫時與你無緣,你應該打開 web-runtime.js 文件,不過當你打開這個文件時,你發現第一行是這樣的:

import Vue from 'core/index'

依照此思路,最終咱們尋找到Vue構造函數的位置應該是在 src/core/instance/index.js 文件中,其實咱們猜也猜獲得,上面介紹目錄的時候說過:instance 是存放Vue構造函數設計相關代碼的目錄。總結一下,咱們尋找的過程是這樣的:

尋找 Vue 構造函數路線

咱們回頭看一看 src/core/instance/index.js 文件,很簡單:

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

引入依賴,定義 Vue 構造函數,而後以Vue構造函數爲參數,調用了五個方法,最後導出 Vue。這五個方法分別來自五個文件:init.js state.js render.js events.js 以及 lifecycle.js

打開這五個文件,找到相應的方法,你會發現,這些方法的做用,就是在 Vue 的原型 prototype 上掛載方法或屬性,經歷了這五個方法後的Vue會變成這樣:

// initMixin(Vue)    src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}

// stateMixin(Vue)    src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}

// renderMixin(Vue)    src/core/instance/render.js **************************************************
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
Vue.prototype._s = _toString
Vue.prototype._v = createTextVNode
Vue.prototype._n = toNumber
Vue.prototype._e = createEmptyVNode
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = function(){}
Vue.prototype._o = function(){}
Vue.prototype._f = function resolveFilter (id) {}
Vue.prototype._l = function(){}
Vue.prototype._t = function(){}
Vue.prototype._b = function(){}
Vue.prototype._k = function(){}

// eventsMixin(Vue)    src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}

// lifecycleMixin(Vue)    src/core/instance/lifecycle.js **************************************************
Vue.prototype._mount = function(){}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype._updateFromParent = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}

這樣就結束了嗎?並無,根據咱們以前尋找 Vue 的路線,這只是剛剛開始,咱們追溯路線往回走,那麼下一個處理 Vue 構造函數的應該是 src/core/index.js 文件,咱們打開它:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Vue.version = '__VERSION__'

export default Vue

這個文件也很簡單,從 instance/index 中導入已經在原型上掛載了方法和屬性後的 Vue,而後導入 initGlobalAPIisServerRendering,以後將Vue做爲參數傳給 initGlobalAPI ,最後又在 Vue.prototype 上掛載了 $isServer ,在 Vue 上掛載了 version 屬性。

initGlobalAPI 的做用是在 Vue 構造函數上掛載靜態屬性和方法,Vue 在通過 initGlobalAPI 以後,會變成這樣:

// src/core/index.js / src/core/global-api/index.js
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
    components: {
        KeepAlive
    },
    directives: {},
    filters: {},
    _base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}

Vue.prototype.$isServer
Vue.version = '__VERSION__'

其中,稍微複雜一點的就是 Vue.options,你們稍微分析分析就會知道他的確長成那個樣子。下一個就是 web-runtime.js 文件了,web-runtime.js 文件主要作了三件事兒:

一、覆蓋 Vue.config 的屬性,將其設置爲平臺特有的一些方法
二、Vue.options.directivesVue.options.components 安裝平臺特有的指令和組件
三、在 Vue.prototype 上定義 __patch__$mount

通過 web-runtime.js 文件以後,Vue 變成下面這個樣子:

// 安裝平臺特定的utils
Vue.config.isUnknownElement = isUnknownElement
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.mustUseProp = mustUseProp
// 安裝平臺特定的 指令 和 組件
Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: {},
    _base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount

這裏你們要注意的是 Vue.options 的變化。另外這裏的 $mount 方法很簡單:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return this._mount(el, hydrating)
}

首先根據是不是瀏覽器環境決定要不要 query(el) 獲取元素,而後將 el 做爲參數傳遞給 this._mount()

最後一個處理 Vue 的文件就是入口文件 web-runtime-with-compiler.js 了,該文件作了兩件事:

一、緩存來自 web-runtime.js 文件的 $mount 函數

const mount = Vue.prototype.$mount

而後覆蓋覆蓋了 Vue.prototype.$mount

二、在 Vue 上掛載 compile

Vue.compile = compileToFunctions

compileToFunctions 函數的做用,就是將模板 template 編譯爲render函數。

至此,咱們算是還原了 Vue 構造函數,總結一下:

一、Vue.prototype 下的屬性和方法的掛載主要是在 src/core/instance 目錄中的代碼處理的

二、Vue 下的靜態屬性和方法的掛載主要是在 src/core/global-api 目錄下的代碼處理的

三、web-runtime.js 主要是添加web平臺特有的配置、組件和指令,web-runtime-with-compiler.js 給Vue的 $mount 方法添加 compiler 編譯器,支持 template

4、一個貫穿始終的例子

在瞭解了 Vue 構造函數的設計以後,接下來,咱們一個貫穿始終的例子就要登場了,掌聲有請:

let v = new Vue({
    el: '#app',
    data: {
        a: 1,
        b: [1, 2, 3]
    }
})

好吧,我認可這段代碼你家沒滿月的孩子都會寫了。這段代碼就是咱們貫穿始終的例子,它就是這篇文章的主線,在後續的講解中,都會以這段代碼爲例,當講到必要的地方,會爲其添加選項,好比講計算屬性的時候固然要加上一個 computed 屬性了。不過在最開始,我只傳遞了兩個選項 el 以及 data,「咱們看看接下來會發生什麼,讓咱們拭目以待「 —- NBA球星在接受採訪時最喜歡說這句話。

當咱們按照例子那樣編碼使用Vue的時候,Vue都作了什麼?

想要知道Vue都幹了什麼,咱們就要找到 Vue 初始化程序,查看 Vue 構造函數:

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)
}

咱們發現,_init() 方法就是Vue調用的第一個方法,而後將咱們的參數 options 透傳了過去。在調用 _init() 以前,還作了一個安全模式的處理,告訴開發者必須使用 new 操做符調用 Vue。根據以前咱們的整理,_init() 方法應該是在 src/core/instance/init.js 文件中定義的,咱們打開這個文件查看 _init() 方法:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++
    // 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)
    callHook(vm, 'beforeCreate')
    initState(vm)
    callHook(vm, 'created')
    initRender(vm)
  }

_init() 方法在一開始的時候,在 this 對象上定義了兩個屬性:_uid_isVue,而後判斷有沒有定義 options._isComponent,在使用 Vue 開發項目的時候,咱們是不會使用 _isComponent 選項的,這個選項是 Vue 內部使用的,按照本節開頭的例子,這裏會走 else 分支,也就是這段代碼:

vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )

這樣 Vue 第一步所作的事情就來了:使用策略對象合併參數選項

能夠發現,Vue使用 mergeOptions 來處理咱們調用Vue時傳入的參數選項(options),而後將返回值賦值給 this.$options (vm === this),傳給 mergeOptions 方法三個參數,咱們分別來看一看,首先是:resolveConstructorOptions(vm.constructor),咱們查看一下這個方法:

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = Ctor.super.options
    const cachedSuperOptions = Ctor.superOptions
    const extendOptions = Ctor.extendOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed
      Ctor.superOptions = superOptions
      extendOptions.render = options.render
      extendOptions.staticRenderFns = options.staticRenderFns
      extendOptions._scopeId = options._scopeId
      options = Ctor.options = mergeOptions(superOptions, extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

這個方法接收一個參數 Ctor,經過傳入的 vm.constructor 咱們能夠知道,其實就是 Vue 構造函數自己。因此下面這句代碼:

let options = Ctor.options

至關於:

let options = Vue.options

你們還記得 Vue.options 嗎?在尋找Vue構造函數一節裏,咱們整理了 Vue.options 應該長成下面這個樣子:

Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: {},
    _base: Vue
}

以後判斷是否認義了 Vue.super ,這個是用來處理繼承的,咱們後續再講,在本例中,resolveConstructorOptions 方法直接返回了 Vue.options。也就是說,傳遞給 mergeOptions 方法的第一個參數就是 Vue.options

傳給 mergeOptions 方法的第二個參數是咱們調用Vue構造函數時的參數選項,第三個參數是 vm 也就是 this 對象,按照本節開頭的例子那樣使用 Vue,最終運行的代碼應該以下:

vm.$options = mergeOptions(
      // Vue.options
    {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
    },
    // 調用Vue構造函數時傳入的參數選項 options
    {
        el: '#app',
        data: {
            a: 1,
            b: [1, 2, 3]
        }
    },
    // this
    vm
  )

瞭解了這些,咱們就能夠看看 mergeOptions 到底作了些什麼了,根據引用尋找到 mergeOptions 應該是在 src/core/util/options.js 文件中定義的。這個文件第一次看可能會頭大,下面是我處理後的簡略展現,你們看上去應該更容易理解了:

// 一、引用依賴
import Vue from '../instance/index'
其餘引用...

// 二、合併父子選項值爲最終值的策略對象,此時 strats 是一個空對象,由於 config.optionMergeStrategies = Object.create(null)
const strats = config.optionMergeStrategies
// 三、在 strats 對象上定義與參數選項名稱相同的方法
strats.el = 
strats.propsData = function (parent, child, vm, key){}
strats.data = function (parentVal, childVal, vm)

config._lifecycleHooks.forEach(hook => {
  strats[hook] = mergeHook
})

config._assetTypes.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

strats.watch = function (parentVal, childVal)

strats.props =
strats.methods =
strats.computed = function (parentVal: ?Object, childVal: ?Object)
// 默認的合併策略,若是有 `childVal` 則返回 `childVal` 沒有則返回 `parentVal`
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

// 四、mergeOptions 中根據參數選項調用同名的策略方法進行合併處理
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {

  // 其餘代碼
  ...

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options

}

上面的代碼中,我省略了一些工具函數,例如 mergeHookmergeAssets 等等,惟一須要注意的是這段代碼:

config._lifecycleHooks.forEach(hook => {
  strats[hook] = mergeHook
})

config._assetTypes.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

config 對象引用自 src/core/config.js 文件,最終的結果就是在 strats 下添加了相應的生命週期選項的合併策略函數爲 mergeHook,添加指令(directives)、組件(components)、過濾器(filters)等選項的合併策略函數爲 mergeAssets

這樣看來就清晰多了,拿咱們貫穿本文的例子來講:

let v = new Vue({
    el: '#app',
    data: {
        a: 1,
        b: [1, 2, 3]
    }
})

其中 el 選項會使用 defaultStrat 默認策略函數處理,data 選項則會使用 strats.data 策略函數處理,而且根據 strats.data 中的邏輯,strats.data 方法最終會返回一個函數:mergedInstanceDataFn

這裏就不詳細的講解每個策略函數的內容了,後續都會講到,這裏咱們仍是抓住主線理清思路爲主,只須要知道Vue在處理選項的時候,使用了一個策略對象對父子選項進行合併。並將最終的值賦值給實例下的 $options 屬性即:this.$options,那麼咱們繼續查看 _init() 方法在合併完選項以後,又作了什麼:

合併完選項以後,Vue 第二部作的事情就來了:初始化工做與Vue實例對象的設計

前面講了 Vue 構造函數的設計,而且整理了 Vue原型屬性與方法Vue靜態屬性與方法,而 Vue 實例對象就是經過構造函數創造出來的,讓咱們來看一看 Vue 實例對象是如何設計的,下面的代碼是 _init() 方法合併完選項以後的代碼:

/* 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)
    callHook(vm, 'beforeCreate')
    initState(vm)
    callHook(vm, 'created')
    initRender(vm)

根據上面的代碼,在生產環境下會爲實例添加兩個屬性,而且屬性值都爲實例自己:

vm._renderProxy = vm
vm._self = vm

而後,調用了四個 init* 方法分別爲:initLifecycleinitEventsinitStateinitRender,且在 initState 先後分別回調了生命週期鉤子 beforeCreatecreated,而 initRender 是在 created 鉤子執行以後執行的,看到這裏,也就明白了爲何 created 的時候不能操做DOM了。由於這個時候尚未渲染真正的DOM元素到文檔中。created 僅僅表明數據狀態的初始化完成。

根據四個 init* 方法的引用關係打開對應的文件查看對應的方法,咱們發現,這些方法是在處理Vue實例對象,以及作一些初始化的工做,相似整理Vue構造函數同樣,我一樣針對Vue實例作了屬性和方法的整理,以下:

// 在 Vue.prototype._init 中添加的屬性         **********************************************************
this._uid = uid++
this._isVue = true
this.$options = {
    components,
    directives,
    filters,
    _base,
    el,
    data: mergedInstanceDataFn()
}
this._renderProxy = this
this._self = this

// 在 initLifecycle 中添加的屬性        **********************************************************
this.$parent = parent
this.$root = parent ? parent.$root : this

this.$children = []
this.$refs = {}

this._watcher = null
this._inactive = false
this._isMounted = false
this._isDestroyed = false
this._isBeingDestroyed = false

// 在 initEvents     中添加的屬性         **********************************************************
this._events = {}
this._updateListeners = function(){}

// 在 initState 中添加的屬性        **********************************************************
this._watchers = []
    // initData
    this._data

// 在 initRender     中添加的屬性     **********************************************************
this.$vnode = null // the placeholder node in parent tree
this._vnode = null // the root of the child tree
this._staticTrees = null
this.$slots
this.$scopedSlots
this._c
this.$createElement

以上就是一個Vue實例所包含的屬性和方法,除此以外要注意的是,在 initEvents 中除了添加屬性以外,若是有 vm.$options._parentListeners 還要調用 vm._updateListeners() 方法,在 initState 中又調用了一些其餘init方法,以下:

export function initState (vm: Component) {
  vm._watchers = []
  initProps(vm)
  initMethods(vm)
  initData(vm)
  initComputed(vm)
  initWatch(vm)
}

最後在 initRender 中若是有 vm.$options.el 還要調用 vm.$mount(vm.$options.el),以下:

if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }

這就是爲何若是不傳遞 el 選項就須要手動 mount 的緣由了。

那麼咱們依照咱們本節開頭的的例子,以及初始化的前後順序來逐一看一看都發生了什麼。咱們將 initState 中的 init* 方法展開來看,執行順序應該是這樣的(從上到下的順序執行):

initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initProps(vm)
initMethods(vm)
initData(vm)
initComputed(vm)
initWatch(vm)
callHook(vm, 'created')
initRender(vm)

首先是 initLifecycle,這個函數的做用就是在實例上添加一些屬性,而後是 initEvents,因爲 vm.$options._parentListeners 的值爲 undefined 因此也僅僅是在實例上添加屬性, vm._updateListeners(listeners) 並不會執行,因爲咱們只傳遞了 eldata,因此 initPropsinitMethodsinitComputedinitWatch 這四個方法什麼都不會作,只有 initData 會執行。最後是 initRender,除了在實例上添加一些屬性外,因爲咱們傳遞了 el 選項,因此會執行 vm.$mount(vm.$options.el)

綜上所述:按照咱們的例子那樣寫,初始化工做只包含兩個主要內容即:initDatainitRender

5、經過 initData 看Vue的數據響應系統

Vue的數據響應系統包含三個部分:ObserverDepWatcher。關於數據響應系統的內容真的已經被文章講爛了,因此我就簡單的說一下,力求你們能理解就ok,咱們仍是先看一下 initData 中的代碼:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? data.call(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
  let i = keys.length
  while (i--) {
    if (props && hasOwn(props, keys[i])) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else {
      proxy(vm, keys[i])
    }
  }
  // observe data
  observe(data)
  data.__ob__ && data.__ob__.vmCount++
}

首先,先拿到 data 數據:let data = vm.$options.data,你們還記得此時 vm.$options.data 的值應該是經過 mergeOptions 合併處理後的 mergedInstanceDataFn 函數嗎?因此在獲得 data 後,它又判斷了 data 的數據類型是否是 ‘function’,最終的結果是:data 仍是咱們傳入的數據選項的 data,即:

data: {
    a: 1,
    b: [1, 2, 3]
}

而後在實例對象上定義 _data 屬性,該屬性與 data 是相同的引用。

而後是一個 while 循環,循環的目的是在實例對象上對數據進行代理,這樣咱們就能經過 this.a 來訪問 data.a 了,代碼的處理是在 proxy 函數中,該函數很是簡單,僅僅是在實例對象上設置與 data 屬性同名的訪問器屬性,而後使用 _data 作數據劫持,以下:

function proxy (vm: Component, key: string) {
  if (!isReserved(key)) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return vm._data[key]
      },
      set: function proxySetter (val) {
        vm._data[key] = val
      }
    })
  }
}

作完數據的代理,就正式進入響應系統,

observe(data)

咱們說過,數據響應系統主要包含三部分:ObserverDepWatcher,代碼分別存放在:observer/index.jsobserver/dep.js 以及 observer/watcher.js 文件中,這回咱們換一種方式,咱們先不看其源碼,你們先跟着個人思路來思考,最後回頭再去看代碼,你會有一種:」奧,不過如此「的感受。

假如,咱們有以下代碼:

var data = {
    a: 1,
    b: {
        c: 2
    }
}

observer(data)

new Watch('a', () => {
    alert(9)
})
new Watch('a', () => {
    alert(90)
})
new Watch('b.c', () => {
    alert(80)
})

這段代碼目的是,首先定義一個數據對象 data,而後經過 observer 對其進行觀測,以後定義了三個觀察者,當數據有變化時,執行相應的方法,這個功能使用Vue的實現原來要如何去實現?其實就是在問 observer 怎麼寫?Watch 構造函數又怎麼寫?接下來咱們逐一實現。

首先,observer 的做用是:將數據對象data的屬性轉換爲訪問器屬性:

class Observer {
    constructor (data) {
        this.walk(data)
    }
    walk (data) {
        // 遍歷 data 對象屬性,調用 defineReactive 方法
        let keys = Object.keys(data)
        for(let i = 0; i < keys.length; i++){
            defineReactive(data, keys[i], data[keys[i]])
        }
    }
}

// defineReactive方法僅僅將data的屬性轉換爲訪問器屬性
function defineReactive (data, key, val) {
    // 遞歸觀測子屬性
    observer(val)

    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val
        },
        set: function (newVal) {
            if(val === newVal){
                return
            }
            // 對新值進行觀測
            observer(newVal)
        }
    })
}

// observer 方法首先判斷data是否是純JavaScript對象,若是是,調用 Observer 類進行觀測
function observer (data) {
    if(Object.prototype.toString.call(data) !== '[object Object]') {
        return
    }
    new Observer(data)
}

上面的代碼中,咱們定義了 observer 方法,該方法檢測了數據data是否是純JavaScript對象,若是是就調用 Observer 類,並將 data 做爲參數透傳。在 Observer 類中,咱們使用 walk 方法對數據data的屬性循環調用 defineReactive 方法,defineReactive 方法很簡單,僅僅是將數據data的屬性轉爲訪問器屬性,並對數據進行遞歸觀測,不然只能觀測數據data的直屬子屬性。這樣咱們的第一步工做就完成了,當咱們修改或者獲取data屬性值的時候,經過 getset 即能獲取到通知。

咱們繼續往下看,來看一下 Watch

new Watch('a', () => {
    alert(9)
})

如今的問題是,Watch 要怎麼和 observer 關聯???????咱們看看 Watch 它知道些什麼,經過上面調用 Watch 的方式,傳遞給 Watch 兩個參數,一個是 ‘a’ 咱們能夠稱其爲表達式,另一個是回調函數。因此咱們目前只能寫出這樣的代碼:

class Watch {
    constructor (exp, fn) {
        this.exp = exp
        this.fn = fn
    }
}

那麼要怎麼關聯呢,你們看下面的代碼會發生什麼:

class Watch {
    constructor (exp, fn) {
        this.exp = exp
        this.fn = fn
        data[exp]
    }
}

多了一句 data[exp],這句話是在幹什麼?是否是在獲取 data 下某個屬性的值,好比 exp 爲 ‘a’ 的話,那麼 data[exp] 就至關於在獲取 data.a 的值,那這會放生什麼?你們不要忘了,此時數據 data 下的屬性已是訪問器屬性了,因此這麼作的結果會直接觸發對應屬性的 get 函數,這樣咱們就成功的和 observer 產生了關聯,但這樣還不夠,咱們仍是沒有達到目的,不過咱們已經無限接近了,咱們繼續思考看一下可不能夠這樣:

既然在 Watch 中對錶達式求值,可以觸發 observerget,那麼可不能夠在 get 中收集 Watch 中函數呢?

答案是能夠的,不過這個時候咱們就須要 Dep 出場了,它是一個依賴收集器。咱們的思路是:data 下的每個屬性都有一個惟一的 Dep 對象,在 get 中收集僅針對該屬性的依賴,而後在 set 方法中觸發全部收集的依賴,這樣就搞定了,看以下代碼:

class Dep {
    constructor () {
        this.subs = []
    }
    addSub () {
        this.subs.push(Dep.target)
    }
    notify () {
        for(let i = 0; i < this.subs.length; i++){
            this.subs[i].fn()
        }
    }
}
Dep.target = null
function pushTarget(watch){
    Dep.target = watch
}

class Watch {
    constructor (exp, fn) {
        this.exp = exp
        this.fn = fn
        pushTarget(this)
        data[exp]
    }
}

上面的代碼中,咱們在 Watch 中增長了 pushTarget(this),能夠發現,這句代碼的做用是將 Dep.target 的值設置爲該Watch對象。在 pushTarget 以後咱們纔對表達式進行求值,接着,咱們修改 defineReactive 代碼以下

function defineReactive (data, key, val) {
    observer(val)
    let dep = new Dep()        // 新增
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.addSub()    // 新增
            return val
        },
        set: function (newVal) {
            if(val === newVal){
                return
            }
            observer(newVal)
            dep.notify()    // 新增
        }
    })
}

如標註,新增了三句代碼,咱們知道,Watch 中對錶達式求值會觸發 get 方法,咱們在 get 方法中調用了 dep.addSub,也就執行了這句代碼:this.subs.push(Dep.target),因爲在這句代碼執行以前,Dep.target 的值已經被設置爲一個 Watch 對象了,因此最終結果就是收集了一個 Watch 對象,而後在 set 方法中咱們調用了 dep.notify,因此當data屬性值變化的時候,就會經過 dep.notify 循環調用全部收集的Watch對象中的回調函數:

notify () {
    for(let i = 0; i < this.subs.length; i++){
        this.subs[i].fn()
    }
}

這樣 observerDepWatch 三者就聯繫成爲一個有機的總體,實現了咱們最初的目標,完整的代碼能夠戳這裏:observer-dep-watch。這裏還給你們挖了個坑,由於咱們沒有處理對數組的觀測,因爲比較複雜而且這又不是咱們討論的重點,若是你們想了解能夠戳個人這篇文章:JavaScript實現MVVM之我就是想監測一個普通對象的變化,另外,在 Watch 中對錶達式求值的時候也只作了直接子屬性的求值,因此若是 exp 的值爲 ‘a.b’ 的時候,就不能夠用了,Vue的作法是使用 . 分割表達式字符串爲數組,而後遍歷一下對其進行求值,你們能夠查看其源碼。以下:

/**
 * Parse simple path.
 */
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  } else {
    const segments = path.split('.')
    return function (obj) {
      for (let i = 0; i < segments.length; i++) {
        if (!obj) return
        obj = obj[segments[i]]
      }
      return obj
    }
  }
}

Vue 的求值代碼是在 src/core/util/lang.js 文件中 parsePath 函數中實現的。總結一下Vue的依賴收集過程應該是這樣的:

Vue的依賴收集過程

實際上,Vue並無直接在 get 中調用 addSub,而是調用的 dep.depend,目的是將當前的 dep 對象收集到 watch 對象中,若是要完整的流程,應該是這樣的:(你們注意數據的每個字段都擁有本身的 dep 對象和 get 方法。)

Vue完整的收集依賴的流程

這樣 Vue 就創建了一套數據響應系統,以前咱們說過,按照咱們的例子那樣寫,初始化工做只包含兩個主要內容即:initDatainitRender。如今 initData 咱們分析完了,接下來看一看 initRender

6、經過 initRender 看Vue的 render(渲染) 與 re-render(從新渲染)

initRender 方法中,由於咱們的例子中傳遞了 el 選項,因此下面的代碼會執行:

if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }

這裏,調用了 $mount 方法,在還原Vue構造函數的時候,咱們整理過全部的方法,其中 $mount 方法在兩個地方出現過:

一、在 web-runtime.js 文件中:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return this._mount(el, hydrating)
}

它的做用是經過 el 獲取相應的DOM元素,而後調用 lifecycle.js 文件中的 _mount 方法。

二、在 web-runtime-with-compiler.js 文件中:

// 緩存了來自 web-runtime.js 的 $mount 方法
const mount = Vue.prototype.$mount
// 重寫 $mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 根據 el 獲取相應的DOM元素
  el = el && query(el)
  // 不容許你將 el 掛載到 html 標籤或者 body 標籤
  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
  // 若是咱們沒有寫 render 選項,那麼就嘗試將 template 或者 el 轉化爲 render 函數
  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) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        warn,
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 調用已經緩存下來的 web-runtime.js 文件中的 $mount 方法
  return mount.call(this, el, hydrating)
}

分析一下可知 web-runtime-with-compiler.js 的邏輯以下:

一、緩存來自 web-runtime.js 文件的 $mount 方法

二、判斷有沒有傳遞 render 選項,若是有直接調用來自 web-runtime.js 文件的 $mount 方法

三、若是沒有傳遞 render 選項,那麼查看有沒有 template 選項,若是有就使用 compileToFunctions 函數根據其內容編譯成 render 函數

四、若是沒有 template 選項,那麼查看有沒有 el 選項,若是有就使用 compileToFunctions 函數將其內容(template = getOuterHTML(el))編譯成 render 函數

五、將編譯成的 render 函數掛載到 this.$options 屬性下,並調用緩存下來的 web-runtime.js 文件中的 $mount 方法

簡單的用一張圖表示 mount 方法的調用關係,從上至下調用:

mount調用關係

不過無論怎樣,咱們發現這些步驟的最終目的是生成 render 函數,而後再調用 lifecycle.js 文件中的 _mount 方法,咱們看看這個方法作了什麼事情,查看 _mount 方法的代碼,這是簡化過得:

Vue.prototype._mount = function (
    el?: Element | void,
    hydrating?: boolean
  ): Component {
    const vm: Component = this

    // 在Vue實例對象上添加 $el 屬性,指向掛載點元素
    vm.$el = el

    // 觸發 beforeMount 生命週期鉤子
    callHook(vm, 'beforeMount')

    vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
    }, noop)

    // 若是是第一次mount則觸發 mounted 生命週期鉤子
    if (vm.$vnode == null) {
      vm._isMounted = true
      callHook(vm, 'mounted')
    }
    return vm
  }

上面的代碼很簡單,該註釋的都註釋了,惟一須要看的就是這段代碼:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

看上去很眼熟有沒有?咱們平時使用Vue都是這樣使用 watch的:

this.$watch('a', (newVal, oldVal) => {

})
// 或者
this.$watch(function(){
    return this.a + this.b
}, (newVal, oldVal) => {

})

第一個參數是 表達式或者函數,第二個參數是回調函數,第三個參數是可選的選項。原理是 Watch 內部對錶達式求值或者對函數求值從而觸發數據的 get 方法收集依賴。但是 _mount 方法中使用 Watcher 的時候第一個參數 vm 是什麼鬼。咱們不妨去看看源碼中 $watch 函數是如何實現的,根據以前還原Vue構造函數中所整理的內容可知:$warch 方法是在 src/core/instance/state.js 文件中的 stateMixin 方法中定義的,源碼以下:

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

咱們能夠發現,$warch 實際上是對 Watcher 的一個封裝,內部的 Watcher 的第一個參數實際上也是 vm 即:Vue實例對象,這一點咱們能夠在 Watcher 的源碼中獲得驗證,代開 observer/watcher.js 文件查看:

export default class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object = {}
  ) {

  }
}

能夠發現真正的 Watcher 第一個參數實際上就是 vm。第二個參數是表達式或者函數,而後以此類推,因此如今再來看 _mount 中的這段代碼:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

忽略第一個參數 vm,也就說,Watcher 內部應該對第二個參數求值,也就是運行這個函數:

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

因此 vm._render() 函數被第一個執行,該函數在 src/core/instance/render.js 中,該方法中的代碼不少,下面是簡化過的:

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    // 解構出 $options 中的 render 函數
    const {
      render,
      staticRenderFns,
      _parentVnode
    } = vm.$options
    ...

    let vnode
    try {
      // 運行 render 函數
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      ...
    }

    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

_render 方法首先從 vm.$options 中解構出 render 函數,你們應該記得:vm.$options.render 方法是在 web-runtime-with-compiler.js 文件中經過 compileToFunctions 方法將 templateel 編譯而來的。解構出 render 函數後,接下來便執行了該方法:

vnode = render.call(vm._renderProxy, vm.$createElement)

其中使用 call 指定了 render 函數的做用域環境爲 vm._renderProxy,這個屬性在咱們整理實例對象的時候知道,他是在 Vue.prototype._init 方法中被添加的,即:vm._renderProxy = vm,其實就是Vue實例對象自己,而後傳遞了一個參數:vm.$createElement。那麼 render 函數究竟是幹什麼的呢?讓咱們根據上面那句代碼猜一猜,咱們已經知道 render 函數是從 templateel 編譯而來的,若是沒錯的話應該是返回一個虛擬DOM對象。咱們不妨使用 console.log 打印一下 render 函數,當咱們的模板這樣編寫時:

<ul id="app">
  <li>{{a}}</li>
</ul>

打印的 render 函數以下:

render函數1

咱們修改模板爲:

<ul id="app">
  <li v-for="i in b">{{a}}</li>
</ul>

打印出來的 render 函數以下:

render函數2

其實瞭解Vue2.x版本的同窗都知道,Vue提供了 render 選項,做爲 template 的代替方案,同時爲JavaScript提供了徹底編程的能力,下面兩種編寫模板的方式實際是等價的:

// 方案一:
new Vue({
    el: '#app',
    data: {
        a: 1
    },
    template: '<ul><li>{{a}}</li><li>{{a}}</li></ul>'
})

// 方案二:
new Vue({
    el: '#app',
    render: function (createElement) {
        createElement('ul', [
            createElement('li', this.a),
            createElement('li', this.a)
        ])
    }
})

如今咱們再來看咱們打印的 render 函數:

function anonymous() {
    with(this){
        return _c('ul', { 
            attrs: {"id": "app"}
        },[
            _c('li', [_v(_s(a))])
        ])
    }
}

是否是與咱們本身寫 render 函數很像?由於 render 函數的做用域被綁定到了Vue實例,即:render.call(vm._renderProxy, vm.$createElement),因此上面代碼中 _c_v_s 以及變量 a至關於Vue實例下的方法和變量。你們還記得諸如 _c_v_s 這樣的方法在哪裏定義的嗎?咱們在整理Vue構造函數的時候知道,他們在 src/core/instance/render.js 文件中的 renderMixin 方法中定義,除了這些以外還有諸如:_l_m_o 等等。其中 _l 就在咱們使用 v-for 指令的時候出現了。因此如今你們知道爲何這些方法都被定義在 render.js 文件中了吧,由於他們就是爲了構造出 render 函數而存在的。

如今咱們已經知道了 render 函數的長相,也知道了 render 函數的做用域是Vue實例自己即:this(或vm)。那麼當咱們執行 render 函數時,其中的變量如:a,就至關於:this.a,咱們知道這是在求值,因此 _mount 中的這段代碼:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

vm._render 執行的時候,所依賴的變量就會被求值,並被收集爲依賴。按照Vue中 watcher.js 的邏輯,當依賴的變量有變化時不只僅回調函數被執行,實際上還要從新求值,即還要執行一遍:

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

這實際上就作到了 re-render,由於 vm._update 就是文章開頭所說的虛擬DOM中的最後一步:patch

vm_render 方法最終返回一個 vnode 對象,即虛擬DOM,而後做爲 vm_update 的第一個參數傳遞了過去,咱們看一下 vm_update 的邏輯,在 src/core/instance/lifecycle.js 文件中有這麼一段代碼:

if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }

若是尚未 prevVnode 說明是首次渲染,直接建立真實DOM。若是已經有了 prevVnode 說明不是首次渲染,那麼就採用 patch 算法進行必要的DOM操做。這就是Vue更新DOM的邏輯。只不過咱們沒有將 virtual DOM 內部的實現。

如今咱們來好好理理思路,當咱們寫以下代碼時:

new Vue({
    el: '#app',
    data: {
        a: 1,
        b: [1, 2, 3]
    }
})

Vue 所作的事:

一、構建數據響應系統,使用 Observer 將數據data轉換爲訪問器屬性;將 el 編譯爲 render 函數,render 函數返回值爲虛擬DOM

二、在 _mount 中對 _update 求值,而 _update 又會對 render 求值,render 內部又會對依賴的變量求值,收集爲被求值的變量的依賴,當變量改變時,_update 又會從新執行一遍,從而作到 re-render

用一張詳細一點的圖表示就是這樣的:

詳細流程

到此,咱們從大致流程,挑着重點的走了一遍Vue,可是還有不少細節咱們沒有說起,好比:

一、將模板轉爲 render 函數的時候,實際是先生成的抽象語法樹(AST),再將抽象語法樹轉成的 render 函數,並且這一整套的代碼咱們也沒有說起,由於他在複雜了,其實這部份內容就是在完正則。

二、咱們也沒有詳細的講 Virtual DOM 的實現原理,網上已經有文章講了,你們能夠搜一搜

三、咱們的例子中僅僅傳遞了 eldata 選項,你們知道 Vue 支持的選項不少,好比咱們都沒有講到,但都是舉一反三的,好比你搞清楚了 data 選項再去看 computed 選項或者 props 選項就會很容易,好比你知道了 Watcher 的工做機制再去看 watch 選項就會很容易。

本篇文章做爲Vue源碼的啓蒙文章,也許還有不少缺陷,全當拋磚引玉了。

相關文章
相關標籤/搜索