React 架構(gòu)的演變 - 從同步到異步
寫這篇文章的目的,主要是想弄懂 React 最新的 fiber 架構(gòu)到底是什么東西,但是看了網(wǎng)上的很多文章,要不模棱兩可,要不就是一頓復(fù)制粘貼,根本看不懂,于是開始認(rèn)真鉆研源碼。鉆研過程中,發(fā)現(xiàn)我想得太簡單了,React 源碼的復(fù)雜程度遠(yuǎn)超我的想象,于是打算分幾個(gè)模塊了剖析,今天先講一講 React 的更新策略從同步變?yōu)楫惒降难葑冞^程。
從 setState 說起
React 16 之所以要進(jìn)行一次大重構(gòu),是因?yàn)?React 之前的版本有一些不可避免的缺陷,一些更新操作,需要由同步改成異步。所以我們先聊聊 React 15 是如何進(jìn)行一次 setState 的。
- import React from 'react';
- class App extends React.Component {
- state = { val: 0 }
- componentDidMount() {
- // 第一次調(diào)用
- this.setState({ val: this.state.val + 1 });
- console.log('first setState', this.state);
- // 第二次調(diào)用
- this.setState({ val: this.state.val + 1 });
- console.log('second setState', this.state);
- // 第三次調(diào)用
- this.setState({ val: this.state.val + 1 }, () => {
- console.log('in callback', this.state)
- });
- }
- render() {
- return <div> val: { this.state.val } </div>
- }
- }
- export default App;
}}export default App;
熟悉 React 的同學(xué)應(yīng)該知道,在 React 的生命周期內(nèi),多次 setState 會(huì)被合并成一次,這里雖然連續(xù)進(jìn)行了三次 setState,state.val 的值實(shí)際上只重新計(jì)算了一次。
render結(jié)果
每次 setState 之后,立即獲取 state 會(huì)發(fā)現(xiàn)并沒有更新,只有在 setState 的回調(diào)函數(shù)內(nèi)才能拿到最新的結(jié)果,這點(diǎn)通過我們?cè)诳刂婆_(tái)輸出的結(jié)果就可以證實(shí)。
控制臺(tái)輸出
網(wǎng)上有很多文章稱 setState 是『異步操作』,所以導(dǎo)致 setState 之后并不能獲取到最新值,其實(shí)這個(gè)觀點(diǎn)是錯(cuò)誤的。setState 是一次同步操作,只是每次操作之后并沒有立即執(zhí)行,而是將 setState 進(jìn)行了緩存,mount 流程結(jié)束或事件操作結(jié)束,才會(huì)拿出所有的 state 進(jìn)行一次計(jì)算。如果 setState 脫離了 React 的生命周期或者 React 提供的事件流,setState 之后就能立即拿到結(jié)果。
我們修改上面的代碼,將 setState 放入 setTimeout 中,在下一個(gè)任務(wù)隊(duì)列進(jìn)行執(zhí)行。
- import React from 'react';
- class App extends React.Component {
- state = { val: 0 }
- componentDidMount() {
- setTimeout(() => {
- // 第一次調(diào)用
- this.setState({ val: this.state.val + 1 });
- console.log('first setState', this.state);
- // 第二次調(diào)用
- this.setState({ val: this.state.val + 1 });
- console.log('second setState', this.state);
- });
- }
- render() {
- return <div> val: { this.state.val } </div>
- }
- }
- export default App;
可以看到,setState 之后就能立即看到state.val 的值發(fā)生了變化。
控制臺(tái)輸出
為了更加深入理解 setState,下面簡單講解一下React 15 中 setState 的更新邏輯,下面的代碼是對(duì)源碼的一些精簡,并非完整邏輯。
舊版本 setState 源碼分析
setState 的主要邏輯都在 ReactUpdateQueue 中實(shí)現(xiàn),在調(diào)用 setState 后,并沒有立即修改 state,而是將傳入的參數(shù)放到了組件內(nèi)部的 _pendingStateQueue 中,之后調(diào)用 enqueueUpdate 來進(jìn)行更新。
- // 對(duì)外暴露的 React.Component
- function ReactComponent() {
- this.updater = ReactUpdateQueue;
- }
- // setState 方法掛載到原型鏈上
- ReactComponent.prototype.setState = function (partialState, callback) {
- // 調(diào)用 setState 后,會(huì)調(diào)用內(nèi)部的 updater.enqueueSetState
- this.updater.enqueueSetState(this, partialState);
- if (callback) {
- this.updater.enqueueCallback(this, callback, 'setState');
- }
- };
- var ReactUpdateQueue = {
- enqueueSetState(component, partialState) {
- // 在組件的 _pendingStateQueue 上暫存新的 state
- if (!component._pendingStateQueue) {
- component._pendingStateQueue = [];
- }
- var queue = component._pendingStateQueue;
- queue.push(partialState);
- enqueueUpdate(component);
- },
- enqueueCallback: function (component, callback, callerName) {
- // 在組件的 _pendingCallbacks 上暫存 callback
- if (component._pendingCallbacks) {
- component._pendingCallbacks.push(callback);
- } else {
- component._pendingCallbacks = [callback];
- }
- enqueueUpdate(component);
- }
- }
enqueueUpdate 首先會(huì)通過 batchingStrategy.isBatchingUpdates 判斷當(dāng)前是否在更新流程,如果不在更新流程,會(huì)調(diào)用 batchingStrategy.batchedUpdates() 進(jìn)行更新。如果在流程中,會(huì)將待更新的組件放入 dirtyComponents 進(jìn)行緩存。
- var dirtyComponents = [];
- function enqueueUpdate(component) {
- if (!batchingStrategy.isBatchingUpdates) {
- // 開始進(jìn)行批量更新
- batchingStrategy.batchedUpdates(enqueueUpdate, component);
- return;
- }
- // 如果在更新流程,則將組件放入臟組件隊(duì)列,表示組件待更新
- dirtyComponents.push(component);
- }
batchingStrategy 是 React 進(jìn)行批處理的一種策略,該策略的實(shí)現(xiàn)基于 Transaction,雖然名字和數(shù)據(jù)庫的事務(wù)一樣,但是做的事情卻不一樣。
- class ReactDefaultBatchingStrategyTransaction extends Transaction {
- constructor() {
- this.reinitializeTransaction()
- }
- getTransactionWrappers () {
- return [
- {
- initialize: () => {},
- close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
- },
- {
- initialize: () => {},
- close: () => {
- ReactDefaultBatchingStrategy.isBatchingUpdates = false;
- }
- }
- ]
- }
- }
- var transaction = new ReactDefaultBatchingStrategyTransaction();
- var batchingStrategy = {
- // 判斷是否在更新流程中
- isBatchingUpdates: false,
- // 開始進(jìn)行批量更新
- batchedUpdates: function (callback, component) {
- // 獲取之前的更新狀態(tài)
- var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
- // 將更新狀態(tài)修改為 true
- ReactDefaultBatchingStrategy.isBatchingUpdates = true;
- if (alreadyBatchingUpdates) {
- // 如果已經(jīng)在更新狀態(tài)中,等待之前的更新結(jié)束
- return callback(callback, component);
- } else {
- // 進(jìn)行更新
- return transaction.perform(callback, null, component);
- }
- }
- };
Transaction 通過 perform 方法啟動(dòng),然后通過擴(kuò)展的 getTransactionWrappers 獲取一個(gè)數(shù)組,該數(shù)組內(nèi)存在多個(gè) wrapper 對(duì)象,每個(gè)對(duì)象包含兩個(gè)屬性:initialize、close。perform 中會(huì)先調(diào)用所有的 wrapper.initialize,然后調(diào)用傳入的回調(diào),最后調(diào)用所有的 wrapper.close。
- class Transaction {
- reinitializeTransaction() {
- this.transactionWrappers = this.getTransactionWrappers();
- }
- perform(method, scope, ...param) {
- this.initializeAll(0);
- var ret = method.call(scope, ...param);
- this.closeAll(0);
- return ret;
- }
- initializeAll(startIndex) {
- var transactionWrappers = this.transactionWrappers;
- for (var i = startIndex; i < transactionWrappers.length; i++) {
- var wrapper = transactionWrappers[i];
- wrapper.initialize.call(this);
- }
- }
- closeAll(startIndex) {
- var transactionWrappers = this.transactionWrappers;
- for (var i = startIndex; i < transactionWrappers.length; i++) {
- var wrapper = transactionWrappers[i];
- wrapper.close.call(this);
- }
- }
- }
transaction.perform
React 源代碼的注釋中,也形象的展示了這一過程。
- /*
- * wrappers (injected at creation time)
- * + +
- * | |
- * +-----------------|--------|--------------+
- * | v | |
- * | +---------------+ | |
- * | +--| wrapper1 |---|----+ |
- * | | +---------------+ v | |
- * | | +-------------+ | |
- * | | +----| wrapper2 |--------+ |
- * | | | +-------------+ | | |
- * | | | | | |
- * | v v v v | wrapper
- * | +---+ +---+ +---------+ +---+ +---+ | invariants
- * perform(anyMethod) | | | | | | | | | | | | maintained
- * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
- * | | | | | | | | | | | |
- * | | | | | | | | | | | |
- * | | | | | | | | | | | |
- * | +---+ +---+ +---------+ +---+ +---+ |
- * | initialize close |
- * +-----------------------------------------+
- */
我們簡化一下代碼,再重新看一下 setState 的流程。
- // 1. 調(diào)用 Component.setState
- ReactComponent.prototype.setState = function (partialState) {
- this.updater.enqueueSetState(this, partialState);
- };
- // 2. 調(diào)用 ReactUpdateQueue.enqueueSetState,將 state 值放到 _pendingStateQueue 進(jìn)行緩存
- var ReactUpdateQueue = {
- enqueueSetState(component, partialState) {
- var queue = component._pendingStateQueue || (component._pendingStateQueue = []);
- queue.push(partialState);
- enqueueUpdate(component);
- }
- }
- // 3. 判斷是否在更新過程中,如果不在就進(jìn)行更新
- var dirtyComponents = [];
- function enqueueUpdate(component) {
- // 如果之前沒有更新,此時(shí)的 isBatchingUpdates 肯定是 false
- if (!batchingStrategy.isBatchingUpdates) {
- // 調(diào)用 batchingStrategy.batchedUpdates 進(jìn)行更新
- batchingStrategy.batchedUpdates(enqueueUpdate, component);
- return;
- }
- dirtyComponents.push(component);
- }
- // 4. 進(jìn)行更新,更新邏輯放入事務(wù)中進(jìn)行處理
- var batchingStrategy = {
- isBatchingUpdates: false,
- // 注意:此時(shí)的 callback 為 enqueueUpdate
- batchedUpdates: function (callback, component) {
- var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
- ReactDefaultBatchingStrategy.isBatchingUpdates = true;
- if (alreadyBatchingUpdates) {
- // 如果已經(jīng)在更新狀態(tài)中,重新調(diào)用 enqueueUpdate,將 component 放入 dirtyComponents
- return callback(callback, component);
- } else {
- // 進(jìn)行事務(wù)操作
- return transaction.perform(callback, null, component);
- }
- }
- };
啟動(dòng)事務(wù)可以拆分成三步來看:
先執(zhí)行 wrapper 的 initialize,此時(shí)的 initialize 都是一些空函數(shù),可以直接跳過;
然后執(zhí)行 callback(也就是 enqueueUpdate),執(zhí)行 enqueueUpdate 時(shí),由于已經(jīng)進(jìn)入了更新狀態(tài),batchingStrategy.isBatchingUpdates 被修改成了 true,所以最后還是會(huì)把 component 放入臟組件隊(duì)列,等待更新;
后面執(zhí)行的兩個(gè) close 方法,第一個(gè)方法的 flushBatchedUpdates 是用來進(jìn)行組件更新的,第二個(gè)方法用來修改更新狀態(tài),表示更新已經(jīng)結(jié)束。
- getTransactionWrappers () {
- return [
- {
- initialize: () => {},
- close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
- },
- {
- initialize: () => {},
- close: () => {
- ReactDefaultBatchingStrategy.isBatchingUpdates = false;
- }
- }
- ]
- }
flushBatchedUpdates 里面會(huì)取出所有的臟組件隊(duì)列進(jìn)行 diff,最后更新到 DOM。
- function flushBatchedUpdates() {
- if (dirtyComponents.length) {
- runBatchedUpdates()
- }
- };
- function runBatchedUpdates() {
- // 省略了一些去重和排序的操作
- for (var i = 0; i < dirtyComponents.length; i++) {
- var component = dirtyComponents[i];
- // 判斷組件是否需要更新,然后進(jìn)行 diff 操作,最后更新 DOM。
- ReactReconciler.performUpdateIfNecessary(component);
- }
- }
performUpdateIfNecessary() 會(huì)調(diào)用 Component.updateComponent(),在updateComponent() 中,會(huì)從 _pendingStateQueue 中取出所有的值來更新。
- // 獲取最新的 state
- _processPendingState() {
- var inst = this._instance;
- var queue = this._pendingStateQueue;
- var nextState = { ...inst.state };
- for (var i = 0; i < queue.length; i++) {
- var partial = queue[i];
- Object.assign(
- nextState,
- typeof partial === 'function' ? partial(inst, nextState) : partial
- );
- }
- return nextState;
- }
- // 更新組件
- updateComponent(prevParentElement, nextParentElement) {
- var inst = this._instance;
- var prevProps = prevParentElement.props;
- var nextProps = nextParentElement.props;
- var nextState = this._processPendingState();
- var shouldUpdate =
- !shallowEqual(prevProps, nextProps) ||
- !shallowEqual(inst.state, nextState);
- if (shouldUpdate) {
- // diff 、update DOM
- } else {
- inst.props = nextProps;
- inst.state = nextState;
- }
- // 后續(xù)的操作包括判斷組件是否需要更新、diff、更新到 DOM
- }
setState 合并原因
按照剛剛講解的邏輯,setState 的時(shí)候,batchingStrategy.isBatchingUpdates 為 false 會(huì)開啟一個(gè)事務(wù),將組件放入臟組件隊(duì)列,最后進(jìn)行更新操作,而且這里都是同步操作。講道理,setState 之后,我們可以立即拿到最新的 state。
然而,事實(shí)并非如此,在 React 的生命周期及其事件流中,batchingStrategy.isBatchingUpdates 的值早就被修改成了 true。可以看看下面兩張圖:
Mount
事件調(diào)用
在組件 mount 和事件調(diào)用的時(shí)候,都會(huì)調(diào)用 batchedUpdates,這個(gè)時(shí)候已經(jīng)開始了事務(wù),所以只要不脫離 React,不管多少次 setState 都會(huì)把其組件放入臟組件隊(duì)列等待更新。一旦脫離 React 的管理,比如在 setTimeout 中,setState 立馬變成單打獨(dú)斗。
Concurrent 模式
React 16 引入的 Fiber 架構(gòu),就是為了后續(xù)的異步渲染能力做鋪墊,雖然架構(gòu)已經(jīng)切換,但是異步渲染的能力并沒有正式上線,我們只能在實(shí)驗(yàn)版中使用。異步渲染指的是 Concurrent 模式,下面是官網(wǎng)的介紹:
“Concurrent 模式是 React 的新功能,可幫助應(yīng)用保持響應(yīng),并根據(jù)用戶的設(shè)備性能和網(wǎng)速進(jìn)行適當(dāng)?shù)恼{(diào)整。
優(yōu)點(diǎn)
除了 Concurrent 模式,React 還提供了另外兩個(gè)模式, Legacy 模式依舊是同步更新的方式,可以認(rèn)為和舊版本保持一致的兼容模式,而 Blocking 模式是一個(gè)過渡版本。
模式差異
Concurrent 模式說白就是讓組件更新異步化,切分時(shí)間片,渲染之前的調(diào)度、diff、更新都只在指定時(shí)間片進(jìn)行,如果超時(shí)就暫停放到下個(gè)時(shí)間片進(jìn)行,中途給瀏覽器一個(gè)喘息的時(shí)間。
“瀏覽器是單線程,它將 GUI 描繪,時(shí)間器處理,事件處理,JS 執(zhí)行,遠(yuǎn)程資源加載統(tǒng)統(tǒng)放在一起。當(dāng)做某件事,只有將它做完才能做下一件事。如果有足夠的時(shí)間,瀏覽器是會(huì)對(duì)我們的代碼進(jìn)行編譯優(yōu)化(JIT)及進(jìn)行熱代碼優(yōu)化,一些 DOM 操作,內(nèi)部也會(huì)對(duì) reflow 進(jìn)行處理。reflow 是一個(gè)性能黑洞,很可能讓頁面的大多數(shù)元素進(jìn)行重新布局。瀏覽器的運(yùn)作流程: 渲染 -> tasks -> 渲染 -> tasks -> 渲染 -> ....這些 tasks 中有些我們可控,有些不可控,比如 setTimeout 什么時(shí)候執(zhí)行不好說,它總是不準(zhǔn)時(shí);資源加載時(shí)間不可控。但一些JS我們可以控制,讓它們分派執(zhí)行,tasks的時(shí)長不宜過長,這樣瀏覽器就有時(shí)間優(yōu)化 JS 代碼與修正 reflow !總結(jié)一句,就是讓瀏覽器休息好,瀏覽器就能跑得更快。-- by 司徒正美 《React Fiber架構(gòu)》
模式差異
這里有個(gè) demo,上面是一個(gè)🌟圍繞☀️運(yùn)轉(zhuǎn)的動(dòng)畫,下面是 React 定時(shí) setState 更新視圖,同步模式下,每次 setState 都會(huì)造成上面的動(dòng)畫卡頓,而異步模式下的動(dòng)畫就很流暢。
同步模式:
同步模式
異步模式:
異步模式
如何使用
雖然很多文章都在介紹 Concurrent 模式,但是這個(gè)能力并沒有真正上線,想要使用只能安裝實(shí)驗(yàn)版本。也可以直接通過這個(gè) cdn :https://unpkg.com/browse/react@0.0.0-experimental-94c0244ba/ 。
- npm install react@experimental react-dom@experimental
如果要開啟 Concurrent 模式,不能使用之前的 ReactDOM.render,需要替換成 ReactDOM.createRoot,而在實(shí)驗(yàn)版本中,由于 API 不夠穩(wěn)定, 需要通過 ReactDOM.unstable_createRoot來啟用 Concurrent 模式。
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.unstable_createRoot(
- document.getElementById('root')
- ).render(<App />);
setState 合并更新
還記得之前 React15 的案例中,setTimeout 中進(jìn)行 setState ,state.val 的值會(huì)立即發(fā)生變化。同樣的代碼,我們拿到 Concurrent 模式下運(yùn)行一次。
- import React from 'react';
- class App extends React.Component {
- state = { val: 0 }
- componentDidMount() {
- setTimeout(() => {
- // 第一次調(diào)用
- this.setState({ val: this.state.val + 1 });
- console.log('first setState', this.state);
- // 第二次調(diào)用
- this.setState({ val: this.state.val + 1 });
- console.log('second setState', this.state);
- this.setState({ val: this.state.val + 1 }, () => {
- console.log(this.state);
- });
- });
- }
- render() {
- return <div> val: { this.state.val } </div>
- }
- }
- export default App;
控制臺(tái)輸出
說明在 Concurrent 模式下,即使脫離了 React 的生命周期,setState 依舊能夠合并更新。主要原因是 Concurrent 模式下,真正的更新操作被移到了下一個(gè)事件隊(duì)列中,類似于 Vue 的 nextTick。
更新機(jī)制變更
我們修改一下 demo,然后看下點(diǎn)擊按鈕之后的調(diào)用棧。
- import React from 'react';
- class App extends React.Component {
- state = { val: 0 }
- clickBtn() {
- this.setState({ val: this.state.val + 1 });
- }
- render() {
- return (<div>
- <button onClick={() => {this.clickBtn()}}>click add</button>
- <div>val: { this.state.val }</div>
- </div>)
- }
- }
- export default App;
調(diào)用棧
調(diào)用棧
onClick 觸發(fā)后,進(jìn)行 setState 操作,然后調(diào)用 enquueState 方法,到這里看起來好像和之前的模式一樣,但是后面的操作基本都變了,因?yàn)?React 16 中已經(jīng)沒有了事務(wù)一說。
- Component.setState() => enquueState() => scheduleUpdate() => scheduleCallback()
- => requestHostCallback(flushWork) => postMessage()
真正的異步化邏輯就在 requestHostCallback、postMessage 里面,這是 React 內(nèi)部自己實(shí)現(xiàn)的一個(gè)調(diào)度器:https://github.com/facebook/react/blob/v16.13.1/packages/scheduler/index.js。
- function unstable_scheduleCallback(priorityLevel, calback) {
- var currentTime = getCurrentTime();
- var startTime = currentTime + delay;
- var newTask = {
- id: taskIdCounter++,
- startTime: startTime, // 任務(wù)開始時(shí)間
- expirationTime: expirationTime, // 任務(wù)終止時(shí)間
- priorityLevel: priorityLevel, // 調(diào)度優(yōu)先級(jí)
- callback: callback, // 回調(diào)函數(shù)
- };
- if (startTime > currentTime) {
- // 超時(shí)處理,將任務(wù)放到 taskQueue,下一個(gè)時(shí)間片執(zhí)行
- // 源碼中其實(shí)是 timerQueue,后續(xù)會(huì)有個(gè)操作將 timerQueue 的 task 轉(zhuǎn)移到 taskQueue
- push(taskQueue, newTask)
- } else {
- requestHostCallback(flushWork);
- }
- return newTask;
- }
requestHostCallback 的實(shí)現(xiàn)依賴于 MessageChannel,但是 MessageChannel 在這里并不是做消息通信用的,而是利用它的異步能力,給瀏覽器一個(gè)喘息的機(jī)會(huì)。說起 MessageChannel,Vue 2.5 的 nextTick 也有使用,但是 2.6 發(fā)布時(shí)又取消了。
vue@2.5
MessageChannel 會(huì)暴露兩個(gè)對(duì)象,port1 和 port2,port1 發(fā)送的消息能被 port2 接收,同樣 port2 發(fā)送的消息也能被 port1 接收,只是接收消息的時(shí)機(jī)會(huì)放到下一個(gè) macroTask 中。
- var { port1, port2 } = new MessageChannel();
- // port1 接收 port2 的消息
- port1.onmessage = function (msg) { console.log('MessageChannel exec') }
- // port2 發(fā)送消息
- port2.postMessage(null)
- new Promise(r => r()).then(() => console.log('promise exec'))
- setTimeout(() => console.log('setTimeout exec'))
- console.log('start run')
執(zhí)行結(jié)果
可以看到,port1 接收消息的時(shí)機(jī)比 Promise 所在的 microTask 要晚,但是早于 setTimeout。React 利用這個(gè)能力,給了瀏覽器一個(gè)喘息的時(shí)間,不至于被餓死。
還是之前的案例,同步更新時(shí)沒有給瀏覽器任何喘息,造成視圖的卡頓。
同步更新
異步更新時(shí),拆分了時(shí)間片,給了瀏覽器充分的時(shí)間更新動(dòng)畫。
異步更新
還是回到代碼層面,看看 React 是如何利用 MessageChannel 的。
- var isMessageLoopRunning = false; // 更新狀態(tài)
- var scheduledHostCallback = null; // 全局的回調(diào)
- var channel = new MessageChannel();
- var port = channel.port2;
- channel.port1.onmessage = function () {
- if (scheduledHostCallback !== null) {
- var currentTime = getCurrentTime();
- // 重置超時(shí)時(shí)間
- deadline = currentTime + yieldInterval;
- var hasTimeRemaining = true;
- // 執(zhí)行 callback
- var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
- if (!hasMoreWork) {
- // 已經(jīng)沒有任務(wù)了,修改狀態(tài)
- isMessageLoopRunning = false;
- scheduledHostCallback = null;
- } else {
- // 還有任務(wù),放到下個(gè)任務(wù)隊(duì)列執(zhí)行,給瀏覽器喘息的機(jī)會(huì)
- port.postMessage(null);
- }
- } else {
- isMessageLoopRunning = false;
- }
- };
- requestHostCallback = function (callback) {
- // callback 掛載到 scheduledHostCallback
- scheduledHostCallback = callback;
- if (!isMessageLoopRunning) {
- isMessageLoopRunning = true;
- // 推送消息,下個(gè)隊(duì)列隊(duì)列調(diào)用 callback
- port.postMessage(null);
- }
- };
再看看之前傳入的 callback(flushWork),調(diào)用 workLoop,取出 taskQueue 中的任務(wù)執(zhí)行。
- // 精簡了相當(dāng)多的代碼
- function flushWork(hasTimeRemaining, initialTime) {
- return workLoop(hasTimeRemaining, initialTime);
- }
- function workLoop(hasTimeRemaining, initialTime) {
- var currentTime = initialTime;
- // scheduleCallback 進(jìn)行了 taskQueue 的 push 操作
- // 這里是獲取之前時(shí)間片未執(zhí)行的操作
- currentTask = peek(taskQueue);
- while (currentTask !== null) {
- if (currentTask.expirationTime > currentTime) {
- // 超時(shí)需要中斷任務(wù)
- break;
- }
- currentTask.callback(); // 執(zhí)行任務(wù)回調(diào)
- currentTime = getCurrentTime(); // 重置當(dāng)前時(shí)間
- currentTask = peek(taskQueue); // 獲取新的任務(wù)
- }
- // 如果當(dāng)前任務(wù)不為空,表明是超時(shí)中斷,返回 true
- if (currentTask !== null) {
- return true;
- } else {
- return false;
- }
- }
可以看出,React 通過 expirationTime 來判斷是否超時(shí),如果超時(shí)就把任務(wù)放到后面來執(zhí)行。所以,異步模型中 setTimeout 里面進(jìn)行 setState,只要當(dāng)前時(shí)間片沒有結(jié)束(currentTime 小于 expirationTime),依舊可以將多個(gè) setState 合并成一個(gè)。
接下來我們?cè)僮鲆粋€(gè)實(shí)驗(yàn),在 setTimeout 中連續(xù)進(jìn)行 500 次的 setState,看看最后生效的次數(shù)。
- import React from 'react';
- class App extends React.Component {
- state = { val: 0 }
- clickBtn() {
- for (let i = 0; i < 500; i++) {
- setTimeout(() => {
- this.setState({ val: this.state.val + 1 });
- })
- }
- }
- render() {
- return (<div>
- <button onClick={() => {this.clickBtn()}}>click add</button>
- <div>val: { this.state.val }</div>
- </div>)
- }
- }
- export default App;
先看看同步模式下:
同步模式
再看看異步模式下:
異步模式
最后 setState 的次數(shù)是 81 次,表明這里的操作在 7 個(gè)時(shí)間片下進(jìn)行的。
總結(jié)這篇文章前后花費(fèi)時(shí)間比較久,看 React 的源碼確實(shí)很痛苦,因?yàn)橹皼]有了解過,剛開始是看一些文章的分析,但是很多模棱兩可的地方,無奈只能在源碼上進(jìn)行 debug,而且一次性看了 React 15、16 兩個(gè)版本的代碼,感覺腦子都有些不夠用了。
當(dāng)然這篇文章只是簡單介紹了更新機(jī)制從同步到異步的過程,其實(shí) React 16 的更新除了異步之外,在時(shí)間片的劃分、任務(wù)的優(yōu)先級(jí)上還有很多細(xì)節(jié),這些東西放到下篇文章來講,不知不覺又是一個(gè)新坑。
本文轉(zhuǎn)載自微信公眾號(hào)「更了不起的前端」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系更了不起的前端公眾號(hào)。