【源码系列#02】Vue3响应式原理(Effect)
專欄分享:vue2源碼專欄,vue3源碼專欄,vue router源碼專欄,玩具項目專欄,硬核??推薦??
歡迎各位ITer關注點贊收藏??????
Vue3中響應數據核心是 reactive , reactive 的實現是由 proxy 加 effect 組合,上一章節我們利用 proxy 實現了一個簡易版的 reactive,# 【源碼系列#01】Vue3響應式原理(Reactive)。接下來讓我們一起手寫下 effect 的源碼
effect
effect 作為 reactive 的核心,主要負責收集依賴,更新依賴
在學習 effect之前,我們再來看下這張圖
- targetMap:存儲了每個 "響應性對象屬性" 關聯的依賴;類型是 WeakMap
- depsMap:存儲了每個屬性的依賴;類型是 Map
- dep:存儲了我們的 effects ,一個 effects 集,這些 effect 在值發生變化時重新運行;類型是 Set
編寫effect函數
// 當前正在執行的effect
export let activeEffect = undefined
export class ReactiveEffect {
// @issue2
// 這里表示在實例上新增了parent屬性,記錄父級effect
public parent = null
// 記錄effect依賴的屬性
public deps = []
// 這個effect默認是激活狀態
public active = true
// 用戶傳遞的參數也會傳遞到this上 this.fn = fn
constructor(public fn, public scheduler) {}
// run就是執行effect
run() {
// 這里表示如果是非激活的情況,只需要執行函數,不需要進行依賴收集
if (!this.active) {
return this.fn()
}
// 這里就要依賴收集了 核心就是將當前的effect 和 稍后渲染的屬性關聯在一起
try {
// 記錄父級effect
this.parent = activeEffect
activeEffect = this
// 當稍后調用取值操作的時候 就可以獲取到這個全局的activeEffect了
return this.fn()
} finally {
// 還原父級effect
activeEffect = this.parent
}
}
}
export function effect(fn, options: any = {}) {
// 這里fn可以根據狀態變化 重新執行, effect可以嵌套著寫
const _effect = new ReactiveEffect(fn) // 創建響應式的effect
// issue1
_effect.run() // 默認先執行一次
}
@issue1 effect 默認會先執行一次
依賴收集
const targetMap = new WeakMap()
export function track(target, type, key) {
// @issue3
// 我們只想在我們有activeEffect時運行這段代碼
if (!activeEffect) return
let depsMap = targetMap.get(target) // 第一次沒有
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // key -> name / age
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 單向指的是 屬性記錄了effect, 反向記錄,應該讓effect也記錄他被哪些屬性收集過,這樣做的好處是為了可以清理
trackEffects(dep)
}
export function trackEffects(dep) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect) // 去重了
if (shouldTrack) {
dep.add(activeEffect)
// @issue4
// 存放的是屬性對應的set
activeEffect.deps.push(dep) // 讓effect記錄住對應的dep, 稍后清理的時候會用到
}
}
}
@issue3 當activeEffect有值時,即只在effect運行時執行track依賴收集
@issue4 雙向記錄 ,一個屬性對應多個effect,一個effect對應多個屬性
一個屬性對應多個 effect: 在之前的 depsMap 圖中,我們得知,一個屬性映射一個 dep(即 effect 集合,類型為 Set)
一個effect對應多個屬性: 在 effect 中,有一個 deps 屬性,她記錄了此 effect 依賴的每一個屬性所對應的 dep。讓 effect 記錄對應的 dep, 目的是在稍后清理的時候會用到
觸發更新
export function trigger(target, type, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return // 觸發的值不在模板中使用
let effects = depsMap.get(key) // 找到了屬性對應的effect
// 永遠在執行之前 先拷貝一份來執行, 不要關聯引用
if (effects) {
triggerEffects(effects)
}
}
export function triggerEffects(effects) {
effects.forEach(effect => {
// 我們在執行effect的時候 又要執行自己,那我們需要屏蔽掉,不要無限調用,【避免由activeEffect觸發trigger,再次觸發當前effect。 activeEffect -> fn -> set -> trigger -> 當前effect】
// @issue5
if (effect !== activeEffect) {
effect.run() // 否則默認刷新視圖
}
})
}
@issue5 避免由run觸發trigger,無限遞歸循環
我們在執行 effect 的時候,又要執行自己,那我們需要屏蔽掉,不要無限調用【避免由 activeEffect 觸發 trigger,再次觸發當前 effect。 activeEffect -> fn -> set -> trigger -> 當前effect】
舉個栗子
const { effect, reactive } = VueReactivity
const data = { name: '柏成', age: 13, address: { num: 517 } }
const state = reactive(data)
// vue3中的代理都是用proxy來解決的
// 此effect函數默認會先執行一次, 對響應式數據取值(取值的過程中數據會依賴于當前的effect)
effect(() => {
state.age = Math.random()
document.getElementById('app').innerHTML = state.name + '今年' + state.age + '歲了'
})
// 稍后name和age變化會重新執行effect函數
setTimeout(() => {
state.age = 18
}, 1000)
分支切換與cleanup
// 每次執行effect的時候清理一遍依賴,再重新收集,雙向清理
function cleanupEffect(effect) {
// deps 里面裝的是name對應的effect, age對應的effect
const { deps } = effect
for (let i = 0; i < deps.length; i++) {
// 解除effect,重新依賴收集
deps[i].delete(effect)
}
effect.deps.length = 0
}
export class ReactiveEffect {
// @issue3
// 這里表示在實例上新增了parent屬性,記錄父級effect
public parent = null
// 記錄effect依賴的屬性
public deps = []
// 這個effect默認是激活狀態
public active = true
// 用戶傳遞的參數也會傳遞到this上 this.fn = fn
constructor(public fn, public scheduler) {} // @issue8 - scheduler
// run就是執行effect
run() {
// 這里表示如果是非激活的情況,只需要執行函數,不需要進行依賴收集
if (!this.active) {
return this.fn()
}
// 這里就要依賴收集了 核心就是將當前的effect 和 稍后渲染的屬性關聯在一起
try {
// 記錄父級effect
this.parent = activeEffect
activeEffect = this
// 這里我們需要在執行用戶函數之前將之前收集的內容清空
cleanupEffect(this) // @issue6
// 當稍后調用取值操作的時候 就可以獲取到這個全局的activeEffect了
return this.fn() // @issue1
} finally {
// 還原父級effect
activeEffect = this.parent
}
}
}
export function triggerEffects(effects) {
// 先拷貝,防止死循環,new Set 后產生一個新的Set
effects = new Set(effects) // @issue7
effects.forEach(effect => {
// 我們在執行effect的時候 又要執行自己,那我們需要屏蔽掉,不要無限調用,【避免由activeEffect觸發trigger,再次觸發當前effect。 activeEffect -> fn -> set -> trigger -> 當前effect】
if (effect !== activeEffect) {
effect.run() // 否則默認刷新視圖
}
})
}
@issue6 分支切換 - cleanupEffect。我們需要在執行用戶函數之前將之前收集的內容清空,雙向清理,在渲染時我們要避免副作用函數產生的遺留,舉個栗子,我們再次修改name,原則上不應更新頁面
每次副作用函數執行時,可以先把它從所有與之關聯的依賴集合中刪除。當副作用函數執行完畢后,響應式數據會與副作用函數之間建立新的依賴關系,而分支切換后,與副作用函數沒有依賴關系的響應式數據則不會再建立依賴,這樣副作用函數遺留的問題就解決了;
const { effect, reactive } = VueReactivity
const state = reactive({ flag: true, name: '柏成', age: 24 })
effect(() => {
// 我們期望的是每次執行effect的時候都可以清理一遍依賴,重新收集
// 副作用函數 (effect執行渲染了頁面)
console.log('render')
document.body.innerHTML = state.flag ? state.name : state.age
})
setTimeout(() => {
state.flag = false
setTimeout(() => {
// 修改name,原則上不更新頁面
state.name = '李'
}, 1000)
}, 1000)
@issue7 分支切換 - 死循環。遍歷 set 對象時,先 delete 再 add,會出現死循環
在調用循環遍歷 Set 集合時,如果一個值已經被訪問過了,但該值被刪除,并重新添加到集合,如果此時循環遍歷沒有結束,那該值會被重新訪問。
參考資料:ECMAScript Language Specification
提示:語言規范說的是forEach時是這樣的,實測 for of 遍歷Set會有同樣的問題。
看一下 triggerEffects 方法,遍歷了 effects
export function triggerEffects(effects) {
effects.forEach(effect => { effect.run() })
}
在 effect.run 方法中
- 執行
cleanupEffect(effect),清理一遍依賴
deps[i].delete(effect)
- 執行
this.fn(),重新執行函數,重新收集依賴
// track() 方法中
dep.add(activeEffect) // 將副作用函數activeEffect添加到響應式依賴中
解決方法:
let effect = () => {};
let deps = new Set([effect])
deps.forEach(item=>{
console.log('>>>')
deps.delete(effect);
deps.add(effect)
}); // 這樣就導致死循環了
// 解決方案如下,先拷貝一份,遍歷的Set對象 和 操作(delete、add)的Set對象不是同一個即可
let effect = () => {};
let deps = new Set([effect])
const newDeps = new Set(deps)
newDeps.forEach(item=>{
console.log('>>>')
deps.delete(effect);
deps.add(effect)
});
effect嵌套
// 當前正在執行的effect
export let activeEffect = undefined
export class ReactiveEffect {
// @issue2
// 這里表示在實例上新增了parent屬性,記錄父級effect
public parent = null
// 記錄effect依賴的屬性
public deps = []
// 這個effect默認是激活狀態
public active = true
// 用戶傳遞的參數也會傳遞到this上 this.fn = fn
constructor(public fn, public scheduler) {}
// run就是執行effect
run() {
// 這里表示如果是非激活的情況,只需要執行函數,不需要進行依賴收集
if (!this.active) {
return this.fn()
}
// 這里就要依賴收集了 核心就是將當前的effect 和 稍后渲染的屬性關聯在一起
try {
// 記錄父級effect
this.parent = activeEffect
activeEffect = this
// 當稍后調用取值操作的時候 就可以獲取到這個全局的activeEffect了
return this.fn()
} finally {
// 還原父級effect
activeEffect = this.parent
}
}
}
export function effect(fn, options: any = {}) {
// 這里fn可以根據狀態變化 重新執行, effect可以嵌套著寫
const _effect = new ReactiveEffect(fn) // 創建響應式的effect
// issue1
_effect.run() // 默認先執行一次
}
@issue2 利用 parent 解決effect嵌套問題,effect 嵌套的場景在 Vue.js 中常常出現,如:Vue中的渲染函數(render)就是在一個effect中執行的,嵌套組件就會伴隨著嵌套 effect
- 解決effect嵌套問題----棧方式------------------------vue2/vue3.0初始版本
// 運行effect,此effect入棧,運行完畢,最后一個effect出棧,屬性關聯棧中的最后一個effect
[e1] -> [e1,e2] -> [e1]
effect(() => { // activeEffect = e1
state.name // name -> e1
effect(() => { // activeEffect = e2
state.age // age -> e2
})
// activeEffect = e1
state.address // address = e1
})
- 解決effect嵌套問題----樹形結構方式----------------vue3后續版本
// 這個執行流程 就類似于一個樹形結構
effect(()=>{ // parent = null activeEffect = e1
state.name // name -> e1
effect(()=>{ // parent = e1 activeEffect = e2
state.age // age -> e2
effect(()=> { // parent = e2 activeEffect = e3
state.sex // sex -> e3
}) // activeEffect = e2
}) // activeEffect = e1
state.address // address -> e1
effect(()=>{ // parent = e1 activeEffect = e4
state.age // age -> e4
})
})
停止effect和調度執行
export class ReactiveEffect {
// @issue8 - stop
stop() {
if (this.active) {
this.active = false
cleanupEffect(this) // 停止effect的收集
}
}
}
export function effect(fn, options: any = {}) {
// 這里fn可以根據狀態變化 重新執行, effect可以嵌套著寫
const _effect = new ReactiveEffect(fn, options.scheduler) // 創建響應式的effect @issue8 - scheduler
_effect.run() // 默認先執行一次
// @issue8 - stop
// 綁定this,run方法內的this指向_effect,若不綁定,這樣調用run方法時,runner(),則指向undefined
const runner = _effect.run.bind(_effect)
// 將effect掛載到runner函數上,調用stop方式時可以這樣調用 runner.effect.stop()
runner.effect = _effect
return runner
}
export function triggerEffects(effects) {
// 先拷貝,防止死循環,new Set 后產生一個新的Set
effects = new Set(effects) // @issue7
effects.forEach(effect => {
// 我們在執行effect的時候 又要執行自己,那我們需要屏蔽掉,不要無限調用,【避免由activeEffect觸發trigger,再次觸發當前effect。 activeEffect -> fn -> set -> trigger -> 當前effect】
if (effect !== activeEffect) {
// @issue8 - scheduler
if (effect.scheduler) {
effect.scheduler() // 如果用戶傳入了調度函數,則執行調度函數
} else {
effect.run() // 否則默認刷新視圖
}
}
})
}
如何使用 stop 和 scheduler ?舉個小栗子
- 當我們調用
runner.effect.stop()時,就雙向清理了 effect 的所有依賴,后續state.age發生變化后,將不再重新更新頁面 - 基于 scheduler 調度器,我們可以控制頁面更新的周期,下面例子中,會在1秒后,頁面由 30 變為 5000
let waiting = false
const { effect, reactive } = VueReactivity
const state = reactive({ flag: true, name: 'jw', age: 30, address: { num: 10 } })
let runner = effect(
() => {
// 副作用函數 (effect執行渲染了頁面)
document.body.innerHTML = state.age
},
{
scheduler() {
// 調度 如何更新自己決定
console.log('run')
if (!waiting) {
waiting = true
setTimeout(() => {
runner()
waiting = false
}, 1000)
}
},
},
)
// 清理 effect 所有依賴,state.age 發生變化后,將不再重新更新頁面
// runner.effect.stop()
state.age = 1000
state.age = 2000
state.age = 3000
state.age = 4000
state.age = 5000
effect.ts
完整代碼如下
/**
* @issue1 effect默認會先執行一次
* @issue2 activeEffect 只在effect運行時執行track保存
* @issue3 parent 解決effect嵌套問題
* @issue4 雙向記錄 一個屬性對應多個effect,一個effect對應多個屬性 √
* @issue5 避免由run觸發trigger,遞歸循環
* @issue6 分支切換 cleanupEffect
* @issue7 分支切換 死循環,set循環中,先delete再add,會出現死循環
* @issue8 自定義調度器 類似Vue3中的effectScope stop 和 scheduler
*/
// 當前正在執行的effect
export let activeEffect = undefined
// @issue6
// 每次執行effect的時候清理一遍依賴,再重新收集,雙向清理
function cleanupEffect(effect) {
// deps 里面裝的是name對應的effect, age對應的effect
const { deps } = effect
for (let i = 0; i < deps.length; i++) {
// 解除effect,重新依賴收集
deps[i].delete(effect)
}
effect.deps.length = 0
}
export class ReactiveEffect {
// @issue3
// 這里表示在實例上新增了parent屬性,記錄父級effect
public parent = null
// 記錄effect依賴的屬性
public deps = []
// 這個effect默認是激活狀態
public active = true
// 用戶傳遞的參數也會傳遞到this上 this.fn = fn
constructor(public fn, public scheduler) {} // @issue8 - scheduler
// run就是執行effect
run() {
// 這里表示如果是非激活的情況,只需要執行函數,不需要進行依賴收集
if (!this.active) {
return this.fn()
}
// 這里就要依賴收集了 核心就是將當前的effect 和 稍后渲染的屬性關聯在一起
try {
// 記錄父級effect
this.parent = activeEffect
activeEffect = this
// 這里我們需要在執行用戶函數之前將之前收集的內容清空
cleanupEffect(this) // @issue6
// 當稍后調用取值操作的時候 就可以獲取到這個全局的activeEffect了
return this.fn() // @issue1
} finally {
// 還原父級effect
activeEffect = this.parent
}
}
// @issue8 - stop
stop() {
if (this.active) {
this.active = false
cleanupEffect(this) // 停止effect的收集
}
}
}
export function effect(fn, options: any = {}) {
// 這里fn可以根據狀態變化 重新執行, effect可以嵌套著寫
const _effect = new ReactiveEffect(fn, options.scheduler) // 創建響應式的effect @issue8 - scheduler
_effect.run() // 默認先執行一次
// @issue8 - stop
// 綁定this,run方法內的this指向_effect,若不綁定,這樣調用run方法時,runner(),則指向undefined
const runner = _effect.run.bind(_effect)
// 將effect掛載到runner函數上,調用stop方式時可以這樣調用 runner.effect.stop()
runner.effect = _effect
return runner
}
// 對象 某個屬性 -》 多個effect
// WeakMap = {對象:Map{name:Set-》effect}}
// {對象:{name:[]}}
// 多對多 一個effect對應多個屬性, 一個屬性對應多個effect
const targetMap = new WeakMap()
export function track(target, type, key) {
// 我們只想在我們有activeEffect時運行這段代碼
if (!activeEffect) return // @issue2
let depsMap = targetMap.get(target) // 第一次沒有
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key) // key -> name / age
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 單向指的是 屬性記錄了effect, 反向記錄,應該讓effect也記錄他被哪些屬性收集過,這樣做的好處是為了可以清理
trackEffects(dep)
}
export function trackEffects(dep) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect) // 去重了
if (shouldTrack) {
dep.add(activeEffect)
// @issue4
// 存放的是屬性對應的set
activeEffect.deps.push(dep) // 讓effect記錄住對應的dep, 稍后清理的時候會用到
}
}
}
export function trigger(target, type, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return // 觸發的值不在模板中使用
let effects = depsMap.get(key) // 找到了屬性對應的effect
// 永遠在執行之前 先拷貝一份來執行, 不要關聯引用
if (effects) {
triggerEffects(effects)
}
}
export function triggerEffects(effects) {
// 先拷貝,防止死循環,new Set 后產生一個新的Set
effects = new Set(effects) // @issue7
effects.forEach(effect => {
// 我們在執行effect的時候,有時候會改變屬性,那我們需要屏蔽掉,不要無限調用,【避免由activeEffect觸發trigger,再次觸發當前effect。 activeEffect -> fn -> set -> trigger -> 當前effect】
// @issue5
if (effect !== activeEffect) {
// @issue8 - scheduler
if (effect.scheduler) {
effect.scheduler() // 如果用戶傳入了調度函數,則執行調度函數
} else {
effect.run() // 否則默認刷新視圖
}
}
})
}
參考資料
Vue3響應式系統實現原理(二) - CherishTheYouth - 博客園
總結
以上是生活随笔為你收集整理的【源码系列#02】Vue3响应式原理(Effect)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 梦见很多泥鳅和蛇是什么意思
- 下一篇: html5倒计时秒杀怎么做,vue 设