Говоря о механизме обновления setState

React.js
Говоря о механизме обновления setState

Студенты, понимающие React, должны быть правыsetStateфункции слишком знакомы,setStateОн также часто используется в качестве вопроса на собеседовании, чтобы проверить знакомство соискателей работы с React.

Я тоже закидываю сюда вопрос, читатели могут подумать над ответом на этот вопрос, прежде чем читать статью.

Каждый раз устанавливайте одно и то же значение для состояния компонента React, например.setState({count: 1}). Отрисовываются ли когда-либо компоненты React? Если да, то почему? Если нет, то почему?

Репродукция сцены

В ответ на вышеуказанные проблемы сначала выполняется простая проверка воспроизведения.

场景复现

Как показано на рисунке, компонент приложения имеет кнопку настройки.Каждый раз, когда нажимается кнопка настройки, для состояния текущего компонента будет установлено одно и то же значение.{count: 1}, когда компонент отрисовывается, количество отрисовок автоматически увеличивается на 1. Код выглядит следующим образом:

Компонент приложения

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 全局变量,用于记录组件渲染次数
let renderTimes = 0;

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }

  handleClick = () => {
    this.setState({ count: 1 });
  };

  render() {
    renderTimes += 1;

    return (
      <div>
        <h3>场景复现:</h3>
        <p>每次点击“设置”按钮,当前组件的状态都会被设置成相同的数值。</p>
        <p>当前组件的状态: {this.state.count}</p>
        <p>
          当前组件发生渲染的次数:
          <span style={{ color: 'red' }}>{renderTimes}</span>
        </p>
        <div>
          <button onClick={this.handleClick}>设置</button>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

Фактический результат проверки показан ниже.Каждый раз, когда нажимается кнопка настройки, компонент приложения будет повторно отображаться.

场景复现操作

2. Оптимизация производительности

Итак, как уменьшить повторный рендеринг компонентов приложения? доОптимизация производительности React. Говоря о компонентах PureComponent и мемо-компонентахВ этой статье подробноPureComponentВнутренний механизм реализации доступен здесьPureComponentкомпонентов для уменьшения дублирования рендеринга.

Фактические результаты проверки показаны ниже, а оптимизированный компонент приложения больше не генерирует повторную визуализацию.

性能优化

Но есть проблема с деталями, о которой вы, возможно, не задумывались в своей обычной работе:

использоватьPureComponentКомпоненты могут уменьшить повторный рендеринг компонентов приложения, значит ли это, что состояние компонентов приложения не изменилось? То есть является ли ссылочный адрес последним адресом?

Без лишних слов, давайте протестируем и проверим эту проблему.Код выглядит следующим образом:

Компоненты приложения

import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';

// 全局变量,用于记录组件渲染次数
let renderTimes = 0;
// 全局变量,记录组件的上次状态
let lastState = null;

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
    lastState = this.state; // 初始化,地址保持一致
  }

  handleClick = () => {
    console.log(`当前组件状态是否是上一次状态:${this.state === lastState}`);

    this.setState({ count: 1 });
    // 更新上一次状态
    lastState = this.state;
  };

  render() {
    renderTimes += 1;

    return (
      <div>
        <h3>场景复现:</h3>
        <p>每次点击“设置”按钮,当前组件的状态都会被设置成相同的数值。</p>
        <p>当前组件的状态: {this.state.count}</p>
        <p>
          当前组件发生渲染的次数:
          <span style={{ color: 'red' }}>{renderTimes}</span>
        </p>
        <div>
          <button onClick={this.handleClick}>设置</button>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

В компоненте APP мы передаем глобальную переменнуюlastStateдля записи последнего состояния компонента. Когда кнопка настройки нажата, она будет сравнивать, равно ли текущее состояние компонента предыдущему состоянию, то есть совпадает ли адрес ссылки?

引用地址变化

В окне консоли мы обнаруживаем, что хотяPureComponentКомпонент уменьшает повторную отрисовку компонента App, но адрес ссылки состояния компонента App изменился Почему это?

Далее мы ответим на эти два вопроса и поговорим об исходном коде React V16.9.0.setStateмеханизм обновления состояния. В процессе интерпретации для лучшего понимания исходного кода часть исходного кода будет удалена.

В-третьих, механизм обновления состояния setState.

В процессе интерпретации исходного кода была организована функцияsetStateБлок-схема взаимосвязи вызовов выглядит следующим образом:

setState更新机制

Как видно из рисунка выше, функцияsetStateОтношения вызова в основном делятся на следующие две части:

  • Добавьте состояние для обновления в очередь обновлений;
  • Создает запланированное задание. Задача планирования проходит через очередь обновлений и вычисляет конечное состояние для обновления, обновляет его до экземпляра компонента, а затем завершает операцию рендеринга компонента.

Ниже приводится подробное описание этих двух частей вместе с исходным кодом.

3.1 Войдите в очередь обновлений

3.1.1 Определение функции setState

Взято изReactBaseClasses.jsдокумент.

Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

функцияsetStateсодержит два параметраpartialStateа такжеcallbackpartialStateУказывает частичное состояние, которое необходимо обновить,callbackЭто функция обратного вызова после обновления статуса.

3.1.2 определение функции enqueueSetState

Взято изReactFiberClassComponent.jsдокумент.

enqueueSetState(inst, payload, callback) {
  const fiber = getInstance(inst);
  const currentTime = requestCurrentTime();
  const suspenseConfig = requestCurrentSuspenseConfig();
  const expirationTime = computeExpirationForFiber(
    currentTime,
    fiber,
    suspenseConfig,
  );

  // 创建一个update对象
  const update = createUpdate(expirationTime, suspenseConfig);
  // payload存放的是要更新的状态,即partialState
  update.payload = payload;

  // 如果定义了callback,则将callback挂载在update对象上
  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }

  // ...省略...

  // 将update对象添加至更新队列中
  enqueueUpdate(fiber, update);
  // 添加调度任务
  scheduleWork(fiber, expirationTime);
},

функцияenqueueSetStateсоздастupdateобъект и обновит состояниеpartialState, функция обратного вызова после обновления статусаcallbackи срок действия рендераexpirationTimeи т.д. будут смонтированы на этом объекте. затемupdateОбъект добавляется в очередь обновлений, и создается запланированная задача.

Если компонент вызывается несколько раз перед рендерингомsetState, будет несколькоupdateОбъекты будут добавлены в очередь обновлений по очереди, а также будет создано несколько задач планирования.

3.1.3 определение функции createUpdate

Взято изReactUpdateQueue.jsдокумент.

export function createUpdate(
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
): Update<*> {
  let update: Update<*> = {
    expirationTime,
    suspenseConfig,

    // 添加TAG标识,表示当前操作是UpdateState,后续会用到。
    tag: UpdateState,
    payload: null,
    callback: null,

    next: null,
    nextEffect: null,
  };

  return update;
}

функцияcreateUpdateсоздастupdateобъект для хранения обновленного состоянияpartialState, функция обратного вызова после обновления статусаcallbackи срок действия рендераexpirationTime.

3.2 Механизм обновления состояния setState

Как видно из рисунка выше, каждый вызовsetStateКаждая функция создает запланированную задачу. Затем после серии вызовов функций функция в конечном итоге будет вызванаupdateClassComponent.

Красная область на рисунке включает в себя множество точек знаний, которые имеют мало общего с механизмом обновления состояния, который мы хотим обсудить.На этот раз это не является предметом нашего обсуждения, поэтому мы сначала пропускаем его и ждем последующего исследования (копания). .

Кратко расскажем о том, как шаг за шагом обновляется состояние экземпляра компонента.

3.2.1 Функция getstatefromupdate

Взято изReactUpdateQueue.jsдокумент.

function getStateFromUpdate<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  update: Update<State>,
  prevState: State,
  nextProps: any,
  instance: any,
): any {
  switch (update.tag) {

    // ....省略 ....

    // 见3.3节内容,调用setState会创建update对象,其属性tag当时被标记为UpdateState
    case UpdateState: {
      // payload 存放的是要更新的状态state
      const payload = update.payload;
      let partialState;

      // 获取要更新的状态
      if (typeof payload === 'function') {
        partialState = payload.call(instance, prevState, nextProps);
      } else {
        partialState = payload;
      }

      // partialState 为null 或者 undefined,则视为未操作,返回上次状态
      if (partialState === null || partialState === undefined) {
        return prevState;
      }

      // 注意:此处通过Object.assign生成一个全新的状态state, state的引用地址发生了变化。
      return Object.assign({}, prevState, partialState);
    }

    // .... 省略 ....
  }

  return prevState;
}

getStateFromUpdateОсновная функция функции — хранить в объекте обновленияupdateВверхpartialStateс последнимprevStateОбъединяйте объекты для создания нового состояния.

Уведомление:

  • Object.assignПервый параметр — пустой объект, что означает, что адрес ссылки нового объекта состояния изменился.
  • Object.assignДелается поверхностная копия, а не глубокая копия.

3.2.2 функция processUpdateQueue

Взято изReactUpdateQueue.jsдокумент.

export function processUpdateQueue<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  props: any,
  instance: any,
  renderExpirationTime: ExpirationTime,
): void {
  // ...省略...

  // 获取上次状态prevState
  let newBaseState = queue.baseState;

  /**
   * 若在render之前多次调用了setState,则会产生多个update对象。这些update对象会以链表的形式存在queue中。
   * 现在对这个更新队列进行依次遍历,并计算出最终要更新的状态state。
   */
  let update = queue.firstUpdate;
  let resultState = newBaseState;
  while (update !== null) {
    // ...省略...

    /**
     * resultState作为参数prevState传入getStateFromUpdate,然后getStateFromUpdate会合并生成
     * 新的状态再次赋值给resultState。完成整个循环遍历,resultState即为最终要更新的state。
     */
    resultState = getStateFromUpdate(
      workInProgress,
      queue,
      update,
      resultState,
      props,
      instance,
    );
    // ...省略...

    // 遍历下一个update对象
    update = update.next;
  }

  // ...省略...

  // 将处理后的resultState更新到workInProgess上
  workInProgress.memoizedState = resultState;
}

