Vue 3 中的響應式原理可謂是很是之重要,經過學習 Vue3 的響應式原理,不只能讓咱們學習到 Vue.js 的一些設計模式和思想,還能幫助咱們提升項目開發效率和代碼調試能力。 javascript
在這以前,我也寫了一篇《探索 Vue.js 響應式原理》 ,主要介紹 Vue 2 響應式的原理,這篇補上 Vue 3 的。 html
因而最近在 Vue Mastery 上從新學習 Vue3 Reactivity 的知識,此次收穫更大。本文將帶你們從頭開始學習如何實現簡單版 Vue 3 響應式,幫助你們瞭解其核心,後面閱讀 Vue 3 響應式相關的源碼可以更加駕輕就熟。前端
當咱們在學習 Vue 3 的時候,能夠經過一個簡單示例,看看什麼是 Vue 3 中的響應式:vue
<!-- HTML 內容 --> <div id="app"> <div>Price: {{price}}</div> <div>Total: {{price * quantity}}</div> <div>getTotal: {{getTotal}}</div> </div>
const app = Vue.createApp({ // ① 建立 APP 實例 data() { return { price: 10, quantity: 2 } }, computed: { getTotal() { return this.price * this.quantity * 1.1 } } }) app.mount('#app') // ② 掛載 APP 實例
經過建立 APP 實例和掛載 APP 實例便可,這時能夠看到頁面中分別顯示對應數值:java
當咱們修改 price
或 quantity
值的時候,頁面上引用它們的地方,內容也能正常展現變化後的結果。這時,咱們會好奇爲什麼數據發生變化後,相關的數據也會跟着變化,那麼咱們接着往下看。react
在普通 JS 代碼執行中,並不會有響應式變化,好比在控制檯執行下面代碼:git
let price = 10, quantity = 2; const total = price * quantity; console.log(`total: ${total}`); // total: 20 price = 20; console.log(`total: ${total}`); // total: 20
從這能夠看出,在修改 price
變量的值後, total
的值並無發生改變。github
那麼如何修改上面代碼,讓 total
可以自動更新呢?咱們其實能夠將修改 total
值的方法保存起來,等到與 total
值相關的變量(如 price
或 quantity
變量的值)發生變化時,觸發該方法,更新 total
便可。咱們能夠這麼實現:設計模式
let price = 10, quantity = 2, total = 0; const dep = new Set(); // ① const effect = () => { total = price * quantity }; const track = () => { dep.add(effect) }; // ② const trigger = () => { dep.forEach( effect => effect() )}; // ③ track(); console.log(`total: ${total}`); // total: 0 trigger(); console.log(`total: ${total}`); // total: 20 price = 20; trigger(); console.log(`total: ${total}`); // total: 40
上面代碼經過 3 個步驟,實現對 total
數據進行響應式變化:api
① 初始化一個 Set
類型的 dep
變量,用來存放須要執行的反作用( effect
函數),這邊是修改 total
值的方法;
② 建立 track()
函數,用來將須要執行的反作用保存到 dep
變量中(也稱收集反作用);
③ 建立 trigger()
函數,用來執行 dep
變量中的全部反作用;
在每次修改 price
或 quantity
後,調用 trigger()
函數執行全部反作用後, total
值將自動更新爲最新值。
(圖片來源:Vue Mastery)
一般,咱們的對象具備多個屬性,而且每一個屬性都須要本身的 dep
。咱們如何存儲這些?好比:
let product = { price: 10, quantity: 2 };
從前面介紹咱們知道,咱們將全部反作用保存在一個 Set
集合中,而該集合不會有重複項,這裏咱們引入一個 Map
類型集合(即 depsMap
),其 key
爲對象的屬性(如: price
屬性), value
爲前面保存反作用的 Set
集合(如: dep
對象),大體結構以下圖:
(圖片來源:Vue Mastery)
實現代碼:
let product = { price: 10, quantity: 2 }, total = 0; const depsMap = new Map(); // ① const effect = () => { total = product.price * product.quantity }; const track = key => { // ② let dep = depsMap.get(key); if(!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); } const trigger = key => { // ③ let dep = depsMap.get(key); if(dep) { dep.forEach( effect => effect() ); } }; track('price'); console.log(`total: ${total}`); // total: 0 effect(); console.log(`total: ${total}`); // total: 20 product.price = 20; trigger('price'); console.log(`total: ${total}`); // total: 40
上面代碼經過 3 個步驟,實現對 total
數據進行響應式變化:
① 初始化一個 Map
類型的 depsMap
變量,用來保存每一個須要響應式變化的對象屬性(key
爲對象的屬性, value
爲前面 Set
集合);
② 建立 track()
函數,用來將須要執行的反作用保存到 depsMap
變量中對應的對象屬性下(也稱收集反作用);
③ 建立 trigger()
函數,用來執行 dep
變量中指定對象屬性的全部反作用;
這樣就實現監聽對象的響應式變化,在 product
對象中的屬性值發生變化, total
值也會跟着更新。
若是咱們有多個響應式數據,好比同時須要觀察對象 a
和對象 b
的數據,那麼又要如何跟蹤每一個響應變化的對象?
這裏咱們引入一個 WeakMap 類型的對象,將須要觀察的對象做爲 key
,值爲前面用來保存對象屬性的 Map 變量。代碼以下:
let product = { price: 10, quantity: 2 }, total = 0; const targetMap = new WeakMap(); // ① 初始化 targetMap,保存觀察對象 const effect = () => { total = product.price * product.quantity }; const track = (target, key) => { // ② 收集依賴 let depsMap = targetMap.get(target); if(!depsMap){ targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if(!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); } const trigger = (target, key) => { // ③ 執行指定對象的指定屬性的全部反作用 const depsMap = targetMap.get(target); if(!depsMap) return; let dep = depsMap.get(key); if(dep) { dep.forEach( effect => effect() ); } }; track(product, 'price'); console.log(`total: ${total}`); // total: 0 effect(); console.log(`total: ${total}`); // total: 20 product.price = 20; trigger(product, 'price'); console.log(`total: ${total}`); // total: 40
上面代碼經過 3 個步驟,實現對 total
數據進行響應式變化:
① 初始化一個 WeakMap
類型的 targetMap
變量,用來要觀察每一個響應式對象;
② 建立 track()
函數,用來將須要執行的反作用保存到指定對象( target
)的依賴中(也稱收集反作用);
③ 建立 trigger()
函數,用來執行指定對象( target
)中指定屬性( key
)的全部反作用;
這樣就實現監聽對象的響應式變化,在 product
對象中的屬性值發生變化, total
值也會跟着更新。
大體流程以下圖:
(圖片來源:Vue Mastery)
在上一節內容中,介紹瞭如何在數據發生變化後,自動更新數據,但存在的問題是,每次須要手動經過觸發 track()
函數蒐集依賴,經過 trigger()
函數執行全部反作用,達到數據更新目的。
這一節未來解決這個問題,實現這兩個函數自動調用。
這裏咱們引入 JS 對象訪問器的概念,解決辦法以下:
track()
函數自動收集依賴;trigger()
函數執行全部反作用;那麼如何攔截 GET 和 SET 操做?接下來看看 Vue2 和 Vue3 是如何實現的:
Object.defineProperty()
函數實現;Proxy
和 Reflect
API 實現;須要注意的是:Vue3 使用的 Proxy
和 Reflect
API 並不支持 IE。
Object.defineProperty()
函數這邊就很少作介紹,能夠閱讀文檔,下文將主要介紹 Proxy
和 Reflect
API。
一般咱們有三種方法讀取一個對象的屬性:
.
操做符:leo.name
;[]
: leo['name']
;Reflect
API: Reflect.get(leo, 'name')
。這三種方式輸出結果相同。
Proxy 對象用於建立一個對象的代理,從而實現基本操做的攔截和自定義(如屬性查找、賦值、枚舉、函數調用等)。語法以下:
const p = new Proxy(target, handler)
參數以下:
p
的行爲。咱們經過官方文檔,體驗一下 Proxy API:
let product = { price: 10, quantity: 2 }; let proxiedProduct = new Proxy(product, { get(target, key){ console.log('正在讀取的數據:',key); return target[key]; } }) console.log(proxiedProduct.price); // 正在讀取的數據: price // 10
這樣就保證咱們每次在讀取 proxiedProduct.price
都會執行到其中代理的 get 處理函數。其過程以下:
(圖片來源:Vue Mastery)
而後結合 Reflect 使用,只需修改 get 函數:
get(target, key, receiver){ console.log('正在讀取的數據:',key); return Reflect.get(target, key, receiver); }
輸出結果仍是同樣。
接下來增長 set 函數,來攔截對象的修改操做:
let product = { price: 10, quantity: 2 }; let proxiedProduct = new Proxy(product, { get(target, key, receiver){ console.log('正在讀取的數據:',key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver){ console.log('正在修改的數據:', key, ',值爲:', value); return Reflect.set(target, key, value, receiver); } }) proxiedProduct.price = 20; console.log(proxiedProduct.price); // 正在修改的數據: price ,值爲: 20 // 正在讀取的數據: price // 20
這樣便完成 get 和 set 函數來攔截對象的讀取和修改的操做。爲了方便對比 Vue 3 源碼,咱們將上面代碼抽象一層,使它看起來更像 Vue3 源碼:
function reactive(target){ const handler = { // ① 封裝統一處理函數對象 get(target, key, receiver){ console.log('正在讀取的數據:',key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver){ console.log('正在修改的數據:', key, ',值爲:', value); return Reflect.set(target, key, value, receiver); } } return new Proxy(target, handler); // ② 統一調用 Proxy API } let product = reactive({price: 10, quantity: 2}); // ③ 將對象轉換爲響應式對象 product.price = 20; console.log(product.price); // 正在修改的數據: price ,值爲: 20 // 正在讀取的數據: price // 20
這樣輸出結果仍然不變。
經過上面代碼,咱們已經實現一個簡單 reactive()
函數,用來將普通對象轉換爲響應式對象。可是還缺乏自動執行 track()
函數和 trigger()
函數,接下來修改上面代碼:
const targetMap = new WeakMap(); let total = 0; const effect = () => { total = product.price * product.quantity }; const track = (target, key) => { let depsMap = targetMap.get(target); if(!depsMap){ targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if(!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); } const trigger = (target, key) => { const depsMap = targetMap.get(target); if(!depsMap) return; let dep = depsMap.get(key); if(dep) { dep.forEach( effect => effect() ); } }; const reactive = (target) => { const handler = { get(target, key, receiver){ console.log('正在讀取的數據:',key); const result = Reflect.get(target, key, receiver); track(target, key); // 自動調用 track 方法收集依賴 return result; }, set(target, key, value, receiver){ console.log('正在修改的數據:', key, ',值爲:', value); const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if(oldValue != result){ trigger(target, key); // 自動調用 trigger 方法執行依賴 } return result; } } return new Proxy(target, handler); } let product = reactive({price: 10, quantity: 2}); effect(); console.log(total); product.price = 20; console.log(total); // 正在讀取的數據: price // 正在讀取的數據: quantity // 20 // 正在修改的數據: price ,值爲: 20 // 正在讀取的數據: price // 正在讀取的數據: quantity // 40
(圖片來源:Vue Mastery)
在上一節代碼中,還存在一個問題: track
函數中的依賴( effect
函數)是外部定義的,當依賴發生變化, track
函數收集依賴時都要手動修改其依賴的方法名。
好比如今的依賴爲 foo
函數,就要修改 track
函數的邏輯,多是這樣:
const foo = () => { /**/ }; const track = (target, key) => { // ② // ... dep.add(foo); }
那麼如何解決這個問題呢?
接下來引入 activeEffect
變量,來保存當前運行的 effect 函數。
let activeEffect = null; const effect = eff => { activeEffect = eff; // 1. 將 eff 函數賦值給 activeEffect activeEffect(); // 2. 執行 activeEffect activeEffect = null;// 3. 重置 activeEffect }
而後在 track
函數中將 activeEffect
變量做爲依賴:
const track = (target, key) => { if (activeEffect) { // 1. 判斷當前是否有 activeEffect let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); // 2. 添加 activeEffect 依賴 } }
使用方式修改成:
effect(() => { total = product.price * product.quantity });
這樣就能夠解決手動修改依賴的問題,這也是 Vue3 解決該問題的方法。完善一下測試代碼後,以下:
const targetMap = new WeakMap(); let activeEffect = null; // 引入 activeEffect 變量 const effect = eff => { activeEffect = eff; // 1. 將反作用賦值給 activeEffect activeEffect(); // 2. 執行 activeEffect activeEffect = null;// 3. 重置 activeEffect } const track = (target, key) => { if (activeEffect) { // 1. 判斷當前是否有 activeEffect let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); // 2. 添加 activeEffect 依賴 } } const trigger = (target, key) => { const depsMap = targetMap.get(target); if (!depsMap) return; let dep = depsMap.get(key); if (dep) { dep.forEach(effect => effect()); } }; const reactive = (target) => { const handler = { get(target, key, receiver) { const result = Reflect.get(target, key, receiver); track(target, key); return result; }, set(target, key, value, receiver) { const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (oldValue != result) { trigger(target, key); } return result; } } return new Proxy(target, handler); } let product = reactive({ price: 10, quantity: 2 }); let total = 0, salePrice = 0; // 修改 effect 使用方式,將反作用做爲參數傳給 effect 方法 effect(() => { total = product.price * product.quantity }); effect(() => { salePrice = product.price * 0.9 }); console.log(total, salePrice); // 20 9 product.quantity = 5; console.log(total, salePrice); // 50 9 product.price = 20; console.log(total, salePrice); // 100 18
思考一下,若是把第一個 effect
函數中 product.price
換成 salePrice
會如何:
effect(() => { total = salePrice * product.quantity }); effect(() => { salePrice = product.price * 0.9 }); console.log(total, salePrice); // 0 9 product.quantity = 5; console.log(total, salePrice); // 45 9 product.price = 20; console.log(total, salePrice); // 45 18
獲得的結果徹底不一樣,由於 salePrice
並非響應式變化,而是須要調用第二個 effect
函數纔會變化,也就是 product.price
變量值發生變化。
代碼地址:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/05-activeEffect.js
熟悉 Vue3 Composition API 的朋友可能會想到 Ref,它接收一個值,並返回一個響應式可變的 Ref 對象,其值能夠經過 value
屬性獲取。
ref:接受一個內部值並返回一個響應式且可變的 ref 對象。ref 對象具備指向內部值的單個 property .value。
官網的使用示例以下:
const count = ref(0) console.log(count.value) // 0 count.value++ console.log(count.value) // 1
咱們有 2 種方法實現 ref 函數:
rective
函數const ref = intialValue => reactive({value: intialValue});
這樣是能夠的,雖然 Vue3 不是這麼實現。
const ref = raw => { const r = { get value(){ track(r, 'value'); return raw; }, set value(newVal){ raw = newVal; trigger(r, 'value'); } } return r; }
使用方式以下:
let product = reactive({ price: 10, quantity: 2 }); let total = 0, salePrice = ref(0); effect(() => { salePrice.value = product.price * 0.9 }); effect(() => { total = salePrice.value * product.quantity }); console.log(total, salePrice.value); // 18 9 product.quantity = 5; console.log(total, salePrice.value); // 45 9 product.price = 20; console.log(total, salePrice.value); // 90 18
在 Vue3 中 ref 實現的核心也是如此。
代碼地址:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/06-ref.js
用過 Vue 的同窗可能會好奇,上面的 salePrice
和 total
變量爲何不使用 computed
方法呢?
沒錯,這個能夠的,接下來一塊兒實現個簡單的 computed
方法。
const computed = getter => { let result = ref(); effect(() => result.value = getter()); return result; } let product = reactive({ price: 10, quantity: 2 }); let salePrice = computed(() => { return product.price * 0.9; }) let total = computed(() => { return salePrice.value * product.quantity; }) console.log(total.value, salePrice.value); product.quantity = 5; console.log(total.value, salePrice.value); product.price = 20; console.log(total.value, salePrice.value);
這裏咱們將一個函數做爲參數傳入 computed
方法,computed
方法內經過 ref
方法構建一個 ref 對象,而後經過 effct
方法,將 getter
方法返回值做爲 computed
方法的返回值。
這樣咱們實現了個簡單的 computed
方法,執行效果和前面同樣。
這一節介紹如何去從 Vue 3 倉庫打包一個 Reactivity 包來學習和使用。
準備流程以下:
git clone https://github.com/vuejs/vue-next.git
yarn install
yarn build reactivity
上一步構建完的內容,會保存在 packages/reactivity/dist
目錄下,咱們只要在本身的學習 demo 中引入該目錄的 reactivity.cjs.js 文件便可。
const { reactive, computed, effect } = require("./reactivity.cjs.js");
在源碼的 packages/reactivity/src
目錄下,有如下幾個主要文件:
effect
/ track
/ trigger
;reactive
方法並建立 ES6 Proxy;
(圖片來源:Vue Mastery)
本文帶你們從頭開始學習如何實現簡單版 Vue 3 響應式,實現了 Vue3 Reactivity 中的核心方法( effect
/ track
/ trigger
/ computed
/ref
等方法),幫助你們瞭解其核心,提升項目開發效率和代碼調試能力。
我是王平安,若是個人文章對你有幫助,請點個 贊👍🏻 支持我一下
個人公衆號:前端自習課,每日清晨,享受一篇前端優秀文章。歡迎你們加入個人前端羣,一塊兒分享和交流技術,vx: pingan8787
。