本文做者:葛星html
React 實現了使用 Virtual DOM 來描述 UI 的方式,經過對比兩棵樹的差別最小化的更新 DOM,這樣使得用戶的代碼變的傻瓜,可是同時也來帶了一些問題。這個核心的問題就在於 diff 計算並不是是免費的,在元素較多的狀況下,整個 diff 計算的過程可能會持續很⻓時間,形成動畫丟幀或者很難響應用戶的操做,形成用戶體驗降低。前端
爲何會出現這個問題,主要是由於下面兩個緣由:react
上面兩個緣由缺一不可,由於若是 JS 執行, UI 不會阻塞 ,其實用戶也不會有所感知。下面讓咱們看下比較常見的性能優化手段。git
通常咱們會採用下面的方式來優化性能github
對函數使用防抖的方式進行優化。這種方式將 UI 的更新推遲到用戶輸入完畢。這樣用戶在輸入的時候就不會感受到卡頓。瀏覽器
class App extends Component {
onChange = () => {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(
() =>
this.setState({
ds: [],
}),
200
);
};
render() {
return (
<div> <input onChange={this.onChange} /> <list ds={this.state.ds} /> </div>
);
}
}
複製代碼
經過 shouldComponentUpdate 或者 PureComponent 的方式進行優化。這種方式經過淺對比先後兩次的 props 和 state 讓 React 跳過沒必要要的 diff 計算。性能優化
class App extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
!shallowEqual(nextProps, this.props) ||
!shallowEqual(nextState, this.state)
);
}
render() {
return (
<div> <input onChange={this.onChange} /> <list ds={this.state.ds} /> </div>
);
}
}
複製代碼
這種方式有下面三個須要注意的點:markdown
a. 只能採用淺比較的方式,這樣更深層次的對象更新的時候沒法比較,而若是採用深比較的方式,若是你比較對象的時間比 React diff 的時間還要久,得不償失。多線程
b. 對象的引用關係,在對於 state 的賦值的時候,主要注意對象的引用關係,好比下面的代碼就會讓這個組件沒法更新架構
class App extends PureComponent {
state = {
record: {},
};
componentDidMount() {
const { record } = this.state;
record.name = "demo";
this.setState({
record,
});
}
render() {
return <>{this.state.record.name}</>;
}
}
複製代碼
c. 函數的執行值發生改變。這種狀況在於函數裏面用到了 props 和 state 以外的變量,這些變量可能發生了改變
class App extends PureComponent {
cellRender = (value, index, record) => {
return record.name + this.name;
};
render() {
return <List cellRender={this.cellRender} />;
}
}
複製代碼
經過相似於 Vue@2.x 和 Mobx 的方式實現觀察對象來進行局部更新。這種方式要求用戶在使用的時候避免使用 setState 方法。
@inject("color")
@observer
class Btn extends React.Component {
render() {
return (
<button style={{ color: this.props.color }}>{this.props.text}</button>
);
}
}
<Provider color="red">
<MessageList> <Btn /> </MessageList>
</Provider>;
複製代碼
對於這個例子,color 變化的時候, 只有 Button 會從新渲染。
其實對於80%的狀況,上面的三種方式已經知足這些場景的性能優化,可是上面所說的都是在應用層面的優化,其實對於開發者提出了必定的要求,有什麼方式能夠在底層進行一些優化呢?
很是慶幸的是瀏覽器推出了requestIdleCallback 的 API, 這個 API 可讓瀏覽器在空閒時期的時候執行腳本,大概如下面的方式使用:
requestIdleCallback((deadline) => {
if (deadline.timeRemaining() > 0) {
} else {
requestIdleCallback(otherTasks);
}
});
複製代碼
上面的例子主要是說若是瀏覽器在當前幀沒有空閒時間了,則開啓另外一個空閒期調用。(注:大概在 2018 年的時候, Facebook 拋棄了 requestIdleCallback 的原生 API,討論)
以前咱們說過 React 的 diff 計算會花費大量的時間,因此咱們思考下若是咱們將 diff 計算放在裏面執行是否就能解決體驗的問題呢?答案是確定的,可是這會面臨下面幾個問題:
再看 React 的 Fiber 以前咱們先來研究下怎麼使用 Fiber 的思惟方式來改寫斐波那契數列,在計算機科學裏,有這樣一句話「任何遞歸的程序均可以使用循環實現」。爲了讓程序能夠中斷,遞歸的程序必須改寫爲循環。
遞歸下斐波那契數列寫法:
function fib(n) {
if (n <= 2) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
複製代碼
若是咱們採用 Fiber 的思路將其改寫爲循環,就須要展開程序,保留執行的中間態,這裏的中間態咱們定義爲下面的結構,雖然這個例子並不能和 React Fiber 的對等。
function fib(n) {
let fiber = { arg: n, returnAddr: null, a: 0 };
// 標記循環
rec: while (true) {
// 當展開徹底後,開始計算
if (fiber.arg <= 2) {
let sum = 1;
// 尋找父級
while (fiber.returnAddr) {
fiber = fiber.returnAddr;
if (fiber.a === 0) {
fiber.a = sum;
fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
continue rec;
}
sum += fiber.a;
}
return sum;
} else {
// 先展開
fiber = { arg: fiber.arg - 1, returnAddr: fiber, a: 0 };
}
}
}
複製代碼
實際上 React Fiber 正是受到了上面的啓發,咱們能夠看到因爲 Fiber 的思路對執行程序進行了展開,大概相似於下面的結構,和程序執行的堆棧很是類似,這段代碼的意思是先像左邊同樣展開整個結構,當 fiber
的入參小於 2 的時候,再不斷的尋找父級知道沒有父節點,最後獲得 sum
值。
左側是展開的結構,右側是向上堆疊的調用棧示意圖
因此 Fiber 比 Stack 的方式要花費更多的內存佔用和執行性能。這個例子有更直觀的展現。 可是爲何 React 基於 Fiber 的思路會讓 JS 執行性能提高呢,這是由於有其餘的優化在其中,好比不須要兼容舊有的瀏覽器,代碼量的縮減等等。
如今咱們來看一看一個 Fiber Node 的結構,以下圖所示,一個很是典型的鏈表的結構,這種設計方式實際也受上面展開堆棧方式的啓發,而相對於 15 版本而言,增長了不少屬性。
{
tag, // 標記一些特殊的組件類型,好比Fragment,ContextProvider等
type, // 組件的節點的真實的描述,好比div, Button等
key, // key和15同樣,若是key一致,下次這個節點能夠被複用
child, // 節點的孩子
sibling, // 節點的兄弟節點
return, // 實際上就是該節點的父級節點
pendingProps, // 開始的時候設置pendingProps
memoizedProps, // 結束的時候設置memoizedProps, 若是二者相同的話,直接複用以前的stateNode
pendingWorkPriority, // 當前節點的優先級,
stateNode, // 當前節點關聯的組件的instance
effectTag // 標記當前的fiber須要被操做的類型,好比刪除,更新等等
...
}
複製代碼
咱們能夠採用上面相似遍歷展開的斐波那契數列同樣遍歷 Fiber Node 的 root ,其實就是一個比較簡單的鏈表遍歷方法。
在實施 Fiber 的過程當中,爲了更好的實現擴展性的需求,衍生出了 React Reconciler 這個獨立的包,咱們能夠經過這個玩意自定義一個 Custom Renderer。它定義了一系列標準化的接口,使咱們沒必要關心 Fiber 內部是如何工做的,就能夠經過虛擬 DOM 的方式驅動宿主環境。
一個較爲完整的探索 Custom Renderer 的例子
下面一個標準化的 Custom Renderer 的啓動代碼,咱們只須要實現 HostConfig 的部分就可使用 React Reconclier 的調度能力:
import Reconciler from 'react-reconclier';
const HostConfig = {};
const CustomRenderer = Reconciler(HostConfig)
let root;
const render = function(children, container) {
if(!root) {
root = CustomRenderer.createContainer(container);
}
CustomRenderer.updateContainer(children, root);
}
render(<App/>, doucment.querySelector('#root')
複製代碼
HostConfig 中最核心的方法是 createInstance
,爲 type 類型建立一個實例,若是宿主環境是 Web ,能夠直接調用 createElement
方法
createInstance(type,props,rootContainerInstance,hostContext) {
// 轉換props
return document.createElement(
type,
props,
);
}
複製代碼
衍生一下,如今跨端的方案,基本上這種運行時的方案均可以利用 CustomRenderer 的思路,來實現一碼多端。舉個簡單的例子,假設了我寫了下面的代碼
function App() {
return <Button />;
}
複製代碼
Button 具體應該使用什麼對應的實現渲染,能夠在createInstance
裏作個攔截,固然也能夠對不一樣的端實現不一樣的 Renderer 。 下面一個僞代碼
Mobile Renderer
import { MobileButton } from 'xxx';
createInstance(type,props,rootContainerInstance,hostContext) {
const components = {
Button: MobileButton
}
return new components[type](props) // 僞代碼
}
複製代碼
雖然看起來 CustomRenderer 很好,實際上在整個 API 的設計上,爲了 Web 作了一些妥協。好比單獨爲文本設計的 shouldSetTextContent
, createTextInstance
方法,基本上是由於 Web 對某些元素文本操做的緣由,沒有辦法使用統一的 document.createElement
,而必須使用document.createTextNode
,其實在不少其餘的渲染場景下都不須要單獨實現這些方法或者直接返回 false
React DOM 的實現
export function shouldSetTextContent(type: string, props: Props): boolean {
return (
type === 'textarea' ||
type === 'option' ||
type === 'noscript' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
props.dangerouslySetInnerHTML.__html != null)
);
}
複製代碼
其餘的一些 Renderer
export function shouldSetTextContent() {
return false;
}
複製代碼
本文主要探尋下 React Fiber 想要解決的問題,包括 Fiber 架構受到的一些啓發,及在實施了 Fiber 架構後的衍生產物 Custom Renderer 的應用,但願有更多的場景能夠利用到 Custom Renderer 的能力, 這裏提供一些社區常見的 Custom Renderer。最後,本文僅表明我的觀點,若有錯誤歡迎批評指正。
參考資料
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!