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

批量更新DOM(Vue.nextTick与React的setState分析笔记) #8

Open
Cyrilszq opened this issue Mar 14, 2017 · 0 comments
Open

批量更新DOM(Vue.nextTick与React的setState分析笔记) #8

Cyrilszq opened this issue Mar 14, 2017 · 0 comments

Comments

@Cyrilszq
Copy link
Owner

Cyrilszq commented Mar 14, 2017

Vue中的批量更新DOM

关于批量更新DOM文档中有这么一段介绍:

默认情况下, Vue 的 DOM 更新是异步执行的。理解这一点非常重要。当侦测到数据变化时, Vue 会打开一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。假如一个 watcher 在一个事件循环中被触发了多次,它只会被推送到队列中一次。然后,在进入下一次的事件循环时, Vue 会清空队列并进行必要的 DOM 更新。在内部,Vue 会使用 MutationObserver 来实现队列的异步处理,如果不支持则会回退到 setTimeout(fn, 0)

这么做可以有效避免无效的更新,比如一个数据在一个事件循环中多次改变,则如果按正常的策略会多次触发watcher中的回调,重新构建虚拟DOM进行diff处理,而事实上这是不必要的,其实重复的watcher只要执行一次。所以Vue利用js中的事件循环进行优化减少不必要的计算和DOM操作。
我主要阅读的关于Vue.nextTick这一块源码,来看一下Vue是如何利用事件循环的。在此之前需要明白两个东西,microtaskMutationObserver前者可以看知乎上的一个讨论Promise的队列与setTimeout的队列的有何关联。后者直接看看MDN就好,MutationObserver

首先看如何使用:

Vue.nextTick(function () {
  console.log('nextTick')
})

即传递一个回调函数,该函数在事件循环结束时调用,下面来看一下这块的源码(/src/core/util/env.js):

export const nextTick = (function () {
  const callbacks = [] //存放所有回调
  let pending = false //记录当前是否有回调在执行
  let timerFunc //一个函数,包装了一个能添加到microtask的函数

  // 在这里执行所有的回调,而这个函数的执行时机在下一个事件循环中
  function nextTickHandler () {
    pending = false
    // 将callbacks数组复制执行,因为如果在nextTick的回调函数中继续执行Vue.nextTick()
    // 则cb会不断被push到callbacks中,导致callbacks一直执行
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // 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 if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    // 如果能用Promise则通过Promise.then把nextTickHandler添加到microtask
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      if (isIOS) setTimeout(noop)
    }
  } else if (typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
   // 如果能用MutationObserver,则人为的创建一个textNode,
   // 并让MutationObserver监听这个textNode,在timerFunc中改变这个textNode,
   // 由此触发MutationObserver的回调(这里涉及MutationObserver的工作方式,看看MDN文档就好),
   // 实现在下一次事件循环执行nextTickHandler的目的
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    //都不行只能用setTimeout实现,将nextTickHandler添加到macrotask中
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
      if (cb) cb.call(ctx)
      if (_resolve) _resolve(ctx)
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(resolve => {
        _resolve = resolve
      })
    }
  }
})()

首先nextTick是一个自执行函数,返回值是queueNextTick()函数(主要利用闭包来保存一些变量)。当Vue.nextTick()执行时,执行的就是queueNextTick()函数,在这里将回调放入callbacks数组保存。并且用了一个pending标志位做了一次判断,它的主要作用是保证timerFunc()这个函数在一轮事件循环中只执行一次(因为在执行timerFunc()前将它置为true,而只有下一轮事件循环开始时它才能被置为false),timerFunc()这个函数仅仅包装了一个能添加到microtask的函数(如promise.then,MutationObserver),具体注释中有。关于选用哪种方式利用事件循环,从代码中可以看出,优先使用Promise,不存在则使用MutationObserver,都不存在则用setTimeout。

清楚这个函数的工作原理差不多就明白了Vue异步更新DOM的原理了,因为Vue会把一轮事件循环(即一次task)中所有触发的watcher去重后添加到一个队列里,然后将这个队列交由Vue.nextTick(),即将这个队列添加到microtask中,这样在本次task结束后,按照规则就会取出所有的microtask执行它们,实现DOM的更新。

React是如何做的?

react通过setState这个API改变state,那么它是如何工作的?

React 源码剖析系列 - 解密 setState这篇文章提到一个例子:

componentDidMount() {
  this.setState({val: this.state.val + 1});
  console.log(this.state.val);    // 第 1 次 log

  this.setState({val: this.state.val + 1});
  console.log(this.state.val);    // 第 2 次 log

  setTimeout(() => {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 第 3 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 第 4 次 log
  }, 0);
}

输出结果是0 0 2 3原因文章中也有提到。
此外Change Detection And Batch Update这篇文章也有一个总结:

在React调用的方法中连续setState走的是批量更新,此外走的是连续更新

就是说如果方法是通过React调用的比如生命周期函数,React的事件处理等,那么会进行批量更新,自己调用的方法,比如setTimeout,xhr等则是连续更新。

  • 2017-07-07更新
    最近在看Under the hood: ReactJS,part-1又提到了事务这个概念,回过头来又看了一遍发现之前一直好奇的问题原来如此简单。关于react将需要更新的组件放到dirtyComponents这里可以理解,但是是在什么时机去更改batchingStrategy.isBatchingUpdates的值,或者说执行ReactUpdates.flushBatchedUpdates的时机,因为对于vue来说就是一个microtask,很好理解。现在才知道原来如果在react生命周期如componentDidMounted调用setState,其实componentDidMounted就处在一个事务中,那么当它执行完的时候就该执行transaciton.closeAll,在这里处理批量更新

可以参看这张图,来自Under the hood: ReactJS

参考链接

Vue源码详解之nextTick
vue早期源码学习系列之五:批处理更新DOM

@Cyrilszq Cyrilszq changed the title 异步更新DOM(Vue.nextTick与React的setState分析笔记) 批量更新DOM(Vue.nextTick与React的setState分析笔记) Mar 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant