requestIdleCallback
利用瀏覽器的空余時間執行任務,如果有更高優先級的任務要執行時,當前執行的任務可以被終止。
function calc(deadline) {
// deadline.timeRemaining() 獲取瀏覽器的空余時間
if(deadline.timeRemaining() > 1) {
...
}
requestIdleCallback(calc) // 如果有更高優先級的事情會打斷,所以需要重新執行
}
requestIdleCallback(calc)
因為每一幀畫面被分到的時間是16ms,而實際上不需要這么多,就會有一些剩余的時間
Fiber 說明
現有性能問題
在現有React中,更新過程是同步的,這可能會導致性能問題。
當React決定要加載或者更新組件樹時,會做很多事,比如
- 調用各個組件的生命周期函數
- 計算和比對Virtual DOM(采用循環加遞歸)
- 最后更新DOM樹
這整個過程是同步進行的,也就是說只要一個加載或者更新過程開始,那React就以不破樓蘭終不還的氣概,一鼓作氣運行到底,中途絕不停歇。當組件樹比較龐大的時候,問題就來了。
假如更新一個組件需要1毫秒,如果有200個組件要更新,那就需要200毫秒,在這200毫秒的更新過程中,瀏覽器那個唯一的主線程都在專心運行更新操作,無暇去做任何其他的事情。想象一下,在這200毫秒內,用戶往一個input元素中輸入點什么,敲擊鍵盤也不會獲得響應,因為渲染輸入按鍵結果也是瀏覽器主線程的工作,但是瀏覽器主線程被React占着呢,抽不出空,最后的結果就是用戶敲了按鍵看不到反應,等React更新過程結束之后,咔咔咔那些按鍵一下子出現在input元素里了。
這就是所謂的界面卡頓,很不好的用戶體驗。
React Fiber的方式
破解JavaScript中同步操作時間過長的方法其實很簡單——分片。
把一個耗時長的任務分成很多小片,每一個小片的運行時間很短,雖然總時間依然很長,但是在每個小片執行完之后,都給其他任務一個執行的機會,這樣唯一的線程就不會被獨占,其他任務依然有運行的機會。
React Fiber把更新過程碎片化,每執行完一段更新過程,就把控制權交還給React負責任務協調的模塊,看看有沒有其他緊急任務要做,如果沒有就繼續去更新,如果有緊急任務,那就去做緊急任務。
維護每一個分片的數據結構,就是Fiber。
為什么叫Fiber呢?
大家應該都清楚進程(Process)和線程(Thread)的概念,在計算機科學中還有一個概念叫做Fiber,英文含義就是“纖維”,意指比Thread更細的線,也就是比線程(Thread)控制得更精密的並發處理機制。
我想說的是,其實對大部分React使用者來說,也不用深究React Fiber是如何實現的,除非實現方式真的對我們的使用方式有影響,我們也不用要學會包子怎么做的才吃包子對不對?
但是,React Fiber的實現改變還真的讓我們的代碼方式要做一些調整。
React Fiber對現有代碼的影響
理想情況下,React Fiber應該完全不影響現有代碼,但可惜並完全是這樣,要吃這個包子還真要知道一點這個包子怎么做的,你如果不喜歡吃甜的就不要吃糖包子,對不對?
在React Fiber中,一次更新過程會分成多個分片完成,所以完全有可能一個更新任務還沒有完成,就被另一個更高優先級的更新過程打斷,這時候,優先級高的更新任務會優先處理完,而低優先級更新任務所做的工作則會完全作廢,然后等待機會重頭再來。
因為一個更新過程可能被打斷,所以React Fiber一個更新過程被分為兩個階段(Phase):第一個階段Reconciliation Phase和第二階段Commit Phase。
在第一階段Reconciliation Phase,React Fiber會找出需要更新哪些DOM,這個階段是可以被打斷的;但是到了第二階段Commit Phase,那就一鼓作氣把DOM更新完,絕不會被打斷。(也是因為第一階段已經找到了最小化操作dom的方式)
這兩個階段大部分工作都是React Fiber做,和我們相關的也就是生命周期函數。
以render函數為界,第一階段可能會調用下面這些生命周期函數,說是“可能會調用”是因為不同生命周期調用的函數不同。
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
下面這些生命周期函數則會在第二階段調用。
- componentDidMount
- componentDidUpdate
- componentWillUnmount
因為第一階段的過程會被打斷而且“重頭再來”,就會造成意想不到的情況。
比如說,一個低優先級的任務A正在執行,已經調用了某個組件的componentWillUpdate函數,接下來發現自己的時間分片已經用完了,於是冒出水面,看看有沒有緊急任務,哎呀,真的有一個緊急任務B,接下來React Fiber就會去執行這個緊急任務B,任務A雖然進行了一半,但是沒辦法,只能完全放棄,等到任務B全搞定之后,任務A重頭來一遍,注意,是重頭來一遍,不是從剛才中段的部分開始,也就是說,componentWillUpdate函數會被再調用一次。
在現有的React中,每個生命周期函數在一個加載或者更新過程中絕對只會被調用一次;在React Fiber中,不再是這樣了,第一階段中的生命周期函數在一次加載和更新過程中可能會被多次調用
使用React Fiber之后,一定要檢查一下第一階段相關的這些生命周期函數,看看有沒有邏輯是假設在一個更新過程中只調用一次,有的話就要改了。
我們挨個看一看這些可能被重復調用的函數。
- componentWillReceiveProps,即使當前組件不更新,只要父組件更新也會引發這個函數被調用,所以多調用幾次沒啥,通過!
- shouldComponentUpdate,這函數的作用就是返回一個true或者false,不應該有任何副作用,多調用幾次也無妨,通過!
- render,應該是純函數,多調用幾次無妨,通過!
只剩下componentWillMount和componentWillUpdate這兩個函數往往包含副作用,所以當使用React Fiber的時候一定要重點看這兩個函數的實現。
Fiber算法原理
/**
* 任務隊列
*/
const taskQueue = createTaskQueue()
/**
* 要執行的子任務
*/
let subTask = null
// 等待被提交
let pendingCommit = null
export const scheduleUpdate = (instance, partialState) => {
taskQueue.push({
from: "class_component",
instance,
partialState
})
requestIdleCallback(performTask)
}
export const render = (element, dom) => {// 初始時jsx是root的子集
/**
* 1. 向任務隊列中添加任務
* 2. 指定在瀏覽器空閑時執行任務
*/
/**
* 任務就是通過 vdom 對象 構建 fiber 對象
*/
taskQueue.push({
dom,
props: { children: element }
})
/**
* 指定在瀏覽器空閑的時間去執行任務
*/
requestIdleCallback(performTask)
}
const performTask = deadline => {
/**
* 執行任務, 大的任務要拆分成小的任務
* 需要循環去調用
*/
workLoop(deadline)
/**
* 判斷任務是否存在
* 判斷任務隊列中是否還有任務沒有執行
* 再一次告訴瀏覽器在空閑的時間執行任務
*/
if (subTask || !taskQueue.isEmpty()) {
requestIdleCallback(performTask)
}
}
const workLoop = deadline => {
/**
* 如果子任務不存在 就去獲取子任務
*/
if (!subTask) {
subTask = getFirstTask()
}
/**
* 如果任務存在並且瀏覽器有空余時間就調用
* executeTask 方法執行任務 接受任務 返回新的任務
*/
while (subTask && deadline.timeRemaining() > 1) {
subTask = executeTask(subTask)
}
if (pendingCommit) {
// 就是執行第二階段的方法
commitAllWork(pendingCommit)
}
}
const executeTask = fiber => {
/**
* 構建子級fiber對象
*/
if (fiber.tag === "class_component") {
if (fiber.stateNode.__fiber && fiber.stateNode.__fiber.partialState) {
fiber.stateNode.state = {
...fiber.stateNode.state,
...fiber.stateNode.__fiber.partialState
}
}
reconcileChildren(fiber, fiber.stateNode.render())
} else if (fiber.tag === "function_component") {
reconcileChildren(fiber, fiber.stateNode(fiber.props))
} else {
reconcileChildren(fiber, fiber.props.children)
}
/**
* 如果子級存在 返回子級
* 將這個子級當做父級 構建這個父級下的子級
*/
if (fiber.child) {
return fiber.child
}
/**
* 如果存在同級 返回同級 構建同級的子級
* 如果同級不存在 返回到父級 看父級是否有同級
*/
let currentExecutelyFiber = fiber
while (currentExecutelyFiber.parent) {
/**
* while是從左側沒有子節點開始
* current先是左側,完了以后是右側,讓當前父級包含左側和右側
* 父級和子集進行合並,子集和當前進行合並
* while走完current就是最外層的fiber對象root
*/
currentExecutelyFiber.parent.effects = currentExecutelyFiber.parent.effects.concat(
currentExecutelyFiber.effects.concat([currentExecutelyFiber])
)
if (currentExecutelyFiber.sibling) {
return currentExecutelyFiber.sibling
}
currentExecutelyFiber = currentExecutelyFiber.parent
}
pendingCommit = currentExecutelyFiber
}
const reconcileChildren = (fiber, children) => {
/**
* children 可能對象 也可能是數組,
* render的時候傳給children的element就是對象,createElement返回的就是數組
* 將children 轉換成數組
*/
const arrifiedChildren = arrified(children)
let index = 0
let numberOfElements = arrifiedChildren.length// 例子中只有兩個子集
/**
* 循環過程中的循環項 就是子節點的 virtualDOM 對象
*/
let element = null
/**
* 子級 fiber 對象
*/
let newFiber = null
/**
* 上一個兄弟 fiber 對象
*/
let prevFiber = null
let alternate = null // 備份fiber節點
if (fiber.alternate && fiber.alternate.child) {
alternate = fiber.alternate.child // 第一個子節點的備份節點,下面的element是第一個子節點
}
while (index < numberOfElements || alternate) {
/**
* 子級 virtualDOM 對象
*/
element = arrifiedChildren[index]
if (!element && alternate) {
/**
* 刪除操作
*/
alternate.effectTag = "delete"
fiber.effects.push(alternate)
} else if (element && alternate) {
/**
* 更新
*/
newFiber = {
type: element.type,
props: element.props,
tag: getTag(element),
effects: [],
effectTag: "update",
parent: fiber,
alternate
}
if (element.type === alternate.type) {
/**
* 類型相同
*/
newFiber.stateNode = alternate.stateNode
} else {
/**
* 類型不同
*/
newFiber.stateNode = createStateNode(newFiber)
}
} else if (element && !alternate) {
/**
* 初始渲染
*/
/**
* 子級 fiber 對象
*/
newFiber = {
type: element.type,
props: element.props,
tag: getTag(element),
effects: [],
effectTag: "placement",
parent: fiber
}
/**
* 為fiber節點添加DOM對象或組件實例對象
*/
newFiber.stateNode = createStateNode(newFiber)
}
if (index === 0) {
fiber.child = newFiber
} else if (element) {
prevFiber.sibling = newFiber
}
if (alternate && alternate.sibling) {
alternate = alternate.sibling
} else {
alternate = null
}
// 更新
prevFiber = newFiber
index++
}
}
const getFirstTask = () => {
/**
* 從任務隊列中獲取任務
*/
const task = taskQueue.pop()
// 更新的時候會走這里setState時
if (task.from === "class_component") {
const root = getRoot(task.instance)
task.instance.__fiber.partialState = task.partialState
return {
props: root.props,
stateNode: root.stateNode,
tag: "host_root",
effects: [],
child: null,
alternate: root
}
}
/**
* 返回最外層節點的fiber對象
*/
return {
props: task.props,
stateNode: task.dom,
tag: "host_root",
effects: [],
child: null,
alternate: task.dom.__rootFiberContainer
}
}
const commitAllWork = fiber => {
/**
* 循環 effets 數組 構建 DOM 節點樹
*/
fiber.effects.forEach(item => {
if (item.tag === "class_component") {
item.stateNode.__fiber = item
}
if (item.effectTag === "delete") {
item.parent.stateNode.removeChild(item.stateNode)
} else if (item.effectTag === "update") {
/**
* 更新
*/
if (item.type === item.alternate.type) {
/**
* 節點類型相同
*/
updateNodeElement(item.stateNode, item, item.alternate)
} else {
/**
* 節點類型不同
*/
item.parent.stateNode.replaceChild(
item.stateNode,
item.alternate.stateNode
)
}
} else if (item.effectTag === "placement") {
/**
* 向頁面中追加節點
*/
/**
* 當前要追加的子節點
*/
let fiber = item
/**
* 當前要追加的子節點的父級
*/
let parentFiber = item.parent
/**
* 找到普通節點父級 排除組件父級
* 因為組件父級是不能直接追加真實DOM節點的
*/
while (
parentFiber.tag === "class_component" ||
parentFiber.tag === "function_component"
) {
parentFiber = parentFiber.parent
}
/**
* 如果子節點是普通節點 找到父級 將子節點追加到父級中
*/
if (fiber.tag === "host_component") {
parentFiber.stateNode.appendChild(fiber.stateNode)
}
}
})
/**
* 備份舊的 fiber 節點對象 根節點fiber對象
*/
fiber.stateNode.__rootFiberContainer = fiber
}
const arrified = arg => (Array.isArray(arg) ? arg : [arg])
const createReactInstance = fiber => {
let instance = null
if (fiber.tag === "class_component") {
instance = new fiber.type(fiber.props)
} else {
instance = fiber.type
}
return instance
}
const createStateNode = fiber => {
if (fiber.tag === "host_component") {
return createDOMElement(fiber)
} else {
return createReactInstance(fiber)
}
}
const createTaskQueue = () => {
const taskQueue = []
return {
/**
* 向任務隊列中添加任務
*/
push: item => taskQueue.push(item),
/**
* 從任務隊列中獲取任務
*/
pop: () => taskQueue.shift(),
/**
* 判斷任務隊列中是否還有任務
*/
isEmpty: () => taskQueue.length === 0
}
}
const getRoot = instance => {
let fiber = instance.__fiber
while (fiber.parent) {
fiber = fiber.parent
}
return fiber
}
const getTag = vdom => {
if (typeof vdom.type === "string") {// div span
return "host_component"
} else if (Object.getPrototypeOf(vdom.type) === Component) {// 構造函數
return "class_component"
} else {
return "function_component"
}
}
export class Component {
constructor(props) {
this.props = props
}
setState(partialState) {
scheduleUpdate(this, partialState)
}
}
DOM
export default function updateNodeElement(
newElement,
virtualDOM,
oldVirtualDOM = {}
) {
// 獲取節點對應的屬性對象
const newProps = virtualDOM.props || {}
const oldProps = oldVirtualDOM.props || {}
if (virtualDOM.type === "text") {
if (newProps.textContent !== oldProps.textContent) {
if (virtualDOM.parent.type !== oldVirtualDOM.parent.type) {
virtualDOM.parent.stateNode.appendChild(
document.createTextNode(newProps.textContent)
)
} else {
virtualDOM.parent.stateNode.replaceChild(
document.createTextNode(newProps.textContent),
oldVirtualDOM.stateNode
)
}
}
return
}
Object.keys(newProps).forEach(propName => {
// 獲取屬性值
const newPropsValue = newProps[propName]
const oldPropsValue = oldProps[propName]
if (newPropsValue !== oldPropsValue) {
// 判斷屬性是否是否事件屬性 onClick -> click
if (propName.slice(0, 2) === "on") {
// 事件名稱
const eventName = propName.toLowerCase().slice(2)
// 為元素添加事件
newElement.addEventListener(eventName, newPropsValue)
// 刪除原有的事件的事件處理函數
if (oldPropsValue) {
newElement.removeEventListener(eventName, oldPropsValue)
}
} else if (propName === "value" || propName === "checked") {
newElement[propName] = newPropsValue
} else if (propName !== "children") {
if (propName === "className") {
newElement.setAttribute("class", newPropsValue)
} else {
newElement.setAttribute(propName, newPropsValue)
}
}
}
})
// 判斷屬性被刪除的情況
Object.keys(oldProps).forEach(propName => {
const newPropsValue = newProps[propName]
const oldPropsValue = oldProps[propName]
if (!newPropsValue) {
// 屬性被刪除了
if (propName.slice(0, 2) === "on") {
const eventName = propName.toLowerCase().slice(2)
newElement.removeEventListener(eventName, oldPropsValue)
} else if (propName !== "children") {
newElement.removeAttribute(propName)
}
}
})
}
export default function createDOMElement(virtualDOM) {
let newElement = null
if (virtualDOM.type === "text") {
// 文本節點
newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
// 元素節點
newElement = document.createElement(virtualDOM.type)
updateNodeElement(newElement, virtualDOM)
}
return newElement
}
export default function createElement(type, props, ...children) {
const childElements = [].concat(...children).reduce((result, child) => {
if (child !== false && child !== true && child !== null) {
if (child instanceof Object) {
result.push(child)
} else {
result.push(createElement("text", { textContent: child }))
}
}
return result
}, [])
return {
type,
props: Object.assign({ children: childElements }, props)
}
}
代碼思路
利用瀏覽器的空余時間來執行DOM的比對過程,virtualDOM的比對不會長期占用主線程了,如果有高優先級的任務要執行,就會暫時終止virtualDOM的比對過程,先去執行高優先級的任務。然后再回過頭繼續執行vdom的比對任務,這樣頁面就不會發生卡頓現象了。
由於遞歸需要一層一層進入,一層一層退出,這個過程不能中斷,所以如果要實現任務的終止再繼續就必須放棄遞歸,只采用循環來執行比對的過程,因為循環是可以終止的,只要將循環的條件保存下來,下一次任務就可以繼續從中斷的地方繼續執行了。
如果任務要執行中斷再繼續,任務的單元就必須要小,這樣的話即使任務沒有執行完就被終止了,重新執行任務的代價就要小很多,所以我們要做任務的拆分,將一個大的任務拆分成很多小的任務來執行,virtualdom的比對任務要如何進行任務拆分呢,以前是將整顆virtualdom樹的比對看成是一個任務,現在我樹中每一個節點的比對看成一個任務,這樣一個大的任務就被拆分成一個個小的任務了。
為了實現任務的終止再繼續,將DOM比對的算法拆分成了兩個部分,第一部分就是vdom對象的比對,第二部分是真實dom的更新,其中vdom對象的比對過程是可以終止的,真實dom的更新是不可以終止的,具體過程這樣的:在編寫用戶界面的時候仍然使用jsx,babel會將jsx轉換為React.createElement方法的調用,在調用后會返回vdom對象。接下來就可以執行第一個階段了,就是構建Fiber對象,采用循環的方式從virtualdom對象當中找到每一個內部的vdom對象,為每一個vdom對象構建fiber對象,也是js對象,是從vdom對象演化來的,除了type,props和children以為還存儲了更多的信息,其中有一個核心的信息就是當前節點要執行的操作,比如你是想刪除這個節點呢還是想更新這個節點,還是新增這個節點,當所有節點的fiber對象構建完成之后,還要將所有的fiber對象存儲在一個數組中,接下來就可以執行第二個階段的操作了,就是循環fiber數組,在循環的過程當中,根據fibe對象存儲的當前節點要執行的操作的類型將這個操作應用在真實dom對象上,這就是一個大概流程
_
-
.prettierrc.js 代碼格式化工具
-
.nvmrc node管理的工具
下載nvm可以有很多的node版本:nvm list,需要用的時候切一下
-
editorconfig 對代碼IDE做規范
-
dangerfile 比如報哪些錯誤的話讓CI不通過等
-
script文件夾:打包工具,配置等的一些文件
-
packages:存放源碼