提及雙向綁定可能你們都會說:Vue內部經過Object.defineProperty方法屬性攔截的方式,把data對象裏每一個數據的讀寫轉化成getter/setter,當數據變化時通知視圖更新。雖然一句話把大概原理歸納了,可是其內部的實現方式仍是值得深究的,本文就以從簡入繁的形式給你們擼一遍,讓你們瞭解雙向綁定的技術細節。html
讓咱們的數據變得可觀測,實現原理不難,利用Object.defineProperty從新定義對象的屬性描述符。vue
/** * 把一個對象的每一項都轉化成可觀測對象 * @param { Object } obj 對象 */ function observable(obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); keys.forEach((key) => { defineReactive(obj, key, obj[key]) }) return obj; } /** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */ function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log(`${key}屬性被讀取了`); return val; }, set(newVal) { console.log(`${key}屬性被修改了`); val = newVal; } }) } let car = observable({ 'brand': 'BMW', 'price': 3000 }) //測試 console.log(car.brand);
先給一張思惟導向圖吧(圖盜的,連接:https://www.cnblogs.com/libin-1/p/6893712.html),本文章不涉及Compile部分。
這張圖我就不解釋,咱們先跟着一步一步的把代碼擼出來,再回頭來看這張圖,問題不大。
建議在讀以前必定要了解觀察者模式和發佈訂閱模式以及其區別,一篇簡單的文章總結了一下兩種模式的區別(連接:https://www.cnblogs.com/chenlei987/p/10504956.html),Vue的雙向綁定使用的就是觀察者模式,其中Dep對象就是觀察者的目標對象,而Watcher就是觀察者,而後等待Dep對象的通知更新的,其中update方法是由watcher本身管理的,並不是如發佈訂閱模式由目標對象去管理,在觀察者模式中,目標對象管理的訂閱者列表應該是Watcher自己,而不是事件/訂閱主題。react
var Vue = (function (){ class Vue{ constructor (options = {}){ //簡化處理 this.$options = options; let data = (this._data = typeof this.$options.data == 'function' ? this.$options.data() : this.$options.data); Object.keys(data).forEach(key =>{ this._proxy(key) }); // 監聽數據 //observe(data); } _proxy (key){ //用this這個對象去代理 this._data這個對象裏面的key Object.defineProperty(this, key, { configurable: true, enumerable: true, set: (val) => { this._data[key] = val }, get: () =>{ return this._data[key] } }) } } return Vue; } let VM = new Vue({ data (){ return { a: 1, arr: [1,2,3,4,5,6] } }, }); //說明 _proxy代理成功了 console.log(VM.a); VM.a = 2; console.log(VM.a);
注:下面我所說的"data裏面"就是指vue實例的data屬性。
上面代碼Vue類的constructor裏面我註釋了一行代碼,下面我取消註釋,而且開始定義observe函數git
// 監聽數據 observe(data);
在定義observe方法以前,首先明白咱們observe要作什麼?
實參是data數據,咱們要遍歷整個data數據的key,爲data數據的每個key都用Object.defineProperty去從新定義它的 getter和setter函數,從而使其可觀測。面試
class Observer{ constructor (value){ this.value = value; this.walk(value); } // 遍歷屬性值並監聽 walk(value) { Object.keys(value).forEach(key => this.convert(key, value[key])); } // 執行監聽的具體方法 convert(key, val) { defineReactive(this.value, key, val); } } function defineReactive(obj, key, val) { const dep = new Dep(); // 給當前屬性的值添加監聽 let chlidOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { //do something // if (Dep.target) { // dep.depend(); //} return val; }, set: newVal => { if (val === newVal) return; val = newVal; //do something // 對新值進行監聽 //chlidOb = observe(newVal); // 通知全部訂閱者,數值被改變了 //dep.notify(); }, }); } function observe(value) { // 當值不存在,或者不是複雜數據類型時,再也不須要繼續深刻監聽 if (!value || typeof value !== 'object') { return; } return new Observer(value); }
看到在get和set函數裏面的do something了嗎,能夠理解爲在data裏面的每一個key的設置和獲取都被咱們截取到了,在每一個key的設置和獲取時咱們能夠幹些事情了。好比更數據對應的DOM。
要作什麼呢?
get函數: 從思惟圖圖1能夠看出須要把當前的Watcher添加進Dep對象,等待數據更新,調用回調。
set函數: 數據更新,Dep對象通知全部訂閱的watcher更新,調用回調,更新視圖。數組
先聲明一個Watcher類,用於添加進Dep對象並通知更新視圖使用。瀏覽器
let uid = 0; class Watcher { constructor(vm, expOrFn, cb) { // 設置id,用於區分新Watcher和只改變屬性值後新產生的Watcher this.id = uid++; this.vm = vm; // 被訂閱的數據必定來自於當前Vue實例 this.cb = cb; // 當數據更新時想要作的事情 this.expOrFn = expOrFn; // 被訂閱的數據 this.val = this.get(); // 維護更新以前的數據 } // 對外暴露的接口,用於在訂閱的數據被更新時,由訂閱者管理員(Dep)調用 update() { this.run(); } addDep(dep) { // 若是在depIds的hash中沒有當前的id,能夠判斷是新Watcher,所以能夠添加到dep的數組中儲存 // 此判斷是避免同id的Watcher被屢次儲存 //這裏要是不限制重複,你會發如今響應的過程當中,Dep實例下的subs會成倍的增長watcher。多輸入幾個字瀏覽器就卡死了。 if (!dep.depIds.hasOwnProperty(this.id)) { dep.addSubs(this); dep.depIds[this.id] = dep; } } run() { const val = this.get(); if (val !== this.val) { this.val = val; this.cb.call(this.vm, val); } } get() { // 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者 Dep.target = this; //注意:在這裏獲取該屬性 從而就觸發了defineProperty的get方法,該watcher已經進入Dep的subs隊列了 const val = this.vm._data[this.expOrFn]; //初始化執行一遍回調 this.cb.call(this.vm, val); // 置空,用於下一個Watcher使用 Dep.target = null; return val; } }
上面代碼咱們先從constructor看起,接受三個參數,vm當前的vue實例,expOrFn實例化時該watcher實例所 表明/處理 的"data裏面"(‘data裏面’上面有解釋,這裏提醒一下)的哪一個值,cb,回調函數,也就是當數據更新後須要作什麼(天然是更新DOM咯)。
而後在constructor裏面還調用了 this.get()。詳細看一下get函數的定義,兩行代碼須要注意:app
// 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者 Dep.target = this; //注意:在這裏獲取該屬性 從而就觸發了defineProperty的get方法,該watcher已經進入Dep的subs隊列了 const val = this.vm._data[this.expOrFn];
Dep.target = this;
肯定了當前的活動的watcher實例,Dep.target
咱們能夠認爲它是一個全局變量,用於存放當前活動的watcher實例。
const val = this.vm._data[this.expOrFn];
獲取數據,這句話其實就已經觸發了其自身的getter方法(這點要注意,否則你連流程都理解不通)。
進入了getter方法,也就把當前活動的實例的watcher添加進dep對象等待更新。
添加進Dep對象後,置空,用於下一個Watcher使用 Dep.target = null;
ide
一直在說dep對象,咱們必定要知道dep對象就是觀察者模式裏面的目標對象,用於存放watcher和負責通知更新的。
下面來定義一個Dep對象,放到class Watcher前面。 注意Dep的做用範圍.函數
class Dep{ constructor (){ this.depIds = {}; // hash儲存訂閱者的id,避免重複的訂閱者 //訂閱者列表 watcher實例列表 this.subs = []; } depend (){ Dep.target.addDep(this);//至關於調用this.addSubs 將 watcher實例添加進訂閱列表 等待通知更新 //原本按照咱們的理解,在denpend裏面是須要將watcher添加進 Dep對象, 等待通知更新的,因此應該調用 this.addSubs(Dep.target) //可是因爲須要解耦 因此 先調用 watcher的addDep 在addDep中調用Dep實例的addSubs //簡化理解就是 將 watcher實例添加進訂閱列表 等待通知更新 } addSubs (sub) { //這裏的sub確定是watcher實例 this.subs.push(sub); } notify (){ //監聽到值的變化,通知全部訂閱者watcher更新 this.subs.forEach((sub) =>{ sub.update(); }); } } Dep.target = null;//存儲當前活動的watcher
再改改defineReactive,把註釋打開
function defineReactive(obj, key, val) { const dep = new Dep(); // 給當前屬性的值添加監聽 let chlidOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { // 若是Dep類存在target屬性,將其添加到dep實例的subs數組中 // target指向一個Watcher實例,每一個Watcher都是一個訂閱者 // Watcher實例在實例化過程當中,會讀取data中的某個屬性,從而觸發當前get方法 if (Dep.target) { dep.depend(); } return val; }, set: newVal => { if (val === newVal) return; val = newVal; // 對新值進行監聽 chlidOb = observe(newVal); // 通知全部訂閱者,數值被改變了 dep.notify(); }, }); }
而後起一個watcher來監聽
先給Vue暴露一個方法 $watcher 能夠調用實例化Watcher。
class Vue{ constructor (options = {}){ //簡化處理 this.$options = options; let data = (this._data = typeof this.$options.data == 'function' ? this.$options.data() : this.$options.data); Object.keys(data).forEach(key =>{ this._proxy(key) }); // 監聽數據 observe(data); } // 對外暴露調用訂閱者的接口,內部主要在指令中使用訂閱者 $watch(expOrFn, cb) { //property須要監聽的屬性 cb在監聽到更新後的回調 new Watcher(this, expOrFn, cb); } _proxy (key){ //用this這個對象去代理 this._data這個對象裏面的key Object.defineProperty(this, key, { configurable: true, enumerable: true, set: (val) => { this._data[key] = val }, get: () =>{ return this._data[key] } }) } }
html部分
<h3>Vue雙向綁定</h3> <input type="text" id="input"> <p id="react"></p> <h3>Vue數組雙向綁定</h3> <input type="text" id="arr-input"> <p id="arr-reat"></p>
let reactElement = document.querySelector("#react"); let input = document.getElementById('input'); input.addEventListener('keyup', function (e) { VM.a = e.target.value; }); VM.$watch('a', val => reactElement.innerHTML = val); //監聽屬性 a 當a發生改變時 //數組的響應並不能實現 let arrReactElement = document.querySelector("#arr-reat"); let arrInput = document.getElementById('arr-input'); arrInput.addEventListener('keyup', function (e) { VM.arr.push(e.target.value); console.log(VM.arr); }); VM.$watch('arr', val => arrReactElement.innerHTML = val); //監聽屬性 a 當a並無發生改變時
VM.$watch就能夠實例化一個watcher,從而去劫持data裏面某個屬性的改變,在改變時調用回調函數。
數組的改變並無實現。上面的代碼見https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0.html
在說這個以前咱們先去看一看vue官網對於數組更新檢測的說明,連接:https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84%E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B
總的來講,對於數組支持更新的只是數組原型上的方法,對於vm.items[index] = newValue是不支持的。
其實Object.defineProperty對於數組都是不支持的,根據消息vue3.0用的proxy對於數組獲得了完美的支持,可是兼容性不怎麼樣。
既然vue實現了對數組原型方法的支持,那麼咱們也來讓咱們的例子對數組方法也支持吧。
原理不難,vue對於全部的數組原型方法都寫了一層hack,讓其支持更新。那麼下面咱們就一步一步來實現。
/** * Define a expOrFn. */ function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); } //數組改變的監聽 var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator() { var args = [], len = arguments.length; while (len--) args[len] = arguments[len]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); //調用該數組下的 __ob__.dep 詳細可見class Observer的constructor裏的註釋 return result }); });
上面代碼準備了一個arrayMethods的對象,它繼承自Array.prototype,而且對methodsToPatch裏面的方法進行了改寫,後面咱們會把arrayMethods這個對象掛到"data裏面"每一個數組下,讓該數組調用數組原生方法,好比[].push其實調用的是arrayMethods裏面被改寫的方法,從而在該數組改變時獲取到該數組的更新。
下面開始掛載arrayMethods對象,在掛載我以前咱們看到有一個this.__ob__屬性,這裏的this指向要觀測的數組。這個__ob__就是前面的observe對象,而且每一個observe下面還有一個dep對象。下面咱們來理清楚這層關係。
class Observer{ constructor (value){ this.value = value; //下面兩行代碼雖然很簡單,可是咱們須要從這裏理清楚關係 //假如 有數據如 {a: [1,2,3], b: 1}, 而後調用oberve(vm.a),vm當前vue實例 //會自動掛載 __ob__ 和 __ob__.dep // 那麼對數組a進行oberserve的對象就是a.__ob__, 它所對應的dep對象就是 a.__ob__.dep //詳細使用能夠在對數組的方法進行hack的時候 使用到 def(value, '__ob__', this);//讓被監聽的數據都帶上一個不可枚舉的屬性 __ob__ 表明observe對象 this.dep = new Dep();//首先每一個oberserve實例下有一個dep對象 //在這裏處理數組 if (Array.isArray(value)){ //調用數組的hack方法, 讓數組也能被監聽 arrayMethods var arrayKeys = Object.getOwnPropertyNames(arrayMethods); for (var i = 0, l = arrayKeys.length; i < l; i++) { var key = arrayKeys[i]; def(value, key, arrayMethods[key]); } } else{ //對象 遍歷key 添加監聽 this.walk(value); } } //Observer的其餘方法 //... }
上面代碼首先給每一個值掛載__ob__屬性(不可枚舉),而後給每一個Obeserve對象掛載Dep對象。而後根據value的類型,若是是數組就會掛載arrayMethods方法。
如今咱們來理清數組在哪裏依賴收集,在哪裏通知更新的。
在對數組hack的方法裏面(上上一段代碼)有一段ob.dep.notify(); 這裏通知更新,因此依賴收集也必定要收集到value.__ob__.dep對象裏面,兩個dep對象應該是相同的,下面咱們來看看依賴收集寫在哪裏的。
function defineReactive(obj, key, val) { const dep = new Dep(); // 給當前屬性的值添加監聽 let childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { // 若是Dep類存在target屬性,將其添加到dep實例的subs數組中 // target指向一個Watcher實例,每一個Watcher都是一個訂閱者 // Watcher實例在實例化過程當中,會讀取data中的某個屬性,從而觸發當前get方法 if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(val)) { dependArray(val); } } } return val; }, set: newVal => { if (val === newVal) return; val = newVal; // 對新值進行監聽 childOb = observe(newVal); // 通知全部訂閱者,數值被改變了 dep.notify(); }, }); } function dependArray(value) { for (var e = (void 0), i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); } } }
數組雖然在Object.defineProperty裏面set方法沒法響應,可是get方法是沒有問題的,因此在數組get的時候,判斷val若是是array,會調用value.__ob__.dep.depend進行依賴收集。與上面依賴通知使用了贊成個dep對象,也就是掛載在自身的__ob__.dep。
寫到這裏咱們就徹底實現對數組原生方法的支持了。
下面看一下效果 代碼地址:https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E5%AF%B9%E6%95%B0%E7%BB%84%E7%9A%84%E6%94%AF%E6%8C%81.html
html部分
<h3>Vue雙向綁定</h3> <input type="text" id="input"> <p id="react"></p> <h3>Vue數組雙向綁定</h3> <input type="text" id="arr-input"> <p id="arr-reat"></p> <h3>Vue對nextTick實現</h3> <button id="addBtn">加100000次</button> <p id="react-tick"></p>
let reactElement = document.querySelector("#react"); let input = document.getElementById('input'); input.addEventListener('keyup', function (e) { VM.a = e.target.value; }); VM.$watch('a', val => reactElement.innerHTML = val); //監聽屬性 a 當a發生改變時 //數組的響應並能實現 let arrReactElement = document.querySelector("#arr-reat"); let arrInput = document.getElementById('arr-input'); arrInput.addEventListener('keyup', function (e) { VM.arr.push(e.target.value); console.log(VM.arr); }); VM.$watch('arr', val => arrReactElement.innerHTML = val); //監聽屬性 a 當a發生改變時 let reactTick = document.querySelector("#react-tick"); VM.$watch('tickData', val => { console.log(val); reactTick.innerHTML = val; }); //監聽屬性 a 當a發生改變時 document.querySelector('#addBtn').addEventListener('click', function () { for (let i = 0; i < 100000; i++) { VM.tickData = i; } }, false)
效果:
vue官網對nextTick的解釋:
nextTick若是本身實現就是在下一個envet loop執行,不在本次同步任務中執行。
本身實現一個簡單的:
//nextTick的實現 let callbacks = []; let pending = false; function nextTick(cb) { callbacks.push(cb); if (!pending) { pending = true; setTimeout(flushCallbacks, 0); } } function flushCallbacks() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } }
簡單理解: 在本次event loop中收集cb(任務),放到下一個event loop去執行。 關於不知道event loop的能夠參考這篇文章:https://www.cnblogs.com/chenlei987/p/10479433.html,我總結的很簡單。我參考的http://www.ruanyifeng.com/blog/2014/10/event-loop.html。
在理解event loop的同時也須要同時瞭解 microtask和macrotask的區別。
好了言歸正傳,在vue的'data裏面'某個屬性發生了改變,並被觀測到後,調用了watcher.update,並不會當即調用watcher.run去更新視圖,它會通過nextTick以後再更新視圖,提及來有點牽強。
仍是第四部=步的代碼,沒有實現對nextTick的優化。
代碼:
<h3>Vue雙向綁定</h3> <input type="text" id="input"> <p id="react"></p> <h3>Vue數組雙向綁定</h3> <input type="text" id="arr-input"> <p id="arr-reat"></p> <h3>Vue對nextTick實現</h3> <button id="addBtn">加1000次</button> <p id="react-tick"></p> let reactTick = document.querySelector("#react-tick"); VM.$watch('tickData', val => { console.log(val); reactTick.innerHTML = val; }); //監聽屬性 a 當a發生改變時 document.querySelector('#addBtn').addEventListener('click', function () { for (let i = 0; i < 1000; i++) { VM.tickData = i; } }, false)
效果是這樣的:
如今的效果是VM.tickData加1000次,那麼cb(回調)就會調用1000次,這樣是很是影響性能的,咱們想要的效果是不管VM.tickData在本次event loop加多少次,都不會觸發回調,只須要在VM.tickData加完以後,觸發一次最終的cb(回調)就ok了。
下面咱們就來實現這種優化,代碼很少。
//nextTick的實現 let callbacks = []; let pending = false; function nextTick(cb) { callbacks.push(cb); if (!pending) { pending = true; setTimeout(flushCallbacks, 0); } } function flushCallbacks() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } } let has = {}; let queue = []; let waiting = false; function queueWatcher(watcher) { const id = watcher.id; if (has[id] == null) { has[id] = true; queue.push(watcher); if (!waiting) { waiting = true; nextTick(flushSchedulerQueue); } } } function flushSchedulerQueue() { let watcher, id; for (index = 0; index < queue.length; index++) { watcher = queue[index]; id = watcher.id; has[id] = null; watcher.run(); } waiting = false; }
而後更改Watcher裏面的update方法,並不直接調用watcher.run,而是通過queueWatcher控制
update() { queueWatcher(this); // this.run(); }
代碼地址:https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E5%AF%B9nextTck%E7%9A%84%E6%94%AF%E6%8C%81.html
若是面試官問我關於雙向綁定的問題,從這三個方面去回答,Object.definproperty,觀察者模式,nextTick,固然,你須要把這三個點聯繫起來去描述,相信我你把上面的看懂了,聯繫起來徹底沒問題的,你是最棒的!
https://codepen.io/xiaomuzhu/pen/jxBRgj/ https://www.jianshu.com/p/2df6dcddb0d7