超詳細Vue2.1.7原始碼學習
Vue2.1.7原始碼學習
原本文章的名字叫做《原始碼解析》,不過後來想想,還是用“原始碼學習”來的合適一點,在沒有徹底掌握原始碼中的每一個字母之前,“解析”就有點標題黨了。建議在看這篇文章之前,最好開啟2.1.7的原始碼對照著看,這樣可能更容易理解。另外本人水平有限,文中有錯誤或不妥的地方望大家多多指正共同成長。
補充:Vue 2.2 剛剛釋出,作為一個系列文章的第一篇,本篇文章主要從Vue程式碼的組織,Vue建構函式的還原,原型的設計,以及引數選項的處理和已經被寫爛了的資料繫結與如何使用 Virtual DOM 更新檢視入手。從整體的大方向觀察框架,這麼看來 V2.1.7
對於理解 V2.2
很久之前寫過一篇文章:JavaScript實現MVVM之我就是想監測一個普通物件的變化,文章開頭提到了我寫部落格的風格,還是那句話,只寫努力讓小白,甚至是小學生都能看明白的文章。這不免會導致對於某些同學來說這篇文章有些墨跡,所以大家根據自己的喜好,可以詳細的看,也可以跳躍著看。
一、從瞭解一個開源專案入手
要看一個專案的原始碼,不要一上來就看,先去了解一下專案本身的元資料和依賴,除此之外最好也瞭解一下 PR 規則,Issue Reporting 規則等等。特別是“前端”開源專案,我們在看原始碼之前第一個想到的應該是:package.json
在 package.json
檔案中,我們最應該關注的就是 scripts
欄位和 devDependencies
以及 dependencies
欄位,通過 scripts
欄位我們可以知道專案中定義的指令碼命令,通過 devDependencies
和 dependencies
欄位我們可以瞭解專案的依賴情況。
瞭解了這些之後,如果有依賴我們就 npm install
安裝依賴就ok了。
除了 package.json
之外,我們還要閱讀專案的貢獻規則文件,瞭解如何開始,一個好的開源專案肯定會包含這部分內容的,Vue也不例外:https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md
├── 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 中的常用命令部分,來了解如何開始這個專案了,我們可以看到這樣的介紹:
# 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原始碼一邊看自己寫的程式碼想怎麼玩怎麼玩。
二、看原始碼的小提示
在真正步入原始碼世界之前,我想簡單說一說看原始碼的技巧:
注重大體框架,從巨集觀到微觀
當你看一個專案程式碼的時候,最好是能找到一條主線,先把大體流程結構摸清楚,再深入到細節,逐項擊破,拿Vue舉個栗子:假如你已經知道Vue中資料狀態改變後會採用virtual DOM的方式更新DOM,這個時候,如果你不瞭解virtual DOM,那麼聽我一句“暫且不要去研究內部具體實現,因為這會是你喪失主線”,而你僅僅需要知道virtual DOM分為三個步驟:
一、createElement(): 用 JavaScript物件(虛擬樹) 描述 真實DOM物件(真實樹)
二、diff(oldNode, newNode) : 對比新舊兩個虛擬樹的區別,收集差異
三、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下的程式碼時,目的性就會更強,就更容易理解。
三、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建構函式設計相關程式碼的目錄。總結一下,我們尋找的過程是這樣的:
我們回頭看一看 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,然後匯入 initGlobalAPI
和 isServerRendering
,之後將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
檔案主要做了三件事兒:
1、覆蓋
Vue.config
的屬性,將其設定為平臺特有的一些方法
2、Vue.options.directives
和Vue.options.components
安裝平臺特有的指令和元件
3、在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
了,該檔案做了兩件事:
1、快取來自 web-runtime.js
檔案的 $mount
函式
const mount = Vue.prototype.$mount
然後覆蓋覆蓋了 Vue.prototype.$mount
2、在 Vue 上掛載 compile
Vue.compile = compileToFunctions
compileToFunctions 函式的作用,就是將模板 template
編譯為render函式。
至此,我們算是還原了 Vue 建構函式,總結一下:
1、
Vue.prototype
下的屬性和方法的掛載主要是在src/core/instance
目錄中的程式碼處理的2、
Vue
下的靜態屬性和方法的掛載主要是在src/core/global-api
目錄下的程式碼處理的3、
web-runtime.js
主要是新增web平臺特有的配置、元件和指令,web-runtime-with-compiler.js
給Vue的$mount
方法新增compiler
編譯器,支援template
。
四、一個貫穿始終的例子
在瞭解了 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
檔案中定義的。這個檔案第一次看可能會頭大,下面是我處理後的簡略展示,大家看上去應該更容易理解了:
// 1、引用依賴
import Vue from '../instance/index'
其他引用...
// 2、合併父子選項值為最終值的策略物件,此時 strats 是一個空物件,因為 config.optionMergeStrategies = Object.create(null)
const strats = config.optionMergeStrategies
// 3、在 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
}
// 4、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
}
上面的程式碼中,我省略了一些工具函式,例如 mergeHook
和 mergeAssets
等等,唯一需要注意的是這段程式碼:
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</code> 屬性即:<code>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*
方法分別為:initLifecycle
、initEvents
、initState
、initRender
,且在 initState
前後分別回調了生命週期鉤子 beforeCreate
和 created
,而 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</code> 還要呼叫 <code>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</code> 的值為 <code>undefined</code> 所以也僅僅是在例項上新增屬性, <code>vm._updateListeners(listeners)</code> 並不會執行,由於我們只傳遞了 <code>el</code> 和 <code>data</code>,所以 <code>initProps</code>、<code>initMethods</code>、<code>initComputed</code>、<code>initWatch</code> 這四個方法什麼都不會做,只有 <code>initData</code> 會執行。最後是 <code>initRender</code>,除了在例項上新增一些屬性外,由於我們傳遞了 <code>el</code> 選項,所以會執行 <code>vm.$mount(vm.$options.el)
。
綜上所述:按照我們的例子那樣寫,初始化工作只包含兩個主要內容即:initData
和 initRender
。
五、通過 initData 看Vue的資料響應系統
Vue的資料響應系統包含三個部分:Observer
、Dep
、Watcher
。關於資料響應系統的內容真的已經被文章講爛了,所以我就簡單的說一下,力求大家能理解就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</code>,大家還記得此時 <code>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)
我們說過,資料響應系統主要包含三部分:Observer
、Dep
、Watcher
,程式碼分別存放在:observer/index.js
、observer/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屬性值的時候,通過 get
和 set
即能獲取到通知。
我們繼續往下看,來看一下 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
中對錶達式求值,能夠觸發observer
的get
,那麼可不可以在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()
}
}
這樣 observer
、Dep
、Watch
三者就聯絡成為一個有機的整體,實現了我們最初的目標,完整的程式碼可以戳這裡: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並沒有直接在 get
中呼叫 addSub
,而是呼叫的 dep.depend
,目的是將當前的 dep 物件收集到 watch 物件中,如果要完整的流程,應該是這樣的:(大家注意資料的每一個欄位都擁有自己的 dep
物件和 get
方法。)
這樣 Vue 就建立了一套資料響應系統,之前我們說過,按照我們的例子那樣寫,初始化工作只包含兩個主要內容即:initData
和 initRender
。現在 initData
我們分析完了,接下來看一看 initRender
六、通過 initRender 看Vue的 render(渲染) 與 re-render(重新渲染)
在 initRender
方法中,因為我們的例子中傳遞了 el
選項,所以下面的程式碼會執行:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
這裡,呼叫了 $mount</code> 方法,在還原Vue建構函式的時候,我們整理過所有的方法,其中 <code>$mount
方法在兩個地方出現過:
1、在 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
方法。
2、在 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
的邏輯如下:
1、快取來自 web-runtime.js
檔案的 $mount
方法
2、判斷有沒有傳遞 render
選項,如果有直接呼叫來自 web-runtime.js
檔案的 $mount 方法
3、如果沒有傳遞 render
選項,那麼檢視有沒有 template
選項,如果有就使用 compileToFunctions
函式根據其內容編譯成 render
函式
4、如果沒有 template
選項,那麼檢視有沒有 el
選項,如果有就使用 compileToFunctions
函式將其內容(template = getOuterHTML(el))編譯成 render
函式
5、將編譯成的 render
函式掛載到 this.$options</code> 屬性下,並呼叫快取下來的 <code>web-runtime.js</code> 檔案中的 $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</code> 函式是如何實現的,根據之前還原Vue建構函式中所整理的內容可知:<code>$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</code> 中解構出 <code>render</code> 函式,大家應該記得:<code>vm.$options.render
方法是在 web-runtime-with-compiler.js
檔案中通過 compileToFunctions
方法將 template
或 el
編譯而來的。解構出 render
函式後,接下來便執行了該方法:
vnode = render.call(vm._renderProxy, vm.$createElement)
其中使用 call
指定了 render
函式的作用域環境為 vm._renderProxy
,這個屬性在我們整理例項物件的時候知道,他是在 Vue.prototype._init
方法中被新增的,即:vm._renderProxy = vm
,其實就是Vue例項物件本身,然後傳遞了一個引數:vm.$createElement
。那麼 render
函式到底是幹什麼的呢?讓我們根據上面那句程式碼猜一猜,我們已經知道 render
函式是從 template
或 el
編譯而來的,如果沒錯的話應該是返回一個虛擬DOM物件。我們不妨使用 console.log
列印一下 render
函式,當我們的模板這樣編寫時:
<ul id="app">
<li>{{a}}</li>
</ul>
列印的 render
函式如下:
我們修改模板為:
<ul id="app">
<li v-for="i in b">{{a}}</li>
</ul>
打印出來的 render
函式如下:
其實瞭解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 所做的事:
1、構建資料響應系統,使用
Observer
將資料data轉換為訪問器屬性;將el
編譯為render
函式,render
函式返回值為虛擬DOM2、在
_mount
中對_update
求值,而_update
又會對render
求值,render
內部又會對依賴的變數求值,收集為被求值的變數的依賴,當變數改變時,_update
又會重新執行一遍,從而做到re-render
。
用一張詳細一點的圖表示就是這樣的:
到此,我們從大體流程,挑著重點的走了一遍Vue,但是還有很多細節我們沒有提及,比如:
1、將模板轉為 render
函式的時候,實際是先生成的抽象語法樹(AST),再將抽象語法樹轉成的 render
函式