本系列文章總共三篇:javascript
課前小問題:
- JSX 是如何實現的
- jsx 裏面爲何沒法寫 for 循環
- 爲何咱們須要在 react 文件頂部引入 react 庫
- 爲何用戶自定義的組件要大寫開頭
- 爲何 class 要寫成 className(同理爲何 style 裏面的 css 屬性要寫成駝峯形式)
- virtual dom 是什麼
- diff 算法是什麼
- 爲何動態生成的列表須要設置key
- 爲何不建議在 componentWillMount 裏面執行反作用
- class component 的生命週期是如何實現的
React 的核心思想
- 聲明式
- 組件化
- 一次學習隨處編寫
要實現這些須要哪些手段
- 經過模板的方式操做視圖(JSX)
- 怎麼將模板插入到真實的DOM中,爲了性能須要虛擬DOM
- 怎麼調度更新兩種方式:stack reconciler 和 fiber reconciler
- 要實現一次學習隨處編寫須要將各個部分的包抽離出來
JSX 是如何實現的
- 爲何須要 jsx:zh-hans.reactjs.org/docs/introd…
- 怎麼將 js 和 html 寫在一塊兒:由於只有 js 是圖靈完備的語言,因此要用 js 來描述 html
問題:假若有以下的 html 結構咱們如何用 js 來描述它呢?css
<div class="parent">
<span>child1</span>
<span>child2</span>
</div>
複製代碼
咱們獲取能夠經過這樣的 js 對象來描述它html
const element = {
type: 'div',
props: {className: 'parent'},
children: [
{
type: 'span',
props: null,
children: ['child1']
},
{
type: 'span',
props: null,
children: ['child2']
}
]
}
複製代碼
- 顯然若是像上面這麼寫很麻煩,因此 react 使用 jsx 這種語法擴展
- 將 jsx => reactElement 是經過 babel 來實現的(babel-preset-react)
在線體驗連接:babeljs.io/repljava
本節解決的問題
- JSX 是如何實現的
- jsx 裏面爲何沒法寫 for 循環
- 爲何咱們須要在 react 文件頂部引入 react 庫
- 爲何用戶自定義的組件要大寫開頭
- 爲何 class 要寫成 className(同理爲何 style 裏面的 css 屬性要寫成駝峯形式)
虛擬DOM是什麼
- 結論:JS對象模擬DOM樹
- 爲何須要虛擬DOM:
- 由於對 dom 的操做是很昂貴的,容易形成性能問題(瀏覽器的重排和重繪)【這裏能夠經過devtool演示】
- 虛擬dom能夠結合diff算法實現儘量少的dom操做
- 建立跨平臺的應用
本節解決的問題
Diff 算法
- diff 算法就是找到新老兩個虛擬DOM樹之間的差別(相似於 git dif)
- 若是單純對兩個樹進行比較的時間複雜度是 n² 再加上更新就變成了 n³
- react diff 算法
- 前提:
- 兩個相同組件產生相似的 DOM 結構,不一樣的組件產生不一樣的 DOM 結構
- 對於同一層次的一組子節點,它們能夠經過惟一的 id 進行區分。
本節解決的問題
- diff 算法是什麼
- 爲何動態生成的列表須要設置key
協調(Reconciliation)
概念:按照個人理解就是 更新 -> DOM 變化 這之間的流程,它包括了diff 算法。
react有兩套協調算法:stack reconciler(react@15.x) 和 fiber reconciler(react@16.x)react
注:在 react 中掛載階段也算是更新git
stack reconciler
概念
在 react 16 以前的調度算法被稱做 stack reconciler,它的特色是自頂向下的更新方式,好比調用 this.setState() 以後的流程就像這樣:
this.setState() => 生成虛擬 DOM => diff 算法比較 => 找到要更新的元素 => 放到更新隊列裏
github
想想:這樣實現有什麼問題沒有?算法
問題
若是整個應用很大,會致使 js 的執行長期佔據主線程,瀏覽器沒法及時響應用戶的操做,進而致使頁面顯示的卡頓。
假設咱們有一個大的三角形組件,它由不少的子組件組成,每一個子組件中數字會不斷改變,與此同時整個三角形會不斷的變寬和變窄,接下來讓咱們看看兩個 reconciler 的表現如何:
api
咱們能夠發現 stack reconciler 下的渲染是不如 fiber reconciler 來的流程的,這是由於在 stack reconciler 裏面更新時同步的,自頂向下的更新方式,只要更新過程開始就會「一條道走到黑」直到全部節點的比對所有完成,這樣的方式若是節點數量比較少還好,若是想上面這種狀況節點數量不少,假設有 200 個節點,每一個節點進行 diff 算法須要 1ms,那麼所有比對完就須要 200ms,也就是說在這 200ms 裏面瀏覽器沒法處理其它任務,好比沒法渲染視圖,通常 30 幀及以上會讓人感到流暢,那每幀就須要 33.3ms,顯然 200ms > 33.3ms,因此至關於由於 JS 的長時間執行致使幀率變得很低,等到 200ms 以後瀏覽器將以前漏掉的頁面渲染一會兒呈現的時候你就會感受到不連貫也就是卡頓了。瀏覽器
這裏須要補充一下關於瀏覽器幀的概念
咱們知道要實現流暢的顯示效果,刷新頻率(FPS)就不能過低,現代瀏覽器通常的刷新頻率是 60FPS,因此每一幀分到的時間是 1000/60 ≈ 16 ms,在這 16 ms 中瀏覽器會安排以下事項
- 處理用戶的交互
- JS 解析執行
- 幀開始。窗口尺寸變動,頁面滾去等的處理
- rAF(requestAnimationFrame)
- 佈局
- 繪製
能夠看到瀏覽器這一幀裏面要作的事情着實很多,若是 js 引擎的執行佔用的時間過長那勢必致使其它任務的執行(好比響應用戶交互)要延後,這也就是致使卡頓的緣由。
Fiber reconciler
概念
react 16 版本對之前的 stack reconciler 進行了一次重寫,就是 fiber reconciler,它的目的是爲了解決 stack reconciler 中固有的問題,同時解決一些歷史遺留問題。
咱們指望的是可以實現以下目標:
- 可以把可中斷的任務切片處理
- 可以調整優先級,重置並複用任務
- 可以在父元素與子元素之間交錯處理,以支持 React 中的佈局
- 可以在
render()
中返回多個元素
- 更好地支持錯誤邊界
要實現這些特性一個很重要的點就是:切片,把一個很大的任務拆分紅不少小任務,每一個小任務作完以後看看是否須要讓位於其它優先級更高的任務,這樣就能保證這個惟一的線程不會被一直佔用,咱們把每個切片(小任務)就叫作一個 fiber。
**
在什麼時候進行切片?
react 的整個執行流程分爲兩個大的階段:
- Phase1: render/reconciliation
- Phase2: commit
第一個階段負責產生 Virtual DOM -> diff 算法 -> 產生要執行的 update,第二個階段負責將更新渲染到 DOM Tree 上,若是咱們在第二個階段進行分片肯能會致使頁面顯示的不連貫,會影響用戶體驗,因此將其放在第一個階段去分片更爲合理。
切出來的是什麼呢?
切片出來就是 fiber 對象,fiber 翻譯爲纖維,在計算機科學中叫協程或纖程,一種比線程耕細粒度的任務單元,雖然 JS 原生並無該機制(大概),可是我想 react 大概是想借用該思想來實現更精細的操控任務的執行吧。
如何調度任務執行?
React 經過兩個 JS 底層 api 來實現:
它倆的區別是 requestIdleCallback 是須要等待瀏覽器空閒的時候纔會執行而 requestAnimationFrame 是每幀都會執行,因此高優先級的任務交給 requestAnimationFrame 低優先級的任務交給 requestIdleCallback 去處理,可是爲了不由於瀏覽器一直被佔用致使低優先級任務一直沒法執行,requestIdleCallback 還提供了一個 timeout 參數指定超過該事件就強制執行回調。
實現
接下來咱們來經過一個例子看看 fiber reconciler 是如何實現的,不過在此以前咱們先來認識一些 react 源碼中的名詞。
名詞解釋
上面咱們提到了 Fiber,它表示 react 中最小的一個工做單元, 在 react 中 ClassComponent,FunctionComponent,普通 DOM 節點,文本節點都對應一個 Fiber 對象,Fiber 對象的本質其實就是一個 Javascript 對象。
child 是 Fiber 對象上的屬性,指向的是它的子節點(Fiber)
sibling 是 Fiber 對象上的屬性,指向的是它的兄弟節點(Fiber)
return 是 Fiber 對象上的屬性,指向的是它的父節點(Fiber)
stateNode 是 Fiber 對象上的屬性,表示的是 Fiber 對象對應的實例對象,好比 Class 實例、DOM 節點等
current 表示已經完成更新的 Fiber 對象
workInProgress 表示正在更新的 Fiber 對象
alternate 用來指向 current 或 workInProgress,current 的 alternate 指向 workInProgress,而 workInProgress 的 alternate 指向 current
FiberRoot 表示整個應用的起點,它內部保存着 container 信息,對應的 fiber 對象(RootFiber)等
RootFiber 表示整個 fiber tree 的根節點,它內部的 stateNode 指向 FiberRoot,它的 return 爲 null
Fiber tree
咱們這裏要注意的是 fiber tree 不一樣於傳統的 Virtual DOM 是樹形結構,fiber 的 child 只指向第一個 子節點,可是能夠經過 sibling 找到其兄弟節點,因此整個結構看起來更像是一個鏈表結構。
舉個例子
咱們有這樣一個 App 組件,它包含一個 button 和一個 List 組件,當點擊 button 的時候 List 組件內的數字會進行平方運算,另外在 App 組件外還有一個 button,它不是用 react 建立的,它的做用是點擊時會放大字體。
掛載階段
第一次渲染階段也就是掛載階段,react 會自上而下的建立整個 fiber tree,建立順序是同 FiberRoot 開始,經過一個叫作 work loop 的循環來不斷建立 fiber 節點,循環會先遍歷子節點(child),當沒有子節點再遍歷兄弟節點(sibling),最終達到所有建立的目的,以咱們的 demo 爲例,fiber 的建立順序以下圖箭頭所示。
構建好的 fiber tree 以下圖所示
更新階段
這時,咱們經過點擊【^2】按鈕來產生一個更新,產生的更新會放入 List 組件的更新隊列裏(update queue),在 fiber reconciler 中異步的更新並不會當即處理,它會執行調度(schedule)程序,讓調度程序判斷何時讓它執行,調度程序就是經過上面所說的 requestIdleCallback 來判斷什麼時候處理更新。
當主進程把控制權交給咱們的時候咱們就能夠開始執行更新了,咱們把這個階段叫作 work loop,work loop 是一個循環,它循環的去執行下一個任務,在執行以前要先判斷剩餘的時間是否夠用,因此對於 work loop 它要追蹤兩個東西:下一個工做單元和剩餘時間。
目前個人剩餘時間是 13ms,下一個要執行的任務是 HostRoot,這裏須要注意經過 this.setState 產生的更新也會先從根節點開始遍歷,react 會經過產生更新的那個 fiber 對象(List)向上找到對應的 HostRoot。
react 會保留以前生成的 fiber tree 咱們管它叫 current fiber tree,而後新生成一個 workInProgress tree,它用來計算出產生的變化,執行 reconciliation 的過程,HostRoot 能夠直接經過克隆舊的 HostRoot 產生,此時新的 HostRoot 的 child 還指向老的 List 節點。
當 HostRoot 處理完成以後就會向下尋找它的子節點也就是 List,因此下一個任務就是 List,咱們一樣能夠克隆以前的 List 節點,克隆好以後 react 會判斷一下是否還有剩餘的時間,發現還有剩餘的時間,那麼開始執行 List 節點的更新任務。
當咱們執行 List 的更新時咱們發現 List 的 update queue 裏面有 update,因此 react 要處理該更新
當咱們處理完 update queue 以後會判斷 List 節點上面是否有 effect 要處理,好比 componentDidUpdate ,getSnapshotBeforeUpdate。
由於 List 產生了新的 state,因此 react 會調用它的 render 方法返回新的 VDOM,接下來就用到了 diff 算法了,根據新舊節點的類型來判斷是否能夠複用,能夠複用的話就直接複製舊的節點,不然就刪除掉舊的節點建立新的節點,對於咱們的例子來講新舊節點類型一致能夠複用。
下一個要處理的任務就是 List 的第一個 child:button,咱們仍是在處理以前先檢查一下是否還有剩餘的時間,接下來的事情我想你大概也能猜到了。
爲了顯示出 fiber 的做用咱們此時假設用戶點擊了放大字體的按鈕,這個邏輯和 react 無關,徹底由原生 JS 實現,此時一個 callback 生成須要等待主線程去處理,可是此時主線程並不會當即處理,由於此時距離下一幀還有剩餘的時間,主線程仍是會先處理 react 相關的任務。
對於 button 節點它沒有任何更新,並且也沒有子節點(文本節點不算),因此咱們能夠執行完成邏輯(completeUnitOfWork)這裏 react 會比對新舊節點屬性的變化,記錄在 fiber 對象的 upddateQueue 裏,接着 react 會找 button 的兄弟節點(sibling)也就是第一個 Item。
接着 work loop 去查看是否有剩餘的時間,發現還有剩餘時間,那接下來的執行過程其實和 List 是相似的。
若是 Item 組件裏面有 shouldComponentUpdate,那麼 react 會調用它,返回的結果就是 shouldUpdate,react 用它來判斷是否須要更新 DOM 節點,對於第一個 Item 來講它以前的 props 是 1 平方以後仍是 1,因此 props 沒有發生變化,shouldComponentUpdate 返回 false,react 就不會給它標記任何 effect。
接着咱們繼續遍歷 Item 的兄弟節點(sibling)也就是第二個 Item,此時第二個 Item 的 props 發生了變化從 2 => 4,因此 shouldComponentUpdate 返回了 true,咱們給 Item 標記一個 effect (Placement)。
接下來咱們來處理第二個 Item 下的 div,此時咱們還有一點剩餘時間,因此咱們仍是能夠繼續處理它。
對於 div 來講它的文本內容從 2 => 4 發生了變化,因此它也須要被標記一個 effect(Placement)。
當 div 完成更新以後,發現它沒有子節點也沒有兄弟節點,這時候會對父節點執行完成操做(completeUnitOfWork),在這個階段會將子節點產生的 effect 合併到 父節點的 effect 鏈上。
接着 react 會找第二個 Item 的兄弟節點(sibling)也就是第三個 Item,此時 work loop 進行 deadline 判斷的時候發現已經沒有剩餘時間了,此時 react 會將執行權交還給主進程,可是 react 還有剩餘的任務沒有執行完,因此它會在以前結束的地方等待主進程空閒時繼續完成剩餘工做。
下面就是主進程處理放大字體的任務,此時 react 的內容並無發生改變,儘管 react 知道第二個節點的值變成了 4。
當主進程處理完任務以後就會回來繼續執行 react 的剩餘任務
接下來就是最後兩個任務了,和第二個 Item 執行過程相似,最終會產生兩個 effect tag,被掛載到以前的 effect 以後,最終節點的 effect tag 會和合併到父節點的 effect 鏈上。
當已經完成對整個 fiber tree 的最後一個節點更新後,react 會開始不斷向上尋找父節點執行完成工做(completeUnitOfWork),若是父節點是普通 DOM 節點會比對屬性的變化放在 update queue 上,同時將子節點產生的 effect 鏈掛載在本身的 effect 鏈上。
當 react 遍歷完最後一個 fiber 節點也就是 HostRoot,咱們的第一個階段(Reconciliation phase)就完成了,咱們將把這個 HostRoot 交給第二個階段(Commit phase)進行處理。
在執行 commit 階段以前咱們還會判斷一下時間是否夠用
接下來開始第二個階段(Commit phase),react 將會從第一個 effect 開始遍歷更新,對於 DOM 元素就執行對應的正刪改的操做,對於 Class 組件會將執行對應的聲明周期函數:componentDidMount、componentDidUpdate、componentWillUnmount,解除 ref 的綁定等。
commit 階段執行完成後 DOM 已經更新完成,這個時候 workInProgress tree 就是當前 App 的最新狀態了,因此此時 react 將會把 current 指向 workInProgress tree。
上面的整個流程可讓瀏覽器的任務不被一直打斷,可是還有一個問題沒有解決,若是 react 當前處理的任務耗時過長致使後面更緊急的任務沒法快速響應,那該怎麼辦呢?
react 的作法是設置優先級,經過調度算法(schedule)來找到高優先級的任務讓它先執行,也就是說高優先級的任務會打斷低優先級的任務,等到高優先級的任務執行完成以後再去執行低優先級的任務,注意是從頭開始執行。
對咱們的影響
fiber reconciler 將一個更新分紅兩個階段(Phase):Reconciliation Phase 和 Commit Phase,第一個階段也就是咱們上面所說的協調的過程,它的做用是找出哪些 dom 是須要更新的,這個階段是能夠被打斷的;第二個階段就是將找出的那些 dom 渲染出來的過程,這個階段是不能被打斷的。
對咱們有影響的就是這兩個階段會調用的生命週期函數,以 render 函數爲界,第一個階段會調用如下生命週期函數:
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
第二個階段會調用的生命週期函數:
- componentDidMount
- componentDidUpdate
- componentWillUnmount
由於 fiber reconciler 會致使第一個階段被屢次執行因此咱們須要注意在第一階段的生命週期函數裏不要執行那些只能調用一次的操做。
本節解決的問題
- 爲何不建議在 componentWillMount 裏面執行反作用
- class component 的生命週期是如何實現的
參考資料
Github
包含帶註釋的源碼、demos和流程圖
github.com/kwzm/learn-…