Перед визуализацией компонента React мы обычно вызываем его несколько раз.setState, каждый раз, когда вы звонитеsetStateсгенерирует объект обновления. Эти объекты обновления будут храниться в очереди очереди в виде связанного списка.processUpdateQueueФункция будет проходить очередь по очереди, и каждый обход будет проходить предыдущий.prevStateс объектом обновленияpartialStateСлияние, когда все обходы завершены, можно рассчитать конечное состояние, которое нужно обновить, и в это время оно будет сохранено в workInProgress.memoizedStateхарактеристики.

3.2.3 функция updateClassInstance

Взято изReactFiberClassComponent.jsдокумент.

function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderExpirationTime: ExpirationTime,
): boolean {
  // 获取当前实例
  const instance = workInProgress.stateNode;

  // ...省略...

  const oldState = workInProgress.memoizedState;
  let newState = (instance.state = oldState);
  let updateQueue = workInProgress.updateQueue;

  // 如果更新队列不为空,则处理更新队列,并将最终要更新的state赋值给newState
  if (updateQueue !== null) {
    processUpdateQueue(
      workInProgress,
      updateQueue,
      newProps,
      instance,
      renderExpirationTime,
    );
    newState = workInProgress.memoizedState;
  }

  // ...省略...

  /**
   * shouldUpdate用于标识组件是否要进行渲染,其值取决于组件的shouldComponentUpdate生命周期执行结果,
   * 亦或者PureComponent的浅比较的返回结果。
   */
  const shouldUpdate = checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    );

  if (shouldUpdate) {
     // 如果需要更新,则执行相应的生命周期函数
     if (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
        typeof instance.componentWillUpdate === 'function') {
      startPhaseTimer(workInProgress, 'componentWillUpdate');
      if (typeof instance.componentWillUpdate === 'function') {
        instance.componentWillUpdate(newProps, newState, nextContext);
      }
      if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
        instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
      }
      stopPhaseTimer();
    }
    // ...省略...
  }

  // ...省略...

  /**
   * 不管shouldUpdate的值是true还是false,都会更新当前组件实例的props和state的值,
   * 即组件实例的state和props的引用地址发生变化。也就是说即使我们采用PureComponent来减少无用渲染,
   * 但并不代表该组件的state或者props的引用地址没有发生变化!!!
   */
  instance.props = newProps;
  instance.state = newState;

  return shouldUpdate;
}

