八、异步更新队列$textTick

Vue

# 功能

数据更新后,更新到DOM之后才会去执行nextTick函数

  • Vue 更新 DOM 是异步执行的,批量的
    • 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
  • vm.$nextTick(function () { /* 操作 DOM */ }) / Vue.nextTick(function () {})

# 实例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js 01 component example</title>
  </head>
  <body>
    <div id="app">
      <!-- 设置ref属性将来可以获取到p标签 -->
      <p ref="p1">{{ msg }}</p>
    </div>

    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          msg: 'Hello nextTick',
          name: 'name',
          title: 'title'
        },
        mounted() {
          // 这里修改了data中的值,当值改变的时候不会立即去更新DOM,这个更新是一个异步的过程,也就是说当这里数据变化的时候在下面立即获取标签上的内容是获取不到最新值的,如果想获得最新的值,就要调用$nextTick
          this.msg = 'Hello world'
          this.name = 'xm'
          this.title = 'subTitle'
          // 不用$nextTick,立即获取内容,获取的还是之前的DOM
          console.log(this.$refs.p1.textContent)
          // 在$nextTick函数中,视图已经更新完毕,所以可以获取视图上的最新数据
          this.$nextTick(() => {
            console.log(this.$refs.p1.textContent)
          })
        }
      })
    </script>
  </body>
</html>

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

可以看到执行之后的结果

image

# 源码解析

# 使用方式

  1. 手动使用vm.$nextTick()
  2. 在Watcher的queueWatcher中执行nextTick(整个watcher的执行过程是在nextTick中调用的)

# 定义位置

  • 静态方法
    • core/global-api/index.js
    • Vue.nextTick = nextTick
  • 实例方法
    • core/instance/render.js
    • Vue.prototype.$nextTick也调用的是nextTick

# 代码分析

所以这里跳转到nextTick函数定义的位置

// 第一个是回调函数,可选
// 第二个是上下文,就是Vue实例,可选
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // callback是数组,里面存了所有的回调函数,往数组中push了回调函数的调用
  callbacks.push(() => {
    // 如果用户传了cb回调函数,因为用户传递的要进行错误判断
    // 如果没有传递cb就判断_resolve,_resolve有值就直接调用_resolve,这个_resolve就是下面的代码中,接收promise传进来的resolve
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 判断队列是否正在被处理,如果没有被处理,就进入
  if (!pending) {
    // 设置当前队列正在被处理
    pending = true
    // 这个函数就是遍历所有的callback数组,然后执行每一个callback函数
    timerFunc()
  }
  // $flow-disable-line
  // 如果没有cb且Promise对象存在就将返回的reslove设置给_resolve
  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

下面主要看一下timerFunc里面做了什么

let timerFunc
// ios大于等于9.3.3是不会使用promise,会存在潜在的问题,所以会降级成setTimeout
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It

// 如果Promise对象存在就调用timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 定义一个promise对象,让promise对象去处理flushCallbacks,使用微任务的形式去处理,是在本次同步任务循环之后开始执行微任务
  // nextTick中优先使用微任务
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // ios大于等于9.3.3是不会使用promise,会存在潜在的问题,所以会降级成setTimeout
    if (isIOS) setTimeout(noop)
  }
  // 标记nextTick使用的是微任务
  isUsingMicroTask = true
// 判断当前不是IE浏览器并且支持MutationObserver
// MutationObserver对象的作用是监听DOM对象的改变,如果改变之后会执行一个回调函数,这个函数也是以微任务的形式执行
// MutationObserver这个对象在IE10和IE11中才支持,并在11中又不是完全支持,有些小问题
// 这里兼容的是PhantomJS, iOS7, Android 4.4这些浏览器
} 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
// 如果不支持Pormise也不支持MutationObserver,就会降级成setImmediate
// 类似定时器,与setTimeout的区别在于这个只有两个地方支持,一个是IE浏览器,一个是nodejs
// 那为啥不用优先用setImmediate,因为他的性能比setTimeout好,setTimeout虽然写的是0,最快也要等4毫秒才去执行,而setImmediate会立即执行
// 在nodejs中打印,setImmediate始终在setTimeout之前执行
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
// 所有的东西都不支持就降级成setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
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

看下flushCallbacks这个函数中做了什么,这个函数不是直接调用的,而是通过上面的Promise对象调用的

function flushCallbacks () {
  // 先将pending设置为false,表示处理已经结束
  pending = false
  // 将callbacks数据备份一份之后将callbacks数组清空
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 然后将备份的callbacks数组进行遍历调用
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
1
2
3
4
5
6
7
8
9
10
11

nextTick的作用是获取DOM上最新的数据,当微任务执行的时候,DOM元素还没有渲染到浏览器上,此时如何获取值的呢?

  • 当nextTick里的回调函数执行之前,数据已经被改变了,当重新改变这个数据的时候,其实会立即发送通知,通知watcher渲染视图.在watcher中会先把DOM上的数据进行更新,即更改DOM树

  • 至于这个DOM什么时候更新到浏览器,是在当前这次事件循环结束之后才会执行DOM的更新操作

  • nextTick内部如果使用promise的话,即微任务的话,其实在获取微任务的时候,是从DOM树上直接获取数据的,此时的DOM还没有渲染到浏览器上

  • nextTick中优先使用微任务处理回调函数,如果浏览器不支持微任务,会降级成宏任务

更新时间: 2022-01-13 21:13