Vue バッチ更新 DOM 実装手順

Vue バッチ更新 DOM 実装手順

シーン紹介

SFC (単一ファイルコンポーネント) では、次のようなロジックを記述することがよくあります。

<テンプレート>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}</span>
  </div>  
</テンプレート>
<script type="javascript">
エクスポートデフォルト{
  データ() {
    戻る {
      : 0,
      0 です
    }
  },
  作成された() {
    // ロジックコード
    これ.a = 1
    これ.b = 2
  }
}
</スクリプト>

ご存知のとおり、this.a と this.b の割り当て操作が完了すると、Vue は this.a と this.b の対応する DOM 更新関数をマイクロタスクに配置します。メインスレッドの同期タスクの実行を待機した後、マイクロタスクはキューから取り出され、実行されます。 Vue の公式ドキュメント「詳細なレスポンシブ原則 - レスポンシブ プロパティの宣言」でどのように説明されているかを見てみましょう。

気づいていないかもしれませんが、Vue は DOM の更新を非同期的に行います。データの変更が検出される限り、Vue はキューを開き、同じイベント ループで発生するすべてのデータの変更をバッファリングします。

では、Vue はどのようにしてこの機能を実現するのでしょうか?この質問に答えるには、Vue ソースコードの中核部分であるレスポンシブ原則を深く理解する必要があります。

深い応答性

まず、this.a と this.b に値を割り当てた後に何が起こるかを見てみましょう。開発に Vue CLI を使用する場合、main.js ファイルに新しい Vue() インスタンス化操作が含まれます。 Vue のソースコードはフローを使用して記述されるため、理解コストが目に見えないほど増加します。便宜上、npm vue パッケージの dist フォルダーにある vue.js ソース コードを直接見てみましょう。 「function Vue」を検索すると、次のソースコードが見つかりました。

関数 Vue (オプション) {
  if (!(このインスタンス Vue)
  ){
    warn('Vue はコンストラクターなので、`new` キーワードで呼び出す必要があります');
  }
  this._init(オプション);
}

非常にシンプルなソースコードです。ソースコードは想像していたほど難しくありません。このような予想外の驚きとともに、_init 関数を探し続けて、この関数が何を行うかを確認します。

Vue.prototype._init = 関数 (オプション) {
  var vm = this;
  // uid
  vm._uid = uid$3++;

  var 開始タグ、終了タグ;
  /* イスタンブールは無視します */
  if (config.performance && mark) {
    startTag = "vue-perf-start:" + (vm._uid);
    endTag = "vue-perf-end:" + (vm._uid);
    マーク(開始タグ);
  }

  // これを回避するためのフラグ
  vm._isVue = true;
  // マージオプション
  if (オプション && options._isComponent) {
    // 内部コンポーネントのインスタンス化を最適化
    // 動的オプションのマージは非常に遅く、
    // 内部コンポーネント オプションには特別な処理が必要です。
    initInternalComponent(vm、オプション);
  } それ以外 {
    vm.$options = mergeOptions(
      コンストラクタオプションを解決します(vm.constructor)、
      オプション || {},
      仮想
    );
  }
  /* イスタンブールは無視します。その他 */
  {
    プロキシサーバを初期化します。
  }
  // 本当の自分をさらけ出す
  vm._self を vm に追加します。
  ライフサイクルを初期化します(vm);
  イベントを初期化します(vm);
  initRender(vm);
  フックを呼び出します(vm、'beforeCreate')。
  initInjections(vm); // データ/プロパティの前にインジェクションを解決する
  初期化状態(vm);
  initProvide(vm); // data/props の後に provide を解決する
  callHook(vm, 'created');

  /* イスタンブールは無視します */
  if (config.performance && mark) {
    vm._name = formatComponentName(vm, false);
    マーク(endTag);
    measure(("vue " + (vm._name) + " init"), startTag, endTag);
  }

  (vm.$options.el)の場合{
    vm.$mount(vm.$options.el);
  }
}

とりあえず上記の判断は無視して、以下の主なロジックに直接進みましょう。 _init 関数は、initLifeCycle、initEvents、initRender、callHook、initInjections、initState、initProvide、および 2 番目の callHook 関数を連続して実行していることがわかります。関数の名前から、具体的な意味を知ることができます。一般的に言えば、このコードは次の2つの部分に分かれています。

  1. 初期化ライフサイクル、イベントフック、レンダリング関数を完了したら、beforeCreateライフサイクルに入ります(beforeCreate関数を実行します)。
  2. 初期化注入値、ステータス、提供値を完了したら、作成したライフサイクルに入ります(作成した関数を実行します)

