魚滑怎么制作教程搜索引擎優(yōu)化seo的英文全稱是
通過(guò)上一篇文章已經(jīng)初始化項(xiàng)目,集成了ts
和jest
。本篇實(shí)現(xiàn)Vue3中響應(yīng)式模塊里的reactive
方法。
前置知識(shí)要求
如果你熟練掌握Map, Set, Proxy, Reflect,可直接跳過(guò)這部分。
Map
Map是一種用于存儲(chǔ)鍵值對(duì)的集合,并且能夠記住鍵的原始插入順序。 其中鍵和值可以是任意類型的數(shù)據(jù)。
初始化,添加,獲取
let myMap = new Map()myMap.set('name', 'wendZzoo')
myMap.set('age', 18)myMap.get('name')
myMap.get('age')
Map 中的一個(gè)鍵只能出現(xiàn)一次,它在 Map 的集合中是獨(dú)一無(wú)二的,重復(fù)設(shè)置的會(huì)被覆蓋
myMap.set('name', 'jack')
Map 的鍵和值可以是任意類型的數(shù)據(jù)
myMap.set({name: 'wendZzoo'}, [{age: 18}])
刪除
let myMap = new Map()
myMap.set('name', 'Tom')
myMap.delete('name')
key
數(shù)據(jù)類型是對(duì)象時(shí),需要使用對(duì)應(yīng)的引用來(lái)刪除鍵值對(duì)
let myMap = new Map()
let key = [{name: 'Tom'}]
myMap.set(key, 'Hello')
myMap.delete(key)// 如果使用不同的引用來(lái)嘗試刪除鍵值對(duì)
// 它將無(wú)法正常工作
// 因?yàn)镸ap無(wú)法識(shí)別這兩個(gè)引用是相同的鍵
myMap.set([{name: 'Tom'}], 'Hello')
myMap.delete([{name: 'Tom'}])
Set
Set是一種集合數(shù)據(jù)結(jié)構(gòu),它允許存儲(chǔ)唯一的值,無(wú)重復(fù)項(xiàng)。Set對(duì)象可以存儲(chǔ)任何類型的值,包括基本類型和對(duì)象引用。
let mySet = new Set()mySet.add('wendZzoo')
mySet.add(18)
mySet.add({province: 'jiangsu', city: 'suzhou'})
可迭代
for (let key of mySet) {console.log(key)
}
Proxy
Proxy 對(duì)象用于創(chuàng)建一個(gè)對(duì)象的代理,從而實(shí)現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。
Vue 響應(yīng)式的前提就是需要數(shù)據(jù)劫持,在 JS 中有兩種劫持 property 訪問(wèn)的方式:getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持舊版本瀏覽器的限制,而在 Vue 3 中則使用了 Proxy 來(lái)創(chuàng)建響應(yīng)式對(duì)象。
創(chuàng)建 Proxy 對(duì)象時(shí),需要提供兩個(gè)參數(shù):目標(biāo)對(duì)象 target(被代理的對(duì)象)和一個(gè)處理程序?qū)ο?handler(用于定義攔截行為的方法)。
其中 handler 常用的有 get,set 方法。
handler.get() 方法用于攔截對(duì)象的讀取屬性操作,完整使用可以參考:MDN
它接收三個(gè)參數(shù):
- target:目標(biāo)對(duì)象
- property:被獲取的屬性名
- receiver:Proxy 或者繼承 Proxy 的對(duì)象
const obj = {name: 'wendZzoo', age: 18}
let myProxy = new Proxy(obj, {get: (target, property, receiver) => {console.log('收集依賴')return target[property]}
})// 執(zhí)行 myProxy.name
// 執(zhí)行 myProxy.age
handler.set() 方法是設(shè)置屬性值操作的捕獲器,完整使用參考:MDN
它接收四個(gè)參數(shù)
- target:目標(biāo)對(duì)象
- property:將被設(shè)置的屬性名或 Symbol、
- value:新屬性值
- receiver:最初被調(diào)用的對(duì)象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型鏈上,或以其他方式被間接地調(diào)用(因此不一定是 proxy 本身)
const obj = {name: 'wendZzoo', age: 18}
let myProxy = new Proxy(obj, {get: (target, property, receiver) => {console.log('收集依賴')return target[property]},set: (target, property, value, receiver) => {console.log('觸發(fā)依賴')target[property] = valuereturn true}
})
myProxy.name = 'Jack'
myProxy.age = 20
Proxy 提供了一種機(jī)制,通過(guò)攔截和修改目標(biāo)對(duì)象的操作來(lái)實(shí)現(xiàn)自定義行為,在 get 和 set 方法打印日志的地方,也就是 Vue3 實(shí)現(xiàn)依賴收集和觸發(fā)依賴的地方。
Reflect
Reflect 是一個(gè)內(nèi)置的對(duì)象,它提供攔截 JS 操作的方法。這讓它可以完美的和 Proxy 配合,Proxy 提供了對(duì)對(duì)象攔截的時(shí)機(jī)位置,Reflect 提供攔截方法。
Reflect 不是一個(gè)構(gòu)造函數(shù),因此不能 new 進(jìn)行調(diào)用,更像 Math 對(duì)象,作為一個(gè)函數(shù)來(lái)調(diào)用,它所有的屬性和方法都是靜態(tài)的。
常用的方法有 get,set。
Reflect.get方法允許你從一個(gè)對(duì)象中取屬性值,完整使用參考:MDN
它接收三個(gè)參數(shù):
- target:需要取值的目標(biāo)對(duì)象
- propertyKey:需要獲取的值的鍵值
- receiver:如果 target 對(duì)象中指定了getter,receiver 則為 getter 調(diào)用時(shí)的this值
let obj = {name: 'wendZzoo', age: 18}
Reflect.get(obj, 'name')
Reflect.get(obj, 'age')
Reflect.set 方法允許在對(duì)象上設(shè)置屬性,完整使用參考:MDN
它接收三個(gè)參數(shù):
- target:設(shè)置屬性的目標(biāo)對(duì)象
- propertyKey:設(shè)置的屬性的名稱
- value:設(shè)置的值
- receiver:如果遇到 setter,receiver則為setter調(diào)用時(shí)的this值
let obj = {}
Reflect.set(obj, 'name', 'wendZzoo')let arr = ['name', 'address']
Reflect.set(arr, 1, 'age')
Reflect.set(arr, 'length', 1)
更改目錄
src
下新建文件夾reactivity
,新建effect.ts
和reactive.ts
。
tests
文件夾下刪除上一篇文章中用于驗(yàn)證jest
安裝的index.spec.ts
,新建effect.spec.ts
和reactive.spec.ts
。
reactive
先寫單測(cè),明確需要的成果,再根據(jù)這個(gè)需求來(lái)實(shí)現(xiàn)函數(shù)。Vue3的reactive
方法返回一個(gè)對(duì)象的響應(yīng)式代理,那代理的對(duì)象和源對(duì)象是不同的,但是又能和源對(duì)象一樣的嵌套結(jié)構(gòu)。
那單測(cè)可以這樣寫:reactive.spec.ts
import { reactive } from "../reactivity/reactive";describe("reactive", () => {it("happy path", () => {let original = { foo: 1 };let data = reactive(original);expect(data).not.toBe(original);expect(data.foo).toBe(1);});
});
根據(jù)這兩個(gè)斷言,來(lái)實(shí)現(xiàn)現(xiàn)階段的reactive
方法。Vue3中是使用Proxy實(shí)現(xiàn)。
reactive.ts
export function reactive(raw) {return new Proxy(raw, {get: (target, key) => {let res = Reflect.get(target, key);// TODO 依賴收集return res;},set: (target, key, value) => {let res = Reflect.set(target, key, value);// TODO 觸發(fā)依賴return res;},});
}
運(yùn)行reactive
單測(cè),來(lái)驗(yàn)證該方法實(shí)現(xiàn)是否正確,執(zhí)行yarn test reactive
effect
在官網(wǎng)上是沒有單獨(dú)提到這個(gè) API 的,可以在進(jìn)階主題的深入響應(yīng)式系統(tǒng)一篇中找到它的身影。
effect
直接翻譯為作用,意思是使其發(fā)生作用,這個(gè)使其的其就是我們傳入的函數(shù),所以effect
的作用就是讓我們傳入的函數(shù)發(fā)生作用,也就是執(zhí)行這個(gè)函數(shù)。
使用示例
import { reactive, effect } from "vue";let user = reactive({age: 10,
});let nextAge;function setAge() {effect(() => {nextAge = user.age + 1;});console.log(nextAge);
}function updateAge() {user.age++;console.log(nextAge);
}
在沒有使用effect
作用于nextAge
時(shí),直接觸發(fā)updateAge
方法,輸出的nextAge
就是undefined
;
調(diào)用setAge
,effect
中函數(shù)執(zhí)行給nextAge
賦值,響應(yīng)式數(shù)據(jù)user
中age
變化,nextAge
也在繼續(xù)執(zhí)行effect
中函數(shù)。
單測(cè)
那effect
的單測(cè)可以寫成這樣:
import { effect } from "../reactivity/effect";
import { reactive } from "../reactivity/reactive";describe("effect", () => {it("happy path", () => {let user = reactive({age: 10,});let nextAge;effect(() => {nextAge = user.age + 1;});expect(nextAge).toBe(11);});
});
effect
方法就是接收一個(gè)方法,并執(zhí)行它。
effect.ts
class ReactiveEffect {private _fn: any;constructor(fn) {this._fn = fn;}run() {this._fn();}
}export function effect(fn) {let _effect = new ReactiveEffect(fn);_effect.run();
}
通過(guò)抽離成一個(gè)Class
類,去執(zhí)行傳入的 fn
參數(shù)。
再來(lái)執(zhí)行所有的單測(cè),驗(yàn)證是否成功,執(zhí)行yarn test
依賴收集
修改effect
單測(cè),增加一個(gè)斷言,來(lái)判斷當(dāng)age
變化時(shí),nextAge
是否也更新了?
import { effect } from "../reactivity/effect";
import { reactive } from "../reactivity/reactive";describe("effect", () => {it("happy path", () => {let user = reactive({age: 10,});let nextAge;effect(() => {nextAge = user.age + 1;});expect(nextAge).toBe(11);// +++updateruser.age++;expect(nextAge).toBe(12);});
});
執(zhí)行單測(cè)發(fā)現(xiàn)無(wú)法通過(guò),是因?yàn)?code>Proxy代理時(shí)候并沒有實(shí)現(xiàn)依賴收集和觸發(fā)依賴,也就是reactive.ts
中還有兩個(gè) TODO。
但是,首先得清楚什么叫依賴?
引用官方的例子:
let A0 = 1
let A1 = 2
let A2 = A0 + A1console.log(A2) // 3A0 = 2
console.log(A2) // 仍然是 3
當(dāng)我們更改 A0 后,A2 不會(huì)自動(dòng)更新。
那么我們?nèi)绾卧?JavaScript 中做到這一點(diǎn)呢?首先,為了能重新運(yùn)行計(jì)算的代碼來(lái)更新 A2,我們需要將其包裝為一個(gè)函數(shù):
let A2function update() {A2 = A0 + A1
}
然后,我們需要定義幾個(gè)術(shù)語(yǔ):
- 這個(gè) update() 函數(shù)會(huì)產(chǎn)生一個(gè)副作用,或者就簡(jiǎn)稱為作用 (effect),因?yàn)樗鼤?huì)更改程序里的狀態(tài)。
- A0 和 A1 被視為這個(gè)作用的依賴 (dependency),因?yàn)樗鼈兊闹当挥脕?lái)執(zhí)行這個(gè)作用。因此這次作用也可以說(shuō)是一個(gè)它依賴的訂閱者 (subscriber)。
因此我們可以大膽通俗的講,依賴就是指的是觀察者(通常是視圖或副作用函數(shù))對(duì)數(shù)據(jù)的依賴關(guān)系。當(dāng)觀察者需要訪問(wèn)特定數(shù)據(jù)時(shí),它就成為該數(shù)據(jù)的依賴。
那依賴收集呢?
依賴收集是用于追蹤和管理數(shù)據(jù)依賴關(guān)系。常用于實(shí)現(xiàn)響應(yīng)式系統(tǒng),其中數(shù)據(jù)的變化會(huì)自動(dòng)觸發(fā)相關(guān)的更新操作。
當(dāng)數(shù)據(jù)發(fā)生改變時(shí),相關(guān)的視圖或操作也能夠自動(dòng)更新,以保持?jǐn)?shù)據(jù)和界面的同步。依賴收集可以幫助我們建立起數(shù)據(jù)和視圖之間的關(guān)聯(lián),確保數(shù)據(jù)的變化能夠自動(dòng)反映在視圖上。
從代碼層面講,讀取對(duì)象的時(shí)候也就是get操作時(shí),進(jìn)行依賴收集,將目標(biāo)對(duì)象target,對(duì)象中key,Dep實(shí)例做關(guān)聯(lián)映射。
在effect.ts
中定義依賴收集的方法track
。
class ReactiveEffect {private _fn: any;constructor(fn) {this._fn = fn;}run() {reactiveEffect = this;this._fn();}
}let targetMap = new Map();
export function track(target, key) {// target -> key -> deplet depMap = targetMap.get(target);if (!depMap) { // initdepMap = new Map();targetMap.set(target, depMap);}let dep = depMap.get(key);if (!dep) { // initdep = new Set();depMap.set(key, dep);}dep.add(reactiveEffect);
}let reactiveEffect;
export function effect(fn) {let _effect = new ReactiveEffect(fn);_effect.run();
}
觸發(fā)依賴
在設(shè)置對(duì)象屬性時(shí),也就是進(jìn)行set
操作時(shí),觸發(fā)依賴。將每個(gè)屬性上掛載的dep
的Set
結(jié)構(gòu)中的所有作用函數(shù)執(zhí)行。
export function trigger(target, key) {let depMap = targetMap.get(target);let dep = depMap.get(key);for (const effect of dep) {effect.run();}
}
至此,再次執(zhí)行所有單測(cè),yarn test
總結(jié)
- 先通過(guò)單測(cè)入手,明確需要實(shí)現(xiàn)的函數(shù)方法的功能
- 分布實(shí)現(xiàn)功能點(diǎn),即拆分功能點(diǎn),先初步實(shí)現(xiàn)了
reactive
方法簡(jiǎn)單版,只要求原數(shù)據(jù)和代理之后的數(shù)據(jù)不同,但是數(shù)據(jù)結(jié)構(gòu)又要一樣,像深拷貝一樣。 - 通過(guò)
Class
類,實(shí)現(xiàn)effect
方法可以自執(zhí)行其傳入的函數(shù)參數(shù) - 依賴收集,通過(guò)兩個(gè)
Map
結(jié)構(gòu)和一個(gè)Set
結(jié)構(gòu)來(lái)映射數(shù)據(jù)關(guān)系,將所有的fn
存放到dep
中。通過(guò)一個(gè)全局變量reactiveEffect
來(lái)獲取到effct
實(shí)例,為后續(xù)觸發(fā)依賴時(shí),直接拿dep
中每一項(xiàng)去執(zhí)行。 - 觸發(fā)依賴,通過(guò)映射關(guān)系獲取到
dep
,因?yàn)?code>dep是Set
結(jié)構(gòu),可迭代,循環(huán)每項(xiàng)執(zhí)行。