本來文章的名字叫作《源碼解析》,不事後來想一想,仍是用「源碼學習」來的合適一點,在沒有完全掌握源碼中的每個字母以前,「解析」就有點標題黨了。建議在看這篇文章以前,最好打開2.1.7的源碼對照着看,這樣可能更容易理解。另外本人水平有限,文中有錯誤或不妥的地方望你們多多指正共同成長。html
補充:Vue 2.2 剛剛發佈,做爲一個系列文章的第一篇,本篇文章主要從Vue代碼的組織,Vue構造函數的還原,原型的設計,以及參數選項的處理和已經被寫爛了的數據綁定與如何使用 Virtual DOM 更新視圖入手。從總體的大方向觀察框架,這麼看來 V2.1.7
對於理解 V2.2
的代碼不會有太大的影響。該系列文章的後續文章,都會從最新的源碼入手,並對改動的地方作相應的提示。前端
好久以前寫過一篇文章:JavaScript實現MVVM之我就是想監測一個普通對象的變化,文章開頭提到了我寫博客的風格,仍是那句話,只寫努力讓小白,甚至是小學生都能看明白的文章。這難免會致使對於某些同窗來講這篇文章有些墨跡,因此你們根據本身的喜愛,能夠詳細的看,也能夠跳躍着看。vue
要看一個項目的源碼,不要一上來就看,先去了解一下項目自己的元數據和依賴,除此以外最好也瞭解一下 PR 規則,Issue Reporting 規則等等。特別是「前端」開源項目,咱們在看源碼以前第一個想到的應該是:package.json
文件。node
在 package.json
文件中,咱們最應該關注的就是 scripts
字段和 devDependencies
以及 dependencies
字段,經過 scripts
字段咱們能夠知道項目中定義的腳本命令,經過 devDependencies
和 dependencies
字段咱們能夠了解項目的依賴狀況。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源碼一邊看本身寫的代碼想怎麼玩怎麼玩。算法
在真正步入源碼世界以前,我想簡單說一說看源碼的技巧: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下的代碼時,目的性就會更強,就更容易理解。
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
文件主要作了三件事兒:
一、覆蓋
Vue.config
的屬性,將其設置爲平臺特有的一些方法
二、Vue.options.directives
和Vue.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
。
在瞭解了 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 }
上面的代碼中,我省略了一些工具函數,例如 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
屬性即: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
還要調用 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)
並不會執行,因爲咱們只傳遞了 el
和 data
,因此 initProps
、initMethods
、initComputed
、initWatch
這四個方法什麼都不會作,只有 initData
會執行。最後是 initRender
,除了在實例上添加一些屬性外,因爲咱們傳遞了 el
選項,因此會執行 vm.$mount(vm.$options.el)
。
綜上所述:按照咱們的例子那樣寫,初始化工做只包含兩個主要內容即:initData
和 initRender
。
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
,你們還記得此時 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
方法中,由於咱們的例子中傳遞了 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
方法的調用關係,從上至下調用:
不過無論怎樣,咱們發現這些步驟的最終目的是生成 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
方法將 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 所作的事:
一、構建數據響應系統,使用
Observer
將數據data轉換爲訪問器屬性;將el
編譯爲render
函數,render
函數返回值爲虛擬DOM二、在
_mount
中對_update
求值,而_update
又會對render
求值,render
內部又會對依賴的變量求值,收集爲被求值的變量的依賴,當變量改變時,_update
又會從新執行一遍,從而作到re-render
。
用一張詳細一點的圖表示就是這樣的:
到此,咱們從大致流程,挑着重點的走了一遍Vue,可是還有不少細節咱們沒有說起,好比:
一、將模板轉爲 render
函數的時候,實際是先生成的抽象語法樹(AST),再將抽象語法樹轉成的 render
函數,並且這一整套的代碼咱們也沒有說起,由於他在複雜了,其實這部份內容就是在完正則。
二、咱們也沒有詳細的講 Virtual DOM 的實現原理,網上已經有文章講了,你們能夠搜一搜
三、咱們的例子中僅僅傳遞了 el
,data
選項,你們知道 Vue 支持的選項不少,好比咱們都沒有講到,但都是舉一反三的,好比你搞清楚了 data
選項再去看 computed
選項或者 props
選項就會很容易,好比你知道了 Watcher
的工做機制再去看 watch
選項就會很容易。
本篇文章做爲Vue源碼的啓蒙文章,也許還有不少缺陷,全當拋磚引玉了。