その中でも、私たちが注目するデータ応答性の原則部分は、initState 関数にあります。この関数が何をするのか見てみましょう。

関数 initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  opts.props の場合、 initProps(vm, opts.props); }
  opts.methods の場合、 initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } それ以外 {
    観察(vm._data = {}, true /* asRootData */);
  }
  opts.computed の場合、 initComputed(vm, opts.computed); }
  opts.watch の場合、opts.watch は nativeWatch と等しくなります。
    initWatch(vm, opts.watch);
  }
}

ここでは、SFC ファイルの作成時によく見られるいくつかの構成項目 (props、methods、data、computed、watch) が表示されます。 initData 関数を実行する opts.data 部分に注目します。

関数 initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(データ、vm)
    : データ || {};
  if (!isPlainObject(データ)) {
    データ = {};
    警告(
      'データ関数はオブジェクトを返す必要があります:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      仮想
    );
  }
  // インスタンス上のプロキシデータ
  var keys = Object.keys(データ);
  var props = vm.$options.props;
  var メソッド = vm.$options.methods;
  var i = キーの長さ;
  (i--) {
    var キー = keys[i];
    {
      if (メソッド && hasOwn(メソッド、キー)) {
        警告(
          (「メソッド \"" + キー + "\" はすでにデータ プロパティとして定義されています。」)
          仮想
        );
      }
    }
    もしprops && hasOwn(props, key) であれば
      警告(
        「データ プロパティ \"" + キー + "\" はすでにプロパティとして宣言されています。」 +
        "代わりにプロパティのデフォルト値を使用してください。",
        仮想
      );
    } そうでなければ (!isReserved(key)) {
      プロキシ(vm、"_data"、キー);
    }
  }
  // データを観察する
  観察(データ、true /* asRootData */);
}

データ構成項目を記述するときに、それを関数として定義するので、getData 関数はここで実行されます。

関数 getData (データ, vm) {
  // #7573 データゲッターを呼び出すときに依存関係コレクションを無効にする
  プッシュターゲット();
  試す {
    データを返します。call(vm, vm)
  } キャッチ (e) {
    handleError(e, vm, "データ()");
    戻る {}
  ついに
    ポップターゲット();
  }
}

getData 関数が行うことは非常に単純で、コンポーネント インスタンスのコンテキストでデータ関数を実行します。 pushTarget 関数と popTarget 関数は、data 関数の実行前と実行後に実行されることに注意してください。この 2 つの関数については後で説明します。

getData関数を実行した後、initData関数に戻ります。その後ろにループのエラー判定がありますが、今は無視します。そこで、観察関数に移ります。

関数 observe (値、asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    戻る
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = 値.__ob__;
  } それ以外の場合 (
    観察すべき &&
    !isServerRendering() &&
    (Array.isArray(値) || isPlainObject(値)) &&
    Object.isExtensible(値) &&
    !値._isVue
  ){
    ob = 新しいオブザーバー(値);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  戻り値
}

observe 関数は、データ オブジェクトのオブザーバー (ob) を作成します。つまり、Observer をインスタンス化します。Observer のインスタンス化では具体的に何が行われるのでしょうか。引き続きソースコードを見てみましょう。

var Observer = 関数 Observer (値) {
  this.value = 値;
  this.dep = 新しい Dep();
  this.vmCount = 0;
  def(値、'__ob__'、これ);
  Array.isArray(値)の場合{
    (hasProto) の場合 {
      protoAugment(値、配列メソッド);
    } それ以外 {
      copyAugment(値、配列メソッド、配列キー);
    }
    this.observeArray(値);
  } それ以外 {
    this.walk(値);
  }
}

通常の状況では、定義したデータ関数はオブジェクトを返すため、ここでは配列を扱いません。次に、walk 関数の実行を続けます。

Observer.prototype.walk = 関数 walk (obj) {
  var キー = Object.keys(obj);
  (var i = 0; i < keys.length; i++) の場合 {
    Reactive$$1 を定義します(obj、キー[i])。
  }
}

データ関数によって返されるオブジェクト、つまりコンポーネント インスタンスのデータ オブジェクト内の列挙可能なプロパティごとに、defineReactive$$1 関数を実行します。

