Vue 的數據響應式(Vue2 及 Vue3)


什么是數據響應式

從一開始使用 vue 時,對於之前的 jq 開發而言,一個很大的區別就是基本不用手動操作 dom,data 中聲明的數據狀態改變后會自動重新渲染相關的 dom。
換句話說就是 vue 自己知道哪個數據狀態發生了變化及哪里有用到這個數據需要隨之修改。

因此實現數據響應式有兩個重點問題:

  1. 如何知道數據發生了變化?
  2. 如何知道數據變化后哪里需要修改?

對於第一個問題,如何知道數據發生了變化,Vue3 之前使用了 ES5 的一個 API Object.defineProperty Vue3 中使用了 ES6 的 Proxy,都是對需要偵測的數據進行 變化偵測 ,添加 getter 和 setter ,這樣就可以知道數據何時被讀取和修改。

第二個問題,如何知道數據變化后哪里需要修改,Vue 對於每個數據都收集了與之相關的 依賴 ,這里的依賴其實就是一個對象,保存有該數據的舊值及數據變化后需要執行的函數。每個響應式的數據變化時會遍歷通知其對應的每個依賴,依賴收到通知后會判斷一下新舊值有沒有發生變化,如果變化則執行回調函數響應數據變化(比如修改 dom)。

下面詳細分別介紹 Vue2 及 Vue3 的數據變化偵測及依賴收集。

 

Vue2

變化偵測

Object 的變化偵測

轉化響應式數據需要將 Vue 實例上 data 屬性中定義的數據通過遞歸將所有屬性都轉化為 getter/setter 的形式,Vue 中定義了一個 Observer 類來做這個事情。

function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } class Observer { constructor(value) { this.value = value; def(value, '__ob__', this); if (!Array.isArray(value)) { this.walk(value); } } walk(obj) { for (const [key, value] of Object.entries(obj)) { definereactive(obj, key, value); } } }

直接將一個對象傳入 new Observer() 后就對每項屬性都調用 definereactive 函數添加變化偵測,下面定義這個函數:

function defineReactive(data, key, val) { let childOb = observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { // 讀取 data[key] 時觸發 console.log('getter', val); return val; }, set: function (newVal) { // 修改 data[key] 時觸發 console.log('setter', newVal); if (val === newVal) { return; } val = newVal; } }) } function observe(value, asRootData) { if (typeof val !== 'object') { return; } let ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else { ob = new Observer(val); } return ob; }

函數中判斷如果是對象則遞歸調用 Observer 來實現所有屬性的變化偵測,根據 __ob__ 屬性判斷是否已處理過,防止多次重復處理,Observer 處理過后會給數據添加這個屬性,下面寫一個對象試一下:

const people = { name: 'c', age: 12, parents: { dad: 'a', mom: 'b' }, mates: ['d', 'e'] }; new Observer(people); people.name; // getter c people.age++; // getter 12 setter 13 people.parents.dad; // getter {} getter a

打印 people 可以看到所有屬性添加了 getter/setter 方法,讀取 name 屬性時打印了 people.age++ 修改 age 時打印了 getter 12 setter 13 說明 people 的屬性已經被全部成功代理監聽。

Array 的變化偵測

可以看到前面 Observer 中僅對 Object 類型個數據做了處理,為每個屬性添加了 getter/setter,處理后如果屬性值中有數組,通過 屬性名 + 索引 的方式(如:this.people.mates[0])獲取也是會觸發 getter 的。但是如果通過數組原型方法修改數組的值,如 this.people.mates.push('f'),這樣是無法通過 setter 偵測到的,因此,在 Observer 中需要對 Object 和 Array 分別進行單獨的處理。

為偵測到數組原型方法的操作,Vue 中是通過創建一個攔截器 arrayMethods,並將攔截器重新掛載到數組的原型對象上。

下面是攔截器的定義:

const ArrayProto = Array.prototype; const arrayMethods = Object.create(ArrayProto); ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ].forEach(method => { const original = ArrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args) { console.log('mutator:', this, args); return original.apply(this, args); }, enumerable: false, writable: true, configurable: true }) })

這里 arrayMethods 繼承了 Array 的原型對象 Array.prototype,並給它添加了 push pop shift unshift splice sort reverse 這些方法,因為數組是可以通過這些方法進行修改的。添加的 push pop... 方法中重新調用 original(緩存的數組原型方法),這樣就不會影響數組本身的操作。

最后給 Observer 中添加數組的修改:直接將攔截器掛載到數組原型對象上

class Observer { constructor(value) { this.value = value; def(value, '__ob__', this); if (Array.isArray(value)) { value.__proto__ = arrayMethods; } else { this.walk(value); } } walk(obj) { for (const [key, value] of Object.entries(obj)) { defineReactive(obj, key, value); } } }

再來驗證一下:

