阅读 315

nextTick与setState的对比

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

前言

在现代前端框架中,最火的就是vue和react了。现代前端框架的流行极大的提高了前端开发的工程能力和效率上的提高以及解决复杂和通用问题时生态所提供的解决方案。vue和react都是通过数据驱动视图,那么更新视图采用的策略在api上的体现,vue是nextTick, react是setState。

Vue.nextTick

  • 有这么一段vue代码
this.msg = 'hello'
this.msg = 'hello'
this.count = 1
this.color = 'red'
复制代码

问:假如这三个变量都在模版中用到了, 那么执行几次页面渲染

答:是一次

因为我们都知道批量更新可以极大提高性能。也就是说,同步重新渲染在许多情况下效率低下,如果我们知道可能会得到多个更新,所以我们最好批量更新。

在vue中是怎么实现的呢?

我们在仅考虑nextTick的实现情况下(并不考虑收集依赖的策略以及如何处理dom diff)

下面是nextTick稍作简化的源码


export function nextTick(cb, ctx) {
  let _resolve;
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx);
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    // 执行watcher
    timerFunc(); // 清洗队列
  }
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve; //  将resovle函数赋值给_resolve,当 callbacks 回调队列中的函数全都执行完毕的时候,resolve。
    });
  }
}

复制代码

以及该函数依赖的queueWatcher方法

export function queueWatcher(watcher: Watcher) {
  const id = watcher.id;
  //   watch 只会入队一次, 这是一个去重的操作。
  if (has[id] == null) {
    has[id] = true;
    // 如果没有正在执行刷新队列的操作,那么将当前watcher放到queue数组中
    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 > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    // queue the flush
    if (!waiting) {
      waiting = true;

      if (process.env.NODE_ENV !== "production" && !config.async) {
        flushSchedulerQueue();
        return;
      }
      nextTick(flushSchedulerQueue);
    }
  }
}
复制代码

执行流程

  1. 当我们在执行this.msg = 'hello'的时候,触发了this._data.msg的set方法,set会调用该属性中的notify方法,通知该属性的deps中的watcher进行执行。此时该watcher为渲染watcher。
  2. watcher执行update方法会调用queueWatcher 方法将该watcher 放到全局的queue队列中。
  3. 将flushCallbacks函数放入nexttick,等到主线程执行完毕会执行flushCallbacks函数。 即在nexttick中将queue数组中的每一项使用run方法执行, nextTick方法中使用微任务(fallback到setTimeout)去执行。
  4. 如果用户使用了nexttick方法,将用户传入的回调放到queue数组中的最后一项,当 所有的watcher.run全都执行完毕之后, 开始执行用户传入的回调

当我们在一次eventLoop中,如果有this.msg = 'xx' ,因为msg属性只有一个唯一watcher id,所以会被去重,保留最后一个。

但是我们看到一点,当我们执行完this.msg = 'xx' 时,此时dom并没有立即刷新,因为我们也看到了dom的更新是异步的,但是msg值的改变却是同步的,这也比较符合正常的期望, 因为我们改变了这个值,那么这个值应该被立刻改变,但是在react中不是这样的。

setState

当我们要改变视图的时候,React没有如vue这样的对象代理,我们需要显式的调setState这个api去更新数据,用来驱动视图,因为在性能层面上更改了视图以后,用批处理的方式去BatchingUpdate合并的数据,然后再在通过dom diff等一些策略在进行渲染dom层上优化,因为如果每次的状态改变都去重新渲染真实dom,那么它将带来巨大的性能消耗。

为什么setState后获取不到最新的数据

但是与vue不同的是 setState之后在执行之后无法获取到最新的state数据。这是为什么呢?

Redux作者的做出的回应

RFClarification: why is setState asynchronous?

总结一下:

  1. 批量更新对性能上有极大的提升
  2. 保证state与props的一致, 因为在父组件重新渲染之前,是无法知道props的
    1. 因为异步渲染的原因,所以props做不到同步。
    2. 在vue中props也做不到同步,但是vue为什么让它立即拿到值呢?可能是因为vue的最小渲染单元是组件,通过watcher精准的定位组件进行更新。
  3. React要保证所有的状态是安全的。

setState在不同场景下的表现

setState在不同的场景下会是异步也可能会是同步的。

  • setState只有在React合成时间和生命周期函数中是异步的,在非合成事件和setTimeout中是同步的。

因为在react合成事件与钩子函数的执行时机是在更新之前,这个异步指的是更新之前拿不到最新的结果,而不是setState的实现就是异步的。而"异步" 则意味着 batching update。会得到性能优化,而这在非合成事件以及settimeout中是没有的。

如果在合成事件或钩子函数中一个事件循环内对一个相同的属性进行更新,那么会进行合并

this.setState({
    num: 456
})
this.setState({
  num: 567
})
// 会进行合并,只会执行后者的setState
复制代码

获取setState后最新的状态

使用 componentDidUpdate 或者 setState(updater, callback),在 callback 中获取改动之后的 state,如果当你需要改变一个 state 的时候用到前一个 state 的状态,就需要用到上述的方法)

总结

以上就是本人对vue与react在api上的更新策略机制的一些粗浅的理解,作为一个react新手还是无法对框架的整体执行流程没有一个全面的理解,所以没有源码方面的分析,只是表达了一些粗浅的看法。有表达不对的地方,欢迎指正和讨论。一起加油!

文章分类
前端
文章标签