関数defineReactive$$1 (
  オブジェクト、
  鍵、
  ヴァル、
  カスタムセッター、
  浅い
){
  var dep = 新しい Dep();

  var プロパティ = Object.getOwnPropertyDescriptor(obj, key);
  if (プロパティ && property.configurable === false) {
    戻る
  }

  // 定義済みのゲッター/セッターに対応
  var getter = property && property.get;
  var setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[キー];
  }

  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, キー, {
    列挙可能: true、
    設定可能: true、
    取得: 関数reactiveGetter() {
      var value = getter ? getter.call(obj): val;
      if (依存ターゲット) {
        依存関係
        if (childOb) {
          childOb.dep.depend();
          Array.isArray(値)の場合{
            依存配列(値);
          }
        }
      }
      戻り値
    },
    設定: 関数 reactiveSetter (newVal) {
      var value = getter ? getter.call(obj): val;
      /* eslint は自己比較を無効にします */
      if (newVal === 値 || (newVal !== newVal && 値 !== 値)) {
        戻る
      }
      /* eslint を有効にして自己比較をしない */
      if (カスタムセッター) {
        カスタムセッター();
      }
      // #7981: セッターのないアクセサープロパティの場合
      if (getter && !setter) { return }
      if (セッター) {
        setter.call(obj, newVal);
      } それ以外 {
        val = 新しいVal;
      }
      childOb = !shallow && observe(newVal);
      通知します。
    }
  });
}

defineReactive$$1 関数では、最初に依存関係コレクターがインスタンス化されます。次に、Object.defineProperty を使用して、オブジェクト プロパティのゲッター (つまり、上記の get 関数) とセッター (つまり、上記の set 関数) を再定義します。

トリガーゲッター

ある意味では、ゲッターとセッターはコールバック関数として理解できます。オブジェクトのプロパティの値が読み取られると、get 関数 (つまり、ゲッター) がトリガーされ、オブジェクトのプロパティの値が設定されると、set 関数 (つまり、セッター) がトリガーされます。元の例に戻りましょう:

<テンプレート>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}</span>
  </div>  
</テンプレート>
<script type="javascript">
エクスポートデフォルト{
  データ() {
    戻る {
      : 0,
      0 です
    }
  },
  作成された() {
    // ロジックコード
    これ.a = 1
    これ.b = 2
  }
}
</スクリプト>

ここでは、this オブジェクトのプロパティ a と b の値が設定されているため、setter がトリガーされます。上記のセット関数コードを個別に取り出してみましょう。

関数reactiveSetter(newVal){
  var value = getter ? getter.call(obj): val;
  /* eslint は自己比較を無効にします */
  if (newVal === 値 || (newVal !== newVal && 値 !== 値)) {
    戻る
  }
  /* eslint を有効にして自己比較をしない */
  if (カスタムセッター) {
    カスタムセッター();
  }
  // #7981: セッターのないアクセサープロパティの場合
  if (getter && !setter) { return }
  if (セッター) {
    setter.call(obj, newVal);
  } それ以外 {
    val = 新しいVal;
  }
  childOb = !shallow && observe(newVal);
  通知します。
}

セッターは最初にゲッターを実行します。

関数reactiveGetter() {
  var value = getter ? getter.call(obj): val;
  if (依存ターゲット) {
    依存関係
    if (childOb) {
      childOb.dep.depend();
      Array.isArray(値)の場合{
        依存配列(値);
      }
    }
  }
  戻り値
}

ゲッターはまず Dep.target が存在するかどうかを確認します。先ほど getData 関数を実行したとき、Dep.target の初期値は null でした。いつ値が割り当てられたのでしょうか?先ほど getData 関数について説明したとき、pushTarget 関数と popTarget 関数について説明しました。これら 2 つの関数のソース コードは次のとおりです。

依存関係ターゲット = null;
var ターゲットスタック = [];

関数pushTarget(ターゲット){
  ターゲットスタックをプッシュします。
  依存関係ターゲット = ターゲット;
}

関数popTarget() {
  ターゲットスタックをポップします。
  Dep.target = targetStack[targetStack.length - 1];
}

ゲッターを正常に実行するには、まず pushTarget 関数を実行する必要があります。 pushTarget 関数が実行される場所を確認しましょう。 vue.js で pushTarget を検索すると 5 箇所見つかり、定義箇所を除くと実行箇所は 4 箇所あります。
pushTarget 関数が最初に実行される場所。以下はエラーを処理する関数です。通常のロジックはトリガーされません。

関数handleError(err, vm, info) {
  // 無限レンダリングを回避するために、エラー ハンドラーの処理中に依存関係の追跡を無効にします。
  // 参照: https://github.com/vuejs/vuex/issues/1505
  プッシュターゲット();
  試す {
    もし(VM){
      var cur = vm;
      ((cur = cur.$parent)) の間 {
        var フック = cur.$options.errorCaptured;
        if (フック) {
          (var i = 0; i < hooks.length; i++) の場合 {
            試す {
              var capture = hooks[i].call(cur, err, vm, info) === false;
              if (キャプチャ) { 戻り値 }
            } キャッチ (e) {
              globalHandleError(e, cur, 'errorCaptured フック');
            }
          }
        }
      }
    }
    グローバル ハンドル エラー (err、vm、情報)。
  ついに
    ポップターゲット();
  }
}

