【Vue.jsを遊ぶ】非同期キューの件

28866 ワード

引用:
前述のコア編では、Vueの実行時のコアには主にデータ初期化、データ更新、非同期キュー、DOMレンダリングといういくつかの部分が含まれており、非同期キューを理解することはデータ更新を理解する上で非常に重要な一部であり、本稿では、Vueの非同期キューの考え方と実現原理について説明し、Vueの$nextTickについて説明する.
一、Vueの非同期キューは何ですか?
この概念を理解するには、まず例を見てみましょう.
{{ words }}
var vm = new Vue({ el:"#example", data: { name: "Devin", greetings: "Hello" }, computed: { words: function(){ return this.greetings + ' ' + this.name + '!' } }, methods: { clickHanler(){ this.name = 'Devinn'; this.name = 'Devinnzhang'; this.greetings = 'Morning'; } } });

前述の解析から分かるように、Vueにはwatcherをレンダリングし、テンプレートをレンダリングし、computed watcherがプロパティの計算を担当する2つのwatcherが作成されています.clickHandlerをクリックすると、データが変化して2つのwatcherに通知され、watcherが更新されます.ここでwatcherの更新には2つの問題があります.
1、watcherとcomputed watcherをレンダリングし、変化したデータを同時に購読しました.どちらが先に実行され、実行順序はどうですか.
2、1つのイベントサイクルでnameの変化が2回トリガーされ、greetingsが1回トリガーされ、2つのwatcherに対応して全部で何回実行されましたか?DOMは何回レンダリングしましたか? 
データ更新の段階では、watcherが正しい順序で実行され、実行回数(レンダリングwatcherがDOMレンダリングであることに対応)をできるだけ減らすことができることを保証する必要がある複数のwatcherを管理するロールが必要であることがわかります.Vueのこの角色は非同期キューです.
実現原理についてお話しします.
 
二、非同期キュー実現原理
Vueの非同期キューはデータ更新時にオープンしているので、データ更新の論理から見ると:
 /**
   * Define a reactive property on an Object.
   */
  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    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(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          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) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }
  Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update(); //watcher.update() 
    }
  };

データが変化するとdep.notity()が呼び出され、購読したwatcherに更新を通知します.次に本題に入り、watcher更新の論理を見てみましょう.
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

