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:存放源码