pushTarget が実行される 2 番目の場所。これは対応するフック関数を呼び出すためのものです。対応するフック関数が実行されるとトリガーされます。ただし、現在の操作は beforeCreate フックと created フックの間にあり、トリガーされていません。

関数callHook(vm, hook) {
  // #7573 ライフサイクルフックを呼び出すときに依存関係コレクションを無効にする
  プッシュターゲット();
  var ハンドラー = vm.$options[フック];
  var info = hook + "フック";
  if (ハンドラ) {
    (var i = 0, j = handlers.length; i < j; i++) の場合 {
      ハンドラを呼び出し、エラー処理を実行します。
    }
  }
  (vm._hasHookEvent)の場合{
    vm.$emit('hook:' + フック);
  }
  ポップターゲット();
}

pushTarget が実行される 3 番目の場所。これは、ウォッチャーがインスタンス化されるときに実行される関数です。前のコードを確認すると、新しい Watcher の動作は確認できないようです。

Watcher.prototype.get = 関数 get() {
  pushTarget(これを);
  var 値;
  var vm = this.vm;
  試す {
    値 = this.getter.call(vm, vm);
  } キャッチ (e) {
    if (this.user) {
      handleError(e, vm, ("ウォッチャーのゲッター \"" + (this.expression) + "\""));
    } それ以外 {
      投げる
    }
  ついに
    // すべてのプロパティを「タッチ」して、すべて次のように追跡されるようにします。
    // ディープウォッチングの依存関係
    if (this.deep) {
      トラバース(値);
    }
    ポップターゲット();
    this.cleanupDeps();
  }
  戻り値
}

pushTarget が実行される 4 番目の場所は、前の getData 関数です。ただし、getData 関数は defineReactive$$1 関数の前に実行されます。 getData 関数を実行した後、Dep.target は null にリセットされました。

関数 getData (データ, vm) {
  // #7573 データゲッターを呼び出すときに依存関係コレクションを無効にする
  プッシュターゲット();
  試す {
    データを返します。call(vm, vm)
  } キャッチ (e) {
    handleError(e, vm, "データ()");
    戻る {}
  ついに
    ポップターゲット();
  }
}

セッターを直接トリガーすると、ゲッター内のロジックが正常に実行されないようです。さらに、Dep.target も setter で判断されるため、Dep.target のソースが見つからない場合は setter のロジックを続行できないこともわかりました。

Dep.targetを探す

では、Dep.target の値はどこから来るのでしょうか?心配しないでください。_init 関数の操作に戻って、引き続き下を見ていきましょう。

Vue.prototype._init = 関数 (オプション) {
  var vm = this;
  // uid
  vm._uid = uid$3++;

  var 開始タグ、終了タグ;
  /* イスタンブールは無視します */
  if (config.performance && mark) {
    startTag = "vue-perf-start:" + (vm._uid);
    endTag = "vue-perf-end:" + (vm._uid);
    マーク(開始タグ);
  }

  // これを回避するためのフラグ
  vm._isVue = true;
  // マージオプション
  if (オプション && options._isComponent) {
    // 内部コンポーネントのインスタンス化を最適化
    // 動的オプションのマージは非常に遅く、
    // 内部コンポーネント オプションには特別な処理が必要です。
    initInternalComponent(vm、オプション);
  } それ以外 {
    vm.$options = mergeOptions(
      コンストラクタオプションを解決します(vm.constructor)、
      オプション || {},
      仮想
    );
  }
  /* イスタンブールは無視します。その他 */
  {
    プロキシサーバを初期化します。
  }
  // 本当の自分をさらけ出す
  vm._self を vm に追加します。
  ライフサイクルを初期化します(vm);
  イベントを初期化します(vm);
  initRender(vm);
  フックを呼び出します(vm、'beforeCreate')。
  initInjections(vm); // データ/プロパティの前にインジェクションを解決する
  初期化状態(vm);
  initProvide(vm); // data/props の後に provide を解決する
  callHook(vm, 'created');

  /* イスタンブールは無視します */
  if (config.performance && mark) {
    vm._name = formatComponentName(vm, false);
    マーク(endTag);
    measure(("vue " + (vm._name) + " init"), startTag, endTag);
  }

  (vm.$options.el)の場合{
    vm.$mount(vm.$options.el);
  }
}

_init 関数の最後に、vm.$mount 関数が実行されていることがわかりました。この関数は何をするのでしょうか?

