Vue2 源碼分析

FloGlaspie 7年前發布 | 8K 次閱讀 Vue.js 源碼分析 Vue.js開發

分析目標

通過閱讀源碼,對 Vue2 的基礎運行機制有所了解,主要是:

  • Vue2 中數據綁定的實現方式
  • Vue2 中對 Virtual DOM 機制的使用方式

源碼初見

項目構建配置文件為 build/config.js ,定位 vue.js 對應的入口文件為 src/entries/web-runtime-with-compiler.js ,基于 rollup 進行模塊打包。

代碼中使用 flow 進行接口類型標記和檢查,在打包過程中移除這些標記。為了閱讀代碼方便,在 VS Code 中安裝了插件 Flow Language Support ,然后關閉工作區 JS 代碼檢查,這樣界面就清爽很多了。

Vue 應用啟動一般是通過 new Vue({...}) ,所以,先從該構造函數著手。

注:本文只關注 Vue 在瀏覽器端的應用,不涉及服務器端代碼。

Vue 構造函數

文件: src/core/instance/index.js

該文件只是構造函數,Vue 原型對象的聲明分散在當前目錄的多個文件中:

  • init.js: ._init()
  • state.js: .$data .$set() .$delete() .$watch()
  • render.js: ._render() ...
  • events.js: .$on() .$once() .$off() .$emit()
  • lifecycle.js: ._mount() ._update() .$forceUpdate() .$destroy()

構造函數接收參數 options ,然后調用 this._init(options) 。

._init() 中進行初始化,其中會依次調用 lifecycle、events、render、state 模塊中的初始化函數。

Vue2 中應該是為了代碼更易管理,Vue 類的定義分散到了上面的多個文件中。

其中,對于 Vue.prototype 對象的定義,通過 mixin 的方式在入口文件 core/index.js 中依次調用。對于實例對象(代碼中通常稱為 vm )則通過 init 函數在 vm._init() 中依次調用。

Vue 公共接口

文件: src/core/index.js

這里調用了 initGlobalAPI() 來初始化 Vue 的公共接口,包括:

  • Vue.util
  • Vue.set
  • Vue.delete
  • Vue.nextTick
  • Vue.options
  • Vue.use
  • Vue.mixin
  • Vue.extend
  • asset相關接口:配置在 src/core/config.js 中

Vue 啟動過程

調用 new Vue({...}) 后,在內部的 ._init() 的最后,是調用 .$mount() 方法來“啟動”。

在 web-runtime-with-compiler.js 和 web-runtime.js 中,定義了 Vue.prototype.$mount() 。不過兩個文件中的 $mount() 最終調用的是 ._mount() 內部方法,定義在文件 src/core/instance/lifecycle.js 中。

Vue.prototype._mount(el, hydrating)

簡化邏輯后的偽代碼:

vm = this
vm._watcher = new Watcher(vm, updateComponent)

接下來看 Watcher 。

Watcher

文件: src/core/observer/watcher.js

先看構造函數的簡化邏輯:

// 參數:vm, expOrFn, cb, options
this.vm = vm
vm._watchers.push(this)
// 解析 options,略....
// 屬性初始化,略....
this.getter = expOrFn // if `function`
this.value = this.lazy ? undefined : this.get()

由于缺省的 lazy 屬性值為 false ,接著看 .get() 的邏輯:

pushTarget(this) // !
value = this.getter.call(this.vm, this.vm)
popTarget()
this.cleanupDeps()
return value

先看這里對 getter 的調用,返回到 ._mount() 中,可以看到,是調用了 vm._update(vm._render(), hydrating) ,涉及兩個方法:

  • vm._render():返回虛擬節點(VNode)
  • vm._update()

來看 _update() 的邏輯,這里應該是進行 Virtual DOM 的更新:

// 參數:vnode, hydrating
vm = this
prevEl = vm.$el
prevVnode = vm._vnode
prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
  // 初次加載
  vm.$el = vm.__patch__(vm.$el, vnode, ...)
} else {
  // 更新
  vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// 后續屬性配置,略....

參考 Virtual DOM 的一般邏輯,這里是差不多的處理過程,不再贅述。

綜上,這里的 watcher 主要作用應該是在數據發生變更時,觸發重新渲染和更新視圖的處理: vm._update(vm._render()) 。

接下來,我們看下 watcher 是如何發揮作用的,參考 Vue 1.0 的經驗,下面應該是關于依賴收集、數據綁定方面的細節了,而這一部分,和 Vue 1.0 差別不大。

數據綁定

watcher.get() 中調用的 pushTarget() 和 popTarget() 來自文件: src/core/observer/dep.js 。

pushTarget() 和 popTarget() 兩個方法,用于處理 Dep.target ,顯然 Dep.target 在 wather.getter 的調用過程中會用到,調用時會涉及到依賴收集,從而建立起數據綁定的關系。

在 Dep 類的 .dep() 方法中用到了 Dep.target ,調用方式為:

Dep.target.addDep(this)

可以想見,在使用數據進行渲染的過程中,會對數據屬性進行“讀”操作,從而觸發 dep.depend() ,進而收集到這個依賴關系。下面來找一下這樣的調用的位置。

在 state.js 中找到一處, makeComputedGetter() 函數中通過 watcher.depend() 間接調用了 dep.depend() 。不過 computedGetter 應該不是最主要的地方,根據 Vue 1.0 的經驗,還是要找對數據進行“數據劫持”的地方,應該是 defineReactive() 。

defineReactive() 定義在文件 src/core/observer/index.js 。

// 參數:obj, key, val, customSetter?
dep = new Dep()
childOb = observe(val)
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function () {
    // 略,調用了 dep.depend()
  },
  set: function () {
    // 略,調用 dep.notify()
  }
})

