美文网首页程序员让前端飞Vue.js
从源码里解析vue中的nextTick的用法

从源码里解析vue中的nextTick的用法

作者: 88b61f4ab233 | 来源:发表于2018-12-10 22:12 被阅读4次

    今天做了一个需求,场景是这样的:

    在页面拉取一个接口,这个接口返回一些数据,这些数据是这个页面的一个浮层组件要依赖的,然后我在接口一返回数据就展示了这个浮层组件,展示的同时,上报一些数据给后台(这些数据就是父组件从接口拿的),这个时候,神奇的事情发生了,虽然我拿到数据了,但是浮层展现的时候,这些数据还未更新到组件上去。

    父组件:

    <template>
      .....
      <pop ref="pop" :name="name"/>
    </template>
    <script>
    export default {
      .....
      created() {
        ....
        // 请求数据,并从接口获取数据
        Data.get({
          url: xxxx,
          success: (data) => {
            // 问题出现在这里,我们赋值以后直接调用show方法,去展现,show方法调用的同时上报数据,而上报的数据这个时候还未更新到子组件
            this.name = data.name
            this.$refs.pop.show()
          }
        })
      }
    }
    </script>
    

    子组件

    <template>
      <div v-show="isShow">
        ......
      </div>
    </template>
    <script>
    export default {
      .....
      props: ['name'],
      methods: {
        show() {
          this.isShow = true
          // 上报
          Report('xxx', {name: this.name})
        }
      }
    }
    </script>
    

    问题分析:

    原因vue官网上有解析

    可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

    这句话就是说,当我们在父组件设置this.name=name的时候,vue并不会直接更新到子组件中(dom的更新也一样未立即执行),而是把这些更新操作全部放入到一个队列当中,同个组件的所有这些赋值操作,都作为一个watcher的更新操作放入这个队列当中,然后等到事件循环结束的时候,一次性从这个队列当中获取所有的wathcer执行更新操作。在我们这个例子当中,就是我们在调用show的时候,实际上,我们的this.name=name并未真正执行,而是被放入队列中。vue的这种做法是基于优化而做的,毋庸置疑,不然我们如果有n多个赋值vue就执行n多个dom更新,那效率将会非常的低效和不可取的。



    下文中的更新操作指对data的值进行更新的操作,在vue中,都会被放入队列异步执行。

    解决方案:

    1、 使用nextTick来延迟执行show方法(笼统得说,执行所有需要在数据真正更新后的操作

    通过上面的分析我们知道,我们的所有的对vue实例的更新操作,都会先被放入一个队列当中,延迟异步执行,这些异步操作,要么是microtask,要么是macrotask(是microtask还是macroktask取决于环境,nextTick的源码中有所体现),根据事件循环机制,先入队列的先执行,所以如果我们在nextTick当中执行操作就会变成这样。



    2、 使用setTimeout来延迟执行show方法,原理同上

    所以我们的解决方法可以是:

    this.name = data.name
    setTimeout(() => {
     this.$refs.pop.show()
    })
    

    或者

    this.name = data.name
    this.$nextTick(() => {
     this.$refs.pop.show()
    })
    

    前端全栈学习交流圈:866109386,面向1-3经验年前端开发人员,帮助突破技术瓶颈,提升思维能力,群内有大量PDF可供自取,更有干货实战项目视频进群免费领取。

    nextTick的实现原理

    其实nextTick的实现原理是挺简单的,简单点说,就是实现异步,通过不同的执行环境,用不同的方式来实现,保证nextTick里面的回调函数能够异步执行。为什么要这么做呢?因为vue对dom的更新也是异步的呀。

    下面贴出源码:

    /**
     * Defer a task to execute it asynchronously.
     */
    export const nextTick = (function () {
     const callbacks = []
     let pending = false
     let timerFunc
     
     function nextTickHandler () {
      pending = false
      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 */
     if (typeof Promise !== 'undefined' && isNative(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 (!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
      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 */
      timerFunc = () => {
       setTimeout(nextTickHandler, 0)
      }
     }
     
     return function queueNextTick (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
       timerFunc()
      }
      if (!cb && typeof Promise !== 'undefined') {
       return new Promise((resolve, reject) => {
        _resolve = resolve
       })
      }
     }
    })()
    

    前端全栈学习交流圈:866109386,面向1-3经验年前端开发人员,帮助突破技术瓶颈,提升思维能力,群内有大量PDF可供自取,更有干货实战项目视频进群免费领取。

    首先我们看到这个是利用了闭包的特性,返回queueNextTick,所以我们实际调用的nextTick其实就是调用queueNextTick,一调用这个方法,就会把nextTick的回调放入队列callbacks当中,等到合适的时机,会将callbacks中的所有回调取出来执行,以达到延迟执行的目的。为啥要用闭包呢,我觉得有两个原因:

    1、共享变量,比如callbacks、pending和timerFunc。

    2、避免反复判断,即是避免反复判断timerFunc是利用Promise还是利用MutationObserver或是setTimeout来实现异步,这是函数柯里化的一种运用。

    这里有两个最主要的方法需要解释下:

    1、 nextTickHandler
    这个函数,就是把队列中的回调,全部取出来执行,类似于microtask的任务队列。我们通过调用Vue.$nextTick就会把回调全部放入这个队列当中,等到要执行的时候,调用nextTickHandler全部取出来执行。

    2、 timerFunc
    这个变量,它的作用就是通过Promise/Mutationobserver/Settimeout把nextTickHandler放入到真正的任务队列当中,等到事件循环结束,就从任务队列当中取出nextTickHandler来执行,nextTickHandler一执行,callbacks里面的所有回调就会被取出来执行来,这样就达到来延迟执行nextTick传的回调的效果。

    通过这个简单的源码分析,我们可以得出两个结论

    1、nextTick会根据不同的执行环境,异步任务可能为microtask或者macrotask,而不是固定不变的。所以,如果你想让nextTick里面的异步任务统统看成是microtask的话,你会遇到坑的。

    2、nextTick的并不能保证一定能获取得到更新后的dom,这取决于你是先进行数据赋值还是先调用nextTick。比如:

    new Vue({
       el: '#app',
       data() {
        return {
         id: 2
        }
       },
       created() {
         
       },
       mounted() {
        this.$nextTick(() => {
         console.log(document.getElementById('id').textContent) // 这里打印出来的是2,因为先调用了nextTick
        })
        this.id = 3
       }
     })
    

    前端全栈学习交流圈:866109386,面向1-3经验年前端开发人员,帮助突破技术瓶颈,提升思维能力,群内有大量PDF可供自取,更有干货实战项目视频进群免费领取。

    结论

    如果想要获取更新后的DOM或者子组件(依赖父组件的传值),可以在更新操作之后立即使用Vue.nextTick(callback),注意这里的先后顺序,先进行更新操作,再调用nextTick获取更新后的DOM/子组件,源码里面我们知道nextTick是无法保证一定是能够获取得到更新后的DOM/子组件的

    以上所述是小编给大家介绍的vue中的nextTick的使用,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

    相关文章

      网友评论

        本文标题:从源码里解析vue中的nextTick的用法

        本文链接:https://www.haomeiwen.com/subject/cmcjhqtx.html