vue2.0からvue 3.0応答式の原理

14382 ワード

VUE2.0の応答式の原理
この文章の幅は长くて、すでに2.0の応答式の原理に対して熟知しているのは直接この部分をスキップすることができて、それぞれ必要なものを取って、共同で交流します
vueの中で私达が最も多く使うのは応答式のデータで、私达に多くの便利さをもたらして、応答式のデータといえば、つまりデータが変わってビューを更新して、私达は一歩一歩vue 2を见てみます.0では、この機能をどのように実現するか~まず、データを定義します.
let data = {name: "yangbo"}

このデータを変更します
data.name = 'yb'

vueでデータが変更された場合、ビューを更新する必要があることはよく知られています.その場合、ビューを更新する方法が必要です.
function updateView(){
    console.log('         ')
}

そこで質問ですが、このオブジェクトのプロパティ値を設定するとき、updateView()メソッドをどのようにトリガーするか、ここではObjectを使用します.defineProperty()メソッドでは、このメソッドでプロパティを再定義し、オブジェクトのプロパティにgetterとsetterを追加します.
データを観察する方法を定義します.この方法では、definePropertyメソッドを呼び出して、データの属性にgetterとsetterを設定します.definePropertyメソッドではObjectを使用した.defineProperty()は、値を取るとgetメソッドが実行されます.この場合、valueを直接返します.値を再設定すると(data.name='yb')setメソッドが実行され、setメソッドでビューを更新するメソッドを呼び出し、新しい値をvalueに割り当てます.
function watchData(target) {
  if(typeof target !== 'object' || target === null) {
    return target;
  }
  for (let key in target) {
    defineProperty(target, key, target[key])
  }
}
function defineProperty(target, key, value) {
  //    target   key    getter  setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        updateView();
        value = newVal;
      }
    }
  })
};

以上のコードは最初のステップを完了しました.この時、問題がまた来ました.私たちのデータはすべて1層であることはできません.多層のデータである可能性があります.例えば、次のようにします.
let data = {
    name: 'yangbo',
    info: {
        age: '26'
    }
}

内層がオブジェクトである場合、このオブジェクトをもう一度観察します.defineProperty関数にコードを1行追加すると、再帰的になります.
function defineProperty(target, key, value) {
  //       
  watchData(value);
  //    target   key    getter  setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        updateView();
        value = newVal;
      }
    }
  })
};

以上のコードから分かるように、もしあなたのObjectレベルが深いならば、再帰は性能に影響します.次に、次の方法でオブジェクトに値を割り当てると、ビューの更新が何回かトリガーされます.
data.info = {sex: 'man'}
data.info.sex = 'woman'

答えは一度、dataだけです.info={sex:'man'}はビューの更新方法をトリガーします.私たちの上の方法はsexにgetterとsetterを定義していないので、definePropertyに次のようにコードを追加します.
function defineProperty(target, key, value) {
  watchData(value);
  //    target   key    getter  setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        //       ,       Object,        getter setter
        watchData(newVal);
        updateView();
        value = newVal;
      }
    }
  })
};

このとき、ビューを更新する方法が2回実行されます.ここでは、オブジェクトの応答式を基本的に実現します.しかし、よく考えてみると、属性が存在しない場合、新しい属性は応答式になりますか?属性の値が配列であればどうすればいいですか?現在のコードでは、データ内の配列の変更がビューの更新をトリガーしない方法について説明します.
let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);

では、このようにデータを変更しても更新ビューをトリガーします.まず、pushメソッドを呼び出しました.このメソッドはArrayなので、配列のメソッドを書き換える必要があります.ここでは、配列のプロトタイプ上のメソッドを直接書き換えることはできません.そして、配列のすべてのメソッドを手に入れて、その中のいくつかのメソッドを書き換える必要があります.この例ではpushメソッドとpopメソッドのみを書き換え、Array.を作成する必要があります.propertyは同じObjectで、私たちのObjectを修正してもArrayには影響しません.property、ここでは継承を使っています.面接の時にこの問題に遭遇すると思います.
let ArrayProperty = Array.prototype; // Array   
let proto = Object.create(ArrayProperty); //   

