vue3中的響應式原理-effect
effect的基本實現
export let activeEffect = undefined;// 當前正在執行的effect class ReactiveEffect { active = true; deps = []; // 收集effect中使用到的屬性 parent = undefined; constructor(public fn) { } run() { if (!this.active) { // 不是激活狀態 return this.fn(); } try { this.parent = activeEffect; // 當前的effect就是他的父親 activeEffect = this; // 設置成正在激活的是當前effect return this.fn(); } finally { activeEffect = this.parent; // 執行完畢後還原activeEffect this.parent = undefined; } } } export function effect(fn, options?) { const _effect = new ReactiveEffect(fn); // 創建響應式effect _effect.run(); // 讓響應式effect默認執行 }
依賴收集
get(target, key, receiver) { if (key === ReactiveFlags.IS_REACTIVE) { return true; } const res = Reflect.get(target, key, receiver); track(target, 'get', key); // 依賴收集 return res; }
const targetMap = new WeakMap(); // 記錄依賴關系 export function track(target, type, key) { if (activeEffect) { let depsMap = targetMap.get(target); // {對象:map} if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())) // {對象:{ 屬性 :[ dep, dep ]}} } let shouldTrack = !dep.has(activeEffect) if (shouldTrack) { dep.add(activeEffect); activeEffect.deps.push(dep); // 讓effect記住dep,這樣後續可以用於清理 } } }
將屬性和對應的effect維護成映射關系,後續屬性變化可以觸發對應的effect函數重新run
觸發更新
set(target, key, value, receiver) { // 等會賦值的時候可以重新觸發effect執行 let oldValue = target[key] const result = Reflect.set(target, key, value, receiver); if (oldValue !== value) { trigger(target, 'set', key, value, oldValue) } return result; }
export function trigger(target, type, key?, newValue?, oldValue?) { const depsMap = targetMap.get(target); // 獲取對應的映射表 if (!depsMap) { return } const effects = depsMap.get(key); effects && effects.forEach(effect => { if (effect !== activeEffect) effect.run(); // 防止循環 }) }
分支切換與cleanup
在渲染時我們要避免副作用函數產生的遺留
const state = reactive({ flag: true, name: 'jw', age: 30 }) effect(() => { // 副作用函數 (effect執行渲染瞭頁面) console.log('render') document.body.innerHTML = state.flag ? state.name : state.age }); setTimeout(() => { state.flag = false; setTimeout(() => { console.log('修改name,原則上不更新') state.name = 'zf' }, 1000); }, 1000)
function cleanupEffect(effect) { const { deps } = effect; // 清理effect for (let i = 0; i < deps.length; i++) { deps[i].delete(effect); } effect.deps.length = 0; } class ReactiveEffect { active = true; deps = []; // 收集effect中使用到的屬性 parent = undefined; constructor(public fn) { } run() { try { this.parent = activeEffect; // 當前的effect就是他的父親 activeEffect = this; // 設置成正在激活的是當前effect + cleanupEffect(this); return this.fn(); // 先清理在運行 } } }
這裡要註意的是:觸發時會進行清理操作(清理effect),在重新進行收集(收集effect)。在循環過程中會導致死循環。
let effect = () => {}; let s = new Set([effect]) s.forEach(item=>{s.delete(effect); s.add(effect)}); // 這樣就導致死循環瞭
停止effect
export class ReactiveEffect { stop(){ if(this.active){ cleanupEffect(this); this.active = false } } } export function effect(fn, options?) { const _effect = new ReactiveEffect(fn); _effect.run(); const runner = _effect.run.bind(_effect); runner.effect = _effect; return runner; // 返回runner }
調度執行
trigger觸發時,我們可以自己決定副作用函數執行的時機、次數、及執行方式
export function effect(fn, options:any = {}) { const _effect = new ReactiveEffect(fn,options.scheduler); // 創建響應式effect // if(options){ // Object.assign(_effect,options); // 擴展屬性 // } _effect.run(); // 讓響應式effect默認執行 const runner = _effect.run.bind(_effect); runner.effect = _effect; return runner; // 返回runner } export function trigger(target, type, key?, newValue?, oldValue?) { const depsMap = targetMap.get(target); if (!depsMap) { return } let effects = depsMap.get(key); if (effects) { effects = new Set(effects); for (const effect of effects) { if (effect !== activeEffect) { if(effect.scheduler){ // 如果有調度函數則執行調度函數 effect.scheduler() }else{ effect.run(); } } } } }
深度代理
get(target, key, receiver) { if (key === ReactiveFlags.IS_REACTIVE) { return true; } // 等會誰來取值就做依賴收集 const res = Reflect.get(target, key, receiver); track(target, 'get', key); if(isObject(res)){ return reactive(res); } return res; }
當取值時返回的值是對象,則返回這個對象的代理對象,從而實現深度代理
總結
為瞭實現響應式,我們使用瞭new Proxy
effect默認數據變化要能更新,我們先將正在執行的effect作為全局變量,渲染(取值),然後在get方法中進行依賴收集
依賴收集的數據格式weakMap(對象:map(屬性:set(effect))
用戶數據發生變化,會通過對象屬性來查找對應的effect集合,全部執行;
調度器的實現,創建effect時,把scheduler存在實例上,調用runner時,判斷如果有調度器就調用調度器,否則執行runner
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。