모바일 이해의 Vue3 Reactivity의 구조적 후편

42810 단어 Nuxt.jsVue.jstech
전편은 이쪽.
전편에서는 이미 targetMap을 통해 여러 객체를 관리할 수 있게 됐다.
후편에서 수동 호출track,trigger 함수를 자동으로 호출합니다.
track(product, "quantity"); // ←自動で呼ばれて欲しい
product.quantity = 5;
trigger(product, "quantity"); // ←自動で呼ばれて欲しい
하고 싶은 일로
  • 속성이 get에 의해 정해진 시간에 트랙
  • 을 호출합니다
  • 속성이 설정된 시간에 trigger를 호출합니다
    즉, 속성의 get/set을 차단할 수 있다면 좋겠다.
  • 이를 실현하기 위해 Vue3에서는 Reflect와 Proxy 두 ES6에서 가져온 새로운 API(Vue2에서는 Object.defineProoperyt)를 사용했기 때문에 먼저 그것들에 대해 설명한다.

    Reflect


    이른바 Reflect
    JavaScript 작업에 개입할 수 있는 내장 객체를 제공합니다.
    MDN에서
    에서 target에서 지정한 대상의 함수를 호출하거나 속성을 가져올 수 있습니다.
    그중 이번에는 Reflect.get을 사용합니다.
    Reflect.get을 사용하면 대상에서 속성을 얻을 수 있습니다.
    (receiver는 뒤에 설명합니다.)
    Reflect.get(product, 'quantity', receiver) // => 3
    

    Proxy


    Proxy 다음과 같이 대상을 싸서 Proxy(대리인)가 대상에 접근할 수 있도록 합니다.
    예를 들어 Proxy의 두 번째 매개 변수에 처리 프로그램을 전달하면 속성에 접근할 때 get 함수를 차단할 수 있습니다.
    추가 Reflectget과 조합해서 사용하면 다음과 같습니다.
    let product = { price: 5, quantity: 2 };
    let proxiedProduct = new Proxy(product, {
      get(target, key) {
        console.log("Get key", key);
        return target[key];
      }
    });
    // Get key quantity 
    // 2
    console.log(proxiedProduct.quantity);
    
    receiver를 사용하면 target의 대상이 다른 대상에서 계승된 대상 자체임을 보증할 수 있습니다.(자세한 내용은 여기 기사.
    get과 마찬가지로 set도 다음과 같이 정의할 수 있습니다.
    let product = { price: 5, quantity: 2 };
    let proxiedProduct = new Proxy(product, {
      get(target, key, receiver) {
        console.log("Get key", key);
        return Reflect.get(target, key, receiver);
      }
    });
    console.log(proxiedProduct.quantity);
    

    reactive 정의


    리플렉스와 프록시를 파악한 끝에 드디어 실장에 들어갔다.
    매개변수 "reactive"로 전달된 데이터가 Proxy화되어 반환되는 함수를 정의합니다.
    let product = { price: 5, quantity: 2 };
    let proxiedProduct = new Proxy(product, {
      get(target, key, receiver) {
        console.log("Get key", key);
        return Reflect.get(target, key, receiver);
      },
      set(target, key, value, receiver) {
        console.log("Set called", key, value);
        return Reflect.set(target, key, value, receiver);
      }
    });
    proxiedProduct.quantity = 10;
    console.log(proxiedProduct.quantity);
    
    지금 시작하는 get/set을 끊을 수 있습니다.
    즉, 속성의 get/set을 차단할 수 있다면 좋겠다.
    그리고 트랙과 trigger를 각각 실행합니다.
    속성이 get에 호출되었을 때 트랙 사용하기
    속성이 설정되었을 때 trigger를 호출합니다
    function reactive(target) {
      const handler = {
        get(target, key, receiver) {
          console.log("Get key", key);
          return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
          console.log("Set called", key, value);
          return Reflect.set(target, key, value, receiver);
        }
      };
      return new Proxy(target, handler);
    }
    let product = { price: 5, quantity: 2 };
    let proxiedProduct = reactive(product);
    
    이렇게 하면 속성의 변경에 따라track,trigger를 호출할 수 있다.
    나는 당신이 직감적인 견해에 주의를 기울였다고 생각한다. 이것은 Composition API의 reactive에 해당한다.

    드디어 완성됐습니다.🎉


    그러면 마지막으로 정의한reactive 함수가 제대로 실행되었는지 확인하십시오.
    앞부분을 포함하는 코드는 전체적으로 다음과 같다.
    코드 확장
    function reactive(target) {
      const handler = {
        get(target, key, receiver) {
          let result = Reflect.get(target, key, receiver);
          // getされたらtrackを呼び出す
          track(target, key);
          return result;
        },
        set(target, key, value, receiver) {
          let oldValue = target[key];
          let result = Reflect.set(target, key, value, receiver);
          // 値が変更されたらtriggerを呼び出す
          if (result && oldValue !== value) {
            trigger(target, key);
          }
          return result;
        }
      };
      return new Proxy(target, handler);
    }
    
    codesandbox
    요점
    먼저 totalEffect()를 초기화할 때 호출합니다.
    프로젝트, Quantity의 변경 사항을 추적하기 위해 내부에서 트랙을 호출합니다.
    // targetMapを定義
    const targetMap = new WeakMap();
    
    function track(target, key, effect) {
      // targetMapからdepsMapを取得
      let depsMap = targetMap.get(target);
      // depsMapがなければ作成する
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
      }
      // depを取得
      let dep = depsMap.get(key);
      // depが存在しなければ作成する
      if (!dep) {
        depsMap.set(key, (dep = new Set()));
      }
      dep.add(effect);
    }
    
    function trigger(target, key) {
      // targetMapからdepsMapを取得
      const depsMap = targetMap.get(target);
      // なければリターン
      if (!depsMap) {
        return;
      }
      let dep = depsMap.get(key);
      if (dep) {
        dep.forEach((effect) => effect());
      }
    }
    
    function reactive(target) {
      const handler = {
        get(target, key, receiver) {
          let result = Reflect.get(target, key, receiver);
          track(target, key, totalEffect);
          return result;
        },
        set(target, key, value, receiver) {
          let oldValue = Reflect.get(target, key, receiver);
          let result = Reflect.set(target, key, value, receiver);
          if (result && oldValue !== value) {
            trigger(target, key);
          }
          return result;
        }
      };
      return new Proxy(target, handler);
    }
    
    let product = reactive({ price: 200, quantity: 3 });
    let total = 0;
    
    const totalEffect = () => {
      total = product.price * product.quantity;
    };
    // 初期化
    // effect内でpriceとquantityがgetされることで
    // track(product, 'price')
    // track(procut, 'quantity')
    // がコールされる
    totalEffect()
    
    // quantityにsetされることで
    // trigger(product, 'quantity')がコールされ
    // effectが再計算されるがコールされ、totalが更新される。
    product.quantity = 4;
    console.log(total); // => 800
    
    수치를 Quantity로 설정한 후 Proxy 내부에서 trigger를 호출하고 total의 값을 업데이트합니다.
    // 初期化
    // effect内でpriceとquantityがgetされることで
    // track(product, 'price')
    // track(procut, 'quantity')
    // がコールされる
    totalEffect()
    
    이상, 예쁘게 track,trigger 자동 호출, 설정값만 있으면 토탈을 업데이트합니다🎉
    어때요?
    프레임의 인터페이스를 이용하는 것 뿐만 아니라 이번처럼 내부 구조를 이해함으로써 디버깅을 하고 디자인을 배우며 프로그램 디자인을 깊이 있게 이해하면 프로그래밍의 즐거움을 누릴 수 있지 않겠는가.

    [추기] 레프에 대해서.


    reactive가 있다면 많은 사람들이 ref라고 생각할 것 같아서 가볍게 건드려보세요.
    예를 들어 다음과 같은 total이saleProice의 다른 effect의 결과로 대입된 값에 의존하는 상황을 고려한다.
    // quantityにsetされることで
    // trigger(product, 'quantity')がコールされ
    // effectが再計算されるがコールされ、totalが更新される。
    product.quantity = 4;
    console.log(total); // => 800
    
    여기는 제품입니다.price에 대한 변경을 통해 직접적으로 의존하는aleProice가 업데이트되었지만,aleProice에 의존하는total이 업데이트되지 않았음을 알 수 있다.
    let product = reactive({ price: 5, quantity: 2 })
    let salePrice = 0
    let total = 0
    
    effect(() => {
      // totalはsalePriceに依存する
      total = salePrice * product.quantity
    })
    effect(() => {
      salePrice = product.price * 0.9
    })
    
    // salePriceには変更されるが、salePriceに依存するtotalの値は変更されない
    product.price = 10
    console.log(
      `total (should be 18) = ${total} salePrice (should be 9) = ${salePrice}`
      // total (should be 18) = 0 salePrice (should be 9) = 9
    )
    
    거기에서ref를 사용할 수 있습니다.
    이른바
    property만value의reactive 대상으로
    따라서 단일 값을 reactive로 직접 사용할 수 있습니다.
    이번 예에서는 다음과 같은 방법을 통해 반영해야 한다.
    // salePriceには変更されるが、salePriceに依存するtotalの値は変更されない
    product.price = 10
    console.log(
      `total (should be 18) = ${total} salePrice (should be 9) = ${salePrice}`
      // totalが0更新されず0のままになっている。
      // total (should be 18) = 0 salePrice (should be 9) = 9
    )
    

    ref 설치


    ref를 실현하는 두 가지 방법이 있어요.
    첫 번째 방법은 간단하게 리얼리티로 싸는 것이다.
    let product = reactive({ price: 5, quantity: 2 })
    let salePrice = ref(0) // refの使用
    let total = 0
    
    // effectの中では.valueを使うようにする
    effect(() => {
      total = salePrice.value * product.quantity
    })
    effect(() => {
      salePrice.value = product.price * 0.9
    })
    
    그러나 이것은 사실상 Vue3의ref 실시 방식과 다르다.

    ObjectAccess 정보


    그 구현을 보기 전에 먼저 ObjectAccess를 실제로 이해해야 합니다.
    이것은 별명JavaScript computed properties(Vue의 컴퓨터property와 다른 것)으로 다음과 같은 정의get/set로 컴퓨터property를 정의할 수 있습니다.
    function ref(initialValue){
      return reactive({value: initialValue})
    }
    
    이걸 사용하면ref는reactive를 사용하지 않는 간단한 형식으로 정의할 수 있습니다.
    let user = {
      firstName: 'Gregg',
      lastName: 'Pollack',
    
      get fullName() {
        return `${this.firstName} ${this.lastName}`
      },
    
      set fullName(value) {
        [this.firstName, this.lastName] = value.split(' ')
      },
    }
    
    console.log(`Name is ${user.fullName}`)
    user.fullName = 'Adam Jahr'
    console.log(`Name is ${user.fullName}`)
    

    좋은 웹페이지 즐겨찾기