Vue3 원본에 깊이 들어가 응답식 원리를 배우다

Vue2 응답 원리


Vue2를 배웠으면 응답식 원리가 Object라는 것을 알 수 있을 거예요.defineProperty는 데이터를 납치하고 구독 발표를 해서 데이터의 응답을 실현합니다.
Object.defineProperty는 다음과 같은 몇 가지 단점이 있습니다.
  • 초기화할 때 대상의 모든 속성을 옮겨다니며 납치해야 하며, 대상에 끼워 넣은 것이 있으면 귀속해야 한다.초기화할 때 자원을 소모해서 반복적으로 옮겨다니는 데 사용해야 한다.
  • 위에서 볼 수 있듯이 Vue2는 추가, 삭제 대상의 속성에 대해 납치할 수 없으며 Vue를 통과해야 한다.set、Vue.delete에서 작업을 수행합니다.
  • 모든 호출자는 Watcher를 생성하여 메모리를 차지합니다.
  • Set, Map 객체를 납치할 수 없습니다.

  • 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보다 더욱 간단하다.
    실행 순서는 다음과 같습니다.
  • reactive 프록시 응답식 대상을 호출하기;
  • effect를 호출하면 fn을 effect Stack에 저장하고 fn을 실행할 때 Proxy의 get을 터치합니다.
  • Proxy의 get에서track을 터치하여 데이터와fn의 관계를 맺는다.
  • 응답식 데이터를 수정하고 Proxy의 set을 터치합니다.
  • Proxy의 set에서 trigger를 터치하여 해당하는 fn을 찾아 실행합니다.

  • 원리를 똑똑히 알고 원본을 보면 훨씬 간단할 것이다. 다음은 우리 함께 원본을 보러 가자.

    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,
      deleteProperty,
      has,
      ownKeys
    }
    

    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 알고리즘을 해석할 것입니다. 관심이 있으면 저를 주목하는 것을 잊지 마세요. 우리 함께 공부하고 진보합시다.

    좋은 웹페이지 즐겨찾기