響應(yīng)式原理V2 vs V3
vue2的方式使用Object.defineProperty()五续,攔截每一個key坠宴,需要遞歸遍歷對象所有key蒋歌,速度慢歧蕉,數(shù)組響應(yīng)式需要額外實現(xiàn)'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'侧甫,新增或刪除屬性無法監(jiān)聽珊佣,需要使用特殊api,不支持Map披粟、Set咒锻、Class等數(shù)據(jù)結(jié)構(gòu)。
vue3的方式使用Proxy代理整個對象守屉,從而偵查數(shù)據(jù)變化惑艇。
造個輪子
首先實現(xiàn)reactive(obj),借助Proxy代理傳入的obj拇泛,這樣可以攔截對obj的各種訪問滨巴;
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相當于在對象外層加攔截
// http://es6.ruanyifeng.com/#docs/proxy const observed = new Proxy(obj, baseHandler) return observed
}
下面是依賴收集的實現(xiàn),原理如下圖:
相關(guān)api有
- effect(fn):傳入fn俺叭,返回的函數(shù)將是響應(yīng)式的恭取,內(nèi)部代理的數(shù)據(jù)發(fā)生變化,它會再次執(zhí)行
- track(target, key):建立響應(yīng)式函數(shù)與其訪問的目標(target)和鍵(key)之間的映射關(guān)系
- trigger(target, key):根據(jù)track()建立的映射關(guān)系熄守,找到對應(yīng)響應(yīng)式函數(shù)并執(zhí)行它
基本結(jié)構(gòu):
// 臨時存儲響應(yīng)式函數(shù) const effectStack = []
// 將傳入fn轉(zhuǎn)換為一個響應(yīng)式函數(shù)
function effect(fn, options = {}) { }
// 存放響應(yīng)式函數(shù)和目標蜈垮、鍵之間的映射關(guān)系 const targetMap = new WeakMap()
// 依賴收集,創(chuàng)建映射關(guān)系
function track(target, key) { }
// 根據(jù)映射關(guān)系獲取響應(yīng)函數(shù)
function trigger(target, key) { }
實現(xiàn)effect()/track()/trigger()
function effect(fn, options = {}) {
// 創(chuàng)建reactiveEffect
const e = createReactiveEffect(fn, options)
// 執(zhí)行一次觸發(fā)依賴收集
e()
return e
}
function createReactiveEffect(fn, options) {
// 封裝一個高階函數(shù)裕照,除了執(zhí)行fn攒发,還要將自己放入effectStack為依賴收集做準備 const effect = function reactiveEffect(...args) {
if (!effectStack.includes(effect)) {
try {
// 1.effect入棧 effectStack.push(effect) // 2.執(zhí)行fn
return fn(...args)
} finally {
// 3.effect出棧 effectStack.pop()
}
}
return effect
}
function track(target, key) {
// 獲取響應(yīng)式函數(shù)
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 獲取target映射關(guān)系map,不存在則創(chuàng)建
let depMap = targetMap.get(target)
if (!depMap) {
depMap = new Map()
targetMap.set(target, depMap)
}
// 獲取key對應(yīng)依賴集合晋南,不存在則創(chuàng)建
let deps = depMap.get(key)
if (!deps) {
deps = new Set()
depMap.set(key, deps)
}
// 將響應(yīng)函數(shù)添加到依賴集合
deps.add(effect)
}
}
function trigger(target, key) {
// 獲取target對應(yīng)依賴map
const depMap = targetMap.get(target)
if (!depMap) {
return
}
// 獲取key對應(yīng)集合
const deps = depMap.get(key)
if (deps) {
// 執(zhí)行所有響應(yīng)函數(shù)
deps.forEach(dep => dep())
}
}
// 測試
const state = reactive({ foo: 'foo' })
effect(() => {
console.log('effect', state.foo);
})
結(jié)合視圖驗證一下
<script src="03-reactivity.js"></script>
<script>
const obj = { name: 'kkb', age: 8 }
const data = reactive(obj)
// effect()定義我們的更新函數(shù)
effect(() => {
app.innerHTML = ` <h1>${data.name}今年${data.age}歲了</h1>`
})
// 修改一下數(shù)值
setInterval(() => {
data.age++
}, 1000);
</script>
計算屬性也很常用惠猿,可以基于effect實現(xiàn)
const double = computed(() => data.age * 2)
// effect()定義我們的更新函數(shù)
effect(() => {
app.innerHTML = ` <h1>${data.name}今年${data.age}歲了</h1> <p>乘以2是${double.value}歲</p>`
})
computed(fn):可以使傳入fn使之成為響應(yīng)式函數(shù),fn內(nèi)部依賴的數(shù)值發(fā)生變化搬俊,該函數(shù)應(yīng)該重新執(zhí)行 獲得最新的計算結(jié)果紊扬。