Vue.prototype.$mount = 関数 (
  エル、
  水分補給
){
  el = el && inBrowser ? query(el) : 未定義;
  mountComponent(this, el, hydrating) を返します
}

続けて mountComponent 関数を入力して確認してみましょう。

関数 mountComponent (
  vm、
  エル、
  水分補給
){
  vm.$el = el;
  (!vm.$options.render)の場合{
    vm.$options.render = createEmptyVNode;
    {
      /* イスタンブールは無視します */
      ((vm.$options.template && vm.$options.template.charAt(0) !== '#') の場合 ||
        vm.$options.el || el) {
        警告(
          'テンプレートが' + であるVueのランタイムのみのビルドを使用しています。
          'コンパイラが利用できません。テンプレートを ' + に事前コンパイルするか、
          '関数をレンダリングするか、コンパイラに含まれるビルドを使用してください。',
          仮想
        );
      } それ以外 {
        警告(
          「コンポーネントのマウントに失敗しました: テンプレートまたはレンダリング関数が定義されていません。」
          仮想
        );
      }
    }
  }
  callHook(vm, 'beforeMount');

  var コンポーネントを更新します。
  /* イスタンブールは無視します */
  if (config.performance && mark) {
    更新コンポーネント = 関数 () {
      var name = vm._name;
      var id = vm._uid;
      var startTag = "vue-perf-start:" + id;
      var endTag = "vue-perf-end:" + id;

      マーク(開始タグ);
      var vnode = vm._render();
      マーク(endTag);
      measure(("vue " + name + " render"), startTag, endTag);

      マーク(開始タグ);
      vm._update(vnode、ハイドレーション);
      マーク(endTag);
      measure(("vue " + name + " patch"), startTag, endTag);
    };
  } それ以外 {
    updateComponent = 関数(){
      vm._update(vm._render(), ハイドレーション);
    };
  }

  // ウォッチャーのコンストラクタ内でこれを vm._watcher に設定します
  // ウォッチャーの初期パッチは$forceUpdateを呼び出す可能性があるので(例えば、子プロセス内で)、
  // コンポーネントのマウントされたフック)、これはvm._watcherが既に定義されていることに依存します
  新しいウォッチャー(vm、updateComponent、noop、{
    before: 関数before() {
      (vm._isMounted && !vm._isDestroyed) の場合 {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  水分補給 = false;

  // 手動でマウントされたインスタンス、自身にマウントされた呼び出し
  // マウントは、挿入されたフック内のレンダリングによって作成された子コンポーネントに対して呼び出されます
  (vm.$vnode == null)の場合{
    vm._isMounted = true;
    callHook(vm, 'マウント済み');
  }
  戻り値
}

新しいウォッチャー操作があることを知って、うれしい驚きを感じました。確かに、紆余曲折を経て、出口はないと思うかもしれませんが、突然、柳と花が咲く別の村が見えます。ここでインスタンス化されるウォッチャーは、DOM を更新するために使用されるウォッチャーです。 SFC ファイルのテンプレート セクション内のすべての値を順番に読み取ります。これは、対応するゲッターがトリガーされることを意味します。
新しい Watcher は watcher.get 関数を実行し、この関数は pushTarget 関数を実行するため、Dep.target が割り当てられます。ゲッター内のロジックはスムーズに実行されます。

ゲッター

この時点で、ようやく Vue のレスポンシブ原則の核心に到達しました。ゲッターに戻って、Dep.target を取得した後にゲッターが何を行うかを見てみましょう。

関数reactiveGetter() {
  var value = getter ? getter.call(obj): val;
  if (依存ターゲット) {
    依存関係
    if (childOb) {
      childOb.dep.depend();
      Array.isArray(値)の場合{
        依存配列(値);
      }
    }
  }
  戻り値
}

同様に、コードの堅牢性を向上させるための詳細に焦点を当てるのではなく、メインラインに直接焦点を当てます。ご覧のとおり、Dep.target が存在する場合、dep.depend 関数が実行されます。この機能は何をするのでしょうか?コードを見てみましょう:

Dep.prototype.depend = 関数depend() {
  if (依存ターゲット) {
    Dep.target.addDep(this);
  }
}

やることは非常に簡単です。 Dep.target.addDep 関数が実行されます。しかし、Dep.target は実際にはウォッチャーなので、ウォッチャー コードに戻る必要があります。

Watcher.prototype.addDep = 関数 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(これを);
    }
  }
}

同様に、いくつかの小さなロジック処理を無視し、 dep.addSub 関数に焦点を当てます。

Dep.prototype.addSub = 関数 addSub (sub) {
  this.subs.push(sub);
}

これは非常に単純なロジックで、ウォッチャーをキャッシュのサブスクライバーとして配列にプッシュします。この時点で、ゲッターのロジック全体が完成します。その後、popTarget関数が実行され、Dep.targetがnullにリセットされます。

セッター

もう一度ビジネス コードに戻りましょう。

<テンプレート>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}</span>
  </div>  
