Vue双方向データバインドの原理ソースコード解析


Vueトレースデータ変化
一般的なjsオブジェクトをdataオプションとしてVueインスタンスに転送し、Vueはオブジェクトのすべてのプロパティを遍歴し、Object.definePropertyを使用してgetter/setterに変換します.
各コンポーネントインスタンスはwatcherインスタンスに対応し、コンポーネントレンダリング中に関連するデータ属性が依存として記録されます.その後、依存項目のsetterがトリガーされると、watcherに通知され、関連するコンポーネントが再レンダリングされる.
注意事項
Vueが応答式に変換されるには、dataオブジェクト上に属性が存在する必要があります.
すでに作成するインスタンスに対して、Vueはルートレベルの応答式属性を動的に追加することを許さず、Vueを用いることができる.set()を追加
vueの双方向データバインドは、データハイジャック+パブリッシュサブスクリプションによって実現される
  • データハイジャックの実装vuejsでは、受信したobserve(value, asRootData)がハイジャックされたかどうかを検出し、直接返された場合、そうでない場合、valueのインスタンスがvalueでインスタンス化され、Observerメソッドでは、Observerによってオブジェクト属性がObject.definePropertyに変換され、これにより、データが変化したときに一連の操作が行われる.

  • observeメソッドは、次の段階で実行されます.
  • initMixin()->initState()->initData()で実行し、vueインスタンスのdata属性を処理する
  • initMixin()->initState()->initProps()->validateProp()で実行し、propsのプロパティを処理する
  •  function observe (value, asRootData) {
        if (!isObject(value) || value instanceof VNode) {
          return
        }
        var ob;
        //‘__ob__’   ,    Observer    。
        if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
          ob = value.__ob__;
        } else if (
          ...
        ) {
          ob = new Observer(value); //   Observer
        }
        if (asRootData && ob) {
          ob.vmCount++;
        }
        return ob
      }
    

    Observerインスタンス
    var Observer = function Observer (value) {
        this.value = value;
        this.dep = new Dep(); //1      ,          
        this.vmCount = 0;
        def(value, '__ob__', this); //  Object.defineProperty value    __ob__  
        if (Array.isArray(value)) { //  
          if (hasProto) { // var hasProto = '__proto__' in {};
            protoAugment(value, arrayMethods);
          } else {
            copyAugment(value, arrayMethods, arrayKeys);
          }
          this.observeArray(value);
        } else { 
          this.walk(value);
        }
      };
    //Observer     :
    Observer.prototype.walk = function walk (obj) {
        var keys = Object.keys(obj);
        for (var i = 0; i < keys.length; i++) {
          defineReactive$$1(obj, keys[i]); //  obj  keys[i]   get set  
        }
      };
    Observer.prototype.observeArray = function observeArray (items) {
        for (var i = 0, l = items.length; i < l; i++) {
          observe(items[i]); //       ,     observe  
        }
      };
      
      //1        
    var Dep = function Dep () {
        this.id = uid++;
        this.subs = []; //    watcher
      };
    Dep.prototype.addSub = function addSub (sub) {
        this.subs.push(sub);
      };
    
    Dep.prototype.removeSub = function removeSub (sub) {
       remove(this.subs, sub); //         
     };
    
     Dep.prototype.depend = function depend () {
       if (Dep.target) {
         Dep.target.addDep(this); //     ,watcher  ,  watcher   addDep  
       }
     };
    
     Dep.prototype.notify = function notify () {
      ...
       for (var i = 0, l = subs.length; i < l; i++) {
         subs[i].update(); //watcher  ,    watcher   update  
       }
     };
    

    以上はObserverの主なソースコードであり、入力されたオブジェクトを処理し、既存の_ob__属性、説明はすでに処理をブロックしたことを説明して、直接__に戻りますob__属性の値を指定します.そうでなければ,入力されたオブジェクトに対して配列かオブジェクトかを別々に処理する.最終的には、defineReactive$$1()メソッドを使用してオブジェクト属性のgetメソッドとsetメソッドを書き換えます.
    function defineReactive$$1 (
        obj,
        key,
        val,
        customSetter,
        shallow
      ) {
        var dep = new Dep(); //           
    
        var property = Object.getOwnPropertyDescriptor(obj, key);
        //          ,  
        if (property && property.configurable === false) {
          return
        }
        //  get set  
        var getter = property && property.get;
        var setter = property && property.set;
        if ((!getter || setter) && arguments.length === 2) {
          val = obj[key];
        }
        var childOb = !shallow && observe(val);
        //  Object.defineProperty()          ,   get set     
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get: function reactiveGetter () {
            var value = getter ? getter.call(obj) : val;
            if (Dep.target) {
              dep.depend(); //   depend()  ,        watcher addDep()
              if (childOb) {
                childOb.dep.depend();
                if (Array.isArray(value)) {
                  dependArray(value);
                }
              }
            }
            return value
          },
          set: function reactiveSetter (newVal) {
            var value = getter ? getter.call(obj) : val;
            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) { //        ,  
              return
            }
            /* eslint-enable no-self-compare */
            if (customSetter) { //       set  ,  
              customSetter();
            }
            if (getter && !setter) { return }
            if (setter) { //  setter  
              setter.call(obj, newVal);
            } else {
              val = newVal;
            }
            childOb = !shallow && observe(newVal);
            dep.notify(); //  ,    ,dep.notify()       watcher update  ,    
          }
        });
      }
    

    Watcher
    var Watcher = function Watcher (
        vm,
        expOrFn,
        cb,
        options,
        isRenderWatcher
      ) {
        this.vm = vm;
        if (isRenderWatcher) {
          vm._watcher = this;
        }
        vm._watchers.push(this);
        // options
        ...
        this.cb = cb;
        this.id = ++uid$2; // uid for batching
        this.active = true;
        this.dirty = this.lazy; // for lazy watchers
        this.deps = [];
        this.newDeps = [];
        this.depIds = new _Set();
        this.newDepIds = new _Set();
        this.expression = expOrFn.toString();
        // parse expression for getter
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn;
        } else {
          this.getter = parsePath(expOrFn);
          if (!this.getter) {
            this.getter = noop;
            warn(
              "Failed watching path: \"" + expOrFn + "\" " +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
              vm
            );
          }
        }
        this.value = this.lazy
          ? undefined
          : this.get(); //     get  ,            
      };
      //    
      Watcher.prototype.get = function get () {
        pushTarget(this); //    Watcher Dep.target
        var value;
        var vm = this.vm;
        try {
          value = this.getter.call(vm, vm);
        } catch (e) {
          ...
        } finally {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          if (this.deep) {
            traverse(value);
          }
          popTarget();
          this.cleanupDeps();
        }
        return value
      };
      Watcher.prototype.addDep = function addDep (dep) {
        var id = dep.id;
        if (!this.newDepIds.has(id)) {
          this.newDepIds.add(id);
          this.newDeps.push(dep);
          if (!this.depIds.has(id)) {
            dep.addSub(this); //            Dep subs   
          }
        }
      };
     Watcher.prototype.cleanupDeps = function cleanupDeps () {
     //  
        var i = this.deps.length;
        while (i--) {
          var dep = this.deps[i];
          if (!this.newDepIds.has(dep.id)) {
            dep.removeSub(this);
          }
        }
        var tmp = this.depIds;
        this.depIds = this.newDepIds;
        this.newDepIds = tmp;
        this.newDepIds.clear();
        tmp = this.deps;
        this.deps = this.newDeps;
        this.newDeps = tmp;
        this.newDeps.length = 0;
      };
      //  ,Watcher update  ,        ,  
     Watcher.prototype.update = function update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true;
        } else if (this.sync) { //      ,    
          this.run();
        } else { //        
          queueWatcher(this);
        }
      };
      //watcher     ,         
     Watcher.prototype.run = function run () {
        if (this.active) {
          var value = this.get();
          if (
            value !== this.value ||
            isObject(value) ||
            this.deep
          ) {
            //        
            var oldValue = this.value;
            //      
            this.value = value;
            if (this.user) {
              try { //    
                this.cb.call(this.vm, value, oldValue);
              } catch (e) {
                handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
              }
            } else { //    
              this.cb.call(this.vm, value, oldValue);
            }
          }
        }
      };
    

    Watcherはどこで実例化されたのでしょうか
  • stateMinxin -> vue.protorype.$watchメソッドにおける
  • initMixin->initData->initComputed
  • initMixin -> vue.prototype.$mount->mountComponentで
  • 前者は主にvueインスタンスで構成されたwatchとcomputed処理であり、データの変化を同期し、最後にビューとデータをバインドする.
    Vuejsでは、グローバルに実行される方法は、主にgetter/setterであり、プレフィックスに基づいて、それぞれ処理のどの部分であるかを基本的に理解することができる.initMixin(Vue), stateMixin(Vue), eventsMixin(Vue), lifecycleMixin(Vue), renderMixin(Vue), initGlobalAPI(Vue)では、主に初期化の作業、例えば初期化、initMixin(Vue)などが行われ、最後にinitProxy, initLifecycle, initEvents, initRender, initInjection, initState, initProvideが実行され、ビューのマウントが行われる
    ここでinitRenderメソッドでは、vueインスタンスのvm.$mount およびdefineReactive$$1属性を$attrsによってブロック処理する$listenersメソッドではmountComponent(this,el,hydrating)によってマウントおよびビュー更新メソッドが指定されています.
    function mountComponent (
        vm,
        el,
        hydrating
      ) {
        vm.$el = el;
        if (!vm.$options.render) {
          vm.$options.render = createEmptyVNode; //     
          {
            /* istanbul ignore if */
            if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
              vm.$options.el || el) {
              warn(
                'You are using the runtime-only build of Vue where the template ' +
                'compiler is not available. Either pre-compile the templates into ' +
                'render functions, or use the compiler-included build.',
                vm
              );
            } else {
              warn(
                'Failed to mount component: template or render function not defined.',
                vm
              );
            }
          }
        }
        callHook(vm, 'beforeMount');
    
        var updateComponent;
        /* istanbul ignore if */
        if (config.performance && mark) {
          updateComponent = function () {
            var name = vm._name;
            var id = vm._uid;
            var startTag = "vue-perf-start:" + id;
            var endTag = "vue-perf-end:" + id;
    
            mark(startTag);
            var vnode = vm._render();
            mark(endTag);
            measure(("vue " + name + " render"), startTag, endTag);
    
            mark(startTag);
            vm._update(vnode, hydrating);
            mark(endTag);
            measure(("vue " + name + " patch"), startTag, endTag);
          };
        } else {
          updateComponent = function () {
            vm._update(vm._render(), hydrating); //    
          };
        }
    //   Watcher,            ,                      beforeUpdate      
        new Watcher(vm, updateComponent, noop, {
          before: function before () {
            if (vm._isMounted && !vm._isDestroyed) {
              callHook(vm, 'beforeUpdate');
            }
          }
        }, true /* isRenderWatcher */);
        hydrating = false;
        //  vue      mounted()  
        if (vm.$vnode == null) {
          vm._isMounted = true;
          callHook(vm, 'mounted');
        }
        return vm
      }
    

    vm._updateでは、主にビューを更新し、仮想DOMを介してvm.$mountメソッドは、initLifecycleで定義され、実行中にVue.prototype._updateメソッドが呼び出される.vm.__patch__メソッドではvm.__patch__が呼び出され、patchはpatchの戻り値として割り当てられたグローバルメソッドであり、このメソッドがVirtual DOMの実装方法である.
    以上、vue初期化フェーズでは、new Observer()インスタンスを介してdefineReactive$$1メソッドを呼び出してオブジェクトのプロパティをブロックし、getメソッドとsetメソッドを書き換えます.setメソッドで値が変更されると、サブスクライバリストのwatcherのupdateメソッドが実行され、更新されます.domをマウントすると、生成されたDOMツリーオブジェクトnew Watcherがサブスクライバリストに追加され、ビューが変化すると着信_が実行されます.updateの_renderメソッド更新ビュー