vue雙向綁定、Proxy、defineproperty

本文原連接:https://www.jianshu.com/p/2df6dcddb0d7

前言

雙向綁定其實已是一個老掉牙的問題了,只要涉及到MVVM框架就不得不談的知識點,但它畢竟是Vue的三要素之一.javascript

Vue三要素php

  • 響應式: 例如如何監聽數據變化,其中的實現方法就是咱們提到的雙向綁定
  • 模板引擎: 如何解析模板
  • 渲染: Vue如何將監聽到的數據變化和解析後的HTML進行渲染

能夠實現雙向綁定的方法有不少,KnockoutJS基於觀察者模式的雙向綁定,Ember基於數據模型的雙向綁定,Angular基於髒檢查的雙向綁定,本篇文章咱們重點講面試中常見的基於數據劫持的雙向綁定。html

常見的基於數據劫持的雙向綁定有兩種實現,一個是目前Vue在用的Object.defineProperty,另外一個是ES2015中新增的Proxy,而Vue的做者宣稱將在Vue3.0版本後加入Proxy從而代替Object.defineProperty,經過本文你也能夠知道爲何Vue將來會選擇Proxyvue

嚴格來說Proxy應該被稱爲『代理』而非『劫持』,不過因爲做用有不少類似之處,咱們在下文中就再也不作區分,統一叫『劫持』。java

咱們能夠經過下圖清楚看到以上兩種方法在雙向綁定體系中的關係.react

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-6f9b58-1526012269856-2)]es6

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>面試

</figure>segmentfault

基於數據劫持的固然還有已經涼透的Object.observe方法,已被廢棄。api

提早聲明: 咱們沒有對傳入的參數進行及時判斷而規避錯誤,僅僅對核心方法進行了實現.


文章目錄

  1. 基於數據劫持實現的雙向綁定的特色
  2. 基於Object.defineProperty雙向綁定的特色
  3. 基於Proxy雙向綁定的特色

1.基於數據劫持實現的雙向綁定的特色

1.1 什麼是數據劫持

數據劫持比較好理解,一般咱們利用Object.defineProperty劫持對象的訪問器,在屬性值發生變化時咱們能夠獲取變化,從而進行進一步操做。

// 這是將要被劫持的對象 const data = { name: '', }; function say(name) { if (name === '古天樂') { console.log('給你們推薦一款超好玩的遊戲'); } else if (name === '渣渣輝') { console.log('戲我演過不少,可遊戲我只玩貪玩懶月'); } else { console.log('來作個人兄弟'); } } // 遍歷對象,對其屬性值進行劫持 Object.keys(data).forEach(function(key) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { console.log('get'); }, set: function(newVal) { // 當屬性值發生變化時咱們能夠進行額外操做 console.log(`你們好,我係${newVal}`); say(newVal); }, }); }); data.name = '渣渣輝'; //你們好,我係渣渣輝 //戲我演過不少,可遊戲我只玩貪玩懶月 

1.2 數據劫持的優點

目前業界分爲兩個大的流派,一個是以React爲首的單向數據綁定,另外一個是以Angular、Vue爲主的雙向數據綁定。

其實三大框架都是既能夠雙向綁定也能夠單向綁定,好比React能夠手動綁定onChange和value實現雙向綁定,也能夠調用一些雙向綁定庫,Vue也加入了props這種單向流的api,不過都並不是主流賣點。

單向或者雙向的優劣不在咱們的討論範圍,咱們須要討論一下對比其餘雙向綁定的實現方法,數據劫持的優點所在。

  1. 無需顯示調用: 例如Vue運用數據劫持+發佈訂閱,直接能夠通知變化並驅動視圖,上面的例子也是比較簡單的實現data.name = '渣渣輝'後直接觸發變動,而好比Angular的髒檢測則須要顯示調用markForCheck(能夠用zone.js避免顯示調用,不展開),react須要顯示調用setState
  2. 可精確得知變化數據:仍是上面的小例子,咱們劫持了屬性的setter,當屬性值改變,咱們能夠精確獲知變化的內容newVal,所以在這部分不須要額外的diff操做,不然咱們只知道數據發生了變化而不知道具體哪些數據變化了,這個時候須要大量diff來找出變化值,這是額外性能損耗。

1.3 基於數據劫持雙向綁定的實現思路

數據劫持是雙向綁定各類方案中比較流行的一種,最著名的實現就是Vue。

