React — анализ исходного кода setState (читается Xiaobai)

внешний интерфейс GitHub исходный код React.js

1. Сначала прочтите официальную документацию

Давайте сначала посмотрим на определение setState() в официальной документации. Английская документация лучше всего

Реагировать на английскую документацию

Реагировать на китайскую документацию

Во-вторых, практика и проблемы setState()

Давайте сначала рассмотрим самый простой вопрос: после нажатия на кнопку количество увеличивается на 2? 

class NextPage extends Component<Props> {
  static navigatorStyle = {
    tabBarHidden: true
  };

  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  add() {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity
          style={styles.addBtn}
          onPress={() => {
            this.add();
          }}
        >
          <Text style={styles.btnText}>点击+2</Text>
        </TouchableOpacity>

        <Text style={styles.commonText}>当前count {this.state.count}</Text>
      </View>
    );
  }
}

Результат 1



Почему добавить только 1?

Посмотрите на это предложение на официальном сайте

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

Дело в первых двух предложениях, перевод setState() не всегда сразу обновляет компонент, он может пакетировать или откладывать обновление. Это делает потенциальной ловушкой чтение this.state сразу после вызова setState(). Сначала выбросьте правильный ответ нажатия кнопки плюс 2, следующие два метода в порядке

this.setState(preState => {
  return {
    count: preState.count + 1
  };
});
this.setState(preState => {
  return {
    count: preState.count + 1
  };
});

setTimeout(() => {
  this.setState({
    count: this.state.count + 1
  });
  this.setState({
    count: this.state.count + 1
  });
}, 0);



Три, мир исходного кода setState

Я полагаю, что студенты, которые могут прийти сюда, знают, что метод setState() может быть как синхронным, так и асинхронным, когда он синхронный, а когда асинхронный?

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

Примечание. Синхронизация и асинхронность, упомянутые здесь, просто «реализация выглядит как синхронизация или асинхронность, например, в приведенном выше ответе 2 setTimeout кажется синхронным», по сути, setState() все еще асинхронный Неважно, умеете ли вы его читать или нет, немедленно погрузитесь в мир исходного кода. 

1. Как быстро просмотреть исходный код реакции

Перейдите в репозиторий github реакции и клонируйте его напрямую.

репозиторий на github

git clone https://github.com/facebook/react.git


Пока вижу, последняя версия 16.2.0, выбрал код 15.6.0

Один из них — обратиться к результатам анализа предшественников.

Во-вторых, мой уровень ограничен.Если письмо действительно непонятно, студенты также могут читать аналитические статьи других, чтобы не полностью не понять

Как переключать версии? 

1. Найдите соответствующий номер версии


2. Скопируйте исторический номер записи 15.6.0.


3. Откат

git reset --hard 911603b

Как показано на рисунке, успешно откатился на версию 15.6.0.



2. Запись setState => enqueueSetState

Основной принцип: поскольку вы просматриваете исходный код, конечно, вы не читаете код построчно, а просматриваете основную идею, поэтому следующий код будет содержать только основной код и только упоминать или игнорировать сторонний код. подробности.


Входной файл для setState находится вsrc/isomorphic/modern/class/ReactBaseClasses.js

Компоненты React наследуются от React.Component, а setState — это метод React.Component, поэтому для компонентов setState принадлежит его методу-прототипу.

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

partialState, как следует из названия — «частичное состояние», это название, вероятно, означает, что оно не влияет на исходное состояние.


При вызове setState на самом деле вызывается метод enqueueSetState.Мы пошли по подсказкам (я воспользовался глобальным поиском vscode) и нашли этот файлsrc/renderers/shared/stack/reconciler/ReactUpdateQueue.js


Этот файл экспортирует объект ReactUpdateQueue, "очередь обновлений реакции", кодовое имя хорошее, вы можете внести свои комментарии, это шедевр, здесь прописан метод enqueueSetState


3. enqueueSetState => enqueueUpdate

Сначала взгляните на определение enqueueSetState.

  enqueueSetState: function(publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState',
    );
	
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  },

Здесь нужно только обратить внимание на два свойства internalInstance

_pendingStateQueue: очередь для обновления

_pendingCallbacks: обновить очередь обратного вызова

Если значение _pendingStateQueue равно null, присвойте его пустому массиву [], поместите partialState в очередь состояний для обновления _pendingStateQueue и, наконец, выполните enqueueUpdate(internalInstance)

Следующий взгляд на enqueueUpdate

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

Он выполняет метод enqueueUpdate ReactUpdates.

var ReactUpdates = require('ReactUpdates');

Этот файл оказался прямо рядом с нимsrc/renderers/shared/stack/reconciler/ReactUpdates.js

Найдите метод enqueueUpdate


Определяется следующим образом

function enqueueUpdate(component) {
  ensureInjected();

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}


Этот код очень важен для понимания setState

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);

Определение batchingStrategy.isBatchingUpdates batchingStrategy — это стратегия пакетного обновления, isBatchingUpdates указывает, находится ли он в процессе пакетного обновления, значение по умолчанию — false в начале


Приведенное выше предложение означает:

Если он находится в режиме пакетного обновления, то есть когда isBatchingUpdates имеет значение true, операция обновления состояния не выполняется, а компоненты, которые необходимо обновить, добавляются в массив dirtyComponents;

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

Заимствуйте изображение из «Углубленного стека технологий React», стр. 167.



4. Ядро: batchedUpdates => транзакция вызова

А как насчет batchingStrategy.isBatchingUpdates? Кажется, это ключ

Однако найти объект batchingStrategy непросто, он вводится методом инъекции, после недолгих поисков я обнаружил, что batchingStrategy — это ReactDefaultBatchingStrategy. 