</テンプレート>
<script type="javascript">
エクスポートデフォルト{
 データ() {
    戻る {
      : 0,
      0 です
    }
  },
  作成された() {
    // ロジックコード
    これ.a = 1
    これ.b = 2
  }
}
</スクリプト>

作成されたライフサイクルでは、セッターを 2 回トリガーしました。セッター実行のロジックは次のとおりです。

関数reactiveSetter(newVal){
  var value = getter ? getter.call(obj): val;
  /* eslint は自己比較を無効にします */
  if (newVal === 値 || (newVal !== newVal && 値 !== 値)) {
    戻る
  }
  /* eslint を有効にして自己比較をしない */
  if (カスタムセッター) {
    カスタムセッター();
  }
  // #7981: セッターのないアクセサープロパティの場合
  if (getter && !setter) { return }
  if (セッター) {
    setter.call(obj, newVal);
  } それ以外 {
    val = 新しいVal;
  }
  childOb = !shallow && observe(newVal);
  通知します。
}

ここでは、セッターによって最後に実行される関数 dep.notify() のみに注目する必要があります。この関数が何をするのか見てみましょう:

Dep.prototype.notify = 関数notify() {
  // まず購読者リストを安定させる
  var subs = this.subs.slice();
  (!config.async)の場合{
    // 非同期で実行されていない場合、サブはスケジューラでソートされません
    // 正しく実行されるように並べ替える必要があります
    // 注文
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  (var i = 0, l = subs.length; i < l; i++) の場合 {
    subs[i].update();
  }
}

This.subs の各要素はウォッチャーです。上記のゲッター セクションでは、ウォッチャーを 1 つだけ収集しました。 setterが2回トリガーされるため、subs[0].update()、つまりwatcher.update()関数が2回実行されます。この関数が何をするのか見てみましょう:

Watcher.prototype.update = 関数 update() {
  /* イスタンブールは無視します。その他 */
  if (this.lazy) {
    this.dirty = true;
  } それ以外の場合は (this.sync) {
    これを実行してください。
  } それ以外 {
    キューウォッチャー(これ);
  }
}

いつものように、queueWatcher 関数に直接ジャンプします。

関数 queueWatcher (ウォッチャー) {
  var id = ウォッチャーid;
  (has[id] == null)の場合{
    [id] が true である。
    if (!フラッシュ) {
      キューにプッシュします(ウォッチャー)。
    } それ以外 {
      // すでにフラッシュしている場合は、IDに基づいてウォッチャーをスプライスします
      // すでに ID を過ぎている場合は、すぐに次に実行されます。
      var i = キューの長さ - 1;
      i > インデックス && queue[i].id > watcher.id の間 {
        私 - ;
      }
      キュー.splice(i + 1, 0, ウォッチャー);
    }
    // フラッシュをキューに入れる
    if (!待機中) {
      待機 = true;

      (!config.async)の場合{
        スケジューラキューをフラッシュします。
        戻る
      }
      nextTick(スケジューラキューをフラッシュ);
    }
  }
}

ID は同じなので、ウォッチャーのコールバック関数はキューに 1 回だけプッシュされます。ここでまたおなじみの nextTick が登場します。

関数 nextTick (cb, ctx) {
  var _resolve;
  コールバック.push(関数() {
    もし(cb){
      試す {
        cb.call(ctx);
      } キャッチ (e) {
        handleError(e, ctx, 'nextTick');
      }
    } それ以外の場合 (_resolve) {
      _resolve(ctx);
    }
  });
  (!保留中)の場合{
    保留中 = true;
    タイマー関数();
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    新しいPromise(function(resolve){を返す
      _resolve = 解決する;
    })
  }
}

nextTick関数はコールバック関数を再度ラップし、timerFunc()を実行します。

var タイマー関数;

// nextTick動作はマイクロタスクキューを活用し、
// ネイティブの Promise.then または MutationObserver のいずれかを介して。
// MutationObserverはより幅広いサポートを提供していますが、深刻なバグがあります
// iOS 9.3.3以上のUIWebViewではタッチイベントハンドラでトリガーされます。
// 数回トリガーすると完全に動作しなくなります...つまり、ネイティブの場合
// Promise が利用可能なので、それを使用します:
/* istanbul 次を無視、$flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = 関数 () {
    p.then(コールバックをフラッシュします);
    // 問題のあるUIWebViewでは、Promise.thenは完全には機能しませんが、
    // コールバックがプッシュされる奇妙な状態でスタックする可能性があります
    // マイクロタスクキューですが、ブラウザが
    // タイマーの処理など、他の作業も行う必要があります。そのため、
    // 空のタイマーを追加して、マイクロタスク キューを強制的にフラッシュします。
    if (isIOS) { setTimeout(noop); }
  };
  マイクロタスクを使用しているかどうか = true;
} そうでない場合、(!isIE && typeof MutationObserver !== 'undefined' && (
  MutationObserver がネイティブかどうかを確認します。
  // PhantomJS と iOS 7.x
  MutationObserver.toString() === '[オブジェクト MutationObserverConstructor]'
)) {
  // ネイティブのPromiseが利用できない場合はMutationObserverを使用します。
  // 例: PhantomJS、iOS7、Android 4.4
  // (#6466 MutationObserver は IE11 では信頼できません)
  var カウンタ = 1;
  var オブザーバー = 新しい MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  オブザーバー.observe(textNode, {
    文字データ: true
  });
  timerFunc = 関数 () {
    カウンター = (カウンター + 1) % 2;
    textNode.data = String(カウンター);
  };
  マイクロタスクを使用しているかどうか = true;
} そうでない場合 (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // setImmediate にフォールバックします。
  // 技術的には(マクロ)タスクキューを活用します。
  // ただし、setTimeout よりも優れた選択肢です。
  timerFunc = 関数 () {
    即時設定(コールバックをフラッシュ)。
  };
} それ以外 {
  // setTimeout にフォールバックします。
  timerFunc = 関数 () {
    タイムアウトを設定します(コールバックをフラッシュします、0);
  };
}

timerFunc 関数は、マイクロタスクの段階的な低下です。環境のサポート レベルに応じて、Promise、MutationObserver、setImmediate、setTimeout を順番に呼び出します。そして、対応するマイクロタスクまたはシミュレートされたマイクロタスク キューでコールバック関数を実行します。

関数flushSchedulerQueue() {
  現在のフラッシュタイムスタンプ = getNow();
  フラッシュ = true;
  var ウォッチャー、id;

  // フラッシュ前にキューをソートします。
  // これにより次のことが保証されます:
  // 1. コンポーネントは親から子へと更新されます。(親は常に
  // 子より先に作成される)
  // 2. コンポーネントのユーザーウォッチャーは、レンダリングウォッチャーの前に実行されます(
  // ユーザーウォッチャーはレンダリングウォッチャーの前に作成されます)
  // 3. 親コンポーネントのウォッチャー実行中にコンポーネントが破棄された場合、
  // ウォッチャーはスキップできます。
  キューのソート(関数(a, b) { return a.id - b.id; });

  // より多くのウォッチャーがプッシュされる可能性があるため、長さをキャッシュしない
  // 既存のウォッチャーを実行すると
  (インデックス = 0; インデックス < キューの長さ; インデックス++) {
    ウォッチャー = キュー[インデックス];
    if (watcher.before) {
      ウォッチャーの前に();
    }
    id = ウォッチャーid;
    has[id] = null;
    ウォッチャーを実行します。
    // 開発ビルドでは、循環更新をチェックして停止します。
    (has[id] != null)の場合{
      円形[id] = (円形[id] || 0) + 1;
      (円形[id] > MAX_UPDATE_COUNT)の場合{
        警告(
          '無限更新ループが発生している可能性があります' + (
            ウォッチャーユーザー
              ? ("ウォッチャー内の式 \"" + (watcher.expression) + "\"")
              : 「コンポーネント レンダリング関数内」
          )、
          ウォッチャー.vm
        );
        壊す
      }
    }
  }

  // 状態をリセットする前に投稿キューのコピーを保持します
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  スケジューラ状態をリセットします。

  // コンポーネントの更新とアクティブ化のフックを呼び出す
  アクティブ化されたキューを呼び出します。
  更新されたキューを呼び出します。

  // 開発ツールフック
  /* イスタンブールは無視します */
  devtools の場合:
    devtools.emit('フラッシュ');
  }
}

