React 架構的演變 - 從同步到異步


title: React 架構的演變 - 從同步到異步html

date: 2020/09/23前端

categories:vue

  • 前端

tags:react

  • 前端框架
  • JavaScript
  • React

寫這篇文章的目的,主要是想弄懂 React 最新的 fiber 架構究竟是什麼東西,可是看了網上的不少文章,要不模棱兩可,要不就是一頓複製粘貼,根本看不懂,因而開始認真鑽研源碼。鑽研過程當中,發現我想得太簡單了,React 源碼的複雜程度遠超個人想象,因而打算分幾個模塊了剖析,今天先講一講 React 的更新策略從同步變爲異步的演變過程。git

從 setState 提及

React 16 之因此要進行一次大重構,是由於 React 以前的版本有一些不可避免的缺陷,一些更新操做,須要由同步改爲異步。因此咱們先聊聊 React 15 是如何進行一次 setState 的。github

import React from 'react';

class App extends React.Component {

state = { val: 0 }

componentDidMount() {

// 第一次調用

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

console.log('first setState', this.state);

// 第二次調用

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

console.log('second setState', this.state);

// 第三次調用

this.setState({ val: this.state.val + 1 }, () => {

console.log('in callback', this.state)

});

}

render() {

return <div> val: { this.state.val } </div>

}

}

export default App;

熟悉 React 的同窗應該知道,在 React 的生命週期內,屢次 setState 會被合併成一次,這裏雖然連續進行了三次 setState,state.val 的值實際上只從新計算了一次。數據庫

render結果

每次 setState 以後,當即獲取 state 會發現並無更新,只有在 setState 的回調函數內才能拿到最新的結果,這點經過咱們在控制檯輸出的結果就能夠證明。npm

控制檯輸出

網上有不少文章稱 setState 是『異步操做』,因此致使 setState 以後並不能獲取到最新值,其實這個觀點是錯誤的。setState 是一次同步操做,只是每次操做以後並無當即執行,而是將 setState 進行了緩存,mount 流程結束或事件操做結束,纔會拿出全部的 state 進行一次計算。若是 setState 脫離了 React 的生命週期或者 React 提供的事件流,setState 以後就能當即拿到結果。數組

咱們修改上面的代碼,將 setState 放入 setTimeout 中,在下一個任務隊列進行執行。promise

import React from 'react';