次にproto上のメソッドを再定義し、propo上のメソッドを定義する際にArrayProperty上のメソッドを使用する必要があることを覚えておいてください.ここでは配列の元のメソッドを呼び出します.呼び出す前にビューを更新するメソッドを呼び出します.ここではcallメソッドを使用してthisの指向を変更することを忘れないでください.ここでは関数ハイジャックをします.
['push', 'pop'].forEach((method) => {
  proto[method] = function() {
    updateView();
    //   ArrayProperty    
    ArrayProperty[method].call(this, ...arguments);
  }
});

関数はハイジャックされましたが、watchDataという方法では、targetにproto上の方法を見つけるにはどうすればいいのでしょうか.targetが配列であるかどうかを判断し、配列であればprotoをprotoに向けます.
function watchData(target) {
  if(typeof target !== 'object' || target === null) {
    return target;
  }
  if(Array.isArray(target)) {
    //      , target    proto
    target.__proto__ = proto;
  }
  for (let key in target) {
    defineProperty(target, key, target[key])
  }
}

以上で、オブジェクトと配列に対する応答式が完成し、以下に完全なコードが貼られています.
let ArrayProperty = Array.prototype; // Array   
let proto = Object.create(ArrayProperty); //   
['push', 'pop'].forEach((method) => {
  proto[method] = function() {
    updateView();
    //   ArrayProperty    
    ArrayProperty[method].call(this, ...arguments);
  }
})

function updateView(val) {
  console.log('         ');
}
function watchData(target) {
  if(typeof target !== 'object' || target === null) {
    return target;
  }
  if(Array.isArray(target)) {
    //      , target    proto
    target.__proto__ = proto;
  }
  for (let key in target) {
    defineProperty(target, key, target[key])
  }
}
function defineProperty(target, key, value) {
  watchData(value);
  //    target   key    getter  setter
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        watchData(newVal);
        updateView();
        value = newVal;
      }
    }
  })
};
let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);
console.log(data.phone);

// let data = {name: 'yangbo', info: {age: 25}}
// watchData(data);
// data.name = 'yb';
// data.info = {sex: 'man'}
// data.info.sex = 'woman'

