解析Vue.nextTick

不知啥时候起,对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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350