Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue.js 中nextTick 和setTimeout有什么区别 #14

Closed
sevenCon opened this issue Aug 27, 2019 · 0 comments
Closed

vue.js 中nextTick 和setTimeout有什么区别 #14

sevenCon opened this issue Aug 27, 2019 · 0 comments
Labels

Comments

@sevenCon
Copy link
Owner

sevenCon commented Aug 27, 2019

前言

vue.jsnextTick方法, 是非常有用而且也是经常用到的方法, 今天就来探讨一下这个方法实现的细节. 和我们用到setTimeout方法究竟有什么区别?

下面是核心的源码结构

直接奔着主题去, nextTick函数和setTimeout的区别, 主要体现在回调的执行时间.下面的timerFunc则是延迟函数, 有好几个if-else, 按照promise->mutation observer->setImmediate->setTimeout的顺序安排.

let timerFunc;

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 这直接使用原生的Promise 方法.
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // 使用MutationObserver
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // setImmediate
} else {
  // setTimeout
}

export function nextTick(cb?: Function, ctx?: Object) {
  ...
}

详细的代码请移步vuejs-next-tick,这里

正文

深入其中的if-else, 看到这几个判断, 其实我对以下几个问题, 还是挺好奇的.
带着这几个问题, 去源码或者issue中, 找答案, 加深源码理解.

  • setImmediate比如setTimeout好? 区别在哪里?
  • 按照优先使用微任务, 最后使用宏任务的回调顺序, 宏任务的使用具体会出现什么问题?
  • 原生的Promise,isNative的方法是怎么实现?
  • IE 11 也可以用MutationObserver, 为什么要把IE排除在外?
    ,MutationObserver 判断, 为什么需要加一个 MutationObserver.toString() === '[object MutationObserverConstructor]'

带着以上的疑问去源码项目中找答案.

第一个问题: setImmediate比如setTimeout好在哪里?

首先setImmediate的问题, setImmediate可以直接运行, setTimeout一个不好的地方就在js引擎需要和系统的时钟同步, 这个同步的频率在4ms, 也就是说setTimeout的回调, 至少需要4ms, 即使我们这么写setTimeout(fn,0).

但是setImmediate唯一一个不好的地方就是只有IE的浏览器支持,
image

第二个问题: 宏任务究竟有啥子不好?

vue-issue#6813, 在这问题中, 很好的揭示了nextTick使用了setTimeout的问题.
vue.js, v-2.51版本使用了setTimeout, resize下会出现repaint先比执行, 出现页面宽度小于1000px时候, 隐藏导航列表的抖动,

首先捋一下代码的逻辑和重现的步骤. 代码如下面的截图, 红色的区块是重点部分.
image

  1. 宽度小于1000px 时, menu列表项以块元素显示, 宽度大于1000px时, 以内联元素显示, 这时所有的列表项都一行显示.
  2. 监听了window的resize的方法, 屏幕宽度小于1000px时, 会设置showList:false, 隐藏列表项目显示, 但这时会触发隐藏的样式display:list-item,问题就出现在谁在这2者的竞争上.

就是出现设置this.showList=false, 上面,当缩放屏幕的宽度时,会触发2个事件, 一个是UI渲染(display:inline失效, 渲染display:list-item), 一个resize方法,this.showList=false|true,

resize方法会先比UI渲染先执行吗?为什么?

假设resize方法先执行, UI渲染后执行, 那么会有什么问题?

如果resize的方法有dom操作,需要重新UI渲染, 所以如果这一步UI渲染, 是会等待resize方法执行完之后, 执行一次UI渲染? 这样是可以节省开销,

但是,如果resize方法卡死, 或者需要长时间占据cpu呢? 那么岂不是页面resize出现卡死? 而且resize的触发, 肯定会有防抖或节流的, 不可能resize每一个1px都会触发回调, 不然对cpu的压力过大. 所以有好处, 也会出现问题, 看看chrome的是怎么执行的?

image

上面是chrome的performance性能测试, 在下面的Bottom-Up, 可以看到当前帧的具体运行顺序还有运行的时长,

  • 首先进行的是重新计算样式, 更新渲染树,
  • 再执行Event:resize的方法, 在resize方法的回调里面, 执行getComputedStyle

但是, 这个顺序并不能说明问题, 因为这不是一个公平的竞争测试, 2个对比最重要的原因还是拖动调整浏览器的可视宽度的时候, 并不是每次都会触发resize的回调. 但是每次拖动调整浏览器的可视宽度, 即使一个像素的差别,都会触发浏览器端layout, repaint重流和重绘.

但是, 如果我们在resize的代码之中添加

while(true){
    
}

就会发现, 这个时候,即使我们怎么拖动视窗调转大小,都不会渲染, 即使1px的变化, 也不会, 因为这个会浏览器主进程卡死, 后面的渲染进程一直都在队列中, 所以可以知道, 是先执行resize的回调, 再进行渲染操作, 这样可以节省渲染的开销.

继续我们原来的问题, 我们在resize的回调中, 设置了showList, 这个时候,vuejs是利用nextTick的进行模板依赖的更新合并的. 把同一个事件循环的操作进行合并到下一个nextTick中, 而不必每一个赋值都同步更新依赖. 所以才会resize的抖动的问题, 因为UI渲染已经进入宏任务队列,并且排在下一个位置, 如果在resize的回调里面又再会有宏任务, 那么就会UI渲染的下一位置, 所以才出现的抖动, 先进行了一次UI渲染, showList:true或者false再生效.

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue) // => here
    }
  }
}

第三个问题: 怎么判断一个Promise是不是原生的

/* istanbul ignore next */
export function isNative (Ctor: any): boolean {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

才留意到, 原来如果一个原生的方法, 我么打印到控制台的时候, 一般都有native code 的这样的字符串信息, 真是一个平时又容易忽略, 但是又实用的方法. 而且可用来判断所有的方法.

第四个问题: IE 的MutationObserver 有什么问题?

最后一个疑问是, IE下面的使用会有什么问题? 看到这个疑问是在在next-tick的函数注释中, 该问题略微的提了一下, 使用IE mutationObserver在双向绑定的时候, 有可能会出现多次按钮随机丢失字符的情况
,问题issue在这里IE11: Keystrokes missing if v-model is used, 具体的重现在IE11下请看这里, 有IE11环境的可以重现一下试试, 这个问题环境问题, 我并没有重现出来.

但是可以在caniuse上面, 就是关于IE11的issue
image

所以在next-tick的函数中, 就避免了使用IE的mutation Observer的方法

if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')
){ ... }

而对于在 PhantomJS and iOS 7.x, MutationObserver的判断, 需要用MutationObserver.toString() === '[object MutationObserverConstructor'.

小结

看源码和社区博客, github的issue, 各种各样的人发表对问题的深刻见解, 各种思维的碰撞, 真的是一种快捷的进步方式. 所以呼吁更多的人, 如果碰到开源项目的问题,不妨到issue和大家一起讨论, 拥抱开源.

参考

IE11: Keystrokes missing if v-model is used

v-show is firing late on 2.5.1, nextTick callback trigger is to late

本文的github地址vue.js 中nextTick 和setTimeout有什么区别, 如果有侵权或其他问题,请issue留言, 感谢!

@sevenCon sevenCon added the vue label Aug 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant