--文末附視頻教程javascript
本文主要學習掌握 Vue 雙向綁定的核心部分原理。\
代碼爲簡化版,相對比較簡陋。也未考慮數組等其餘處理。\
歡迎一塊兒學習交流。
MVVM 是 Model-View-ViewModel 的簡寫,雙向數據綁定,即視圖影響模型,模型影響數據。它本質上就是MVC 的改進版。vue
Vue2.x 是經過 Object.defineProperty 實現的雙向數據綁定,該方法不支持 ie8 及如下版本。
相關語法直接查看mdn文檔。
其中,屬性描述符有不少個,下面簡單說明一下經常使用的幾個,具體詳細內容直接查看文檔。
下面是給定義 obj 對象定義一個名稱爲 Vue 的屬性。java
Object.defineProperty(obj, 'vue', { configurable: true, writable: true, enmerbale: true, value: 'Hello, Vue', get() { return 'Hello, Vue' } set(val) { console.log(val) } })
configurable: 指定屬性是否能夠配置,若是不設置爲true,則沒法刪除該屬性,例如:delete obj.vue='react'無效node
writable: 指定屬性是否能被賦值運算符改變,若是不設置爲true,則給 vue 屬性賦值,例如:obj.vue='react'無效react
enmerbale: 指定屬性是否能夠被枚舉到,若是不設置爲true,使用 for...in... 或 Object.keys 是讀不到該屬性的git
value: 指定屬性對應的值,與 get 屬性衝突,一塊兒使用會報錯github
get: 訪問該屬性時,若是有設置 get 方法,會執行這個方法並返回segmentfault
set: 修改該屬性值時,若是有設置 set 方法,會執行這個方法,並把新的值做爲參數傳進入 object.vue = 'hello, Vuex'數組
這裏咱們先看看代碼實現,大概瞭解一下整個過程,最後再對整個過程進行分析。緩存
結合代碼、註釋、過程分析能夠更好的理解整個過程。
參考 Vue2.x 源碼實現,與實際 Vue 的實現有差異,但原理上差很少。建議看完能夠繼續深刻學習 Vue 實際源碼
// 模擬 Vue 的入口 function MVVM(options) { var vm = this; vm.$options = options || {}; vm._data = vm.$options.data; /** * initState 主要對數據對處理 * 實現 observe,即對 data/computed 等作響應式處理以及將數據代理到 vm 實例上 */ initState(vm) // 編譯模版 this.$compile = new Compile(options.el || document.body, this) }
這裏的 Compile 只是簡單的模版編譯,與 Vue 實際Compile 有較大區別,實際的 Compile 實現比較複雜,須要通過 parse、optimize、generate 三個階段處理。
function Compile(el, vm) { this.$vm = vm; this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) { // 將原生節點轉爲文檔碎片節點,提升操做效率 this.$fragment = this.node2Fragment(this.$el); // 編譯模版內容,同時進行依賴收集 this.compile(this.$fragment); // 將處理後的 dom 樹掛載到真實 dom 節點中 this.$el.appendChild(this.$fragment); } } // compile 相關方法實現 Compile.prototype = { node2Fragment(el) { const fragment = document.createDocumentFragment(); /** * 將原生節點拷貝到 fragment, * 每次循環都會把 el 中的第一個節點取出來追加到 fragment 後面,直到 el 沒有字節點 */ let child; while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, compile: function (el) { const childNodes = el.childNodes // childNodes 不是標準數組,經過 Array.from 把 childNodes 轉成數組並遍歷處理每個節點。 Array.from(childNodes).forEach(node => { // 利用閉包機制,保存文本節點最初的文本,後面更新根據最初的文本進行替換更新。 const text = node.textContent; // 元素節點,對元素屬性綁定對指令進行處理 if (this.isElementNode(node)) { this.compileElement(node); } // 文本節點而且包含 {{xx}} 字符串對文本,模版內容替換 else if (this.isTextNode(node) && /\{\{(.*)\}\}/.test(text)) { this.compileText(node, RegExp.$1.trim(), text); } // 遞歸編譯子節點的內容 if (node.childNodes && node.childNodes.length) { this.compile(node); } }); }, compileElement: function (node) { const nodeAttrs = node.attributes Array.from(nodeAttrs).forEach(attr => { const attrName = attr.name; // 判斷屬性是不是一個指令,例如: v-text 等 if (this.isDirective(attrName)) { const exp = attr.value; const dir = attrName.substring(2); // 事件指令 if (this.isEventDirective(dir)) { compileUtil.eventHandler(node, this.$vm, exp, dir); } // 普通指令 else { compileUtil[dir] && compileUtil[dir](node, this.$vm, exp); } node.removeAttribute(attrName); } }); }, compileText: function (node, exp) { // compileUtil.text(node, this.$vm, exp); // 利用閉包機制,保存文本節點最初的文本,後面更新根據最初的文本進行替換更新。 const vm = this.$vm let text = node.textContent const updaterFn = updater.textUpdater let value = text.replace(/\{\{(.*)\}\}/, compileUtil._getVMVal(vm, exp)) updaterFn && updaterFn(node, value); new Watcher(vm, exp, function (value) { updaterFn && updaterFn(node, text.replace(/\{\{(.*)\}\}/, value)); }); }, // ... 省略 };
指令集合處理
// 指令處理集合 const compileUtil = { text: function (node, vm, exp) { this.update(node, vm, exp, 'text'); }, // ... 省略 update: function (node, vm, exp, dir) { // 針對不一樣的指令使用不一樣的函數渲染、更新數據。 const updaterFn = updater[dir + 'Updater']; // 這裏取值,而後進行初次的內容渲染 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); new Watcher(vm, exp, function (value, oldValue) { updaterFn && updaterFn(node, value, oldValue); }); }, // ... 省略 }; const updater = { textUpdater: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }, // ... 省略 };
initState
方法主要是對 props
、methods
、data
、computed
和 wathcer
等屬性作了初始化操做。這裏咱們主要實現對 data
跟 computed
的操做。
function initState(vm) { const opts = vm.$options // 初始化 data if (opts.data) { initData(vm) } else { observe(vm._data = {}, true) } // 初始化 computed if (opts.computed) initComputed(vm, opts.computed) }
主要實現如下兩個操做:
observe
方法觀測整個 data
的變化,把 data
也變成響應式,能夠經過 vm._data.xxx
訪問到定義 data
返回函數中對應的屬性。data
函數返回對象的遍歷,經過 proxy
把每個值 vm._data.xxx
都代理到 vm.xxx
上。function initData(vm) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {} // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (methods && hasOwn(methods, key)) { console.log(`Method "${key}" has already been defined as a data property.`, vm) } if (props && hasOwn(props, key)) { console.log(`The data property "${key}" is already declared as a prop. Use prop default value instead.`, vm) } else if (!isReserved(key)) { // 數據代理,實現 vm.xxx -> vm._data.xxx,至關於 vm 上面多了 xxx 這個屬性 proxy(vm, `_data`, key) } } // observe data observe(data, true) }
把每個值 vm._data.xxx
都代理到 vm.xxx
上。
這是一個公用的方法。這裏咱們只是對 data 定義對屬性作裏代理。實際上 vue 還經過這個方法對 props 也作了代理,proxy(vm, '_props', key)
。
// 數據代理,proxy(vm, '_data', key)。 function proxy(target, sourceKey, key) { Object.defineProperty(target, key, { enumerable: true, configurable: true, get: function proxyGetter() { // initData 裏把 vm._data 處理成響應式對象。 // 這裏返回 this['_data'][key],實現 vm[key] -> vm._data[key] return this[sourceKey][key] }, set: function proxySetter(val) { // 這裏修改 vm[key] 其實是修改了 this['_data'][key] this[sourceKey][key] = val } }) }
observe
的功能就是用來監測數據的變化。
function observe(value) { if (!isObject(value)) { return } return new Observer(value); }
Observer
是一個類,它的做用是給對象的屬性添加 getter 和 setter,用於依賴收集和派發更新
class Observer { constructor (value) { this.value = value this.dep = new Dep() this.walk(value) } walk (obj) { // 遍歷 data 對象的 key 調用 defineReactive 方法建立響應式對象 const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } }
defineReactive
的功能就是定義一個響應式對象,給對象動態添加 getter 和 setter,getter 作的事情是依賴收集,setter 作的事情是派發更新。
function defineReactive (obj, key) { // 初始化 Dep,用於依賴收集 const dep = new Dep() let val = obj[key] // 對子對象遞歸調用 observe 方法,這樣就保證了不管 obj 的結構多複雜, // 它的全部子屬性也能變成響應式的對象, // 這樣咱們訪問或修改 obj 中一個嵌套較深的屬性,也能觸發 getter 和 setter。 // 使 foo.bar 等多層的對象也能夠實現響應式。 let childOb = observe(val) // Object.defineProperty 去給 obj 的屬性 key 添加 getter 和 setter Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // Dep.target 指向 watcher if (Dep.target) { // 依賴收集,每一個使用到 data 裏的值的地方,都會調用一次 get,而後就會被收集到一個數組中。 dep.depend() if (childOb) { childOb.dep.depend() } } return val }, set: function reactiveSetter (newVal) { // 當值沒有變化時,直接返回 if (newVal === val) { return } // 對 val 設置新的 val = newVal // 若是新傳入的值時一個對象,須要從新進行 observe,給對象的屬性作響應式處理。 childOb = observe(newVal) dep.notify() } }) }
Dep
是整個 getter 依賴收集的核心,這裏須要特別注意的是它有一個靜態屬性 target
,這是一個全局惟一 Watcher
,這是一個很是巧妙的設計,由於在同一時間只能有一個全局的 Watcher
被計算,另外它的自身屬性 subs
是 Watcher
的數組。
Dep
實際上就是對 Watcher
的一種管理,Dep
脫離 Watcher
單獨存在是沒有意義的。
class Dep { static target; constructor () { // 存放 watcher 的地方 this.subs = [] } addSub (sub) { this.subs.push(sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } // 派發更新 notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null
class Watcher { constructor(vm, expOrFn, cb) { this.vm = vm this.cb = cb this.expOrFn = expOrFn; this.depIds = {}; // 判斷 expOrFn 是否是一個函數,若是不是函數會經過 parsePath 把它變成一個函數。 if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // parsePath 把 expOrFn 變成一個函數 this.getter = parsePath(expOrFn) || function noop (a, b, c) {} } // 取值,觸發依賴收集。 this.value = this.get() } get() { // 這裏 Dep.target 指向 watcher 自己,而後會取值,取值觸發對應屬性的 getter 方法。 // 此時 getter 方法裏面使用的 Dep.target 就有值了。 // 經過一系列的代碼執行 dep.depend() -> Dep.target.addDep(dep) -> dep.addSub(watcher) // 最後把 watcher 存到 subs 數組裏,完成依賴收集。 // 最後把 Dep.target 刪除,保證來 Dep.target 在同一時間內只有惟一一個。 Dep.target = this; const vm = this.vm let value = this.getter.call(vm, vm) Dep.target = null; return value } addDep(dep) { if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this); this.depIds[dep.id] = dep; } } update() { // this.value 是 watcher 緩存的值,用來與改變後的值進行對比,若是先後值沒有變化,就不進行更新。 const value = this.get() const oldValue = this.value if (value !== oldValue) { // 緩存新的值,下次操做用 this.value = value // 以 vm 爲 cb 的 this 值,調用 cb。 // cb 就是 在 new watcher 使傳入的更新函數。會把新的值傳入經過更新函數,更新到視圖上。 this.cb.call(this.vm, value, oldValue) } } }
new MVVM()
的時候,首先,會對 data
、props
、computed
進行初始化,使它們變成響應式的對象。Object.defineProperty
給對象的屬性設置 get
、set
,爲屬性提供 getter、setter 方法,一旦對象擁有了 getter 和 setter,咱們能夠簡單地把這個對象稱爲響應式對象。。Dep
實現依賴收集的。getter 方法中調用了 Dep.depend()
進行收集,Dep.depend()
中又調用了 Dep.target.addDep(this)
。Dep.target
是個很是巧妙的設計,由於在同一時間 Dep.target
只指向一個 Watcher
,使得同一時間內只能有一個全局的 Watcher
被計算。Dep.target.addDep(this)
等於調用 Watcher.addDep(dep)
,裏面又調用了 dep.addSub(this)
把這個全局惟一的 watcher 添加到 dep.subs
數組中,收集了起來,而且 watcher 自己也經過 depIds
收集持有的 Dep
實例。compile
把模版編譯成 render
函數,並在 render
函數中訪問數據對象觸發 getter。這裏咱們是直接在 compile
的時候訪問數據對象觸發 getter。compile
負責內容的渲染與數據更新。compile
編譯模版中的內容,把模版中的 {{xx}} 字符串替換成對應的屬性值時會訪問數據對象觸發 getter,不過此時尚未 watcher
,沒有依賴收集。compile
接下來會實例化 Watcher
,實例化過程會再去取一次值,此時觸發到 getter 纔會進行依賴收集。具體看 Watcher
的 構造函數與 get 方法實現。dep.notify()
,循環調用 subs
裏面保存的 watcher
的 update
方法進行更新。