コールバック関数のコアロジックは、watcher.run 関数を実行することです。

Watcher.prototype.run = 関数run() {
  アクティブの場合
    var 値 = this.get();
    もし (
      値 !== this.value ||
      // ディープウォッチャーとオブジェクト/配列のウォッチャーは、
      // 値が同じ場合、値は
      // 変異しました。
      isObject(値) ||
      これ.深い
    ){
      // 新しい値を設定
      var oldValue = this.value;
      this.value = 値;
      if (this.user) {
        試す {
          this.cb.call(this.vm、値、古い値);
        } キャッチ (e) {
          handleError(e, this.vm, ("ウォッチャーのコールバック \"" + (this.expression) + "\""));
        }
      } それ以外 {
        this.cb.call(this.vm、値、古い値);
      }
    }
  }
}

ウォッチャーのコールバック関数である this.cb 関数を実行します。この時点で、すべてのロジックが完了します。

要約する

もう一度ビジネスシナリオに戻りましょう。

<テンプレート>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}}
  </div>  
</テンプレート>
<script type="javascript">
エクスポートデフォルト{
  データ() {
    戻る {
      : 0,
      0 です
    }
  },
  作成された() {
    // ロジックコード
    これ.a = 1
    これ.b = 2
  }
}
</スクリプト>

