vue2 和 vue3 的mvvm的實現原理是什么?


Vue原理最重要的莫過於響應式,虛擬dom及diff算法,模板編譯。
其中響應式就是雙向綁定。vue是mvvm框架,所謂mvvm,最核心的就是數據驅動視圖,用戶不能直接操作dom,而是直接操作數據,當數據改變的時候,vue內部監聽數據變化然后更新視圖。同樣,用戶在視圖上的操作(事件)也會反過來改變數據。而響應式,則是實現數據驅動視圖的第一步,即監聽數據的變化,使得用戶在設置數據時,可以通知vue內部進行視圖更新。
那么在這之前,我們先了解下擊中實現雙休綁定的做法

幾種實現雙向綁定的做法

目前幾種主流的mvc(vm)框架都實現了單向數據綁定,而我所理解的雙向數據綁定無非就是在單向綁定的基礎上給可輸入元素(input、textare等)添加了change(input)事件,來動態修改model和 view,並沒有多高深。所以無需太過介懷是實現的單向或雙向綁定。
實現數據綁定的做法有大致如下幾種:
發布者-訂閱者模式(backbone.js)
臟值檢查(angular.js)
數據劫持(vue.js)

發布者-訂閱者模式

一般通過sub, pub的方式實現數據和視圖的綁定監聽,更新數據方式通常做法是 vm.set('property', value),這里有篇文章講的比較詳細,有興趣可點這里

這種方式現在畢竟太low了,我們更希望通過 vm.property = value 這種方式更新數據,同時自動更新視圖,於是有了下面兩種方式

臟值檢查

angular.js 是通過臟值檢測的方式比對數據是否有變更,來決定是否更新視圖,最簡單的方式就是通過 setInterval() 定時輪詢檢測數據變動,當然Google不會這么low,angular只有在指定的事件觸發時進入臟值檢測,大致如下:

DOM事件,譬如用戶輸入文本,點擊按鈕等。( ng-click )

XHR響應事件 ( $http )

瀏覽器Location變更事件 ( $location )

Timer事件( $timeout , $interval )

執行 $digest() 或 $apply()

數據劫持

vue2 的 vue.js 則是采用數據劫持結合發布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數據變動時發布消息給訂閱者,觸發相應的監聽回調。

vue 2.0

已經了解到vue是通過數據劫持的方式來做數據綁定的,其中最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,達到監聽數據變動的目的,無疑這個方法是本文中最重要、最基礎的內容之一,如果不熟悉defineProperty,猛戳這里
整理了一下,要實現mvvm的雙向綁定,就必須要實現以下幾點:
1、實現一個數據監聽器Observer,能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者
2、實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數
3、實現一個Watcher,作為連接Observer和Compile的橋梁,能夠訂閱並收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖
4、mvvm入口函數,整合以上三者
上述流程如圖所示:

已經了解到vue是通過數據劫持的方式來做數據綁定的,其中最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,達到監聽數據變動的目的,無疑這個方法是本文中最重要、最基礎的內容之一,如果不熟悉defineProperty,猛戳這里

1、實現Observer

ok, 思路已經整理完畢,也已經比較明確相關邏輯和模塊功能了,let's do it
我們知道可以利用Obeject.defineProperty()來監聽屬性變動
那么將需要observe的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 setter和getter
這樣的話,給這個對象的某個值賦值,就會觸發setter,那么就能監聽到了數據變化。

2、實現Compile

compile主要做的事情是解析模板指令,將模板中的變量替換成數據,然后初始化渲染頁面視圖,並將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖,

這里通過遞歸遍歷保證了每個節點及子節點都會解析編譯到,包括了{{}}表達式聲明的文本節點。指令的聲明規定是通過特定前綴的節點屬性來標記,如<span v-text="content" other-attr中v-text便是指令,而other-attr不是指令,只是普通的屬性。
監聽數據、綁定更新函數的處理是在compileUtil.bind()這個方法中,通過new Watcher()添加回調來接收數據變化的通知

3、實現Watcher

Watcher訂閱者作為Observer和Compile之間通信的橋梁,主要做的事情是:
1、在自身實例化時往屬性訂閱器(dep)里面添加自己
2、自身必須有一個update()方法
3、待屬性變動dep.notice()通知時,能調用自身的update()方法,並觸發Compile中綁定的回調,則功成身退。

vue 3.0

2020.11月14日-16日於多倫多舉辦的 VueConf TO 2018 大會上,尤雨溪發表了名為 Vue3.0 Updates 的主題演講,對 Vue3.0 的更新計划、方向進行了詳細闡述(感興趣的小伙伴可以看看完整的 PPT),表示已經放棄使用了 Object.defineProperty,而選擇了使用更快的原生 Proxy !!
這將會消除了之前 Vue2.x 中基於 Object.defineProperty 的實現所存在的很多限制:無法監聽 屬性的添加和刪除、數組索引和長度的變更,並可以支持 Map、Set、WeakMap 和 WeakSet!

