自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React 架構(gòu)的演變 - 從同步到異步

開發(fā) 架構(gòu)
寫這篇文章的目的,主要是想弄懂 React 最新的 fiber 架構(gòu)到底是什么東西,但是看了網(wǎng)上的很多文章,要不模棱兩可,要不就是一頓復(fù)制粘貼,根本看不懂,于是開始認(rèn)真鉆研源碼。

[[343617]]

寫這篇文章的目的,主要是想弄懂 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 的。

  1. import React from 'react'
  2.  
  3. class App extends React.Component { 
  4.   state = { val: 0 } 
  5.   componentDidMount() { 
  6.     // 第一次調(diào)用 
  7.     this.setState({ val: this.state.val + 1 }); 
  8.     console.log('first setState', this.state); 
  9.  
  10.     // 第二次調(diào)用 
  11.     this.setState({ val: this.state.val + 1 }); 
  12.     console.log('second setState', this.state); 
  13.  
  14.     // 第三次調(diào)用 
  15.     this.setState({ val: this.state.val + 1 }, () => { 
  16.       console.log('in callback', this.state) 
  17.     }); 
  18.   } 
  19.   render() { 
  20.     return <div> val: { this.state.val } </div> 
  21.   } 
  22.  
  23. export default App; 
val: { this.state.val }

}}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í)行。

  1. import React from 'react'
  2.  
  3. class App extends React.Component { 
  4.   state = { val: 0 } 
  5.   componentDidMount() { 
  6.     setTimeout(() => { 
  7.       // 第一次調(diào)用 
  8.       this.setState({ val: this.state.val + 1 }); 
  9.       console.log('first setState', this.state); 
  10.    
  11.       // 第二次調(diào)用 
  12.       this.setState({ val: this.state.val + 1 }); 
  13.       console.log('second setState', this.state); 
  14.     }); 
  15.   } 
  16.   render() { 
  17.     return <div> val: { this.state.val } </div> 
  18.   } 
  19.  
  20. 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)行更新。

  1. // 對(duì)外暴露的 React.Component 
  2. function ReactComponent() { 
  3.   this.updater = ReactUpdateQueue; 
  4. // setState 方法掛載到原型鏈上 
  5. ReactComponent.prototype.setState = function (partialState, callback) { 
  6.   // 調(diào)用 setState 后,會(huì)調(diào)用內(nèi)部的 updater.enqueueSetState 
  7.   this.updater.enqueueSetState(this, partialState); 
  8.   if (callback) { 
  9.     this.updater.enqueueCallback(this, callback, 'setState'); 
  10.   } 
  11. }; 
  12.  
  13. var ReactUpdateQueue = { 
  14.   enqueueSetState(component, partialState) { 
  15.     // 在組件的 _pendingStateQueue 上暫存新的 state 
  16.     if (!component._pendingStateQueue) { 
  17.       component._pendingStateQueue = []; 
  18.     } 
  19.     var queue = component._pendingStateQueue; 
  20.     queue.push(partialState); 
  21.     enqueueUpdate(component); 
  22.   }, 
  23.   enqueueCallback: function (component, callback, callerName) { 
  24.     // 在組件的 _pendingCallbacks 上暫存 callback 
  25.     if (component._pendingCallbacks) { 
  26.       component._pendingCallbacks.push(callback); 
  27.     } else { 
  28.       component._pendingCallbacks = [callback]; 
  29.     } 
  30.     enqueueUpdate(component); 
  31.   } 

enqueueUpdate 首先會(huì)通過 batchingStrategy.isBatchingUpdates 判斷當(dāng)前是否在更新流程,如果不在更新流程,會(huì)調(diào)用 batchingStrategy.batchedUpdates() 進(jìn)行更新。如果在流程中,會(huì)將待更新的組件放入 dirtyComponents 進(jìn)行緩存。

  1. var dirtyComponents = []; 
  2. function enqueueUpdate(component) { 
  3.   if (!batchingStrategy.isBatchingUpdates) { 
  4.    // 開始進(jìn)行批量更新 
  5.     batchingStrategy.batchedUpdates(enqueueUpdate, component); 
  6.     return
  7.   } 
  8.   // 如果在更新流程,則將組件放入臟組件隊(duì)列,表示組件待更新 
  9.   dirtyComponents.push(component); 

batchingStrategy 是 React 進(jìn)行批處理的一種策略,該策略的實(shí)現(xiàn)基于 Transaction,雖然名字和數(shù)據(jù)庫的事務(wù)一樣,但是做的事情卻不一樣。

  1. class ReactDefaultBatchingStrategyTransaction extends Transaction { 
  2.   constructor() { 
  3.     this.reinitializeTransaction() 
  4.   } 
  5.   getTransactionWrappers () { 
  6.     return [ 
  7.       { 
  8.         initialize: () => {}, 
  9.         close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates) 
  10.       }, 
  11.       { 
  12.         initialize: () => {}, 
  13.         close: () => { 
  14.           ReactDefaultBatchingStrategy.isBatchingUpdates = false
  15.         } 
  16.       } 
  17.     ] 
  18.   } 
  19.  
  20. var transaction = new ReactDefaultBatchingStrategyTransaction(); 
  21.  
  22. var batchingStrategy = { 
  23.   // 判斷是否在更新流程中 
  24.   isBatchingUpdates: false
  25.   // 開始進(jìn)行批量更新 
  26.   batchedUpdates: function (callback, component) { 
  27.     // 獲取之前的更新狀態(tài) 
  28.     var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; 
  29.   // 將更新狀態(tài)修改為 true 
  30.     ReactDefaultBatchingStrategy.isBatchingUpdates = true
  31.     if (alreadyBatchingUpdates) { 
  32.       // 如果已經(jīng)在更新狀態(tài)中,等待之前的更新結(jié)束 
  33.       return callback(callback, component); 
  34.     } else { 
  35.       // 進(jìn)行更新 
  36.       return transaction.perform(callback, null, component); 
  37.     } 
  38.   } 
  39. }; 

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。

  1. class Transaction { 
  2.  reinitializeTransaction() { 
  3.     this.transactionWrappers = this.getTransactionWrappers(); 
  4.   } 
  5.  perform(method, scope, ...param) { 
  6.     this.initializeAll(0); 
  7.     var ret = method.call(scope, ...param); 
  8.     this.closeAll(0); 
  9.     return ret; 
  10.   } 
  11.  initializeAll(startIndex) { 
  12.     var transactionWrappers = this.transactionWrappers; 
  13.     for (var i = startIndex; i < transactionWrappers.length; i++) { 
  14.       var wrapper = transactionWrappers[i]; 
  15.       wrapper.initialize.call(this); 
  16.     } 
  17.   } 
  18.  closeAll(startIndex) { 
  19.     var transactionWrappers = this.transactionWrappers; 
  20.     for (var i = startIndex; i < transactionWrappers.length; i++) { 
  21.       var wrapper = transactionWrappers[i]; 
  22.       wrapper.close.call(this); 
  23.     } 
  24.   } 

transaction.perform

 

React 源代碼的注釋中,也形象的展示了這一過程。

  1. /* 
  2. *                       wrappers (injected at creation time
  3. *                                      +        + 
  4. *                                      |        | 
  5. *                    +-----------------|--------|--------------+ 
  6. *                    |                 v        |              | 
  7. *                    |      +---------------+   |              | 
  8. *                    |   +--|    wrapper1   |---|----+         | 
  9. *                    |   |  +---------------+   v    |         | 
  10. *                    |   |          +-------------+  |         | 
  11. *                    |   |     +----|   wrapper2  |--------+   | 
  12. *                    |   |     |    +-------------+  |     |   | 
  13. *                    |   |     |                     |     |   | 
  14. *                    |   v     v                     v     v   | wrapper 
  15. *                    | +---+ +---+   +---------+   +---+ +---+ | invariants 
  16. * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained 
  17. * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> 
  18. *                    | |   | |   |   |         |   |   | |   | | 
  19. *                    | |   | |   |   |         |   |   | |   | | 
  20. *                    | |   | |   |   |         |   |   | |   | | 
  21. *                    | +---+ +---+   +---------+   +---+ +---+ | 
  22. *                    |  initialize                    close    | 
  23. *                    +-----------------------------------------+ 
  24. */ 

我們簡化一下代碼,再重新看一下 setState 的流程。

  1. // 1. 調(diào)用 Component.setState 
  2. ReactComponent.prototype.setState = function (partialState) { 
  3.   this.updater.enqueueSetState(this, partialState); 
  4. }; 
  5.  
  6. // 2. 調(diào)用 ReactUpdateQueue.enqueueSetState,將 state 值放到 _pendingStateQueue 進(jìn)行緩存 
  7. var ReactUpdateQueue = { 
  8.   enqueueSetState(component, partialState) { 
  9.     var queue = component._pendingStateQueue || (component._pendingStateQueue = []); 
  10.     queue.push(partialState); 
  11.     enqueueUpdate(component); 
  12.   } 
  13.  
  14. // 3. 判斷是否在更新過程中,如果不在就進(jìn)行更新 
  15. var dirtyComponents = []; 
  16. function enqueueUpdate(component) { 
  17.   // 如果之前沒有更新,此時(shí)的 isBatchingUpdates 肯定是 false 
  18.   if (!batchingStrategy.isBatchingUpdates) { 
  19.     // 調(diào)用 batchingStrategy.batchedUpdates 進(jìn)行更新 
  20.     batchingStrategy.batchedUpdates(enqueueUpdate, component); 
  21.     return
  22.   } 
  23.   dirtyComponents.push(component); 
  24.  
  25. // 4. 進(jìn)行更新,更新邏輯放入事務(wù)中進(jìn)行處理 
  26. var batchingStrategy = { 
  27.   isBatchingUpdates: false
  28.   // 注意:此時(shí)的 callback 為 enqueueUpdate 
  29.   batchedUpdates: function (callback, component) { 
  30.     var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; 
  31.     ReactDefaultBatchingStrategy.isBatchingUpdates = true
  32.     if (alreadyBatchingUpdates) { 
  33.       // 如果已經(jīng)在更新狀態(tài)中,重新調(diào)用 enqueueUpdate,將 component 放入 dirtyComponents 
  34.       return callback(callback, component); 
  35.     } else { 
  36.       // 進(jìn)行事務(wù)操作 
  37.       return transaction.perform(callback, null, component); 
  38.     } 
  39.   } 
  40. }; 

啟動(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é)束。

  1. getTransactionWrappers () { 
  2.   return [ 
  3.     { 
  4.       initialize: () => {}, 
  5.       close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates) 
  6.     }, 
  7.     { 
  8.       initialize: () => {}, 
  9.       close: () => { 
  10.         ReactDefaultBatchingStrategy.isBatchingUpdates = false
  11.       } 
  12.     } 
  13.   ] 

flushBatchedUpdates 里面會(huì)取出所有的臟組件隊(duì)列進(jìn)行 diff,最后更新到 DOM。

  1. function flushBatchedUpdates() { 
  2.   if (dirtyComponents.length) { 
  3.     runBatchedUpdates() 
  4.   } 
  5. }; 
  6.  
  7. function runBatchedUpdates() { 
  8.   // 省略了一些去重和排序的操作 
  9.   for (var i = 0; i < dirtyComponents.length; i++) { 
  10.     var component = dirtyComponents[i]; 
  11.  
  12.     // 判斷組件是否需要更新,然后進(jìn)行 diff 操作,最后更新 DOM。 
  13.     ReactReconciler.performUpdateIfNecessary(component); 
  14.   } 

performUpdateIfNecessary() 會(huì)調(diào)用 Component.updateComponent(),在updateComponent() 中,會(huì)從 _pendingStateQueue 中取出所有的值來更新。

  1. // 獲取最新的 state 
  2. _processPendingState() { 
  3.   var inst = this._instance; 
  4.   var queue = this._pendingStateQueue; 
  5.  
  6.   var nextState = { ...inst.state }; 
  7.   for (var i = 0; i < queue.length; i++) { 
  8.     var partial = queue[i]; 
  9.     Object.assign( 
  10.       nextState, 
  11.       typeof partial === 'function' ? partial(inst, nextState) : partial 
  12.    ); 
  13.   } 
  14.   return nextState; 
  15. // 更新組件 
  16. updateComponent(prevParentElement, nextParentElement) { 
  17.   var inst = this._instance; 
  18.   var prevProps = prevParentElement.props; 
  19.   var nextProps = nextParentElement.props; 
  20.   var nextState = this._processPendingState(); 
  21.   var shouldUpdate =  
  22.       !shallowEqual(prevProps, nextProps) || 
  23.       !shallowEqual(inst.state, nextState); 
  24.    
  25.   if (shouldUpdate) { 
  26.     // diff 、update DOM 
  27.   } else { 
  28.     inst.props = nextProps; 
  29.     inst.state = nextState; 
  30.   } 
  31.   // 后續(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/ 。

  1. npm install react@experimental react-dom@experimental 

如果要開啟 Concurrent 模式,不能使用之前的 ReactDOM.render,需要替換成 ReactDOM.createRoot,而在實(shí)驗(yàn)版本中,由于 API 不夠穩(wěn)定, 需要通過 ReactDOM.unstable_createRoot來啟用 Concurrent 模式。

  1. import ReactDOM from 'react-dom'
  2. import App from './App'
  3.  
  4. ReactDOM.unstable_createRoot( 
  5.   document.getElementById('root'
  6. ).render(<App />); 

setState 合并更新

還記得之前 React15 的案例中,setTimeout 中進(jìn)行 setState ,state.val 的值會(huì)立即發(fā)生變化。同樣的代碼,我們拿到 Concurrent 模式下運(yùn)行一次。

  1. import React from 'react'
  2.  
  3. class App extends React.Component { 
  4.   state = { val: 0 } 
  5.   componentDidMount() { 
  6.     setTimeout(() => { 
  7.       // 第一次調(diào)用 
  8.       this.setState({ val: this.state.val + 1 }); 
  9.       console.log('first setState', this.state); 
  10.    
  11.       // 第二次調(diào)用 
  12.       this.setState({ val: this.state.val + 1 }); 
  13.       console.log('second setState', this.state); 
  14.        
  15.       this.setState({ val: this.state.val + 1 }, () => { 
  16.         console.log(this.state); 
  17.       }); 
  18.     }); 
  19.   } 
  20.   render() { 
  21.     return <div> val: { this.state.val } </div> 
  22.   } 
  23.  
  24. export default App; 

控制臺(tái)輸出

 

說明在 Concurrent 模式下,即使脫離了 React 的生命周期,setState 依舊能夠合并更新。主要原因是 Concurrent 模式下,真正的更新操作被移到了下一個(gè)事件隊(duì)列中,類似于 Vue 的 nextTick。

更新機(jī)制變更

我們修改一下 demo,然后看下點(diǎn)擊按鈕之后的調(diào)用棧。

  1. import React from 'react'
  2.  
  3. class App extends React.Component { 
  4.   state = { val: 0 } 
  5.   clickBtn() { 
  6.     this.setState({ val: this.state.val + 1 }); 
  7.   } 
  8.   render() { 
  9.     return (<div> 
  10.       <button onClick={() => {this.clickBtn()}}>click add</button> 
  11.       <div>val: { this.state.val }</div> 
  12.     </div>) 
  13.   } 
  14.  
  15. export default App; 

調(diào)用棧

調(diào)用棧

 

onClick 觸發(fā)后,進(jìn)行 setState 操作,然后調(diào)用 enquueState 方法,到這里看起來好像和之前的模式一樣,但是后面的操作基本都變了,因?yàn)?React 16 中已經(jīng)沒有了事務(wù)一說。

  1. Component.setState() => enquueState() => scheduleUpdate() => scheduleCallback() 
  2. => 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。

  1. function unstable_scheduleCallback(priorityLevel, calback) { 
  2.   var currentTime = getCurrentTime(); 
  3.   var startTime = currentTime + delay; 
  4.   var newTask = { 
  5.     id: taskIdCounter++, 
  6.     startTime: startTime,           // 任務(wù)開始時(shí)間 
  7.     expirationTime: expirationTime, // 任務(wù)終止時(shí)間 
  8.     priorityLevel: priorityLevel,   // 調(diào)度優(yōu)先級(jí) 
  9.     callback: callback,             // 回調(diào)函數(shù) 
  10.   }; 
  11.   if (startTime > currentTime) { 
  12.     // 超時(shí)處理,將任務(wù)放到 taskQueue,下一個(gè)時(shí)間片執(zhí)行 
  13.     // 源碼中其實(shí)是 timerQueue,后續(xù)會(huì)有個(gè)操作將 timerQueue 的 task 轉(zhuǎn)移到 taskQueue 
  14.    push(taskQueue, newTask) 
  15.   } else { 
  16.     requestHostCallback(flushWork); 
  17.   } 
  18.   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 中。

  1. var { port1, port2 } = new MessageChannel(); 
  2. // port1 接收 port2 的消息 
  3. port1.onmessage = function (msg) { console.log('MessageChannel exec') } 
  4. // port2 發(fā)送消息 
  5. port2.postMessage(null
  6.  
  7. new Promise(r => r()).then(() => console.log('promise exec')) 
  8. setTimeout(() => console.log('setTimeout exec')) 
  9.  
  10. console.log('start run'

執(zhí)行結(jié)果

 

可以看到,port1 接收消息的時(shí)機(jī)比 Promise 所在的 microTask 要晚,但是早于 setTimeout。React 利用這個(gè)能力,給了瀏覽器一個(gè)喘息的時(shí)間,不至于被餓死。

還是之前的案例,同步更新時(shí)沒有給瀏覽器任何喘息,造成視圖的卡頓。

同步更新

 

異步更新時(shí),拆分了時(shí)間片,給了瀏覽器充分的時(shí)間更新動(dòng)畫。

異步更新

 

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

  1. var isMessageLoopRunning = false; // 更新狀態(tài) 
  2. var scheduledHostCallback = null; // 全局的回調(diào) 
  3. var channel = new MessageChannel(); 
  4. var port = channel.port2; 
  5.  
  6. channel.port1.onmessage = function () { 
  7.   if (scheduledHostCallback !== null) { 
  8.     var currentTime = getCurrentTime(); 
  9.     // 重置超時(shí)時(shí)間 
  10.     deadline = currentTime + yieldInterval; 
  11.     var hasTimeRemaining = true
  12.  
  13.     // 執(zhí)行 callback 
  14.     var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); 
  15.  
  16.     if (!hasMoreWork) { 
  17.       // 已經(jīng)沒有任務(wù)了,修改狀態(tài) 
  18.       isMessageLoopRunning = false
  19.       scheduledHostCallback = null
  20.     } else { 
  21.       // 還有任務(wù),放到下個(gè)任務(wù)隊(duì)列執(zhí)行,給瀏覽器喘息的機(jī)會(huì) 
  22.       port.postMessage(null); 
  23.     } 
  24.   } else { 
  25.     isMessageLoopRunning = false
  26.   } 
  27. }; 
  28.  
  29. requestHostCallback = function (callback) { 
  30.   // callback 掛載到 scheduledHostCallback 
  31.   scheduledHostCallback = callback; 
  32.  
  33.   if (!isMessageLoopRunning) { 
  34.     isMessageLoopRunning = true
  35.     // 推送消息,下個(gè)隊(duì)列隊(duì)列調(diào)用 callback 
  36.     port.postMessage(null); 
  37.   } 
  38. }; 

再看看之前傳入的 callback(flushWork),調(diào)用 workLoop,取出 taskQueue 中的任務(wù)執(zhí)行。

  1. // 精簡了相當(dāng)多的代碼 
  2. function flushWork(hasTimeRemaining, initialTime) { 
  3.   return workLoop(hasTimeRemaining, initialTime); 
  4.  
  5. function workLoop(hasTimeRemaining, initialTime) { 
  6.   var currentTime = initialTime; 
  7.   // scheduleCallback 進(jìn)行了 taskQueue 的 push 操作 
  8.   // 這里是獲取之前時(shí)間片未執(zhí)行的操作 
  9.   currentTask = peek(taskQueue); 
  10.  
  11.   while (currentTask !== null) { 
  12.     if (currentTask.expirationTime > currentTime) { 
  13.       // 超時(shí)需要中斷任務(wù) 
  14.       break; 
  15.     } 
  16.  
  17.     currentTask.callback();         // 執(zhí)行任務(wù)回調(diào) 
  18.     currentTime = getCurrentTime(); // 重置當(dāng)前時(shí)間 
  19.     currentTask = peek(taskQueue);  // 獲取新的任務(wù) 
  20.   } 
  21.  // 如果當(dāng)前任務(wù)不為空,表明是超時(shí)中斷,返回 true 
  22.   if (currentTask !== null) { 
  23.     return true
  24.   } else { 
  25.     return false
  26.   } 

可以看出,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ù)。

  1. import React from 'react'
  2.  
  3. class App extends React.Component { 
  4.   state = { val: 0 } 
  5.   clickBtn() { 
  6.     for (let i = 0; i < 500; i++) { 
  7.       setTimeout(() => { 
  8.         this.setState({ val: this.state.val + 1 }); 
  9.       }) 
  10.     } 
  11.   } 
  12.   render() { 
  13.     return (<div> 
  14.       <button onClick={() => {this.clickBtn()}}>click add</button> 
  15.       <div>val: { this.state.val }</div> 
  16.     </div>) 
  17.   } 
  18.  
  19. 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)。

 

責(zé)任編輯:武曉燕 來源: 更了不起的前端
相關(guān)推薦

2020-09-30 09:15:24

React架構(gòu)遞歸

2024-08-14 08:16:53

2020-10-28 09:12:48

React架構(gòu)Hooks

2022-06-13 06:20:42

setStatereact18

2020-10-13 08:36:30

React 架構(gòu)機(jī)制

2019-07-04 15:16:42

數(shù)據(jù)架構(gòu)Flink數(shù)據(jù)倉庫

2018-06-05 08:36:47

內(nèi)部部署云存儲(chǔ)

2023-08-09 08:00:00

數(shù)據(jù)倉庫數(shù)據(jù)架構(gòu)

2024-12-30 09:55:44

2013-05-29 10:33:16

2015-06-15 09:29:56

聯(lián)想互聯(lián)網(wǎng)

2017-06-29 09:28:37

OracleMariaDB復(fù)制

2020-09-24 22:54:46

大數(shù)據(jù)IT技術(shù)

2009-07-01 09:46:14

火狐界面瀏覽器

2018-03-28 17:18:26

大數(shù)據(jù)

2020-10-27 07:29:43

架構(gòu)系統(tǒng)流量

2020-12-09 08:12:30

系統(tǒng)架構(gòu)

2023-05-29 13:56:00

JSReact

2024-04-26 09:13:34

RPCHTTP協(xié)議

2023-12-29 22:41:12

同步架構(gòu)業(yè)務(wù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)