const people = { name: 'c', age: 12, parents: { dad: 'a', mom: 'b' }, mates: ['d', 'e'] }; new Observer(people); people.mates[0]; // getter (2) ["d", "e"] people.mates.push('f'); // mutator: (2) ["d", "e"] ["f"]

現在數組的修改也能被偵測到了。

依賴收集

目前已經可以對 Object 及 Array 數據的變化進行截獲,那么開始考慮一開始提到的 Vue 響應式數據的第二個問題:如何知道數據變化后哪里需要修改?

最開始已經說過,Vue 中每個數據都需要收集與之相關的依賴,用來表示該數據變化時需要進行的操作行為。

通過數據的變化偵測我們可以知道數據何時被讀取或修改,因此可以在數據讀取時收集依賴,修改時通知依賴更新,這樣就可以實現數據響應式了。

依賴收集在哪

為每個數據都創建一個收集依賴的對象 dep,對外暴露 depend(收集依賴)、notify(通知依賴更新)的兩個方法,內部維護了一個數組用來保存該數據的每項依賴。

對於 Object,可以在 getter 中收集,setter 中通知更新,對 defineReactive 函數修改如下:

function defineReactive(data, key, val) { let childOb = observe(val); // 處理每個響應式數據時都創建一個對象用來收集依賴 let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { // 收集依賴 dep.depend(); return val; }, set: function (newVal) { if (val === newVal) { return; } val = newVal; // 通知依賴更新 dep.notify(); } }) }

上面代碼中依賴是收集在一個 Dep 實例對象上的,下面看一下 Dep 這個類。

class Dep { constructor() { this.subs = []; } addSub(sub) { this.subs.push(sub); } removeSub(sub) { if (this.subs.length) { const index = this.subs.indexOf(sub); this.subs.splice(index, 1); } } depend() { if (window.target) { this.addSub(window.target); } } notify() { const subs = this.subs.slice(); for (let i = 0; i < subs.length; i++) { subs[i].update(); } } }

Dep 的每個實例都有一個保存依賴的數組 subs,收集依賴時是從全局的一個變量上獲取到並插入 subs,通知依賴時就遍歷所有 subs 成員並調用其 update 方法。

Object 的依賴收集和觸發都是在 defineProperty 中進行的,因此 Dep 實例定義在 defineReactive 函數中就可以讓 getter 和 setter 都拿到。

而對於 Array 來說,依賴可以在 getter 中收集,但觸發卻是在攔截器中,為了保證 getter 和 攔截器中都能訪問到 Dep 實例,Vue 中給 Observer 實例上添加了 dep 屬性。

class Observer { constructor(value) { this.value = value; this.dep = new Dep(); def(value, '__ob__', this); if (Array.isArray(value)) { value.__proto__ = arrayMethods; } else { this.walk(value); } } walk(obj) { for (const [key, value] of Object.entries(obj)) { defineReactive(obj, key, value); } } }

Observer 在處理數據響應式時也將自身實例添加到了數據的 __ob__ 屬性上,因此在 getter 和攔截器中都能通過響應式數據本身的 __ob__.dep 拿到其對應的依賴。修改 defineReactive 和 攔截器如下:

function defineReactive(data, key, val) { let childOb = observe(val); let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend(); // 給 Observer 實例上的 dep 屬性收集依賴 if (childOb) { childOb.dep.depend(); } return val; }, ... }) } ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ].forEach(method => { const original = ArrayProto[method]; def(arrayMethods, method, (...args) => { const result = original.apply(this, args); const ob = this.__ob__; ob.dep.notify(); return result; }) })

依賴長什么樣

現在已經知道了依賴保存在每個響應式數據對應的 Dep 實例中的 subs 中,通過上面 Dep 的代碼可以知道,收集的依賴是一個全局對象,且該對象對外暴露了一個 update 方法,記錄了數據變化時需要進行的更新操作(如修改 dom 或 Vue 的 Watch)。

首先這個依賴對象的功能主要有兩點:

  1. 需要主動將自己收集到對應響應式數據的 Dep 實例中;
  2. 保存數據變化時要進行的操作並在 update 方法中調用;

其實就是一個中介角色,Vue 中起名為 Watcher。

class Watcher { constructor(vm, expOrFn, cb) { this.vm = vm; // 保存通過表達式獲取數據的方法 this.getter = parsePath(expOrFn); this.cb = cb; this.value = this.get(); } get() { // 將自身 Watcher 實例掛到全局對象上 window.target = this; // 獲取表達式對應的數據 // 會自動觸發該數據的 getter // getter 中收集依賴時從全局對象上拿到這個 Watcher 實例 let value = this.getter.call(this.vm, this.vm); window.target = undefined; return value; } update() { const oldValue = this.value; this.value = this.get(); // 將舊值與新值傳遞給回調函數 this.cb.call(this.vm, this.value, oldValue); } }

對於第一點,主動將自己收集到 Dep 實例中,Watcher 中設計的非常巧妙,在 get 中將自身 Watcher 實例掛到全局對象上,然后通過獲取數據觸發 getter 來實現依賴收集。

第二點實現很簡單,只需要將構造函數參數中的回調函數保存並在 update 方法中調用即可。

構造函數中的 parsePath 方法就是從 Vue 實例的 data 上通過表達式獲取數據,比如表達式為 "user.name" 則需要解析該字符串然后獲取 data.user.name 數據。

總結

  • 數據先通過調用 new Observer() 為每項屬性添加變化偵測,並創建一個 Dep 實例用來保存相關依賴。在讀取屬性值時保存依賴,修改屬性值時通知依賴;
  • Dep 實例的 subs 屬性為一個數組,保存依賴是向數組中添加,通知依賴時遍歷數組一次調用依賴的 update 方法;
  • 依賴是一個 Watcher 實例,保存了數據變化時需要進行的操作,並將實例自身放到全局的一個位置,然后讀取數據觸發數據的 getter,getter 中從全局指定的位置獲取到該 Watcher 實例並收集在 Dep 實例中。

以上就是 Vue2 中的響應式原理,在 Observer 處理完后,外界只需要通過創建 Watcher 傳入需要監聽的數據及數據變化時的響應回調函數即可。

廣州設計公司https://www.houdianzi.com 我的007辦公資源網站https://www.wode007.com

Vue3

Vue3 中每個功能單獨為一個模塊,並可以單獨打包使用,本文僅簡單討論 Vue3 中與數據響應式相關的 Reactive 模塊,了解其內部原理,與 Vue2 相比又有何不同。

因為該模塊可以單獨使用,先來看一下這個模塊的用法示例:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue3 demo</title> </head> <body> <div id="app"> <div id="count"></div> <button id="btn">+1</button> </div> <script src="./vue3.js"></script> <script> const countEl = document.querySelector('#count') const btnEl = document.querySelector('#btn') // 定義響應式數據 const state = reactive({ count: 0, man: { name: 'pan' } }) // 定義計算屬性 let double = computed(() => { return state.count * 2 }) // 回調函數立即執行一次,內部使用到的數據更新時會重新執行回調函數 effect(() => { countEl.innerHTML = `count is ${state.count}, double is ${double.value}, man's name is ${state.man.name}` }) // 修改響應式數據觸發更新 btnEl.addEventListener('click', () => { state.count++ }, false) </script> </body> </html>

通過示例可以看到實現 Vue3 這個數據響應式需要有 reactive、computed、effect 這幾個函數,下面仍然通過從變化偵測及依賴收集兩個方面介紹,簡單實現這幾個函數。

變化偵測

示例中的 reactive 函數是對數據進行響應式化的,因此該函數的功能就類似於 Vue2 中的 defineReactive 函數的 getter/setter 處理,處理后能夠對數據的獲取及修改操作進行捕獲。

const toProxy = new WeakMap() const toRaw = new WeakMap() const baseHandler = { get(target, key) { console.log('Get', target, key) const res = Reflect.get(target, key) // 遞歸尋找 return typeof res == 'object' ? reactive(res) : res }, set(target, key, val) { console.log('Set', target, key, val) const res = Reflect.set(target, key, val) return res } } function reactive(target) { console.log('reactive', target) // 查詢緩存 let observed = toProxy.get(target) if (observed) { return observed } if (toRaw.get(target)) { return target } observed = new Proxy(target, baseHandler) // 設置緩存 toProxy.set(target, observed) toRaw.set(observed, target) return observed }

reactive 中使用 Proxy 對目標進行代理,代理的行為是 baseHander ,然后對目標對象及代理后的對象進行緩存,防止多次代理。

baseHandler 中就是對數據的獲取及修改進行攔截,並通過 Reflect 執行 get/set 的原本操作,並在獲取值為 Object 時遞歸進行響應式處理。很簡單地就完成了數據的響應式處理。

依賴收集

依賴收集與 Vue2 類似,在 getter 中收集依賴,setter 中觸發依賴,修改 baseHandler 如下:

const baseHandler = { get(target, key) { const res = Reflect.get(target, key) // 收集依賴 track(target, key) return typeof res == 'object' ? reactive(res) : res }, set(target, key, val) { const info = { oldValue: target[key], newValue: val } const res = Reflect.set(target, key, val) // 觸發更新 trigger(target, key, info) return res } }

track 函數收集依賴,trigger 函數觸發依賴更新。

首先需要兩個全局變量,用於保存當前待收集的依賴對象的 effectStack 及一個記錄所有數據及其對應依賴的表 targetMap 。

const effectStack = [] const targetMap = new WeakMap()

接下來定義這收集依賴及觸發依賴更新這兩個函數:

function track(target, key) { // 從棧中拿到待收集的依賴對象 let effect = effectStack[effectStack.length - 1] if (effect) { // 通過 target 及 key 從依賴映射表中拿到對應的依賴列表(Set類型) // 首次需要對依賴映射表初始化 let depsMap = targetMap.get(target) if (depsMap === undefined) { depsMap = new Map() targetMap.set(target, depsMap) } let dep = depsMap.get(key) if (dep === undefined) { dep = new Set() depsMap.set(key, dep) } // 若 target.key 對應的依賴列表中不存在該依賴則收集 if (!dep.has(effect)) { dep.add(effect) } } }
function trigger(target, key, info) { // 依賴映射表中取出 target 相關數據 const depsMap = targetMap.get(target) if (depsMap === undefined) { return } // 普通依賴對象的列表 const effects = new Set() // 計算屬性依賴對象的列表 const computedRunners = new Set() if (key) { // 取出 key 相關的依賴列表遍歷分類存入 effects 及 computedRunners let deps = depsMap.get(key) deps.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } // 遍歷執行所有依賴對象 const run = effect=> effect() effects.forEach(run) computedRunners.forEach(run) }

track 及 trigger 的大致代碼也很簡單,track 是拿到待收集的依賴對象 effect 后收集到 effectStack,trigger 是從 effectStack 拿到對應的依賴列表遍歷執行。

到現在就差這個依賴對象了,根據上面 trigger 函數可以知道,這個依賴 effect 首先是個函數可以執行,並且還有自身屬性,如 computed 表示其為一個計算屬性的依賴,有時會根據該標識進行寫特殊處理。

下面開始介紹這個依賴對象是如何產生的:

// 創建依賴對象 function createReactiveEffect(fn, options) { const effect = function effect(...args) { return run(effect, fn, args) } effect.computed = options.computed effect.lazy = options.lazy return effect } function run(effect, fn, args) { if (!effectStack.includes(effect)) { try { effectStack.push(effect) return fn(...args) } finally { effectStack.pop() } } }

createReactiveEffect 是一個高階函數,內部創建了一個名為 effect 的函數,函數內部返回的是一個 run 函數,run 函數中將依賴 effect 對象存入全局的待收集依賴棧 effectStack 中,並執行傳入的回調函數,該回調函數其實就是一開始示例中 effect 函數傳入的修改 Dom 的函數。也就是說依賴對象作為函數直接執行就會添加依賴到全局棧並執行回調函數。

回調函數中如果有讀取了響應式數據的話則會觸發 proxy 的 get 收集依賴,這時就能從 effectStack 上拿到該依賴對象了。

然后給 effect 增加了 computed lazy 屬性后返回。

最后就是對外暴露的 effect 及 computed 函數了:

// 創建依賴對象並判斷非計算屬性則立即執行 function effect(fn, options = {}) { let e = createReactiveEffect(fn, options) if (!options.lazy) { e() } return e } // computed 內部調用 effect 並添加計算屬性相關的 options function computed(fn) { const runner = effect(fn, { computed: true, lazy: true }) return { effect: runner, get value() { return runner() } } }

computed 就不多說了,effect 就是將傳入的回調函數傳給 createReactiveEffect 創建依賴對象,然后執行依賴對象就會執行回調函數並收集該依賴對象。

總結

  • reactive 將傳入的數據對象使用 proxy 包裝,通過 proxy 的 get set 攔截數據的獲取及修改,與 Vue2 的 defineProperty 一樣,在 get 中收集依賴,在 set 中觸發依賴;
  • effect 函數接受一個回調函數作為參數,將回調函數包裝一下作為依賴對象后執行回調函數,回調函數執行時觸發相關數據的 get 后進行依賴收集;

到此 Vue2 及 Vue3 中的數據響應式原理都分析完了。

 

Vue2 及 Vue3 數據響應式的對比

本次 Vue 對於數據響應式的升級主要在變化偵測部分。

Vue2 中的變化偵測實現對 Object 及 Array 分別進行了不同的處理,Objcet 使用了 
Object.defineProperty API ,Array 使用了攔截器對 Array 原型上的能夠改變數據的方法進行攔截。雖然也實現了數據的變化偵測,但存在很多局限 ,比如對象新增屬性無法被偵測,以及通過數組下邊修改數組內容,也因此在 Vue2 中經常會使用到 $set 這個方法對數據修改,以保證依賴更新。

Vue3 中使用了 es6 的 Proxy API 對數據代理,沒有像 Vue2 中對原數據進行修改,只是加了代理包裝,因此首先性能上會有所改善。其次解決了 Vue2 中變化偵測的局限性,可以不使用 $set 新增的對象屬性及通過下標修改數組都能被偵測到


免責聲明!

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



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