帶來的特性

vue3.0實現響應式
Proxy支持監聽原生數組
Proxy的獲取數據,只會遞歸到需要獲取的層級,不會繼續遞歸
Proxy可以監聽數據的手動新增和刪除

什么是proxy?

上是這么描述的——Proxy對象用於定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數調用等)。
其實就是在對目標對象的操作之前提供了攔截,可以對外界的操作進行過濾和改寫,修改某些操作的默認行為,這樣我們可以不直接操作對象本身,而是通過操作對象的代理對象來間接來操作對象,達到預期的目的~

proxy 語法

ES6 原生提供的 Proxy 語法很簡單,用法如下:

let proxy = new Proxy(target, handler);

參數 target 是用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理), 參數 handler 也是一個對象,其屬性是當執行一個操作時定義代理的行為的函數,也就是自定義的行為。

Proxy 的基本用法就如同上面這樣,不同的是 handler 對象的不同,handler 可以是空對象 {} ,則表示對 proxy 操作就是對目標對象 target 操作
但是要注意的是,handler 不能 設置為 null ,會拋出一個錯誤——Cannot create proxy with a non-object as target or handler!

要想 Proxy 起作用,我們就不能去操作原來對象的對象,也就是目標對象 target ,必須針對的是 Proxy 實例進行操作,否則達不到預期的效果,我們設置 get 方法后,試圖繼續從原對象 obj 中讀取一個不存在的屬性 b , 結果依舊返回 undefined :

對於可以設置、但沒有設置攔截的操作,則對 proxy 對象的處理結果也同樣會作用於原來的目標對象 target 上,怎么理解呢?
們重新定義了 set 方法,所有的屬性設置都返回了 888 , 並沒有對某個特殊的屬性(這里指的是 obj 的 a 屬性 )做特殊的攔截或處理,那么通過 proxyObj.a = 666 操作后的結果同樣也會作用於原來目標對象(obj 對象)上,因此 obj 對象的 a 的值也將會變為 888 !

API

handler 對象是一個容納一批特定屬性的占位符對象。它包含有 Proxy 的各個捕獲器(trap)。

所有的捕捉器是可選的。如果沒有定義某個捕捉器,那么就會保留源對象的默認行為。

handler.getPrototypeOf()
Object.getPrototypeOf 方法的捕捉器。
handler.setPrototypeOf()
Object.setPrototypeOf 方法的捕捉器。
handler.isExtensible()
Object.isExtensible 方法的捕捉器。
handler.preventExtensions()
Object.preventExtensions 方法的捕捉器。
handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor 方法的捕捉器。
handler.defineProperty()
Object.defineProperty 方法的捕捉器。
handler.has()
in 操作符的捕捉器。
handler.get()
屬性讀取操作的捕捉器。
handler.set()
屬性設置操作的捕捉器。
handler.deleteProperty()
delete 操作符的捕捉器。
handler.ownKeys()
Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
handler.apply()
函數調用操作的捕捉器。
handler.construct()
new 操作符的捕捉器。

Proxy的其他應用

dobjs/dob 就是用 proxy 重寫 mobx 的一個方案。
immer 實現不可變數據類型。immer 的做法就是維護一份 state 在內部,劫持所有操作,內部來判斷是否有變化從而最終決定如何返回,具體內容可以看一下mmer.js 簡介及源碼簡析 這篇文章。
都是使用到了對對象進行讀寫攔截,在讀寫中做一些額外的判斷和操作。

總結

Proxy是用來操作對象的,拓展對象的能力。Object.defineProperty() 是對對象屬性進行操作。
vue2.x使用 Object.defineProperty()實現數據的響應式,但是由於 Object.defineProperty()是對對象屬性的操作,所以需要對對象進行深度遍歷去對屬性進行操作。
vue3.0 用 Proxy 是對對象進行攔截操作,無論是對對象做什么樣的操作都會走到 Proxy 的處理邏輯中。雖然proxy語法很簡單,可不是件容易的事,再加上其本身的 Proxy的兼容性方面的問題,所以我們實際應用開發中使用的場景需要更具用戶環境做思考后再使用。
vue3.0、dobjs/dob、immer等庫目前都使用到了 Proxy,對對象進行讀寫攔截,做一些額外的處理

參考:
https://www.jianshu.com/p/2a8ec76e0090
https://segmentfault.com/a/1190000021991591

擴展閱讀:
vue3.0 深入響應式原理
vue3.0 列表選熱
實現雙向綁定Proxy比defineproperty優劣如何?
JavaScript 標准內置對象Proxy
阮一峰ECMAScript 6 入門-Proxy
學會 Proxy 真的可以為所欲為
ImmerJS 淺析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM