网站首页 » 前端开发 » Vue » Vue $nextTick 解密
上一篇:
下一篇:

Vue $nextTick 解密

前言

Vue $nextTick 的用法在网上可以搜索出一大堆,基本都是大同小异,把官方的话搬过来改改,或者把 Vue $nextTick 的源码实现分析一遍,然后,然后你就很容易被带走了,在不归路上起走起远。在这里没有针对谁,也不是说分析源码没有,或者没必要。我觉得源码分析分析到了点上就 OK,点到为止,不然很容易让初学者找不到北,越想越多,还没开始学他就已经想跑路了。

关于 $nextTick 比如官方这么一句话:“将回调延迟到下次 DOM 更新循环之后执行”,然后网上很多文章就直接照抄过,你确定这样别人真的能看懂吗?请问:什么时候才算是下一次DOM 更新?我想很多人的疑问应该大都跟我一样,而不是想看到一句原汁原味的官方话术,所以当你在网上搜索 $nextTick 的时候你会浪费很多时间在这些样板式文章上,我就是其中一个受害者,但也不能说网上关于 $nextTick 的好文章没有。

好了不吐苦水,自学的路,谁不会被误导几次,错了,应该说被误导的事常常发生,所以在这里有一点想跟有缘分的朋友说:我们得在自觉的过程中,学习总结,收集,判断。

比如,搜索结果:

如果是脚本之家你就不用点了,你懂的。

如果是 csdn 或者博客园你可以点进去,但最好不要抱有太多的期待,并且阅读时间建议不要超过两分钟,大概地过和遍就好,毕竟属于它们的时代已经过去了。

如果是知乎或者简书你可以有些许期待,有时候还是可以找到你想要的,不过我个人觉得简书中关于技术类的文章基本都是转载。

如果是掘金那你可以大胆的尝试点进去,副作用不会很多。

如果是 SegmentFault 你可以放心点进去。

如果是 Stack Overflow 你可以二话不说,直接点,前提是你得看得懂英文(永远记住这是优先答复)。

不说了不说了,估计此文一发布肯定会得罪了以上不少嘉宾以及它们的忠实粉丝。

言归正传!

要想知道 $nextTick 到底是个啥,你们就需要对事件循环有一个很清楚的认识,话间刚落,天边飘来一篇美文《JavaScript 事件循环运行机制解密》,花不了你几分钟,耐心地读完它,然后……你可能还是一脸蒙圈的样子,不过客官别急,上第二道菜 $nextTick 源码过家家。

node_modules\vue\src\core\util\next-tick.js
/* @flow */
/* globals MessageChannel */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

const callbacks = []
let pending = false

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

// Here we have async deferring wrappers using both micro and macro tasks.
// In < 2.4 we used micro tasks everywhere, but there are some scenarios where
// micro tasks have too high a priority and fires in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using macro tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use micro task by default, but expose a way to force macro task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) Task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine MicroTask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    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
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a Task instead of a MicroTask.
 */
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

export function nextTick (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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

不用怕,代码有点多,但我们只抓重点。欣赏过前面那篇美文你应该可以从 $nextTick 源码中认出几个关键字:setImmediate、setTimeout、Promise,以及注解中的 “micro and macro tasks”,现在我们在脑中再回味一下那篇美文,是不是有点感觉了?没错,$nextTick 就是通过事件循环来实现的。这里简单说下 $nextTick 源码到底做了什么:属性支持判断,也就是代码中的if、else if、else 以及 if。支持那一种就用哪一种。但可以确定的是肯定会是其中的一种。是 setImmediate、是 setTimeout 还是 Promise 那就只能看环境支持了,说白了这个 $nextTick 就只做了一件实实在在的事,那就是把你传给它的回调函数放到了队列最后。至于它会放到 micro tasks 队列还是 macro tasks 队列那就看运气了(环境支持)。

那为什么把 $nextTick() 中的回调(你要执行的函数,比如:获取某个 DOM 元素)放到队列后就可以实现官方所说的“将回调延迟到下次 DOM 更新循环之后执行”?

对了,忘记告诉你一个天知地知我知,唯你不知的秘密了:每次执行完 macro tasks  队列中的一个任务后都会更新 DOM,别问我为什么,王菲有首歌叫《约定》,约定,约定,约定兄弟,这个我也没法子。家有家规,国有国法,我们得按套路苟且。好了,到这里,就到这里吧!该明白的已经明白了,不明白的也就只能自己看着办了……,但是出于对他人,自己负责任的态度,我决定还是把事件说个一清二楚。我们把整个事情梳理一遍:

1、你要对事件循环有深入的理解,micro tasks 、macro tasks,以及它们都包含了那些事件源,所谓事件源就是 setTimeout 、setImmediate、Promise…..,这些。

2、事件循环的过程就是先从 macro tasks 拿出一个任务,然后再把 micro tasks 中的所有任务依次执行完,完成第一次事件执行,后面的第二次,第n次,只是重复第一次的过程。

3、每次执行完 macro tasks 队列中的一个任务后都会更新 DOM ,也就是说 DOM 更新是在  macro tasks 执行完后,并且在 micro tasks 开始执行之前,而 macro tasks 和 micro tasks 正好组成一个事件循环。

所以不管我们的环境支持哪一种 setImmediate、setTimeout 、Promise 还是其它的…… ,$nextTick(callback) 都可以确保在 DOM 更新后执行回调(callback,也就是你给它传的函数)。

最理想的就是运行环境支持 micro tasks 类的任务,比如:Promise(),因为这样的话,在本轮事件就可以完成。

而如果支持环境比较糟糕,只支持 macro tasks 类的任务,比如:setTimeout(),那么就只能等到下一次事件循环时,执行 macro tasks 首个任务时执行了。

到此,本文就真的已经结束了,不管你明白不明白,它都已经尽力了。

 

 

  • 微信扫一扫,赏我

  • 支付宝扫一扫,赏我

声明

原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

本文永久链接:http://yunkus.com/vue-decryption-nexttick/

Leave a Reply

Your email address will not be published. Required fields are marked *

评论 END