双向绑定篇
面試官: 實現(xiàn)雙向綁定Proxy比defineproperty優(yōu)劣如何?
面試官系列(4): 實現(xiàn)雙向綁定Proxy比defineproperty優(yōu)劣如何?
往期
- 面試官系列(1): 如何實現(xiàn)深克隆
- 面試官系列(2): Event Bus的實現(xiàn)
- 面試官系列(3): 前端路由的實現(xiàn)
前言
雙向綁定其實已經(jīng)是一個老掉牙的問題了,只要涉及到MVVM框架就不得不談的知識點,但它畢竟是Vue的三要素之一.
Vue三要素
- 響應(yīng)式: 例如如何監(jiān)聽數(shù)據(jù)變化,其中的實現(xiàn)方法就是我們提到的雙向綁定
- 模板引擎: 如何解析模板
- 渲染: Vue如何將監(jiān)聽到的數(shù)據(jù)變化和解析后的HTML進(jìn)行渲染
可以實現(xiàn)雙向綁定的方法有很多,KnockoutJS基于觀察者模式的雙向綁定,Ember基于數(shù)據(jù)模型的雙向綁定,Angular基于臟檢查的雙向綁定,本篇文章我們重點講面試中常見的基于數(shù)據(jù)劫持的雙向綁定。
常見的基于數(shù)據(jù)劫持的雙向綁定有兩種實現(xiàn),一個是目前Vue在用的Object.defineProperty,另一個是ES2015中新增的Proxy,而Vue的作者宣稱將在Vue3.0版本后加入Proxy從而代替Object.defineProperty,通過本文你也可以知道為什么Vue未來會選擇Proxy。
嚴(yán)格來講Proxy應(yīng)該被稱為『代理』而非『劫持』,不過由于作用有很多相似之處,我們在下文中就不再做區(qū)分,統(tǒng)一叫『劫持』。
我們可以通過下圖清楚看到以上兩種方法在雙向綁定體系中的關(guān)系.
<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-6f9b58-1526012269856-2)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
基于數(shù)據(jù)劫持的當(dāng)然還有已經(jīng)涼透的Object.observe方法,已被廢棄。
提前聲明: 我們沒有對傳入的參數(shù)進(jìn)行及時判斷而規(guī)避錯誤,僅僅對核心方法進(jìn)行了實現(xiàn).
文章目錄
1.基于數(shù)據(jù)劫持實現(xiàn)的雙向綁定的特點
1.1 什么是數(shù)據(jù)劫持
數(shù)據(jù)劫持比較好理解,通常我們利用Object.defineProperty劫持對象的訪問器,在屬性值發(fā)生變化時我們可以獲取變化,從而進(jìn)行進(jìn)一步操作。
// 這是將要被劫持的對象 const data = {name: '', }; function say(name) { if (name === '古天樂') { console.log('給大家推薦一款超好玩的游戲'); } else if (name === '渣渣輝') { console.log('戲我演過很多,可游戲我只玩貪玩懶月'); } else { console.log('來做我的兄弟'); } } // 遍歷對象,對其屬性值進(jìn)行劫持 Object.keys(data).forEach(function(key) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { console.log('get'); }, set: function(newVal) { // 當(dāng)屬性值發(fā)生變化時我們可以進(jìn)行額外操作 console.log(`大家好,我系${newVal}`); say(newVal); }, }); }); data.name = '渣渣輝'; //大家好,我系渣渣輝 //戲我演過很多,可游戲我只玩貪玩懶月1.2 數(shù)據(jù)劫持的優(yōu)勢
目前業(yè)界分為兩個大的流派,一個是以React為首的單向數(shù)據(jù)綁定,另一個是以Angular、Vue為主的雙向數(shù)據(jù)綁定。
其實三大框架都是既可以雙向綁定也可以單向綁定,比如React可以手動綁定onChange和value實現(xiàn)雙向綁定,也可以調(diào)用一些雙向綁定庫,Vue也加入了props這種單向流的api,不過都并非主流賣點。
單向或者雙向的優(yōu)劣不在我們的討論范圍,我們需要討論一下對比其他雙向綁定的實現(xiàn)方法,數(shù)據(jù)劫持的優(yōu)勢所在。
1.3 基于數(shù)據(jù)劫持雙向綁定的實現(xiàn)思路
數(shù)據(jù)劫持是雙向綁定各種方案中比較流行的一種,最著名的實現(xiàn)就是Vue。
基于數(shù)據(jù)劫持的雙向綁定離不開Proxy與Object.defineProperty等方法對對象/對象屬性的"劫持",我們要實現(xiàn)一個完整的雙向綁定需要以下幾個要點。
<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-1f5ab-1526012269856-1)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
我們看到,雖然Vue運用了數(shù)據(jù)劫持,但是依然離不開發(fā)布訂閱的模式,之所以在系列2做了Event Bus的實現(xiàn),就是因為我們不管在學(xué)習(xí)一些框架的原理還是一些流行庫(例如Redux、Vuex),基本上都離不開發(fā)布訂閱模式,而Event模塊則是此模式的經(jīng)典實現(xiàn),所以如果不熟悉發(fā)布訂閱模式,建議讀一下系列2的文章。
2.基于Object.defineProperty雙向綁定的特點
關(guān)于Object.defineProperty的文章在網(wǎng)絡(luò)上已經(jīng)汗牛充棟,我們不想花過多時間在Object.defineProperty上面,本節(jié)我們主要講解Object.defineProperty的特點,方便接下來與Proxy進(jìn)行對比。
對Object.defineProperty還不了解的請閱讀文檔
兩年前就有人寫過基于Object.defineProperty實現(xiàn)的文章,想深入理解Object.defineProperty實現(xiàn)的推薦閱讀,本文也做了相關(guān)參考。
上面我們推薦的文章為比較完整的實現(xiàn)(400行代碼),我們在本節(jié)只提供一個極簡版(20行)和一個簡化版(150行)的實現(xiàn),讀者可以循序漸進(jìn)地閱讀。
2.1 極簡版的雙向綁定
我們都知道,Object.defineProperty的作用就是劫持一個對象的屬性,通常我們對屬性的getter和setter方法進(jìn)行劫持,在對象的屬性發(fā)生變化時進(jìn)行特定的操作。
我們就對對象obj的text屬性進(jìn)行劫持,在獲取此屬性的值時打印'get val',在更改屬性值的時候?qū)OM進(jìn)行操作,這就是一個極簡的雙向綁定。
const obj = {}; Object.defineProperty(obj, 'text', {get: function() { console.log('get val');  }, set: function(newVal) { console.log('set val:' + newVal); document.getElementById('input').value = newVal; document.getElementById('span').innerHTML = newVal; } }); const input = document.getElementById('input'); input.addEventListener('keyup', function(e){ obj.text = e.target.value; })在線示例 極簡版雙向綁定 by Iwobi (@xiaomuzhu) on CodePen.
2.2 升級改造
我們很快會發(fā)現(xiàn),這個所謂的雙向綁定貌似并沒有什么亂用。。。
原因如下:
那么如何解決上述問題?
Vue的操作就是加入了發(fā)布訂閱模式,結(jié)合Object.defineProperty的劫持能力,實現(xiàn)了可用性很高的雙向綁定。
首先,我們以發(fā)布訂閱的角度看我們第一部分寫的那一坨代碼,會發(fā)現(xiàn)它的監(jiān)聽、發(fā)布和訂閱都是寫在一起的,我們首先要做的就是解耦。
我們先實現(xiàn)一個訂閱發(fā)布中心,即消息管理員(Dep),它負(fù)責(zé)儲存訂閱者和消息的分發(fā),不管是訂閱者還是發(fā)布者都需要依賴于它。
let uid = 0;// 用于儲存訂閱者并發(fā)布消息class Dep { constructor() { // 設(shè)置id,用于區(qū)分新Watcher和只改變屬性值后新產(chǎn)生的Watcher this.id = uid++; // 儲存訂閱者的數(shù)組 this.subs = []; } // 觸發(fā)target上的Watcher中的addDep方法,參數(shù)為dep的實例本身 depend() { Dep.target.addDep(this); } // 添加訂閱者 addSub(sub) { this.subs.push(sub); } notify() { // 通知所有的訂閱者(Watcher),觸發(fā)訂閱者的相應(yīng)邏輯處理 this.subs.forEach(sub => sub.update()); } } // 為Dep類設(shè)置一個靜態(tài)屬性,默認(rèn)為null,工作時指向當(dāng)前的Watcher Dep.target = null;現(xiàn)在我們需要實現(xiàn)監(jiān)聽者(Observer),用于監(jiān)聽屬性值的變化。
// 監(jiān)聽者,監(jiān)聽對象屬性值的變化class Observer { constructor(value) { this.value = value; this.walk(value); } // 遍歷屬性值并監(jiān)聽 walk(value) { Object.keys(value).forEach(key => this.convert(key, value[key])); } // 執(zhí)行監(jiān)聽的具體方法 convert(key, val) { defineReactive(this.value, key, val); } } function defineReactive(obj, key, val) { const dep = new Dep(); // 給當(dāng)前屬性的值添加監(jiān)聽 let chlidOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { // 如果Dep類存在target屬性,將其添加到dep實例的subs數(shù)組中 // target指向一個Watcher實例,每個Watcher都是一個訂閱者 // Watcher實例在實例化過程中,會讀取data中的某個屬性,從而觸發(fā)當(dāng)前get方法 if (Dep.target) { dep.depend(); } return val; }, set: newVal => { if (val === newVal) return; val = newVal; // 對新值進(jìn)行監(jiān)聽 chlidOb = observe(newVal); // 通知所有訂閱者,數(shù)值被改變了 dep.notify(); }, }); } function observe(value) { // 當(dāng)值不存在,或者不是復(fù)雜數(shù)據(jù)類型時,不再需要繼續(xù)深入監(jiān)聽 if (!value || typeof value !== 'object') { return; } return new Observer(value); }那么接下來就簡單了,我們需要實現(xiàn)一個訂閱者(Watcher)。
class Watcher {constructor(vm, expOrFn, cb) { this.depIds = {}; // hash儲存訂閱者的id,避免重復(fù)的訂閱者 this.vm = vm; // 被訂閱的數(shù)據(jù)一定來自于當(dāng)前Vue實例 this.cb = cb; // 當(dāng)數(shù)據(jù)更新時想要做的事情 this.expOrFn = expOrFn; // 被訂閱的數(shù)據(jù) this.val = this.get(); // 維護(hù)更新之前的數(shù)據(jù) } // 對外暴露的接口,用于在訂閱的數(shù)據(jù)被更新時,由訂閱者管理員(Dep)調(diào)用 update() { this.run(); } addDep(dep) { // 如果在depIds的hash中沒有當(dāng)前的id,可以判斷是新Watcher,因此可以添加到dep的數(shù)組中儲存 // 此判斷是避免同id的Watcher被多次儲存 if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this); this.depIds[dep.id] = dep; } } run() { const val = this.get(); console.log(val); if (val !== this.val) { this.val = val; this.cb.call(this.vm, val); } } get() { // 當(dāng)前訂閱者(Watcher)讀取被訂閱數(shù)據(jù)的最新更新后的值時,通知訂閱者管理員收集當(dāng)前訂閱者 Dep.target = this; const val = this.vm._data[this.expOrFn]; // 置空,用于下一個Watcher使用 Dep.target = null; return val; } }那么我們最后完成Vue,將上述方法掛載在Vue上。
class Vue {constructor(options = {}) { // 簡化了$options的處理 this.$options = options; // 簡化了對data的處理 let data = (this._data = this.$options.data); // 將所有data最外層屬性代理到Vue實例上 Object.keys(data).forEach(key => this._proxy(key)); // 監(jiān)聽數(shù)據(jù) observe(data); } // 對外暴露調(diào)用訂閱者的接口,內(nèi)部主要在指令中使用訂閱者 $watch(expOrFn, cb) { new Watcher(this, expOrFn, cb); } _proxy(key) { Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => this._data[key], set: val => { this._data[key] = val; }, }); } }看下效果:
<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-1da193-1526012269854-0)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
</figure>
在線示例 雙向綁定實現(xiàn)---無漏洞版 by Iwobi (@xiaomuzhu) on CodePen.
至此,一個簡單的雙向綁定算是被我們實現(xiàn)了。
2.3 Object.defineProperty的缺陷
其實我們升級版的雙向綁定依然存在漏洞,比如我們將屬性值改為數(shù)組。
let demo = new Vue({data: {list: [1],}, });const list = document.getElementById('list'); const btn = document.getElementById('btn'); btn.addEventListener('click', function() { demo.list.push(1); }); const render = arr => { const fragment = document.createDocumentFragment(); for (let i = 0; i < arr.length; i++) { const li = document.createElement('li'); li.textContent = arr[i]; fragment.appendChild(li); } list.appendChild(fragment); }; // 監(jiān)聽數(shù)組,每次數(shù)組變化則觸發(fā)渲染函數(shù),然而...無法監(jiān)聽 demo.$watch('list', list => render(list)); setTimeout( function() { alert(demo.list); }, 5000, );在線示例 雙向綁定-數(shù)組漏洞 by Iwobi (@xiaomuzhu) on CodePen.
是的,Object.defineProperty的第一個缺陷,無法監(jiān)聽數(shù)組變化。 然而Vue的文檔提到了Vue是可以檢測到數(shù)組變化的,但是只有以下八種方法,vm.items[indexOfItem] = newValue這種是無法檢測的。
push() pop() shift() unshift() splice() sort() reverse()其實作者在這里用了一些奇技淫巧,把無法監(jiān)聽數(shù)組的情況hack掉了,以下是方法示例。
const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; const arrayAugmentations = []; aryMethods.forEach((method)=> { // 這里是原生Array的原型方法 let original = Array.prototype[method]; // 將push, pop等封裝好的方法定義在對象arrayAugmentations的屬性上 // 注意:是屬性而非原型屬性 arrayAugmentations[method] = function () { console.log('我被改變啦!'); // 調(diào)用對應(yīng)的原生方法并返回結(jié)果 return original.apply(this, arguments); }; }); let list = ['a', 'b', 'c']; // 將我們要監(jiān)聽的數(shù)組的原型指針指向上面定義的空數(shù)組對象 // 別忘了這個空數(shù)組的屬性上定義了我們封裝好的push等方法 list.__proto__ = arrayAugmentations; list.push('d'); // 我被改變啦! 4 // 這里的list2沒有被重新定義原型指針,所以就正常輸出 let list2 = ['a', 'b', 'c']; list2.push('d'); // 4由于只針對了八種方法進(jìn)行了hack,所以其他數(shù)組的屬性也是檢測不到的,其中的坑很多,可以閱讀上面提到的文檔。
我們應(yīng)該注意到在上文中的實現(xiàn)里,我們多次用遍歷方法遍歷對象的屬性,這就引出了Object.defineProperty的第二個缺陷,只能劫持對象的屬性,因此我們需要對每個對象的每個屬性進(jìn)行遍歷,如果屬性值也是對象那么需要深度遍歷,顯然能劫持一個完整的對象是更好的選擇。
Object.keys(value).forEach(key => this.convert(key, value[key]));3.Proxy實現(xiàn)的雙向綁定的特點
Proxy在ES2015規(guī)范中被正式發(fā)布,它在目標(biāo)對象之前架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進(jìn)行過濾和改寫,我們可以這樣認(rèn)為,Proxy是Object.defineProperty的全方位加強版,具體的文檔可以查看此處;
3.1 Proxy可以直接監(jiān)聽對象而非屬性
我們還是以上文中用Object.defineProperty實現(xiàn)的極簡版雙向綁定為例,用Proxy進(jìn)行改寫。
const input = document.getElementById('input'); const p = document.getElementById('p'); const obj = {}; const newObj = new Proxy(obj, { get: function(target, key, receiver) { console.log(`getting ${key}!`); return Reflect.get(target, key, receiver); }, set: function(target, key, value, receiver) { console.log(target, key, value, receiver); if (key === 'text') { input.value = value; p.innerHTML = value; } return Reflect.set(target, key, value, receiver); }, }); input.addEventListener('keyup', function(e) { newObj.text = e.target.value; });在線示例 Proxy版 by Iwobi (@xiaomuzhu) on CodePen.
我們可以看到,Proxy直接可以劫持整個對象,并返回一個新對象,不管是操作便利程度還是底層功能上都遠(yuǎn)強于Object.defineProperty。
3.2 Proxy可以直接監(jiān)聽數(shù)組的變化
當(dāng)我們對數(shù)組進(jìn)行操作(push、shift、splice等)時,會觸發(fā)對應(yīng)的方法名稱和length的變化,我們可以借此進(jìn)行操作,以上文中Object.defineProperty無法生效的列表渲染為例。
const list = document.getElementById('list'); const btn = document.getElementById('btn'); // 渲染列表 const Render = { // 初始化 init: function(arr) { const fragment = document.createDocumentFragment(); for (let i = 0; i < arr.length; i++) { const li = document.createElement('li'); li.textContent = arr[i]; fragment.appendChild(li); } list.appendChild(fragment); }, // 我們只考慮了增加的情況,僅作為示例 change: function(val) { const li = document.createElement('li'); li.textContent = val; list.appendChild(li); }, }; // 初始數(shù)組 const arr = [1, 2, 3, 4]; // 監(jiān)聽數(shù)組 const newArr = new Proxy(arr, { get: function(target, key, receiver) { console.log(key); return Reflect.get(target, key, receiver); }, set: function(target, key, value, receiver) { console.log(target, key, value, receiver); if (key !== 'length') { Render.change(value); } return Reflect.set(target, key, value, receiver); }, }); // 初始化 window.onload = function() { Render.init(arr); } // push數(shù)字 btn.addEventListener('click', function() { newArr.push(6); });在線示例 Proxy列表渲染 by Iwobi (@xiaomuzhu) on CodePen.
很顯然,Proxy不需要那么多hack(即使hack也無法完美實現(xiàn)監(jiān)聽)就可以無壓力監(jiān)聽數(shù)組的變化,我們都知道,標(biāo)準(zhǔn)永遠(yuǎn)優(yōu)先于hack。
3.3 Proxy的其他優(yōu)勢
Proxy有多達(dá)13種攔截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具備的。
Proxy返回的是一個新對象,我們可以只操作新的對象達(dá)到目的,而Object.defineProperty只能遍歷對象屬性直接修改。
Proxy作為新標(biāo)準(zhǔn)將受到瀏覽器廠商重點持續(xù)的性能優(yōu)化,也就是傳說中的新標(biāo)準(zhǔn)的性能紅利。
當(dāng)然,Proxy的劣勢就是兼容性問題,而且無法用polyfill磨平,因此Vue的作者才聲明需要等到下個大版本(3.0)才能用Proxy重寫。
下期預(yù)告
下期準(zhǔn)備一篇我們主要講為什么我們需要前端框架,或者換幾種問法,對于此項目你為什么選擇Angular、Vue、React等框架,而不是直接JQuery或者js?不使用框架可能遇到什么問題?使用框架的優(yōu)勢在哪里?框架解決了JQuery解決不了的什么問題?
這個問題是電面神器,問題開放性很好,也不需要面對面摳一些細(xì)節(jié),同時有功底有思考的同學(xué)與跟風(fēng)學(xué)框架的同學(xué)差距很容易暴露出來。
我們會邊解答這個問題邊用Proxy構(gòu)建一個Mini版Vue,構(gòu)建Vue的過程就是我們不斷解決不使用框架的情況下遇到的各種問題的過程。
參考:https://www.jianshu.com/p/2df6dcddb0d7
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。
轉(zhuǎn)載于:https://www.cnblogs.com/still1/p/11008455.html
總結(jié)
- 上一篇: iOS6和iOS7代码的适配(1)
- 下一篇: RHEL7.0系统相关配置