class App extends React.Component {

state = { val: 0 }

componentDidMount() {

setTimeout(() => {

// 第一次調用

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

console.log('first setState', this.state);

// 第二次調用

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 的值發生了變化。

控制檯輸出

爲了更加深刻理解 setState,下面簡單講解一下React 15 中 setState 的更新邏輯,下面的代碼是對源碼的一些精簡,並不是完整邏輯。

舊版本 setState 源碼分析

setState 的主要邏輯都在 ReactUpdateQueue 中實現,在調用 setState 後,並無當即修改 state,而是將傳入的參數放到了組件內部的 _pendingStateQueue 中,以後調用 enqueueUpdate 來進行更新。

// 對外暴露的 React.Component

function ReactComponent() {

this.updater = ReactUpdateQueue;

}

// setState 方法掛載到原型鏈上

ReactComponent.prototype.setState = function (partialState, callback) {

// 調用 setState 後,會調用內部的 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 首先會經過 batchingStrategy.isBatchingUpdates 判斷當前是否在更新流程,若是不在更新流程,會調用 batchingStrategy.batchedUpdates() 進行更新。若是在流程中,會將待更新的組件放入 dirtyComponents 進行緩存。

var dirtyComponents = [];

function enqueueUpdate(component) {

if (!batchingStrategy.isBatchingUpdates) {

// 開始進行批量更新

batchingStrategy.batchedUpdates(enqueueUpdate, component);

return;

}

// 若是在更新流程,則將組件放入髒組件隊列,表示組件待更新

dirtyComponents.push(component);

}

batchingStrategy 是 React 進行批處理的一種策略,該策略的實現基於 Transaction,雖然名字和數據庫的事務同樣,可是作的事情卻不同。

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,

// 開始進行批量更新

batchedUpdates: function (callback, component) {

// 獲取以前的更新狀態

var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

// 將更新狀態修改成 true

ReactDefaultBatchingStrategy.isBatchingUpdates = true;

if (alreadyBatchingUpdates) {

// 若是已經在更新狀態中,等待以前的更新結束

return callback(callback, component);

} else {

// 進行更新

return transaction.perform(callback, null, component);

}

}

};

Transaction 經過 perform 方法啓動,而後經過擴展的 getTransactionWrappers 獲取一個數組,該數組內存在多個 wrapper 對象,每一個對象包含兩個屬性:initializeclose。perform 中會先調用全部的 wrapper.initialize,而後調用傳入的回調,最後調用全部的 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. 調用 Component.setState

ReactComponent.prototype.setState = function (partialState) {

this.updater.enqueueSetState(this, partialState);

};

// 2. 調用 ReactUpdateQueue.enqueueSetState,將 state 值放到 _pendingStateQueue 進行緩存

var ReactUpdateQueue = {

enqueueSetState(component, partialState) {

var queue = component._pendingStateQueue || (component._pendingStateQueue = []);

queue.push(partialState);

enqueueUpdate(component);

}

}

// 3. 判斷是否在更新過程當中,若是不在就進行更新

var dirtyComponents = [];

function enqueueUpdate(component) {

// 若是以前沒有更新,此時的 isBatchingUpdates 確定是 false

if (!batchingStrategy.isBatchingUpdates) {

// 調用 batchingStrategy.batchedUpdates 進行更新

batchingStrategy.batchedUpdates(enqueueUpdate, component);

return;

}

dirtyComponents.push(component);

}

// 4. 進行更新,更新邏輯放入事務中進行處理

var batchingStrategy = {

isBatchingUpdates: false,

// 注意:此時的 callback 爲 enqueueUpdate

batchedUpdates: function (callback, component) {

var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

ReactDefaultBatchingStrategy.isBatchingUpdates = true;

if (alreadyBatchingUpdates) {

// 若是已經在更新狀態中,從新調用 enqueueUpdate,將 component 放入 dirtyComponents

return callback(callback, component);

} else {

// 進行事務操做

return transaction.perform(callback, null, component);

}

}

};

啓動事務能夠拆分紅三步來看:

  1. 先執行 wrapper 的 initialize,此時的 initialize 都是一些空函數,能夠直接跳過;
  2. 而後執行 callback(也就是 enqueueUpdate),執行 enqueueUpdate 時,因爲已經進入了更新狀態,batchingStrategy.isBatchingUpdates 被修改爲了 true,因此最後仍是會把 component 放入髒組件隊列,等待更新;
  3. 後面執行的兩個 close 方法,第一個方法的 flushBatchedUpdates 是用來進行組件更新的,第二個方法用來修改更新狀態,表示更新已經結束。
getTransactionWrappers () {

return [

{

initialize: () => {},

close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)

},

{

initialize: () => {},

close: () => {

ReactDefaultBatchingStrategy.isBatchingUpdates = false;

}

}

]

}

flushBatchedUpdates 裏面會取出全部的髒組件隊列進行 diff,最後更新到 DOM。

function flushBatchedUpdates() {

if (dirtyComponents.length) {

runBatchedUpdates()

}

};

function runBatchedUpdates() {

// 省略了一些去重和排序的操做

for (var i = 0; i < dirtyComponents.length; i++) {

var component = dirtyComponents[i];

// 判斷組件是否須要更新,而後進行 diff 操做,最後更新 DOM。

ReactReconciler.performUpdateIfNecessary(component);

}

}

performUpdateIfNecessary() 會調用 Component.updateComponent(),在 updateComponent() 中,會從 _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;

}

// 後續的操做包括判斷組件是否須要更新、diff、更新到 DOM

}

setState 合併緣由

按照剛剛講解的邏輯,setState 的時候,batchingStrategy.isBatchingUpdatesfalse 會開啓一個事務,將組件放入髒組件隊列,最後進行更新操做,並且這裏都是同步操做。講道理,setState 以後,咱們能夠當即拿到最新的 state。

然而,事實並不是如此,在 React 的生命週期及其事件流中,batchingStrategy.isBatchingUpdates 的值早就被修改爲了 true。能夠看看下面兩張圖:

Mount

事件調用

在組件 mount 和事件調用的時候,都會調用 batchedUpdates,這個時候已經開始了事務,因此只要不脫離 React,無論多少次 setState 都會把其組件放入髒組件隊列等待更新。一旦脫離 React 的管理,好比在 setTimeout 中,setState 立馬變成單打獨鬥。

Concurrent 模式

React 16 引入的 Fiber 架構,就是爲了後續的異步渲染能力作鋪墊,雖然架構已經切換,可是異步渲染的能力並無正式上線,咱們只能在實驗版中使用。異步渲染指的是 Concurrent 模式,下面是官網的介紹:

Concurrent 模式是 React 的新功能,可幫助應用保持響應,並根據用戶的設備性能和網速進行適當的調整。

優勢

除了 Concurrent 模式,React 還提供了另外兩個模式, Legacy 模式依舊是同步更新的方式,能夠認爲和舊版本保持一致的兼容模式,而 Blocking 模式是一個過渡版本。

模式差別

Concurrent 模式說白就是讓組件更新異步化,切分時間片,渲染以前的調度、diff、更新都只在指定時間片進行,若是超時就暫停放到下個時間片進行,中途給瀏覽器一個喘息的時間。

瀏覽器是單線程,它將 GUI 描繪,時間器處理,事件處理,JS 執行,遠程資源加載通通放在一塊兒。當作某件事,只有將它作完才能作下一件事。若是有足夠的時間,瀏覽器是會對咱們的代碼進行編譯優化(JIT)及進行熱代碼優化,一些 DOM 操做,內部也會對 reflow 進行處理。reflow 是一個性能黑洞,極可能讓頁面的大多數元素進行從新佈局。

瀏覽器的運做流程: 渲染 -> tasks -> 渲染 -> tasks -> 渲染 -> ....

這些 tasks 中有些咱們可控,有些不可控,好比 setTimeout 何時執行很差說,它老是不許時;資源加載時間不可控。但一些JS咱們能夠控制,讓它們分派執行,tasks的時長不宜過長,這樣瀏覽器就有時間優化 JS 代碼與修正 reflow !

總結一句,就是讓瀏覽器休息好,瀏覽器就能跑得更快

-- by 司徒正美 《React Fiber架構》

模式差別

這裏有個 demo,上面是一個🌟圍繞☀️運轉的動畫,下面是 React 定時 setState 更新視圖,同步模式下,每次 setState 都會形成上面的動畫卡頓,而異步模式下的動畫就很流暢。

同步模式

同步模式

異步模式

異步模式

如何使用

雖然不少文章都在介紹 Concurrent 模式,可是這個能力並無真正上線,想要使用只能安裝實驗版本。也能夠直接經過這個 cdn :https://unpkg.com/browse/react@0.0.0-experimental-94c0244ba/

npm install react@experimental react-dom@experimental

若是要開啓 Concurrent 模式,不能使用以前的 ReactDOM.render,須要替換成 ReactDOM.createRoot,而在實驗版本中,因爲 API 不夠穩定, 須要經過 ReactDOM.unstable_createRoot 來啓用 Concurrent 模式。

import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.unstable_createRoot(

document.getElementById('root')

).render(<App />);

setState 合併更新

還記得以前 React15 的案例中,setTimeout 中進行 setState ,state.val 的值會當即發生變化。一樣的代碼,咱們拿到 Concurrent 模式下運行一次。

import React from 'react';

class App extends React.Component {

state = { val: 0 }

componentDidMount() {

setTimeout(() => {

// 第一次調用

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

console.log('first setState', this.state);

// 第二次調用

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;

控制檯輸出

說明在 Concurrent 模式下,即便脫離了 React 的生命週期,setState 依舊可以合併更新。主要緣由是 Concurrent 模式下,真正的更新操做被移到了下一個事件隊列中,相似於 Vue 的 nextTick。

更新機制變動

咱們修改一下 demo,而後看下點擊按鈕以後的調用棧。

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;

調用棧

調用棧

onClick 觸發後,進行 setState 操做,而後調用 enquueState 方法,到這裏看起來好像和以前的模式同樣,可是後面的操做基本都變了,由於 React 16 中已經沒有了事務一說。

Component.setState() => enquueState() => scheduleUpdate() => scheduleCallback()

=> requestHostCallback(flushWork) => postMessage()

真正的異步化邏輯就在 requestHostCallbackpostMessage 裏面,這是 React 內部本身實現的一個調度器: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, // 任務開始時間

expirationTime: expirationTime, // 任務終止時間

priorityLevel: priorityLevel, // 調度優先級

callback: callback, // 回調函數

};

if (startTime > currentTime) {

// 超時處理,將任務放到 taskQueue,下一個時間片執行

// 源碼中實際上是 timerQueue,後續會有個操做將 timerQueue 的 task 轉移到 taskQueue

push(taskQueue, newTask)

} else {

requestHostCallback(flushWork);

}

return newTask;

}

requestHostCallback 的實現依賴於 MessageChannel,可是 MessageChannel 在這裏並非作消息通訊用的,而是利用它的異步能力,給瀏覽器一個喘息的機會。提及 MessageChannel,Vue 2.5 的 nextTick 也有使用,可是 2.6 發佈時又取消了。

vue@2.5

MessageChannel 會暴露兩個對象,port1port2port1 發送的消息能被 port2 接收,一樣 port2 發送的消息也能被 port1 接收,只是接收消息的時機會放到下一個 macroTask 中。

var { port1, port2 } = new MessageChannel();

// port1 接收 port2 的消息

port1.onmessage = function (msg) { console.log('MessageChannel exec') }

// port2 發送消息

port2.postMessage(null)

new Promise(r => r()).then(() => console.log('promise exec'))

setTimeout(() => console.log('setTimeout exec'))

console.log('start run')

執行結果

能夠看到,port1 接收消息的時機比 Promise 所在的 microTask 要晚,可是早於 setTimeout。React 利用這個能力,給了瀏覽器一個喘息的時間,不至於被餓死。

仍是以前的案例,同步更新時沒有給瀏覽器任何喘息,形成視圖的卡頓。

同步更新

異步更新時,拆分了時間片,給了瀏覽器充分的時間更新動畫。

異步更新

仍是回到代碼層面,看看 React 是如何利用 MessageChannel 的。

var isMessageLoopRunning = false; // 更新狀態

var scheduledHostCallback = null; // 全局的回調

var channel = new MessageChannel();

var port = channel.port2;

channel.port1.onmessage = function () {

if (scheduledHostCallback !== null) {

var currentTime = getCurrentTime();

// 重置超時時間

deadline = currentTime + yieldInterval;

var hasTimeRemaining = true;

// 執行 callback

var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);

if (!hasMoreWork) {

// 已經沒有任務了,修改狀態

isMessageLoopRunning = false;

scheduledHostCallback = null;

} else {

// 還有任務,放到下個任務隊列執行,給瀏覽器喘息的機會

port.postMessage(null);

}

} else {

isMessageLoopRunning = false;

}

};

requestHostCallback = function (callback) {

// callback 掛載到 scheduledHostCallback

scheduledHostCallback = callback;

if (!isMessageLoopRunning) {

isMessageLoopRunning = true;

// 推送消息,下個隊列隊列調用 callback

port.postMessage(null);

}

};

再看看以前傳入的 callback(flushWork),調用 workLoop,取出 taskQueue 中的任務執行。

// 精簡了至關多的代碼

function flushWork(hasTimeRemaining, initialTime) {

return workLoop(hasTimeRemaining, initialTime);

}

function workLoop(hasTimeRemaining, initialTime) {

var currentTime = initialTime;

// scheduleCallback 進行了 taskQueue 的 push 操做

// 這裏是獲取以前時間片未執行的操做

currentTask = peek(taskQueue);

while (currentTask !== null) {

if (currentTask.expirationTime > currentTime) {

// 超時須要中斷任務

break;

}

currentTask.callback(); // 執行任務回調

currentTime = getCurrentTime(); // 重置當前時間

currentTask = peek(taskQueue); // 獲取新的任務

}

// 若是當前任務不爲空,代表是超時中斷,返回 true

if (currentTask !== null) {

return true;

} else {

return false;

}

}

能夠看出,React 經過 expirationTime 來判斷是否超時,若是超時就把任務放到後面來執行。因此,異步模型中 setTimeout 裏面進行 setState,只要當前時間片沒有結束(currentTime 小於 expirationTime),依舊能夠將多個 setState 合併成一個。

接下來咱們再作一個實驗,在 setTimeout 中連續進行 500 次的 setState,看看最後生效的次數。

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 的次數是 81 次,代表這裏的操做在 81 個時間片下進行的,每一個時間片更新了一次。

總結

這篇文章先後花費時間比較久,看 React 的源碼確實很痛苦,由於以前沒有了解過,剛開始是看一些文章的分析,可是不少模棱兩可的地方,無奈只能在源碼上進行 debug,並且一次性看了 React 1五、16 兩個版本的代碼,感受腦子都有些不夠用了。

固然這篇文章只是簡單介紹了更新機制從同步到異步的過程,其實 React 16 的更新除了異步以外,在時間片的劃分、任務的優先級上還有不少細節,這些東西放到下篇文章來說,不知不覺又是一個新坑。

image

相關文章
相關標籤/搜索