• Vue源码学习之nextTick


    现象

     对于nextTick相信大家都并不陌生,那么nextTick的本质是对JS事件循环的一种应用。那么相信大家都遇到过这种情况,比如说我们对DOM1进行操作并改变DOM1中的数据,下面使用一个变量去获取DOM1中的数据,发现获取到的数据是DOM1变化之前的数据,这就是我们会产生疑惑的地方。

    为什么?

     这是因为Vue对Dom的更新是异步的,异步是代表着当被处理数据是动态变化时,此时对应的Dom未能同步更新就会导致数据已经更新(Model层的数据已经更新),但是视图层并没有更新(Dom未进行更新)。此时我们可以通过使用nextTick的回调函数中获取到更新后的Dom值。

    源码解析

    nextTick

    // 队列
    const callbacks = []
    // 标志变量锁
    let pending = false
    
    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)
        }
      })
      // pending的默认值为false,当调用了timerFunc函数,会将nextTick的回调函数调用放在队列中,然后将pending设置为true
      // 如果pending此时为true说明flushCallbacks还没有被执行,此时调用的nextTick都会将对应的回调函数推入到callbacks队列中
      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

     分析这段代码,我们可以看到在nextTick的外层定义变量就形成了一个闭包callbacks,当每次调用nextTick时实际是在向callbacks这个队列中去添加对应的回调函数进行等待调用。当pending第一次为false的时候,将pending设置为true,然后执行timeFunc函数(稍后我们解读一下timeFunc函数),那么下一次执行nextTick时,就只会把对应的回调函数加入到当前的任务队列中,不会再次执行timerFunc函数。nextTick后续会返回一个Promise实例,并将resolve赋值给_resolve。

    timerFunc

    let timerFunc
    
    // 是否支持Promise?
    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
    // 是否支持MutationObserver
    } 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
    // 查看是否支持setImmediate
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // Fallback to setImmediate.
      // Technically it leverages the (macro) task queue,
      // but it is still a better choice than setTimeout.
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    // 如果都不行则使用setTimeout
    } else {
      // Fallback to 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

     这段代码主要是调用flushCallbacks方法,把其放在相对应的微任务或者宏任务去执行调用。主要通过timeFunc来去调用flushCallbacks方法,依次判断是否支持promise.then->MutationObserver->setImmediate->setTimeout进行优先创建微任务,其次兼容宏任务,无论哪种哪种方式创建的timeFunc最后都会执行flushCallbacks函数。

    flushCallbacks

    function flushCallbacks () {
      // 执行过后就将pending设置为false
      pending = false
      // 对原数组进行深拷贝,这样进行一系列操作的时候就不影响原数组了
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

     对于flushCallbacks负责执行callbacks里的回调,把callbacks进行复制一份,然后将callbacks进行置空,将所拷贝出来数组中的每个函数依次执行一遍。所以flushCallbacks仅仅是用来执行callbacks中的回调函数。

    总结

    1. 当调用nextTick时把nextTick中对应的回调函数放入到callbacks中进行等待执行,并且返回Promise实例,当第一次调用时则会开启pending锁,并执行timerFunc函数将执行事件队列的函数放到微任务/宏任务中,去模拟Vue中自己的异步事件队列。
    2. timerFunc通过一次判断微任务与宏任务,依次对Promise->MutationObserver->setImmediate->setTimeout,来去将执行函数放在微任务/宏任务中
    3. 当事件循环到了微任务/宏任务时,调用flushCallbacks,依次执行对应callbacks中的回调。

     对nextTick的源码分析到此结束啦,希望各位小伙伴能够有所收获~

  • 相关阅读:
    qt day 6
    物体分类__pytorch
    CCRC-DSO学员分享:数据安全官——导师与朋友的双重身份
    【牛客网刷题】VL11-VL24 组合逻辑 & 时序逻辑
    javascript利用xhr对象实现http流的comet轮循,主要是利用readyState等于3的特点
    gorm查询结果集如何匹配数据字典对应的名称
    [BSidesCF 2020]Had a bad day1
    js post下载文件
    Django中的事务
    前端研习录(26)——JavaScript DOM讲解及示例分析
  • 原文地址:https://blog.csdn.net/liu19721018/article/details/125424373