Vue.js非同期アップデートおよびnextTick

24575 ワード

前に書く
この間、プロジェクトを書いている時にnextTickの使用に疑問がありました.各種資料を調べた後、ここでVue.jsの非同期更新の策略とnextTickの用途と原理をまとめます.総括ミスがあれば、指摘してください.
本稿では以下の3点からまとめます.
  • どうしてVue.jsは非同期でビューを更新しますか?
  • JavaScript非同期運転の仕組みはどうなっていますか?
  • はどんな場合にnextTickを使いますか?
  • まず例を見ます
     <template>
      <div>
        <div ref="message">{{message}}div>
        <button @click="handleClick">  button>
      div>
    template>
    
     export default {
        data () {
            return {
                message: 'begin'
            };
        },
        methods () {
            handleClick () {
                this.message = 'end';
                console.log(this.$refs.message.innerText); //  “begin”
            }
        }
    }
    
    印刷された結果は「begin」です.私たちはクリックイベントでメッセージを「end」として付与していますが、実際のDOMノードのinnerHTMLを取得したが、予期していた「begin」が得られなかったのはなぜですか?
    もう一つの例を見ます
     <template>
      <div>
        <div>{{number}}div>
        <div @click="handleClick">clickdiv>
      div>
    template>
    
     export default {
        data () {
            return {
                number: 0
            };
        },
        methods: {
            handleClick () {
                for(let i = 0; i < 10000; i++) {
                    this.number++;
                }
            }
        }
    }
    
    clickイベントをクリックした後、numberは10000回遍歴されます.Vue.js応答式システムでは、前の文章Vue.jsの応答式システムの原理を見てもいいです.私たちはVue.jsが「setter-」Dep-「Watch-」patch-「ビュー」のいくつかの流れを経験することを知っています.
    以前の理解によると、numberが+1されるたびに、numberのsetterをトリガします.上の流れに従って最後に真実のDOMを修正して、DOMが10000回更新されました.考えてみても刺激的です.公式サイトの説明を見てください.Vueは非同期でDOM更新を実行します.データの変化を観察すると、Vueはキューを開けて、同じイベントサイクルで発生するすべてのデータの変化をバッファリングします.同じウォッチが何度か触発されると、列の中に押し込まれるだけです.このようなバッファリング時の重複データの除去は,不要な計算とDOM動作を避ける上で重要であることは明白である.
    JavaScriptの運行メカニズム
    Vue.jsの非同期更新策とnextTickを理解するために、まず以下のJSの運行メカニズムを紹介して、阮一峰先生のJavaScript運行メカニズムを参考にして詳しく説明します.摘出のキーポイントは以下の通りです.JSはシングルスレッドで、同じ時間に一つしかできないという意味です.イベントポーリングに基づいていますが、具体的には以下のステップに分けられます.
    (1)すべての同期タスクはメインスレッドで実行され、実行スタック(execution context stack)を形成する.
    (2)メインライン以外に、「タスクキュー」が存在します.非同期タスクに実行結果がある限り、「タスクキュー」にイベントを配置します.
    (3)一旦「実行スタック」の中のすべての同期タスクが実行されると、システムは「タスクキュー」を読み込み、中にどのようなイベントがあるかを確認します.これらの対応する非同期タスクは、待ち状態を終了し、実行スタックに入り、実行を開始する.
    (4)メインスレッドは上記の第3ステップを繰り返します.
    上の図はメインスレッドとジョブキューの概略図です.メインラインが空になったら、「タスクキュー」を読みに行きます.これはJavaScriptの運行機構です.この過程は繰り返します.メインスレッドの実行プロセスは、tickである.非同期の結果はすべて「タスクキュー」によってスケジュールされます.タスクのキューには主に2つの種類があります.「macrotask」と「microtask」の2つのタイプのtaskがタスクの列に入ります.よくあるmacrotaskはset Timeout、Message Chanel、postMessage、set Immediteがあります.よくあるmicrotaskはMuttionObseverとPromises.thenがあります.
    イベントポーリング
    Vue.jsはデータを修正する際、すぐにデータを修正するのではなく、イベントポーリングと同じデータを更新した後、統一して表示更新する.周知の例:
     //    
    vm.message = 'changed'
    
    //          DOM。    ,    message DOM     
    console.log(vm.$el.textContent) //      'changed'
    
    //    ,nextTick       DOM     
    Vue.nextTick(function(){
        console.log(vm.$el.textContent) //    'changed'
    })
    
    図解:
    アナログnextTick
    nextTickの公式サイトの定義:
    次回のDOM更新サイクルが終了したら、遅延コールを行います.データを修正した直後にこの方法を使用して、更新後のDOMを取得します.
    次のsetTimeoutでnextTickをシミュレートします.まずはcalbacksを定義してnextTickを格納します.次のtick処理コールバック関数の前に、すべてのcbがこのcalbacks配列に格納されます.pendingはラベルの位置で、待つ状態を表しています.次にsetTimeoutはtaskで一つのイベントを作成します.flush Callbacksは実行時にcalbacksの中の全部のcbを順次実行します.
    //   nextTick
    let callbacks = [];
    let pending = false;
    
    function nextTick (cb) {
        callbacks.push(cb);
    
        if (!pending) {
            //           
            pending = true;
            setTimeout(flushCallbacks, 0);
        }
    }
    
    function flushCallbacks () {
        pending = false;
        const copies = callbacks.slice(0);
        callbacks.length = 0;
        for (let i = 0; i < copies.length; i++) {
            copies[i]();
        }
    }
    
    実際のコードはここより複雑で、Vue.jsのソースコードの中で、nextTickは単独のファイルでメンテナンスします.src/core/util/next-tick.jsで:
    /* @flow */
    /* globals MessageChannel */
    
    import { noop } from 'shared/util'
    import { handleError } from './error'
    import { isIOS, isNative } from './env'
    
    const callbacks = []
    let pending = false
    
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    
    // Here we have async deferring wrappers using both microtasks and (macro) tasks.
    // In < 2.4 we used microtasks everywhere, but there are some scenarios where
    // microtasks have too high a priority and fire in between supposedly
    // sequential events (e.g. #4521, #6690) or even between bubbling of the same
    // event (#6566). However, using (macro) tasks everywhere also has subtle problems
    // when state is changed right before repaint (e.g. #6813, out-in transitions).
    // Here we use microtask by default, but expose a way to force (macro) task when
    // needed (e.g. in event handlers attached by v-on).
    let microTimerFunc
    let macroTimerFunc
    let useMacroTask = false
    
    // Determine (macro) task defer implementation.
    // Technically setImmediate should be the ideal choice, but it's only available
    // in IE. The only polyfill that consistently queues the callback after all DOM
    // events triggered in the same loop is by using MessageChannel.
    /* istanbul ignore if */
    if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      macroTimerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else if (typeof MessageChannel !== 'undefined' && (
      isNative(MessageChannel) ||
      // PhantomJS
      MessageChannel.toString() === '[object MessageChannelConstructor]'
    )) {
      const channel = new MessageChannel()
      const port = channel.port2
      channel.port1.onmessage = flushCallbacks
      macroTimerFunc = () => {
        port.postMessage(1)
      }
    } else {
      /* istanbul ignore next */
      macroTimerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
    // Determine microtask defer implementation.
    /* istanbul ignore next, $flow-disable-line */
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      microTimerFunc = () => {
        p.then(flushCallbacks)
        // in problematic UIWebViews, Promise.then doesn't completely break, but
        // it can get stuck in a weird state where callbacks are pushed into the
        // microtask queue but the queue isn't being flushed, until the browser
        // needs to do some other work, e.g. handle a timer. Therefore we can
        // "force" the microtask queue to be flushed by adding an empty timer.
        if (isIOS) setTimeout(noop)
      }
    } else {
      // fallback to macro
      microTimerFunc = macroTimerFunc
    }
    
    /**
     * Wrap a function so that if any code inside triggers state change,
     * the changes are queued using a (macro) task instead of a microtask.
     */
    export function withMacroTask (fn: Function): Function {
      return fn._withTask || (fn._withTask = function () {
        useMacroTask = true
        const res = fn.apply(null, arguments)
        useMacroTask = false
        return res
      })
    }
    
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        if (useMacroTask) {
          macroTimerFunc()
        } else {
          microTimerFunc()
        }
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    
    コメントを付けた後:
     /**
     * Defer a task to execute it asynchronously.
     */
     /*
                    ,    tick   ,        ,    function
                 task  microtask     timerFunc,                    timerFunc
                          
    */
    export const nextTick = (function () {
      /*         */
      const callbacks = []
      /*     ,     timerFunc                  */
      let pending = false
      /*      ,              ,           ,      timerFunc   */
      let timerFunc
    
      /*   tick    */
      function nextTickHandler () {
        /*     ,      (                 ,               ),       push     callbacks  timerFunc             */
        pending = false
        /*    callback*/
        const copies = callbacks.slice(0)
        callbacks.length = 0
        for (let i = 0; i < copies.length; i++) {
          copies[i]()
        }
      }
    
      // the nextTick behavior leverages the microtask queue, which can be accessed
      // via either native Promise.then or MutationObserver.
      // MutationObserver has wider support, however it is seriously bugged in
      // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
      // completely stops working after triggering a few times... so, if native
      // Promise is available, we will use it:
      /* istanbul ignore if */
    
      /*
              ,   Promise、MutationObserver  setTimeout      timerFunc   
            Promise, Promise         MutationObserver,        microtask   ,  setTimeout    ,      。
                           setTimeout, task        ,      。
          :https://www.zhihu.com/question/55364497
      */
      if (typeof Promise !== 'undefined' && isNative(Promise)) {
        /*  Promise*/
        var p = Promise.resolve()
        var logError = err => { console.error(err) }
        timerFunc = () => {
          p.then(nextTickHandler).catch(logError)
          // in problematic UIWebViews, Promise.then doesn't completely break, but
          // it can get stuck in a weird state where callbacks are pushed into the
          // microtask queue but the queue isn't being flushed, until the browser
          // needs to do some other work, e.g. handle a timer. Therefore we can
          // "force" the microtask queue to be flushed by adding an empty timer.
          if (isIOS) setTimeout(noop)
        }
      } else if (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 IE11, iOS7, Android 4.4
        /*    textNode DOM  , MutationObserver   DOM       , DOM           ,         (         ), textNode.data = String(counter)       */
        var counter = 1
        var observer = new MutationObserver(nextTickHandler)
        var textNode = document.createTextNode(String(counter))
        observer.observe(textNode, {
          characterData: true
        })
        timerFunc = () => {
          counter = (counter + 1) % 2
          textNode.data = String(counter)
        }
      } else {
        // fallback to setTimeout
        /* istanbul ignore next */
        /*  setTimeout           */
        timerFunc = () => {
          setTimeout(nextTickHandler, 0)
        }
      }
    
      /*
                 tick   
        cb     
        ctx    
      */
      return function queueNextTick (cb?: Function, ctx?: Object) {
        let _resolve
        /*cb  callbacks */
        callbacks.push(() => {
          if (cb) {
            try {
              cb.call(ctx)
            } catch (e) {
              handleError(e, ctx, 'nextTick')
            }
          } else if (_resolve) {
            _resolve(ctx)
          }
        })
        if (!pending) {
          pending = true
          timerFunc()
        }
        if (!cb && typeof Promise !== 'undefined') {
          return new Promise((resolve, reject) => {
            _resolve = resolve
          })
        }
      }
    })()
    
    鍵はtimeFun()であり,この関数は遅延実行の役割を果たしている.上の紹介から、timeFun()は全部で三つの実現方式があることが分かります.
  • Promise
  • MuttionObserver
  • setTimeout
  • 用途
    nextTickの用途
    アプリケーションシーン:ビューの更新後、新しいビューに基づいて動作する必要があります.
    例を見てください.ショーボタンをクリックすると、元のv-show:falseのinput入力ボックスが表示され、フォーカスを取得します.
     <div id="app">
      <input ref="input" v-show="inputShow">
      <button @click="show">showbutton>  
     div>
    
    new Vue({
      el: "#app",
      data() {
       return {
         inputShow: false
       }
      },
      methods: {
        show() {
          this.inputShow = true
          this.$nextTick(() => {
            this.$refs.input.focus()
          })
        }
      }
    })