基於數據劫持的雙向綁定離不開ProxyObject.defineProperty等方法對對象/對象屬性的"劫持",咱們要實現一個完整的雙向綁定須要如下幾個要點。

  1. 利用ProxyObject.defineProperty生成的Observer針對對象/對象的屬性進行"劫持",在屬性發生變化後通知訂閱者
  2. 解析器Compile解析模板中的Directive(指令),收集指令所依賴的方法和數據,等待數據變化而後進行渲染
  3. Watcher屬於Observer和Compile橋樑,它將接收到的Observer產生的數據變化,並根據Compile提供的指令進行視圖渲染,使得數據變化促使視圖變化

<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運用了數據劫持,可是依然離不開發佈訂閱的模式,之因此在系列2作了Event Bus的實現,就是由於咱們無論在學習一些框架的原理仍是一些流行庫(例如Redux、Vuex),基本上都離不開發佈訂閱模式,而Event模塊則是此模式的經典實現,因此若是不熟悉發佈訂閱模式,建議讀一下系列2的文章。


2.基於Object.defineProperty雙向綁定的特色

關於Object.defineProperty的文章在網絡上已經汗牛充棟,咱們不想花過多時間在Object.defineProperty上面,本節咱們主要講解Object.defineProperty的特色,方便接下來與Proxy進行對比。

Object.defineProperty還不瞭解的請閱讀文檔

兩年前就有人寫過基於Object.defineProperty實現的文章,想深刻理解Object.defineProperty實現的推薦閱讀,本文也作了相關參考。

上面咱們推薦的文章爲比較完整的實現(400行代碼),咱們在本節只提供一個極簡版(20行)和一個簡化版(150行)的實現,讀者能夠按部就班地閱讀。

2.1 極簡版的雙向綁定

咱們都知道,Object.defineProperty的做用就是劫持一個對象的屬性,一般咱們對屬性的gettersetter方法進行劫持,在對象的屬性發生變化時進行特定的操做。

咱們就對對象objtext屬性進行劫持,在獲取此屬性的值時打印'get val',在更改屬性值的時候對DOM進行操做,這就是一個極簡的雙向綁定。

