響應式原理V2 vs V3
vue2的方式使用Object.defineProperty(),攔截每一個key,需要遞歸遍歷對象所有key,速度慢,數組響應式需要額外實現'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort',新增或刪除屬性無法監聽,需要使用特殊api,不支持Map、Set、Class等數據結構。
vue3的方式使用Proxy代理整個對象,從而偵查數據變化。
造個輪子
首先實現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
}
下面是依賴收集的實現,原理如下圖:
相關api有
- effect(fn):傳入fn,返回的函數將是響應式的,內部代理的數據發生變化,它會再次執行
- track(target, key):建立響應式函數與其訪問的目標(target)和鍵(key)之間的映射關系
- trigger(target, key):根據track()建立的映射關系,找到對應響應式函數并執行它
基本結構:
// 臨時存儲響應式函數 const effectStack = []
// 將傳入fn轉換為一個響應式函數
function effect(fn, options = {}) { }
// 存放響應式函數和目標、鍵之間的映射關系 const targetMap = new WeakMap()
// 依賴收集,創建映射關系
function track(target, key) { }
// 根據映射關系獲取響應函數
function trigger(target, key) { }
實現effect()/track()/trigger()
function effect(fn, options = {}) {
// 創建reactiveEffect
const e = createReactiveEffect(fn, options)
// 執行一次觸發依賴收集
e()
return e
}
function createReactiveEffect(fn, options) {
// 封裝一個高階函數,除了執行fn,還要將自己放入effectStack為依賴收集做準備 const effect = function reactiveEffect(...args) {
if (!effectStack.includes(effect)) {
try {
// 1.effect入棧 effectStack.push(effect) // 2.執行fn
return fn(...args)
} finally {
// 3.effect出棧 effectStack.pop()
}
}
return effect
}
function track(target, key) {
// 獲取響應式函數
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 獲取target映射關系map,不存在則創建
let depMap = targetMap.get(target)
if (!depMap) {
depMap = new Map()
targetMap.set(target, depMap)
}
// 獲取key對應依賴集合,不存在則創建
let deps = depMap.get(key)
if (!deps) {
deps = new Set()
depMap.set(key, deps)
}
// 將響應函數添加到依賴集合
deps.add(effect)
}
}
function trigger(target, key) {
// 獲取target對應依賴map
const depMap = targetMap.get(target)
if (!depMap) {
return
}
// 獲取key對應集合
const deps = depMap.get(key)
if (deps) {
// 執行所有響應函數
deps.forEach(dep => dep())
}
}
// 測試
const state = reactive({ foo: 'foo' })
effect(() => {
console.log('effect', state.foo);
})
結合視圖驗證一下
<script src="03-reactivity.js"></script>
<script>
const obj = { name: 'kkb', age: 8 }
const data = reactive(obj)
// effect()定義我們的更新函數
effect(() => {
app.innerHTML = ` <h1>${data.name}今年${data.age}歲了</h1>`
})
// 修改一下數值
setInterval(() => {
data.age++
}, 1000);
</script>
計算屬性也很常用,可以基于effect實現
const double = computed(() => data.age * 2)
// effect()定義我們的更新函數
effect(() => {
app.innerHTML = ` <h1>${data.name}今年${data.age}歲了</h1> <p>乘以2是${double.value}歲</p>`
})
computed(fn):可以使傳入fn使之成為響應式函數,fn內部依賴的數值發生變化,該函數應該重新執行 獲得最新的計算結果。