セッターは 2 回トリガーされましたが、対応するレンダリング関数はマイクロタスク内で 1 回しか実行されませんでした。つまり、dep.notify 関数が通知を送信した後、Vue は対応するウォッチャーを重複排除してキューに入れ、最後にコールバックを実行します。

2 つの割り当て操作によって、実際には同じレンダリング関数がトリガーされ、複数の DOM が更新されることがわかります。これは DOM のバッチ更新と呼ばれます。

Vue の DOM バッチ更新の実装手順についてはこれで終わりです。Vue の DOM バッチ更新に関するより詳しい内容については、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • VUE は DOM を非同期的に更新します - $nextTick を使用して DOM ビューの問題を解決します
  • Vueの仮想DOMがリアルタイムで更新できない問題を解決する
  • Vue.js ソースコードからの非同期 DOM 更新戦略と nextTick の詳細な説明

<<:  mysql 8.0.16 winx64.zip インストールと設定方法のグラフィックチュートリアル

>>:  nginx 503 サービスが一時的に利用できない問題を解決する方法

推薦する

JavaScript+html はフロントエンドページでランダム QR コード検証を実装します

クールなフロントエンドページのランダムQRコード検証を参考までに共有します。具体的な内容は次のとおり...

Vue3カプセル化メッセージメッセージプロンプトインスタンス関数の詳細な説明

目次Vue3 カプセル化メッセージプロンプトインスタンス関数スタイルレイアウトカプセル化メッセージ....

vue-cli で stimulsoft.reports.js を使用する詳細なチュートリアル

vue-cli は stimulsoft.reports.js を使用します (ナニーレベルのチュー...

CSSはボックスコンテナ(div)の高さを常に100%に設定します。

序文ブラウザをどのようにズームしても、ボックス コンテナーの高さを常に 100% に保つ必要がある場...

MySQL への接続時に発生する 1449 および 1045 例外の解決方法

MySQL への接続時に発生する 1449 および 1045 例外の解決方法 mysql 1449:...

VueはOSSを使用して画像や添付ファイルをアップロードします

OSS を使用して Vue プロジェクトに画像や添付ファイルをアップロードするここでは、写真のアップ...

jQueryで大画面スクロール再生効果を実現

この記事では、大画面スクロール効果を実現するためのjQueryの具体的なコードを参考までに紹介します...

Docker の NFS-Ganesha イメージを使用して NFS サーバーを構築する詳細なプロセス

目次1. NFS-Ganeshaの紹介2. NFS-Ganeshaの設定3. NFS-Ganesha...

ラベルタグの使用時に発生する問題の分析と解決策

最近何かをするときにラベル タグを使用しました。以前はラベル タグをほとんど使用していなかったため、...

Vueユーザーが長時間操作せずにログインページからログアウトするように実装する2つの方法

目次問題の説明フロントエンド制御(方法1)アイデアコードバックエンド制御(方法2)アイデアコード要約...

JavaScript WeakMap の使い方の詳しい説明

WeakMap オブジェクトは、キーが弱参照であるキー/値のペアのコレクションです。キーはオブジェク...

Vue の nextTick について話す

データが変更されても、DOM ビューはすぐには更新されません。変更直後にノードまたはその値を取得しよ...

Vueシングルページアプリケーションの事前レンダリング方法の例

目次序文vue-cli 2.0 バージョンvue-cli 3.0 バージョン要約する序文vue-cl...

レスポンシブ Web デザインが価値のない 5 つの理由

この記事は Tom Ewer の Managewp ブログからのもので、現在人気のレスポンシブ デザ...

Ubuntu の起動後にアプリケーションを実行するためのターミナルの設定方法

1.メニューバーにスタートと入力し、スタートアップアプリケーションをクリックして入力します。 2. ...