const obj = {}; Object.defineProperty(obj, 'text', { get: function() { console.log('get val');&emsp; }, 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 升級改造

咱們很快會發現,這個所謂的雙向綁定貌似並無什麼亂用。。。

緣由以下:

  1. 咱們只監聽了一個屬性,一個對象不可能只有一個屬性,咱們須要對對象每一個屬性進行監聽。
  2. 違反開放封閉原則,咱們若是瞭解開放封閉原則的話,上述代碼是明顯違反此原則,咱們每次修改都須要進入方法內部,這是須要堅定杜絕的。
  3. 代碼耦合嚴重,咱們的數據、方法和DOM都是耦合在一塊兒的,就是傳說中的麪條代碼。

那麼如何解決上述問題?

Vue的操做就是加入了發佈訂閱模式,結合Object.defineProperty的劫持能力,實現了可用性很高的雙向綁定。

首先,咱們以發佈訂閱的角度看咱們第一部分寫的那一坨代碼,會發現它的監聽發佈訂閱都是寫在一塊兒的,咱們首先要作的就是解耦。

咱們先實現一個訂閱發佈中心,即消息管理員(Dep),它負責儲存訂閱者和消息的分發,不論是訂閱者仍是發佈者都須要依賴於它。

let uid = 0; // 用於儲存訂閱者併發布消息 class Dep { constructor() { // 設置id,用於區分新Watcher和只改變屬性值後新產生的Watcher this.id = uid++; // 儲存訂閱者的數組 this.subs = []; } // 觸發target上的Watcher中的addDep方法,參數爲dep的實例自己 depend() { Dep.target.addDep(this); } // 添加訂閱者 addSub(sub) { this.subs.push(sub); } notify() { // 通知全部的訂閱者(Watcher),觸發訂閱者的相應邏輯處理 this.subs.forEach(sub => sub.update()); } } // 爲Dep類設置一個靜態屬性,默認爲null,工做時指向當前的Watcher Dep.target = null; 

如今咱們須要實現監聽者(Observer),用於監聽屬性值的變化。

// 監聽者,監聽對象屬性值的變化 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: () => { // 若是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(); }, }); } function observe(value) { // 當值不存在,或者不是複雜數據類型時,再也不須要繼續深刻監聽 if (!value || typeof value !== 'object') { return; } return new Observer(value); } 

那麼接下來就簡單了,咱們須要實現一個訂閱者(Watcher)。

class Watcher { constructor(vm, expOrFn, cb) { this.depIds = {}; // hash儲存訂閱者的id,避免重複的訂閱者 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被屢次儲存 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() { // 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者 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)); // 監聽數據 observe(data); } // 對外暴露調用訂閱者的接口,內部主要在指令中使用訂閱者 $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>

在線示例 雙向綁定實現---無漏洞版 by Iwobi (@xiaomuzhu) on CodePen.

至此,一個簡單的雙向綁定算是被咱們實現了。

2.3 Object.defineProperty的缺陷

其實咱們升級版的雙向綁定依然存在漏洞,好比咱們將屬性值改成數組。

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); }; // 監聽數組,每次數組變化則觸發渲染函數,然而...沒法監聽 demo.$watch('list', list => render(list)); setTimeout( function() { alert(demo.list); }, 5000, ); 

在線示例 雙向綁定-數組漏洞 by Iwobi (@xiaomuzhu) on CodePen.

是的,Object.defineProperty的第一個缺陷,沒法監聽數組變化。 然而Vue的文檔提到了Vue是能夠檢測到數組變化的,可是隻有如下八種方法,vm.items[indexOfItem] = newValue這種是沒法檢測的。

push()
pop()
shift() unshift() splice() sort() reverse() 

其實做者在這裏用了一些奇技淫巧,把沒法監聽數組的狀況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('我被改變啦!'); // 調用對應的原生方法並返回結果 return original.apply(this, arguments); }; }); let list = ['a', 'b', 'c']; // 將咱們要監聽的數組的原型指針指向上面定義的空數組對象 // 別忘了這個空數組的屬性上定義了咱們封裝好的push等方法 list.__proto__ = arrayAugmentations; list.push('d'); // 我被改變啦! 4 // 這裏的list2沒有被從新定義原型指針,因此就正常輸出 let list2 = ['a', 'b', 'c']; list2.push('d'); // 4 

因爲只針對了八種方法進行了hack,因此其餘數組的屬性也是檢測不到的,其中的坑不少,能夠閱讀上面提到的文檔。

咱們應該注意到在上文中的實現裏,咱們屢次用遍歷方法遍歷對象的屬性,這就引出了Object.defineProperty的第二個缺陷,只能劫持對象的屬性,所以咱們須要對每一個對象的每一個屬性進行遍歷,若是屬性值也是對象那麼須要深度遍歷,顯然能劫持一個完整的對象是更好的選擇。

Object.keys(value).forEach(key => this.convert(key, value[key])); 

3.Proxy實現的雙向綁定的特色

Proxy在ES2015規範中被正式發佈,它在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫,咱們能夠這樣認爲,Proxy是Object.defineProperty的全方位增強版,具體的文檔能夠查看此處;

3.1 Proxy能夠直接監聽對象而非屬性

咱們仍是以上文中用Object.defineProperty實現的極簡版雙向綁定爲例,用Proxy進行改寫。

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直接能夠劫持整個對象,並返回一個新對象,不論是操做便利程度仍是底層功能上都遠強於Object.defineProperty

3.2 Proxy能夠直接監聽數組的變化

當咱們對數組進行操做(push、shift、splice等)時,會觸發對應的方法名稱和length的變化,咱們能夠藉此進行操做,以上文中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); }, }; // 初始數組 const arr = [1, 2, 3, 4]; // 監聽數組 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數字 btn.addEventListener('click', function() { newArr.push(6); }); 

在線示例 Proxy列表渲染 by Iwobi (@xiaomuzhu) on CodePen.

很顯然,Proxy不須要那麼多hack(即便hack也沒法完美實現監聽)就能夠無壓力監聽數組的變化,咱們都知道,標準永遠優先於hack。

3.3 Proxy的其餘優點

Proxy有多達13種攔截方法,不限於apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具有的。

Proxy返回的是一個新對象,咱們能夠只操做新的對象達到目的,而Object.defineProperty只能遍歷對象屬性直接修改。

Proxy做爲新標準將受到瀏覽器廠商重點持續的性能優化,也就是傳說中的新標準的性能紅利。

固然,Proxy的劣勢就是兼容性問題,並且沒法用polyfill磨平,所以Vue的做者才聲明須要等到下個大版本(3.0)才能用Proxy重寫。

 

 

做者:流動碼文連接:https://www.jianshu.com/p/2df6dcddb0d7來源:簡書簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。

相關文章
相關標籤/搜索