響應式原理
響應式機制的主要功能就是,可以把普通的JavaScript對象封裝成為響應式對象,攔截數據的讀取和設置操作,實現依賴數據的自動化更新。
Q: 如何才能讓JavaScript對象變成響應式對象?
首先需要認識響應式數據和副作用函數,副作用函數指的是產生副作用的函數。
通過一個demo函數來理解一下:
let val = 1;
function effect() {val = 2;// 修改全局變量,產生副作用
}
effect()console.log(val)// 2
當console打印時,會觸發字段的讀取操作,副作用函數effect執行時,會觸發設置操作。當我們能攔截一個對象的讀取和設置操作時,那事情就變得簡單了。
在Vue2時,通過Object.defineProperty函數實現,Vue3采用Proxy來實現。
根據如上思路,采用Proxy實現方式如下:
// 存儲副作用函數的桶
const bucket = new Set();const data = { text: "summer" }
// 對原始數據的代理
const obj = new Proxy(data, {// 攔截讀取操作get(target, key) {//將副作用函數effect添加到存儲副作用函數的桶中bucket.add(effect)return target[key]},// 攔截設置操作set(target, key, newVal) {target[key] = newVal;// 把副作用函數從桶里取出并執行bucket.forEach(fn => fn())return true}
})
副作用函數可以是任意名字,上面的代碼是幫助我們理解響應式數據的基本實現和工作原理。
從上面的代碼片段中可以看出,一個響應系統的工作流程如下:
當讀取操作發生時,將副作用函數收集到“桶”中;當設置操作發生時,從“桶”中取出副作用函數并執行。
核心API和響應式工具函數
reactive:
reactive是通過ES6中的Proxy特性實現的屬性攔截,所以在reactive函數中我們直接返回new Proxy即可:
export function reactive(target) {if (typeof target!=='object') {console.warn(`reactive ${target} 必須是一個對象`);return target}return new Proxy(target, mutableHandlers);
}
mutableHandlers
mutableHandlers要做的事就是配置Proxy的攔截函數,這里我們只攔截get和set操作,進入到baseHandlers.ts中。
使用createGetter和createSetter來創建get和set函數,mutableHandlers就是配置了get和set的對象返回。
get直接返回讀取的數據,這里的Reflect.get和target[key]實現的結果是一致的;并且返回值是對象的話,還會嵌套執行reactive,并且調用track函數收集依賴。set調用trigger函數,執行track收集的依賴。
const get = createGetter();
const set = createSetter();function createGetter(shallow = false) {return function get(target, key, receiver) {const res = Reflect.get(target, key, isRef(target) ? target : receiver);if (!isReadonly) {track(target, "get", key);}if(isObject(res)) {// 值也是對象的話,需要嵌套調用reactivereturn isReadonly ? readonly(res) : reactive(res)}return res;}
}function createSetter() {return function set(target, key, value, receiver) {const result = Reflect.set(target, key, value, isRef(target) ? target : receiver);if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, 'add', key, value)} else if (hasChanged(value, oldValue)) {trigger(target, 'set', key, value, oldValue)}}return result}
}export const mutableHandlers = {get, set
}
track
在track函數中,我們可以使用一個巨大的targetMap去存儲依賴關系。map的key是我們要代理的target對象,值還是一個depsMap,存儲每一個key依賴的函數,每一個key都可以依賴多個effect。代碼如下:
const targetMap = new WeakMap();export function track(target,type, key) {// 沒有 activeEffect,直接returnif(!activeEffect) return;let depsMap = targetMap.get(target);if(depsMap) {targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key);if(!dep) {depsMap.set(key, (dep = new Set()))}deps.add(activeEffect)
}
trigger
根據targetMap的實現機制,trigger函數實現的思路就是從targetMap中,根據target和key找到對應的依賴函數集合deps,然后遍歷deps執行依賴函數。代碼如下:
export function trigger(target,type, key) {const depsMap = targetMap.get(target);if(!depsMap) {// 沒找到依賴return;}const deps = depsMap.get(key)if(!deps) return;deps.forEach((effectFn) => {// 判斷副作用函數是否存在調度器if(effectFn.scheduler) {effectFn.scheduler()} else {effectFn()}})
}
effect
我們把傳遞進來的fn函數通過effectFn函數包裹執行,在effectFn函數內部,把函數賦值給全局變量activeEffect;然后執行fn()的時候,就會觸發響應式對象的get函數,get函數內部就會把activeEffect存儲到依賴中,完成依賴的收集。
export function effect(fn, options = {}) {// effect嵌套通過隊列管理const effectFn = () => {try {activeEffect = effectFn;return fn();} finally {activeEffect = null;}}if(!options.lazy) {// 沒有配置lazy,直接執行effectFn()}effectFn.scheduler = options.scheduler;return effectFn;
}
effect傳遞的函數,可以通過lazy和scheduler來控制函數的執行時機,默認是同步執行。
scheduler
使用數組管理傳遞的執行任務,最后使用Promise.resolve只執行最后一次,這也是Vue中watchEffect函數的大致原理。
const obj = reactive({ count: 1});
effect(() => {console.log(obj.count)
}, {scheduler: queueJob
});
// 調度器實現
const queue: Function[] = [];
let isFlushing = false;
function queueJob(job: () => void) {if(!isFlushing) {isFlushing = true;Promise.resolve().then(() => {let fn;while(fn = queue.shift()) {fn()}})}
}
ref
ref的執行邏輯比reactive要簡單一些,不需要使用Proxy代理語法,直接使用對象語法中的getter和setter配置,監聽value屬性即可。對象的get value方法,使用track函數去收集依賴,set value方法中使用trigger函數去觸發函數的執行。
export function ref(val) {if(isRef(val)) {return val;}
}export function isRef(val) {return !!(val && val.__isRef)
}// 利用面向對象的getter和setter進行track和trigger
class RefImpl {constructor(val, isShallow: boolean) {this._rawValue = isShallow ? value : toRaw(value)this._value = isShallow ? value : toReactive(value)this.__v_isShallow = isShallow}get value() {track(this, 'value')return this._value;}set value(newValue) {const oldValue = this._rawValue;const useDirectValue =this.__v_isShallow ||isShallow(newValue) ||isReadonly(newValue);newValue = useDirectValue ? newValue : toRaw(newValue);if(hasChanged(newValue,oldValue)) {this._rawValue = newValue;this._value = useDirectValue ? newValue : toReactive(newValue);trigger(this, "value")}}
}export const hasChanged = (value: any, oldValue: any): boolean =>!Object.is(value, oldValue)
ref也可以包裹復雜的數據結構,內部會直接調用reactive來實現,這也解決了日常對ref和reactive使用時機的疑惑,現在可以全部都用ref函數,ref內部會幫我們調用reactive。
computed
computed計算屬性也是一種特殊的effect函數。在computed函數,我們攔截了computed的value屬性,并且定制了effect的lazy和scheduler配置,computed注冊的函數就不會直接執行,而是要通過scheduler函數中對_dirty屬性決定是否執行。
export function computed(getterOrOptions) {let getter, setter;if(isFunction(getterOrOptions)) {getter = getterOrOptions;setter = () => {console.warn('computed value is readonly')}} else {getter = getterOrOptions.get;setter = getterOrOptions.set;}return new ComputedRefImpl(getter, setter);
}class ComputedRefImpl {constructor(getter, setter) {this._setter = setter;this._val = undefined;this._dirty = true;// computed就是一個特殊的effect,設置lazy和執行時機this.effect = effect(getter, {lazy: true,scheduler:() => {if(!this._dirty) {this._dirty = true;trigger(this, 'value')}}})}get value() {track(this, 'value')if(this._dirty) {this._dirty = false;this._val = this.effect()}return this._val;}set value(val) {this._setter(val)}
}
watch
watch本質就是觀測一個響應式數據,當數據發生變化時通知并執行相應的回調函數;實現本質上就是利用了effect以及options.scheduler選項。
function watch(source, cb) {let getter;if(typeof source === 'function') {getter = source;} else {getter = () => traverse(source)}let oldValue, newValue;const effectFn = effect(// 調用traverse遞歸地讀取() => getter, {lazy: true,scheduler() {// 執行副作用函數,獲取新值newValue = effectFn();// 將舊值和新值作為回調函數的參數cb(newValue, oldValue);// 更新舊值oldValue = newValue;}})// 手動調用副作用函數,獲取到舊值oldValue = effectFn();
}function traverse(value, seen = new Set()) {// 如果要讀取的數據是原始值,或者已經被讀取過,那么就不做處理if(!isObject(value) || value === null || seen.has(value)) return;seen.add(value)if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], seen)}} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, seen)})} else if (isPlainObject(value)) {for (const key in value) {traverse(value[key], seen)}for (const key of Object.getOwnPropertySymbols(value)) {if (Object.prototype.propertyIsEnumerable.call(value, key)) {traverse(value[key as any], seen)}}}return value;
}
總結
在探索Vue響應式系統的旅程中,我們深入剖析了其底層邏輯與關鍵實現。響應式原理作為基石,通過數據劫持(Vue2依托Object.defineProperty, Vue3借助更強大的Proxy),搭配依賴收集與派發更新機制,構建起數據與試圖自動同步的橋梁。
核心API與工具函數則是響應式能力的具體延伸:reactive借助mutableHandlers等完成對象響應式轉換,track精準收集依賴、trigger觸發更新,effect與scheduler協同管控副作用執行;ref適配基本類型與對象的響應式需求,computed基于依賴緩存實現高效計算,watch靈活監聽數據變化。這些內容共同編織出Vue響應式系統的完整生態,理解它們,方能在Vue開發中精準駕馭數據驅動視圖的精髓,應對復雜場景時游刃有余,為打造高效、可維護的Vue應用筑牢基礎。