Как видно из приведенного выше кода,updateClassInstanceФункция в основном реализует следующие функции:

  • Пройдите очередь обновлений, сгенерируйте новое состояние и обновите его до состояния экземпляра компонента;
  • Возвращает идентификатор того, следует ли обновлятьshouldUpdate, результат операции этого значения зависит отshouldComponentUpdateРезультат выполнения функции жизненного цикла илиPureComponentНеглубокие результаты сравнения ;
  • еслиshouldUpdateценностьtrue, затем выполните соответствующую функцию жизненного циклаcomponentWillUpdate;

На этом этапе обратите особое внимание на следующие моменты:

  1. Меняется состояние экземпляра компонента, то есть меняется адрес ссылки;
  2. даже еслиPureComponentилиshouldComponentUpdateЧтобы уменьшить бесполезный рендеринг, но адрес ссылки реквизита или состояние экземпляра компонента все равно меняется.

Здесь интерпретируется код, предположительно у каждого есть ответы на два упомянутых ранее вопроса.

3.2.4 функция updateClassComponent

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps,
  renderExpirationTime: ExpirationTime,
) {
  // 获取组件实例
  const instance = workInProgress.stateNode;

  // ...省略...

  let shouldUpdate;

  /**
   * 1. 完成组件实例的state、props的更新;
   * 2. componentWillUpdate、shouldComponentUpdate生命周期函数执行完毕;
   * 3. 获取是否要进行更新的标识shouldUpdate;
   */
  shouldUpdate = updateClassInstance(
    current,
    workInProgress,
    Component,
    nextProps,
    renderExpirationTime,
  );

  /**
   * 1. 如果shouldUpdate值为false,则退出渲染;
   * 2. 执行render函数
   */
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderExpirationTime,
  );

  // 返回下一个任务单元
  return nextUnitOfWork;
}