VUE3.0の応答式の原理
簡易版のvue 2を手書きで書きました.0応答式では、2.0でデフォルトでデータが再帰され、オブジェクトに存在しない属性がブロックされないことがわかります.これらの問題を持って、vue 3を見てみましょう.0の応答式実現原理~
まず、現段階でvue 3をどのように体験するかを話します.0 :
  • vue 3.0ソースcloneローカル
  • git clone [email protected]:vuejs/vue-next.git
    
  • インストール依存度は、ルートディレクトリで
  • を実行します.
    npm run dev
    
  • コンパイルされたvue 3.0はpackage/vue/dist/vueです.global.js,vue.global.jsはあなたのdemoを導入して使用することができて、その前に必ずVue ComponentAPI
  • を見なければなりません
    次にvue 3を使用します.0
    
    
    
      
      
      
      Document
    
    
      
    let proxy = Vue.reactive({name: 'yangbo'}); proxy.name = 'yb';

    vue3.0 的响应式数据是通过es6 的Proxy来实现的,Vue.reactive()返回一个Proxy,从上面的代码可以看出,当let proxy = Vue.reactive({name: 'yangbo'});是应该触发一次更新视图的,而当变更值proxy.name = 'yb';的时候需要再触发一次更新视图,那在vue3.0中是怎么做的呢? vue3.0这里的主要逻辑是通过Vue.effect()这个方法去实现的,effect这个方法会先执行一次,当数据变化的时候会再执行,接下来我们一步一步的区实现,我们这次是用js来实现不是ts,先熟悉思路。

    • 首先我们实现Vue.reactive这个方法
      不知道你是否还记得上面Vue2.0实现响应式的方法,就是一上来就会对数据进行递归,带着这个问题看看vue3.0到底怎么优化的;在这之前还是需要熟悉ES6的Proxy的~
      Vue.reactive返回的是一个响应式对象,所以我们在reactive方法中返回一个响应式对象:
    function reactive(target) {
     return createReactive(target);
    }
    
    function createReactive(target) {
     //        
     if(!(typeof target === 'object' && target !== null)) {
       return target;
     }
     //      
     let baseHandle = {
       get(target, key, receiver) {
         //   
         let datas = Reflect.get(target, key, receiver);
         return datas;
       },
       set(target, key, value, receiver) {
         //   
         let res = Reflect.set(target, key, value, receiver);
         console.log(res);
         return res;
       }
     }
     let observed = new Proxy(target, baseHandle);
     return observed;
    }
    let proxy = reactive({name: 'yangbo'})
    proxy.name = 'yb';
    console.log(proxy.name);
    

    vue 3.0では、一般的にproxyはapi reflectと一緒に使用され、getメソッドではReflect.get(target,key,receiver)とtarget[key]の2つの書き方は等価である.私たちのコードのsetメソッドでは、reflectも使用します.set()は値を設定し、ブール値を返して設定に成功したかどうかを示します.以上のコードで私たちはすでに1層のObjectをエージェントすることができます.多層のObjectは?そこで我々は取得時,つまりgetメソッドで取得する値がObjectかどうかを判断し,Reflectの結果がObjectであれば直接結果エージェント(再帰)を
    get(target, key, receiver) {
          //   
          let datas = Reflect.get(target, key, receiver);
          //           
          return typeof target === 'object' && target !== null ? reactive(datas) : datas;
        }
    

    2.0に比べて、2.0は最初から再帰し、3.0は値を取るときに判断し、必要に応じて再帰する.以上、Objectのエージェントを実装しましたが、同じObjectが複数回エージェントされることを防止するために、new WeakMap()弱参照を使用して、現在のオブジェクトがエージェントされているかどうかを判断するために2つのWeakMapを定義します.
    let sourceProxy = new WeakMap(); //             
    /**
     * sourceProxy
     * {
     *       :      
     * }
     * **/
    let toRaw = new WeakMap(); //              
    /**
     * toRaw
     * {
     *          :   
     * }
     * **/
    

    次にcreateReactiveメソッドを次のように変更します.
    function createReactive(target) {
      //        
      if(!(typeof target === 'object' && target !== null)) {
        return target;
      }
      //   target      
      if(sourceProxy.get(target)) {
        return sourceProxy.get(target);
      }
      //            target,            
      if (toRaw.has(target)) {
        return target;
      }
      //      
      let baseHandle = {
        get(target, key, receiver) {
          //   
          let datas = Reflect.get(target, key, receiver);
          //           
          return typeof target === 'object' && target !== null ? reactive(datas) : datas;
        },
        set(target, key, value, receiver) {
          //   
          let res = Reflect.set(target, key, value, receiver);
          console.log(res);
          return res;
        }
      }
      let observed = new Proxy(target, baseHandle);
      //   weakmap
      sourceProxy.set(target, observed);
      toRaw.set(observed, target);
      return observed;
    }
    

    次に依存収集(サブスクリプションのパブリッシュ)を実現し、前述したeffectメソッドを覚えていますか?デフォルトでは1回実行され、データに依存して変化すると再び実行されます.今、このeffectを実現します.これも応答式の核心です.まず、このeffectメソッドは関数を受信し、この関数を応答式関数にし、この関数をデフォルトで1回実行します.
    function effect(fn) {
      let effect = createReactiveFn(fn); //         
      effect(); //       
    }
    

    次にcreateReactiveFnメソッドを記述します.
    let stacks = [];
    function createReactiveFn(fn) {
      let eFn = function() {
        //   fn   fn    
        return carryOut(eFn, fn)
      }
      return eFn;
    }
    function carryOut(effect, fn) {
      //   fn   fn    
      stacks.push(effect);
      fn();
    }
    

    値を取るとき、つまりgetメソッドでは、値とeffectをマッピングし、値が変化したときに値に対応するeffectを直接実行します.次に、この部分に関連するメソッドを作成し、最後に次のデータ構造を実現します.
    {
      target: {
        key: [fn, fn]
      }
    }
    

    関連付けを作成するためにsubscribeToを作成します.
    let targetMap = new WeakMap();
    function subscribeTo(target, key) {
      let effect = stacks[stacks.length - 1];
      if (effect) { //   ,     
        let maps = targetMap.get(target);
        if (!maps) {
          targetMap.set(target, maps = new Map);
        }
        let deps = maps.get(key);
        if (!deps) {
          maps.set(key, deps = new Set());
        }
        if(!deps.has(effect)) {
          deps.add(effect);
        }
      }
    }
    

    前に書いたeffectをスタックに入れたのを覚えていますか?push(effect); 関連付けが完了すると、このeffectはスタックに意味がないので、スタックをクリアします.
    function carryOut(effect, fn) {
      //   fn   fn    
      stacks.push(effect);
      fn();
      stacks.pop(effect);
    }
    

    関連付けを作成しました.値を更新するとき、setメソッドでtargetMapのeffectを取りに行きます.
    function trigger(target, type, key) {
      let tMap = targetMap.get(target);
      if (tMap) {
        let keyMaps = tMap.get(key);
        //  key   effect  
        if (keyMaps) {
          keyMaps.forEach((eff) => {
            eff();
          })
        }
      }
    }
    

    この文書で実装された完全なコードを以下に示します.
    let sourceProxy = new WeakMap(); //             
    let toRaw = new WeakMap(); //              
    function reactive(target) {
      return createReactive(target);
    }
    function createReactive(target) {
      //        
      if(!(typeof target === 'object' && target !== null)) {
        return target;
      }
      //   target      
      if(sourceProxy.get(target)) {
        return sourceProxy.get(target);
      }
      //            target,            
      if (toRaw.has(target)) {
        return target;
      }
      //      
      let baseHandle = {
        get(target, key, receiver) {
          //   
          let datas = Reflect.get(target, key, receiver);
          //   
          subscribeTo(target, key); //  key         effect
          //           
          return typeof target === 'object' && target !== null ? reactive(datas) : datas;
        },
        set(target, key, value, receiver) {
          //   
          let oldValue = target[key];
          let res = Reflect.set(target, key, value, receiver);
          //               
          if (!target.hasOwnProperty(key)) {
            //     
            console.log('  ');
            trigger(target, 'add', key);
          } else if (oldValue !== value) {
            //     
            console.log('  ');
            trigger(target, 'set', key);
          }
          console.log(res);
          return res;
        }
      }
      let observed = new Proxy(target, baseHandle);
      //   weakmap
      sourceProxy.set(target, observed);
      toRaw.set(observed, target);
      return observed;
    }
    let stacks = [];
    let targetMap = new WeakMap();
    //    
    function effect(fn) {
      let effect = createReactiveFn(fn); //         
      effect(); //       
    }
    function createReactiveFn(fn) {
      let eFn = function() {
        //   fn   fn    
        return carryOut(eFn, fn)
      }
      return eFn;
    }
    function carryOut(effect, fn) {
      //   fn   fn    
      try {
        stacks.push(effect);
        fn();
      } finally {
        stacks.pop(effect);
      }
    }
    function subscribeTo(target, key) {
      let effect = stacks[stacks.length - 1];
      if (effect) { //   ,     
        let maps = targetMap.get(target);
        if (!maps) {
          targetMap.set(target, maps = new Map);
        }
        let deps = maps.get(key);
        if (!deps) {
          maps.set(key, deps = new Set());
        }
        if(!deps.has(effect)) {
          deps.add(effect);
        }
      }
    }
    function trigger(target, type, key) {
      let tMap = targetMap.get(target);
      if (tMap) {
        let keyMaps = tMap.get(key);
        //  key   effect  
        if (keyMaps) {
          keyMaps.forEach((eff) => {
            eff();
          })
        }
      }
    }
    // let proxy = reactive({name: 'yangbo'})
    // proxy.name = 'yb';
    // console.log(proxy.name);
    let proxyObj = reactive({name: 'yangbo'});
    effect(() => {
      console.log(proxyObj.name);
    })
    
    proxyObj.name = 'yb'
    

    以上、vue 3を簡単に模倣する.0では応答式データの実装に対してjsで実装されているが,興味があるのはVue 3を見ることができる.0ソースコードのType Scriptの書き方と実現方法、本編は参考までに~
    参考:Proxy vue-next Vue ComponentAPI