解析Vue.nextTick

作者: yoona幻尘 | 来源:发表于2019-12-14 12:22 被阅读0次

    不知啥时候起,对nextTick的理解出现了偏差,感觉变成了setTimeout一样的延时操作了,好像就一定会在代码执行完成之后执行。有多少人和我一样,当需要等到dom渲染完成之后进行js操作时,就想着使用nextTick。殊不知,离真相越来越远,现在,让我们来梳理一下它的真正用法。

    image.png
    先来看下官网的介绍:在下次 DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。敲黑板划重点,DOM更新之后执行延迟回调,它确实是延迟回调,但是是涉及到DOM更新之后才会执行的。
    我们先来欣赏一下nextTick的源码。
    /**
     * Defer a task to execute it asynchronously.
     * 异步更新队列
     */
    var nextTick = (function() {
        var callbacks = [];
        var pending = false; // 标记位,是否在执行回调函数
        var timerFunc; // 延时操作
    
        function nextTickHandler() {
            pending = false;  
            var copies = callbacks.slice(0);
            callbacks.length = 0;
            for (var i = 0; i < copies.length; i++) {
                copies[i]();
            }
        }
        // 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。
        // 如果同一个 watcher 被多次触发,只会被推入到队列中一次。
        // 这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。
        // 然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
        // Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
         if (typeof Promise !== 'undefined' && isNative(Promise)) {
          var p = Promise.resolve()
          var logError = err => { console.error(err) }
          timerFunc = () => {
          p.then(nextTickHandler).catch(logError)
          if (isIOS) setTimeout(noop)
        }
      } else if (!isIE && typeof MutationObserver !== 'undefined' && (
        isNative(MutationObserver) ||
        // PhantomJS and iOS 7.x
        MutationObserver.toString() === '[object MutationObserverConstructor]'
      )) {
        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, ctx) {
            var _resolve;
            callbacks.push(function() {
                if (cb) {
                    try {
                        cb.call(ctx);
                    } catch (e) {
                        handleError(e, ctx, 'nextTick');
                    }
                } else if (_resolve) {
                    _resolve(ctx);
                }
            });
            if (!pending) {
                pending = true;
                timerFunc();
            }
        }
    })();
    
    Vue.prototype.$nextTick = function(fn) {
        return nextTick(fn, this)
    };
    

    从源码我们能得到下面这张图,主要的关键就在于timeFunc方法中。

    image.png
    timeFunc()一共有三种实现方式:
    • promise
    • MutationObserver
    • setTimeout

    promisesetTimeout很好理解,都是异步任务,在同步任务执行完以及更新DOM的异步任务之后再执行。MutationObserver则是h5的新标准,用来监视DOM变动的接口,它能监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等等。

    理解了源码,也就理解了官网的描述,我们来分析一下nextTick的使用场景。

    场景一:
    生命周期created里要进行dom操作时需使用nextTick。原因是createddom还未渲染

    场景二:
    js更改了视图后,想基于新的视图进行操作时,需要将操作代码放在nextTick

    场景三:
    使用其他插件时,希望在dom动态改变后重新应用该插件,也需要在nextTick中重新应用该插件。如使用better-scroll组件,组件内部数据刷新了,高度变化了,这时我们需要在nextTick中主动调用它的refresh方法以重新计算高度并流畅滚动。

    。。。

    那么是不是nextTick就是万能的呢?它一定会在dom执行完成之后执行?也不一定。

    比如下面这个场景:

    当存在折叠面板时,我们经常会使用watch去监听绑定的值,然后希望绑定的值改变时,动态去改变scroll的高度,这时候我们想用nextTick去实现在dom更新完成之后触发refresh,你会发现,不行。。。why ??? 黑人问号脸。。。

    watch:{
        activeName(newVal, oldVal){
          console.log(document.getElementById('scroll').scrollHeight, '---')
          this.$nextTick(() => {
            console.log(document.getElementById('scroll').scrollHeight, '---')
            console.log('nextTick', new Date().getTime())
            this.$refs.scroll.refresh();
            console.log(document.getElementById('scroll').scrollHeight, '===')
            console.log('nextTick之后', new Date().getTime())
        })
        }
      },
    

    我们从打印的结果会发现,nextTick里的scrollHeight高度并不为真实高度。那么这是为什么呢?是nextTick的问题,还是watch的问题?我比较了一下,既不是watch的问题,也不是nextTick的问题,而是我们watch的对象并不是真实dom所关联的数据,它改变了,还要驱动着其他真正控制着dom改变的数据,这时候,nextTick就失效了。如果我们监听的是真正控制dom的对象,我们会发现我们在nextTick中得到的是准确的dom渲染完成后的。那这时,要解决这种监听并不直接操作domdata时,我们只能通过 setTimeout来实现dom渲染完成后的操作了,并且dom渲染也需要一定的时间。

    还有些关于nextTick在父子组件间的更新机制,推荐看另外一篇文章,写的挺好。https://www.jianshu.com/p/cd299e4d0221

    相关文章

      网友评论

        本文标题:解析Vue.nextTick

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