zoukankan      html  css  js  c++  java
  • Vue.nextTick浅析

    Vue.nextTick浅析

    Vue的特点之一就是响应式,但数据更新时,DOM并不会立即更新。当我们有一个业务场景,需要在DOM更新之后再执行一段代码时,可以借助nextTick实现。以下是来自官方文档的介绍:

    将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

    具体的使用场景和底层代码实现在后面的段落说明和解释。

    用途

    Vue.nextTick( [callback, context] )vm.$nextTick( [callback] )

    前者是全局方法,可以显式指定执行上下文,而后者是实例方法,执行时自动绑定this到当前实例上。

    以下是一个nextTick使用例子:

    ```<div id="app"> <button @click="add">add</button> {{count}} <ul ref="ul"> <li v-for="item in list"> {{item}} </li> </ul> </div> ```
    
    new Vue({
      el: '#app',
      data: {
        count: 0,
        list: []
      },
      methods:{
        add() {
          this.count += 1
          this.list.push(1)
          let li = this.$refs.ul.querySelectorAll('li')
          li.forEach(item=&gt;{
            item.style.color = 'red';
          })
        }
      }
    })
    

    以上的代码,期望在每次新增一个列表项时都使得列表项的字体是红色的,但实际上新增的列表项字体仍是黑色的。尽管data已经更新,但新增的li元素并不立即插入到DOM中。如果希望在DOM更新后再更新样式,可以在nextTick的回调中执行更新样式的操作。

    
    new Vue({
      el: '#app',
      data: {
        count: 0,
        list: []
      },
      methods:{
        add() {
          this.count += 1
          this.list.push(1)
          this.$nextTick(()=&gt;{
              let li = this.$refs.ul.querySelectorAll('li')
              li.forEach(item=&gt;{
              item.style.color = 'red';
            })
          })
        }
      }
    })
    

    解释

    数据更新时,并不会立即更新DOM。如果在更新数据之后的代码执行另一段代码,有可能达不到预想效果。将视图更新后的操作放在nextTick的回调中执行,其底层通过微任务的方式执行回调,可以保证DOM更新后才执行代码。

    源码

    /src/core/instance/index.js,执行方法renderMixin(Vue)Vue.prototype添加了$nextTick方法。实际在Vue.prototype.$nextTick中,执行了nextTick(fn, this),这也是vm.$nextTick( [callback] )自动绑定this到执行上下文的原因。

    nextTick函数在/scr/core/util/next-tick.js声明。在next-tick.js内,使用数组callbacks保存回调函数,pending表示当前状态,使用函数flushCallbacks来执行回调队列。在该方法内,先通过slice(0)保存了回调队列的一个副本,通过设置callbacks.length = 0清空回调队列,最后使用循环执行在副本里的所有函数。

    
    const callbacks = []
    let pending = false
    
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i &lt; copies.length; i++) {
        copies[i]()
      }
    }
    

    接着定义函数marcoTimerFuncmicroTimerFunc

    先判断是否支持setImmediate,如果支持,使用setImmediate执行回调队列;如果不支持,判断是否支持MessageChannel,支持时,在port1监听message,将flushCallbacks作为回调;如果仍不支持MessageChannel,使用setTimeout(flushCallbacks, 0)执行回调队列。不管使用哪种方式,macroTimerFunc最终目的都是在一个宏任务里执行回调队列。

    
    if (typeof setImmediate !== 'undefined' &amp;&amp; isNative(setImmediate)) {
      macroTimerFunc = () =&gt; {
        setImmediate(flushCallbacks)
      }
    } else if (typeof MessageChannel !== 'undefined' &amp;&amp; (
      isNative(MessageChannel) ||
      // PhantomJS
      MessageChannel.toString() === '[object MessageChannelConstructor]'
    )) {
      const channel = new MessageChannel()
      const port = channel.port2
      channel.port1.onmessage = flushCallbacks
      macroTimerFunc = () =&gt; {
        port.postMessage(1)
      }
    } else {
      /* istanbul ignore next */
      macroTimerFunc = () =&gt; {
        setTimeout(flushCallbacks, 0)
      }
    }
    

    然后判断是否支持Promise,支持时,新建一个状态为resolvedPromise对象,并在then回调里执行回调队列,如此,便在一个微任务中执行回调,在IOS的UIWebViews组件中,尽管能创建一个微任务,但这个队列并不会执行,除非浏览器需要执行其他任务;所以使用setTimeout添加一个不执行任何操作的回调,使得微任务队列被执行。如果不支持Promise,使用降级方案,将microTimerFunc指向macroTimerFunc

    
    if (typeof Promise !== 'undefined' &amp;&amp; isNative(Promise)) {
      const p = Promise.resolve()
      microTimerFunc = () =&gt; {
        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
    }
    

    在函数nextTick内,先将函数cb使用箭头函数包装起来并添加到回调队列callbacks。接着判断当前是否正在执行回调,如果不是,将pengding设置为真。判断回调执行是宏任务还是微任务,分别通过marcoTimerFuncmicroTimerFunc来触发回调队列。最后返回一个Promise实例以支持链式调用。

    
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() =&gt; {
        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 &amp;&amp; typeof Promise !== 'undefined') {
        return new Promise(resolve =&gt; {
          _resolve = resolve
        })
      }
    }
    
    

    而全局方法Vue.nextTick/src/core/global-api/index.js中声明,是对函数nextTick的引用,所以使用时可以显示指定执行上下文。

    
     Vue.nextTick = nextTick
    

    小结

    本文关于nextTick的使用场景和源码做了简单的介绍,如果想深入了解这部分的知识,可以去了解一下微任务mircotask和宏任务marcotask

    来源:https://segmentfault.com/a/1190000016495892

  • 相关阅读:
    linux下telnet安装与使用
    用日志记录Linux用户执行的每一条命令(history)
    监视网络接口TCP状态信息数据有多种工具或命令。下面举例一些:
    [C#]Winform后台提交数据且获取远程接口返回的XML数据,转换成DataSet
    C#生成Code128码
    BULK INSERT如何将大量数据高效地导入SQL Server
    SQLite中的时间日期函数
    C# winform小票打印
    c#读写txt文件
    C#-WinForm 串口通信
  • 原文地址:https://www.cnblogs.com/datiangou/p/10136690.html
Copyright © 2011-2022 走看看