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

  • 相关阅读:
    AUDIT审计的一些使用
    HOW TO PERFORM BLOCK MEDIA RECOVERY (BMR) WHEN BACKUPS ARE NOT TAKEN BY RMAN. (Doc ID 342972.1)
    使用BBED理解和修改Oracle数据块
    Using Class of Secure Transport (COST) to Restrict Instance Registration in Oracle RAC [ID 1340831.1]
    调试利器GDB概念
    第4章 思科IOS
    第3章 ip地址和子网划分
    第2章 TCPIP
    2020年阅读过的黑客资源推荐篇
    第1章 计算机网络
  • 原文地址:https://www.cnblogs.com/datiangou/p/10136690.html
Copyright © 2011-2022 走看看