UzumakiItachi
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

UzumakiItachi

起风了,唯有努力生存。
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 手写一个简易的vue
  • vue源码学习--手写vue-router
  • 深入keep-alive
  • new vue的时候做了什么?
  • vue模板编译原理
  • 深入nextTick
    • 前言
    • 分析
  • 深入vue2 diff算法
  • 《Vue2》
hanhanbuku
2023-04-03
目录

深入nextTick

# 前言

vue的异步更新策略是性能优化中一个重要的一环,也是vue自身很巧妙的一个设计。当我们在修改data中的属性的时候,vue并不是立即去更新视图的,而是采用异步更新策略,所以我们在修改data之后是不能立即获取到最新的dom的。vue这么做是为了防止频繁变更数据导致频繁更新视图从而出现的性能问题,所以即使在同一个事件循环中更新了一个data一千次,vue也只会执行一次渲染视图。有些时候可能需要在data更新之后就立马获取dom,这个时候就可以使用vue官方提供的$nextTick方法,下面就一起来深入了解一下这个api吧

在阅读此文之间请先了解js事件循环

nextTick的源码并不长,主要就是利用js的事件循环来做的一个缓冲的效果

/* @flow */
/* globals MutationObserver */

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

export let isUsingMicroTask = false

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]()
  }
}

// 在2.5版本中组合使用microtasks 和macrotasks,但是重绘的时候还是存在一些小问题,而且使用macrotasks在任务队列中会有几个特别奇怪的行为没办法避免,So又回到了之前的状态,在任何地方优先使用microtasks 。
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// 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 next, $flow-disable-line */


// task的执行优先级
// Promise -> MutationObserver -> setImmediate -> setTimeout

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    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)
  }
  isUsingMicroTask = true
} 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
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

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
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

# 分析

其实nextTick内部的逻辑非常简单,除去一些宏任务微任务的降级之外几乎就一二十行代码。他内部主要分为以下几个成员

  • callback:callback是一个保存任务的队列,我们在调用nextTick时传入的回调函数就会被推入这个队列
  • flushCallbacks:调用所有callback的函数,我们存入的callback最终会有这个函数遍历逐一执行
  • timerFunc:经过nextTick内部包装后的一个函数,他本质上是一个微任务或者宏任务(根据浏览器兼容性来降级),由他来触发flushCallbacks
  • pending:状态机,控制是否执行当前的callback

当我们调用一个nextTick的时候大概会发生这些事情:

  1. 首先timerFunc会被初始化成一个最适合当前浏览器的任务(nextTick内部总共采用了四种任务,分别是微任务Promise.then,MutationObserver 宏任务setImmediate,setTimeout。)
  2. 进入nextTick,首先将传入的cb添加进callback数组
  3. 判断当前状态pending是否为false,如果是则去修改状态为true并且调用timerFunc,也就是开始执行我们传入的所有回调
  4. 由于浏览器的js引擎线程和GUI渲染进程互斥的关系,当GUI渲染线程在更新的时候是不会去执行js代码的,所以如果我们的回调总是会被控制在dom渲染结束之后去执行。这也就是nextTick的绝妙之处。
  5. flushCallbacks内部会将pending重置为false,使得我们的下一次循环得以继续执行,并且他会拷贝一份callbacks,然后清空他。这样可以保证下一次循环期间内保存的事件不会被上一次循环调用,也就是每一次事件循环都只调用本次循环内所产生的callback。
编辑 (opens new window)
上次更新: 2023/04/07, 16:21:51
vue模板编译原理
深入vue2 diff算法

← vue模板编译原理 深入vue2 diff算法→

最近更新
01
小程序实现全局状态管理
07-09
02
前端检测更新,自动刷新网页
06-09
03
swiper渲染大量数据的优化方案
06-06
更多文章>
Theme by Vdoing | Copyright © 2023-2025 UzumakiItachi | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式