Vue3 원본에 깊이 들어가 응답식 원리를 배우다
12024 단어 vue.js 프런트엔드 소스
Vue2 응답 원리
Vue2를 배웠으면 응답식 원리가 Object라는 것을 알 수 있을 거예요.defineProperty는 데이터를 납치하고 구독 발표를 해서 데이터의 응답을 실현합니다.
Object.defineProperty는 다음과 같은 몇 가지 단점이 있습니다.
Vue3 응답 원리
Vue3은 위의 질문에 대해 ES6 기본 Proxy를 사용하여 데이터를 에이전트합니다.
Proxy 기본 사용법은 다음과 같습니다.
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
console.log("get: ", key);
// return Reflect.get(target, key);
return target[key];
},
set(target, key, value) {
console.log("set: ", key, " = ", value);
// Reflect.set(target, key, value);
target[key] = value;
return value;
},
});
};
var a = reactive({ count: 1 });
console.log(a.count);
a.count = 2;
console.log(a.count);
// log
// get: count
// 1
// set: count = 2
// get: count
// 2
이렇게 하면 데이터의 변화를 검출할 수 있다.다음은 get에서 수집 의존만 하고 set 알림은 업데이트에 의존합니다.
다음은 effect,track,trigger 방법을 빌려야 한다.
effect 함수는 리셋 함수를 전송합니다. 리셋 함수는 즉시 실행되고 응답식 데이터와 의존 관계를 맺습니다.
track은proxy get에서 실행되며 의존 관계를 맺습니다.
trigger 응답식 데이터가 변할 때 의존 관계에 따라 대응하는 함수를 찾아 실행합니다.
코드는 다음과 같습니다.
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
console.log("[proxy get] ", key);
track(target, key);
// return Reflect.get(target, key);
return target[key];
},
set(target, key, value) {
console.log("[proxy set] ", key, " = ", value);
// Reflect.set(target, key, value);
target[key] = value;
trigger(target, key);
return value;
},
});
};
// effect fn, track fn
const effectStack = [];
// fn
// {
// target: {
// key: [fn, fn];
// }
// }
const targetMap = {};
const track = (target, key) => {
let depsMap = targetMap[target];
if (!depsMap) {
targetMap[target] = depsMap = {};
}
let dep = depsMap[key];
if (!dep) {
depsMap[key] = dep = [];
}
//
const activeEffect = effectStack[effectStack.length - 1];
dep.push(activeEffect);
};
const trigger = (target, key) => {
const depsMap = targetMap[target];
if (!depsMap) return;
const deps = depsMap[key];
// , fn
deps.map(fn => {
fn();
});
};
const effect = (fn) => {
try {
effectStack.push(fn);
fn();
} catch (error) {
effectStack.pop(fn);
}
};
var a = reactive({ count: 1 });
effect(() => {
console.log("[effect] ", a.count);
});
a.count = 2;
// log
// [proxy get] count
// [effect] 1
// [proxy set] count = 2
// [proxy get] count
// [effect] 2
상기 코드는 Vue3의 원본 코드가 아니라 Vue3 응답식의 원리로 Vue2보다 더욱 간단하다.
실행 순서는 다음과 같습니다.
원리를 똑똑히 알고 원본을 보면 훨씬 간단할 것이다. 다음은 우리 함께 원본을 보러 가자.
Vue3 응답 소스
Vue3의 응답식은 프레임워크에 의존하지 않고 React, Angular에서도 사용할 수 있는 독립된 모듈이다.
reactive 함수는packages/reactivity/src/reactive에 있습니다.ts
// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler,
collectionHandlers: ProxyHandler,
proxyMap: WeakMap
) {
// ...
const proxy = new Proxy(
target,
// Set、Map collectionHandlers(mutableCollectionHandlers)
// baseHandlers(mutableHandlers)
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// ...
return proxy
}
다음은 무블핸들러스를 보도록 하겠습니다.
// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler
get과 set 보겠습니다.
// packages/reactivity/src/baseHandlers.ts
const get = /*#__PURE__*/ createGetter()
// ...
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// ...
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
// track
track(target, TrackOpTypes.GET, key)
}
// ...
return res
}
}
``````
// packages/reactivity/src/baseHandlers.ts
const set = /*#__PURE__*/ createSetter()
// ...
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
// ...
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
// trigger
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// trigger
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
이제 트랙을 한번 보도록 하겠습니다.
// packages/reactivity/src/effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
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 = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// ...
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
상반부는 우리가 실현한 논리와 유사합니다. dep가 존재하지 않으면 만듭니다. 단지 Vue는 Map과 Set을 사용합니다. (create Dep 반환값은 Set입니다.)
그 다음은track Effects입니다. 관건적인 코드는 dep와active Effect가 서로 저장하는 것입니다. 우리의 방법은active Effect를 dep에 저장하는 것입니다.
다음은 set에서 호출된 trigger를 보십시오.
// packages/reactivity/src/effect.ts
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map | Set
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
// track ,
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// , target effect
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// length
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// 、 、
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
// deps effect
switch (type) {
// ...
}
}
// deps (targetMap[target][key])
// deps effect
// eventInfo
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
// effect
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
trigger 함수는 길어 보이지만 우리의 예로 간략하게 이해할 수 있습니다. 대응하는 deps를 꺼내서 deps의 effect를 옮겨다니며 실행하는 것입니다.
다음은 effect 함수의 실현을 살펴볼 차례다.
// packages/reactivity/src/effect.ts
export function effect(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// ReactiveEffect
const _effect = new ReactiveEffect(fn)
// ...
// options.lazy
// lazy true
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
export class ReactiveEffect {
active = true
deps: Dep[] = []
// can be attached after creation
computed?: boolean
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
if (!effectStack.includes(this)) {
try {
// effect effectStack
// activeEffect
// track
effectStack.push((activeEffect = this))
enableTracking()
// ...
return this.fn()
} finally {
// ...
resetTracking()
effectStack.pop()
const n = effectStack.length
// effectStack activeEffect
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
stop() {
// ...
}
}
effect를 사용할 때, 우리가 전송한 함수를ReactiveEffect로 봉인합니다.
{ lazy: true }
전송하지 않으면,run 함수를 즉시 실행합니다.run 함수는 activeEffect를 먼저 부여하고 effect Stack에 저장한 다음에 우리가 전송한 리셋 함수를 실행합니다.
리셋 함수를 실행하는 과정은Proxy의 get을 터치하고 get은track을 터치하여 의존 수집을 합니다.
실행이 끝난 후 activeEffect를 effect Stack pop에서 꺼내고 이전 activeEffect를 꺼내서 계속 실행합니다.
왜 effect Stack을 사용합니까?
만약 우리가 effect에서 컴퓨터를 사용한다면, Vue는 먼저 컴퓨터 계산을 실행해야 한다.
컴퓨터 내부에서도 ReactiveEffect를 호출하기 때문에 컴퓨터의 effect를 effect Stack에 저장해야 합니다. 컴퓨터 계산이 끝난 후에 effect Stack pop에서 나가서 우리의 effect를 계속 실행해야 합니다.
이렇게 하면 의존 수집을 완성하고 응답식 데이터가 변할 때trigger를 터치하여 effect에서 전송된 리셋 함수를 다시 실행합니다.
응답 데이터 수정 페이지가 자동으로 업데이트되는 이유는 무엇입니까?지난 글에서 소개한
setupRenderEffect
기억나세요?이 방법도 ReactiveEffect를 이용하여 mount에서 터치
setupRenderEffect
를 하고 터치patch
를 한다.patch
과정에서 응답식 데이터를 사용하여 의존 관계를 구축하고 응답식 데이터가 변할 때 다시 실행setupRenderEffect
한다. 그 다음에 diff가 들어간다. 다음 글은 diff를 상세하게 전개한다.결어
이상은 바로 Vue3의 응답식 원리입니다. 원리를 이해하고 자신의 언어로 명확하게 묘사할 수 있다면 면접은 성공률을 높일 수 있습니다.
자, 이 문장은 여기까지 하자.잘못된 점이 있으면 댓글로 지적해 주시면 감사하겠습니다!
다음 글은 Vue3의 diff 알고리즘을 해석할 것입니다. 관심이 있으면 저를 주목하는 것을 잊지 마세요. 우리 함께 공부하고 진보합시다.