src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.jsКак найти файл — это отдельная категория.Сегодня мы сосредоточимся только на setState, а о другом содержании поговорим позже.


Я полагаю, что некоторые студенты немного сбиты с толку, когда видят это. Это не имеет значения, просто придерживайтесь этого и оставьте детали в стороне. Я просто знаю, что мы нашли ключевой метод batchedUpdates, и мы близки к победе, так что не не сдаваться (первый раз смотрел, так и выживал. , если не получилось один раз, то сделай два, если сильно, то сколько раз?)


Сначала посмотрите на стратегию пакетного обновления — batchingStrategy, что это такое?

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

module.exports = ReactDefaultBatchingStrategy;

Наконец нашел это, свойство isBatchingUpdates и метод batchedUpdates


Если isBatchingUpdates имеет значение true и в настоящее время находится в состоянии транзакции обновления, компонент сохраняется в dirtyComponent, В противном случае вызовите обработку batchedUpdates и инициируйте transaction.perform().

Примечание. Все функции batchUpdate реализуются путем выполнения различных транзакций.

Это концепция транзакции, сначала поймите транзакцию


5. Дела

В этом абзаце давайте прямо процитируем концепции из книги «Углубленный стек технологий React», стр. 169.



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


С точки зрения реализации, Transaction in React предоставляет Mixin для облегчения реализации другими модулями собственных транзакций. Чтобы использовать модуль Transaction, помимо смешивания Transaction Mixin с вашей собственной реализацией транзакции, вам также необходимо реализовать абстрактный интерфейс getTransactionWrappers. Этот интерфейс используется транзакцией для получения всех методов инициализации и закрытия, которые необходимо инкапсулировать, поэтому он должен возвращать массив объектов, каждый из которых имеет методы, чьи ключи инициализируются и закрываются соответственно. 


Следующий код должен помочь вам понять

var Transaction = require('./Transaction');

// 我们自己定义的 Transaction
var MyTransaction = function() {
  // do sth.
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function() {
    return [{
      initialize: function() {
        console.log('before method perform');
      },
      close: function() {
        console.log('after method perform');
      }
    }];
  };
});

var transaction = new MyTransaction();
var testMethod = function() {
  console.log('test');
}
transaction.perform(testMethod);

// before method perform
// test
// after method perform


6. Базовый анализ: стратегия пакетного обновления batchingStrategy

Вернемся к batchingStrategy: стратегия пакетного обновления и рассмотрим ее реализацию в коде.

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};


Вы можете видеть, что начальное значение isBatchingUpdates равно false, а для переменной isBatchingUpdates будет установлено значение true при вызове метода batchedUpdates. Потом по настройкамПредыдущее значение isBatchingUpdatesвыполнять различные процессы


Помните очень важный фрагмент кода, упомянутый выше?

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);

1. Во-первых, обработка самого события клика в большой транзакции (просто запомните это), isBatchingUpdates уже имеет значение true  


2. При вызове setState() вызывается ReactUpdates.batchedUpdates для обработки событий транзакционным способом.  


3. Когда выполняется setState, isBatchingUpdates уже имеет значение true.setState равномерно распределяет обновления по массиву dirtyComponents;


4. Когда транзакция завершается, метод ReactUpdates.flushBatchedUpdates объединяет все временные состояния и вычисляет последние свойства и состояние, а затем закрывает транзакцию пакетами.


До сих пор я не следил за методом ReactUpdates.flushBatchedUpdates.Эта часть включает в себя рендеринг и виртуальный дом.В любом случае, вы знаете, что он используется для выполнения рендеринга. 

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


Я не знаю, вспомнил ли кто-нибудь из моих одноклассников вопрос здесь.?

Флаг isBatchingUpdates устанавливается в значение true при инициации batchedUpdates, когда он сбрасывается в значение false?

Помните метод закрытия транзакции выше, тот же файлsrc/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

// 定义复位 wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

// 定义批更新 wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});

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


Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

Через слияние прототипов метод закрытия транзакции сбрасывает isBatchingUpdates после выполнения enqueueUpdate, а затем инициирует пакетное обновление DOM.  


В этот момент мы обнаружим, что все предыдущие очереди, batchUpdate и т. д. предназначены для этого шага транзакции, а фронт — это только работа по сбору пакетов, и операция пакетного обновления здесь действительно завершена.


7. Вернитесь к исходной теме

add() {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  }

setTimeout(() => {
  this.setState({
    count: this.state.count + 1
  });
  this.setState({
    count: this.state.count + 1
  });
}, 0);


В первом случае, когда выполняется первый setState, он уже находится в большой транзакции, инициированной событием click, и был запущен batchedUpdates, а isBatchingUpdates имеет значение true, поэтому оба setState будут обновляться пакетами, что является асинхронным process. , this.state не изменяется немедленно, выполнение setState просто эквивалентно передаче partialState (часть упомянутого выше состояния) в dirtyComponents и, наконец, выполнению flushBatchedUpdates для повторного рендеринга на этапе закрытия транзакции.


Во втором случае, с setTimeout, два setStates будут выполнены после того, как batchedUpdates batchedUpdates в большой транзакции, вызванной событием click, будут завершены, поэтому они вызовут два batchedUpdatesBatchedUpdates, а также выполнят две транзакции и функцию flushBatchedUpdates, которая эквивалентен процессу синхронного обновления.


позже

Спасибо за ваше терпение, чтобы увидеть здесь, и я надеюсь, что вы узнаете что-то!

Если вы не очень заняты, пожалуйста, закажите звезду⭐[Портал блога Github], это большое поощрение для автора.

Мне нравится делать записи в процессе обучения. Я делюсь некоторыми своими накоплениями и размышлениями о начальном этапе. Я надеюсь общаться с вами и добиваться прогресса. Дополнительные статьи см.[блог амандакелаке на Github]