updateでwatcherがすぐに実行されない(同期を除く)のではなく、queueWatcherが呼び出された(更新されたwatcherがキューに追加された)ことがわかります.queueWatcherの実装を見てください.
/**
   * Push a watcher into the watcher queue.
   * Jobs with duplicate IDs will be skipped unless it's
   * pushed when the queue is being flushed.
   */
  function queueWatcher (watcher) {
    var id = watcher.id;
    console.log('watcherId='+ id + 'exporession=' + watcher.expression);
    if (has[id] == null) {
      //console.log('watcherId='+ id + 'exporession=' + watcher.expression);
      has[id] = true;
      if (!flushing) {
        queue.push(watcher);
      } else { 
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        var i = queue.length - 1;
        while (i > index && queue[i].id > watcher.id) {
          i--;
        }
        queue.splice(i + 1, 0, watcher);
      }
      // queue the flush
      if (!waiting) {
        waiting = true;

        if (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }

ここのqueueWatcherは2つのことをしました.
1、watcherをキューに押し込み、繰り返しのwatcherが1回押し込まれることを知っているため、1つのイベントサイクルで複数回トリガーされたwatcherはキューに1回だけ押し込まれる.例の非同期キューには、watcherとcomputed watcherが1つしかレンダリングされていません.
2、nextTick(flushSchedulerQueue)を呼び出してキュー内のwatcherを非同期実行し、nextTickは非同期を実現し、flushSchedulerQueueはwatcherを遍歴実行する.
nexttickの実装を見てみましょう.
var callbacks = [];
---
function nextTick (cb, ctx) { //cb === flushSchedulerQueue var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } }

nextTickは、まず実行する関数をcallback配列に配置し、timerFunc()を呼び出して非同期スレッドを開き、pushを配列に与える関数を1つずつ実行します.
var timerFunc;


  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
      if (isIOS) { setTimeout(noop); }
    };
    isUsingMicroTask = true;
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Techinically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    // Fallback to setTimeout.
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

前はPromise、MutationObserver(microtask)、後はsetImmediate、setTimeout(macrotask)を利用した互換実装がありますが、ここは非同期で、理解的には最後のsetTimeoutを直接見ればいいのです
  timerFunc = function () {
      setTimeout(flushCallbacks, 0);
  };
  ---
  var pending = false;

  function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }

非同期呼び出しはnextTickでcallbacksに入れた関数を実行し、VueではflushSchedulerQueueであることがわかります.どうしてVue中って言うの?$nextTickインタフェースと関係があるので、flushSchedulerQueueを見てから話します.

/*
* * Flush both queues and run the watchers. */ function flushSchedulerQueue () { currentFlushTimestamp = getNow(); flushing = true; var watcher, id; // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. queue.sort(function (a, b) { return a.id - b.id; }); // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index]; if (watcher.before) { watcher.before(); } id = watcher.id; has[id] = null; watcher.run(); // in dev build, check and stop circular updates. if (has[id] != null) { circular[id] = (circular[id] || 0) + 1; if (circular[id] > MAX_UPDATE_COUNT) { //MAX_UPDATE_COUNT === 100 100 warn( 'You may have an infinite update loop ' + ( watcher.user ? ("in watcher with expression \"" + (watcher.expression) + "\"") : "in a component render function." ), watcher.vm ); break } } } // keep copies of post queues before resetting state var activatedQueue = activatedChildren.slice(); var updatedQueue = queue.slice(); resetSchedulerState(); // call component updated and activated hooks callActivatedHooks(activatedQueue); callUpdatedHooks(updatedQueue); // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush'); } }

flushSchedulerQueueではまずキュー内のすべてのwatcherをidに従ってソートし、その後キューを巡って順番にwatcherを実行します.ソートの原因はwatcherが正しい順序で実行されることを保証することです(watcher間のデータは依存関係がある可能性がありますので、実行の前後順があり、watcherの初期化順序を見ることができます).このときのflushSchedulerQueueはnextTick(flushSchedulerQueue)によって非同期実行となっており、1つのイベントループ(clickHandler)でflushSchedulerQueueを1回だけ実行させ、複数回の実行、レンダリングを回避することを目的としている.
以上が非同期キューの基本的な実現原理である.
ps:前述のnexttickを補足します
まずnextTickではcallBacksが複数のcbをサポートしているが、queueWatcherの呼び出しのため、最初のcbがflushSchedulerQueueであり、queueWatcherではflushSchedulerQueueが実行されていないため、flushSchedulerQueueを追加することは許されないため、非同期呼び出しでは1つのflushSchedulerQueueのみが存在し、呼び出しが完了すると、次のcbが実行されます.
Vueには$nextTickというインタフェースがあり、$nextTickにcbを転送することで、DOMレンダリング後にcb操作を実行するのを待つことができます.$nextTickはここのnextTick関数です.転送されたcbはDOMレンダリング後に実行されるので、flushSchedulerQueueがwatcherの実行、DOMのレンダリングに完了しました.nexttickで待機している実行は、次のとおりです.
[flushSchedulerQueue , cb1, cb2, ...]

 
三、非同期キュー設計のハイライト:
非同期キューの優れた設計には2つの部分があると思います.
第一に、非同期実行は同じイベントループの複数回レンダリングの難題を解決し、簡単だが極めて有効である.
第2に、複数のイベントループは、キュー内で実行されたことを繰り返し圧入することによって解決するが、データ更新の完全性と正確性を保証するために、watcher部分を再更新する必要がある.上の例を変えて理解してください.
{{ words }}
var i = 0; var vm = new Vue({ el:"#example", data: { name: "Mr", greetings: "Hello" }, computed: { words: function(){ return this.greetings + ' ' + this.name + '!' } }, methods: { clickHanler(){ this.name = 'Mr_' + i; this.greetings = 'Morning'; } } });

例では、クリックするたびに、非同期キュー内のcomputed watcherとレンダリングwatcherの更新がトリガーされます.更新が非同期であるため、私が何度も連続的にクリックしたとき、非同期キューが前の遍歴実行中にキューの一部のwatcherを実行した可能性があります.例えば、computed watcher、後続のクリックはこのwatcherを更新する必要があります.この場合、どうすればいいですか.
Vueでキューを繰り返し圧入することで解決されるこの問題は、すでに実行されている場合、カラムの残りの部分にもう一度圧入することで、更新が必要なwatcherが現在watcherを実行する次の部分で実行されるということです.
[ 1, 2, 3, 4, 5 ] 

[ 1, 2, 1, 2, 3, 4, 5 ]  // 1,2     ,         

論理実装queueWatcher
function queueWatcher (watcher) {
    var id = watcher.id;
    console.log('watcherId='+ id + 'exporession=' + watcher.expression);
    if (has[id] == null) { //     
      //console.log('watcherId='+ id + 'exporession=' + watcher.expression);
      has[id] = true;
      if (!flushing) {
        queue.push(watcher);
      } else { 
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        var i = queue.length - 1;
        while (i > index && queue[i].id > watcher.id) {  //index:     watcher  index
          i--;
        }
        queue.splice(i + 1, 0, watcher);
      }
      // queue the flush
      if (!waiting) {
        waiting = true;

        if (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }

 
まとめ:
本稿では,データ更新の観点からVueの非同期キュー設計と実現原理を述べ,主なプロセスは,更新のプロセスがqueueWatcherによって各watcherをキューに入れた後,nextTick(flushSchedulerQueue)によってキュー内のwatcherを非同期で更新することである.総じて言えば、非同期キューは主にデータ更新における複数回のトリガ、複数回のレンダリングの問題を解決し、そのうち単一のイベントループは非同期の方法で解決され、複数回のイベントループは重複してキューに押し込む方法でデータ更新の正確性と完全性を保証する.最後に、DOMの更新を待つ必要がある場合、または現在のデータの更新が完了した後に、いくつかの論理を実行する必要がある場合は、$nextTickを呼び出して実装することができる.
転載先:https://www.cnblogs.com/DevinnZ/p/11065645.html