結合 Vue 1.0 經驗,這里應該就是數據劫持的關鍵了。數據原有的屬性被重新定義,屬性的 get() 被調用時,會通過 dep.depend() 收集依賴關系,記錄到 vm 中;而在 set() 被調用時,則會判斷屬性值是否發生變更,如果發生變更,則通過 dep.notify() 來通知 vm,從而觸發 vm 的更新操作,實現 UI 與數據的同步,這也就是數據綁定后的效果了。

回過頭來看 state.js ,是在 initProps() 中調用了 defineReactive() 。而 initProps() 在 initState() 中調用,后者則是在 Vue.prototype._init() 中被調用。

不過最常用的其實是在 initData() 中,對初始傳入的 data 進行劫持,不過里面的過程稍微繞一些,是將這里的 data 賦值到 vm._data 并且代理到了 vm 上,進一步的處理還涉及 observe() 和 Observer 類。這里不展開了。

綜上,數據綁定的實現過程為:

  • 初始化:new Vue() -> vm._init()
  • 數據劫持:initState(vm) -> initProps(), initData() -> dep.depend()
  • 依賴收集:vm.$mount() -> vm._mount() -> new Watcher() -> vm._render()

渲染

首先來看 initRender() ,這里在 vm 上初始化了兩個與創建虛擬元素相關的方法:

  • vm._c()
  • vm.$createElement()

其內部實現都是調用 createElement() ,來自文件: src/core/vdom/create-element.js 。

而在 renderMixin() 中初始化了 Vue.prototype._render() 方法,其中創建 vnode 的邏輯為:

render = vm.$options.render
try {
  vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
  // ...
}

這里傳入 render() 是一個會返回 vnode 的函數。

接下來看 vm._update() 的邏輯,這部分在前面有介紹,初次渲染時是通過調用 vm.__patch__() 來實現。那么 vm.__patch__() 是在哪里實現的呢?在 _update() 代碼中有句注釋,提到:

// Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.

在文件 web-runtime.js 中,找到了:

Vue.prototype.__patch__ = inBrowser ? patch : noop

顯然示在瀏覽器環境下使用 patch() ,來自: src/platforms/web/runtime/patch.js ,其實現是通過 createPatchFunction() ,來自文件 src/core/vdom/patch 。

OK,以上線索都指向了 vdom 相關的模塊,也就是說,顯然是 vdom 也就是 Virtual DOM 參與了渲染和更新。

不過還有個問題沒有解決,那就是原始的字符串模塊,是如何轉成用于 Virtual DOM 創建的函數調用的呢?這里會有一個解析的過程。

回到入口文件 web-runtime-with-compiler.js ,在 Vue.prototype.$mount() 中,有一個關鍵的調用: compileToFunctions(template, ...) , template 變量值為傳入的參數解析得到的模板內容。

模板解析

文件: src/platforms/web/compiler/index.js

函數 compileToFunctions() 的基本邏輯:

// 參數:template, options?, vm?
res = {}
compiled = compile(template, options)
res.render = makeFunction(compiled.render)
// 拷貝數組元素:
// res.staticRenderFns <= compiled.staticRenderFns
return res

這里對模板進行了編譯( compile() ),最終返回了根據編譯結果得到的 render()、staticRenderFns 。再看 web-runtime-with-compiler.js 中 Vue.prototype.$mount() 的邏輯,則是將這里得到的結果寫入了 vm.$options 中,也就是說,后面 vm._render() 中會使用這里的 render() 。

再來看 compile() 函數,這里是實現模板解析的核心,來做文件 src/compiler/index.js ,基本邏輯為:

// 參數:template, options
ast = parse(template.trim(), options)
optimize(ast, options)
code = generate(ast, options)
return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
}

邏輯很清晰,首先從模板進行解析得到抽象語法樹(ast),進行優化,最后生成結果代碼。整個過程中肯定會涉及到 Vue 的語法,包括指令、組件嵌套等等,不僅僅是得到構建 Virtual DOM 的代碼。

需要注意的是,編譯得到 render 其實是代碼文本,通過 new Function(code) 的方式轉為函數。

總結

Vue2 相比 Vue1 一個主要的區別在于引入了 Virtual DOM,但其 MVVM 的特性還在,也就是說仍有一套數據綁定的機制。

此外,Virtual DOM 的存在,使得原有的視圖模板需要轉變為函數調用的模式,從而在每次有更新時可以重新調用得到新的 vnode,從而應用 Virtual DOM 的更新機制。為此,Vue2 實現了編譯器(compiler),這也意味著 Vue2 的模板可以是純文本,而不必是 DOM 元素。

Vue2 基本運行機制總結為:

  • 文本模板,編譯得到生成 vnode 的函數(render),該過程中會識別并記錄 Vue 的指令和其他語法
  • new Vue() 得到 vm 對象,其中傳入的數據會進行數據劫持處理,從而可以收集依賴,實現數據綁定
  • 渲染過程是將所有數據交由渲染函數(render)進行調用得到 vnode,應該 Virtual DOM 的機制實現初始渲染和更新

寫在最后

對 Vue2 的源碼分析,是基于我之前對 Vue1 的分析和對 Virtual DOM 的了解。

 

 

 

 

來自:http://www.jianshu.com/p/758da47bfdac

 

 本文由用戶 FloGlaspie 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!