Как видно из приведенного выше кода,updateClassComponentФункция в основном реализует следующие функции:

  • Завершите обновление состояния и реквизита экземпляра компонента;
  • воплощать в жизньcomponentWillUpdate,shouldComponentUpdateи другие функции жизненного цикла;
  • Завершите визуализацию экземпляра компонента;
  • Возвращает следующий ожидающий модуль задачи;

4. Резюме

После интерпретации кода в предыдущей главе я считаю, что каждый должен понимать функциюsetStateДолжно быть новое понимание. На два вопроса, упомянутых выше, должны быть свои ответы. Здесь я кратко резюмирую:

каждый раз, когда вызывается функцияsetState, react добавит состояние для обновления в очередь обновлений и сгенерирует задачу планирования. Запланированные задачи выполняют две функции во время выполнения:

  • Пройдите очередь обновления, вычислите новое состояние состояния и обновите его до экземпляра компонента;
  • Согласно идентификацииshouldUpdateчтобы решить, следует ли повторно визуализировать экземпляр компонента, и определитьshouldUpdateзначение зависит отPureComponentРезультат поверхностного сравнения компонентов или функция жизненного циклаshouldComponentUpdateРезультаты;

использоватьPureComponentКомпонент может уменьшить повторную визуализацию экземпляра компонента, но состояние экземпляра компонента получает новое состояние, поэтому адрес ссылки изменился.

Статья пока пишется здесь, если вы считаете, что пост в блоге неплох, то, пожалуйста, поставьте ему палец вверх.

разное: