一步一步进行react源码构建
原文链接:
Build your own React
第一步:观察jsx转化
一个简单的React例子如下
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
jsx文件中的jsx语法会被@babel/preset-react插件所转译,模板会被转译成React.createElement包裹的对象形式
const element = React.createElement("h1", { title: "foo" }, "Hello")
const container = document.getElementById("root")
ReactDOM.render(element, container)
然后React.createElement又会根据传入的对象形式,得到真正的React虚拟DOM
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello"
}
}
const container = document.getElementById("root")
ReactDOM.render(element, container)
最后虚拟dom将被ReactDOM转化为真实dom然后挂载到container上。
第二步:实现React.createElement
Hello所在的位置可能是文本节点,也可能是其他子节点,观察后可以发现createElement其实就是参数到虚拟DOM的转化
// 简易的createElement
function createElement (type, props, children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object' ? child : createElement(child)
)
}
}
}
第三步:实现一个ReactDOM.render函数
得到虚拟dom之后,我们还可以继续实现一个简易的React.render函数
function render (element, container) {
const dom = dom.type === "TEXT_ELEMENT"
? document.createTextNode("") : document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props).filter(isProperty).forEach(key => {
dom[key] = element[key]
})
if (element.props && element.props.children) {
element.props.children.forEach(child = > render(child, dom))
}
container.appendChild(dom)
}
通过以上三步,已经可以实现一个最小的React功能,将jsx渲染到页面上。
第四步:Concurrent Mode(并发模式)
在前面三步,我们的dom元素的构建,appendChild方法的执行都是同步的,如果dom树规模过大,构建的过程会造成js主线程的阻塞,从而可能影响页面input用户的输入,或者动画的执行等高优先级任务,所以我们需要将任务拆分,做到不影响主线程其他任务的执行
let nextUnitOfWork = null
function workLoop (deadline) {
let shouldYeild = false
while (nextUnitOfWork && !shouldYeild) {
// 如果有下一个任务而且不需要等待主线程时间片空余,执行该任务,并且通过preformUnitOfWork返回下一个任务
nextUnitOfWork = preformUnitOfWork(
nextUnitOfWork
)
// deadline.timeRemaining()返回一个时间DOMHighResTimeStamp, 并且是浮点类型的数值,它用来表示当前闲置周期的预估剩余毫秒数,如果剩余时间小于1则不再有空闲时间,当前任务进入阻塞态
shouldYeild = deadline.timeRemaining() < 1
}
// 再次执行requestIdleCallback方法,进入下一轮渲染
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
借助requestIdleCallback我们可以实现任务的拆分,在浏览器空闲时间进行低优先级任务的执行。
第五步:fiber架构
为了对虚拟dom=>真实dom的过程实现任务拆分,我们需要一种数据结构来表示我们的任务执行顺序,这也是fiber架构的除了requestIdleCallback之外的另一个基石
fiber架构创建了一个双向链表的数据结构,也可以叫fiber树,在首次任务执行时会使用深度优先遍历该树,每个节点生成一个链表节点(拥有child、parent、sibing三个指针),preformUnitOfWork通过该链表确定下一个nextUnitOfWork。遍历过程:从root节点开始,首先寻找有没有子节点,没有则寻找有没有sibling节点,如果也没有sibing节点,则回到上一个fiber节点,判断它是否存在sibing节点,直到指针回到root节点。
第四步我们还剩下preformUnitOfWork函数没有实现,他的任务主要有以下三点
- 执行任务,创建元素并添加至parentDom
- 为元素的children创建fiber树
- 选择下一轮需要执行的单元任务
下面把以上逻辑抽象成代码
// 1、从render函数中抽离出createDom函数,用于为当前任务创建真实dom
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
// 2、render函数设置nextUnitOfWork为根节点的任务,render函数将在浏览器空闲时通过workLoop开始执行
function render (element, container) { /* TODO: set next unit of work */ }
// ↓↓↓↓↓↓↓↓↓↓↓
function render (element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element]
}
}
}
然后,我们开始实现performUnitOfWork方法
function performUnitOfWork (fiber) {
// 1、执行任务,创建元素并添加至parentDom
if (!fiber.dom) {
// 如果没有容器则根据fiber内容创建一个dom容器
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
// 把当前节点插入父节点
fiber.parent.dom.appendChild(fiber.dom)
}
// 2、为元素的children创建fiber树
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < element.length) {
// 先创建子元素
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null
}
if (index === 0) {
// 第一个子元素即为当前fiber的子节点
fiber.child = newFiber
} else {
// 第二个子元素通过sibling指针与上一个节点互为兄弟节点
prevSibling.sibling = newFiber
}
// 记录上一个fiber节点
prevSibling = newFiber
index++
}
// 3、寻找下一个工作单元
// 如果有子节点,直接返回字节点作为下一个工作单元
if (fiber.child) return fiber.child
let nextFiber = fiber
while (nextFiber) {
// 如果有兄弟节点,则返回兄弟节点作为下一个工作单元
if (nextFiber.sibling) return nextFiber.sibling
// 否则向上查询,查找父节点的是否有兄弟节点
nextFiber = nextFiber.parent
}
}
以上我们已经实现了一个简易版的不阻塞主线程高优先级任务的fiber架构。
第六步:Render and Commit Phases (提交渲染阶段)
上面的实现还存在一些问题,我们的fiber架构为了不阻塞页面渲染将创建和appendChild分解成了子任务,当我们出发渲染时,页面会出现dom元素一点一点更新的情况,导致用户看到的不是一个完整的渲染后的页面,而是看到页面一点一点地发生变更。
因此,我们需要移除fiber.parent.dom.appendChild(fiber.dom)
这个步骤,把这个任务进行一个收集,最后一次性执行。
function render (element, container) {
// 记录fiber root节点,也就是我们的fiber tree
wipRoot = {
dom: container,
props: {
children: [element]
},
}
nextUnitOfWork = wipRoot
}
let wipRoot = null
function workLoop (deadline) {
// 当前没有剩余的需要执行的子任务时,执行commit,把真实dom渲染到页面上
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
}
// 提交
function commitRoot () {
commitWork(wipRoot.child)
// 防止内存泄漏
wipRoot = null
}
// real commit
function commitWork (fiber) {
if (!fiber) return
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
// 递归执行appendChild
commitWork(fiber.child)
commitWork(fiber.sibling)
}
现在,我们把所有的appendChild操作从performUnitOfWork中进行了抽离,在完整的fiber树构建完毕之后,递归执行commitWork进行append操作。
第七步:Reconciliation(调和阶段)
到目前为止,我们仅完成了数据渲染到页面的整体结构,当我们的数据发生变化,删除或新增或修改元素,我们需要对更新后的fiber tree和之前的fiber tree进行比对,然后同步到页面上。
function commitRoot () {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = vipRoot
wipRoot.child = null
}
function render (element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
// 记录上一次commit的fiber树
alternate: currentRoot
}
deletions = []
}
let currentRoot = null
let deletions = null
function performUnitOfWork (fiber) {
// ...
const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
// 从performUnitOfWork中抽离深度优先遍历的逻辑,从直接创建变成比较
function recpmcileChildren (wipFiber, elements) {
let index = 0
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibing = null
// 如果有oldFiber,要继续进行比较操作,删除原节点
while (index < elements.length || oldFiber !== null) {
const element = elements[index]
let newFiber = null
// compare here
const sameType = oldFiber && element && element.type == oldFiber.type
if (sameType) {
// update the node
newFiber = {
type: oldFiber.type,
props: element.props, // 相同类型的节点取最新的props
dom: oldFiber.dom,
parent: wipFiber, // 父节点被wipFiber所存储
alternate: oldFiber, // 继续使用上次构建的Fiber节点
effectaTag: 'UPDATE' // 标记update
}
}
if (element && !sameType) {
// add this node
newFiber = {
type: element.type, // 更新节点类型
props: element.props, // 取最新节点的props
dom: null,
parent: wipFiber,
alternate: null,
effectTag: 'PLACEMENT'
}
}
if (oldFiber && !sameType) {
// delete the oldFiber's node
oldFiber.effectTag = 'DELETION'
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else {
wipFiber.sibling = newFiber
}
}
}
function commitWork (fiber) {
if (!fiber) return
const domParent = fiber.parent.dom
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
dom.appendChild(fiber.dom)
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
// 更新页面上的元素
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
const isEvent = key => key.startWith('on')
const isProperty = key => key !== "children" && !isEvent(key)
const isNew = (prev, next) => key => prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom (dom, prevProps, nextProps) {
// Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(eventType, prevProps[name])
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set or update new properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
}
第七步:支持函数式组件(Function Components)
function App (props) {
return <h1>Hi { props.name }</h1>
}
const element = <App name="foo">
const container = document.getElementById("root")
ReactDOM.render(element, container)
当我们的组件不再是标签式jsx,而是函数式组件时,我们将其视为一种特殊的type类型,然后对划分任务阶段的performUnitOfWork函数进行改造
function performUnitOfWork (fiber) {
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// ...
function updateFunctionComponent (fiber) {
// 执行函数,作为当前fiber节点的一个children属性,进入调和阶段
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function updateHostComponent (fiber) {/* 非函数式组件,交由调和函数,标记元素修改状态 */}
}
function commitWork (fiber) {
// ...
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
// 针对函数式组件,没有自身的dom时,将其要append的dom元素设为父元素的dom
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
// ...
if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
}
function commitDeletion (fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
}
第八步:支持react hooks
在函数组件中使用hooks,此处实现一个最简易的useState hooks
// 首先初始化一些变量用于增强我们的fiber架构
let wipFiber = null // record prev function fiber
let hookIndex = null //
function updateFunctionComponent (fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
}
function useState (initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: []
}
const action = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
// 首次执行setState时,react会将执行的action推入hook,queue
// 当我们对函数组件进行fiber任务执行阶段时,action会从oldHook.queue中取出,并一个一个执行,并且更新到hook.state上,此时函数组件拿到的就是最新的state了
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
}
nextUnitOfWork = wipRoot
// hook重新创建
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
总结
实现一个简易的react步骤
- 通过createElement实现jsx的转化
- 通过createElement转化得出的虚拟dom,递归实现ReacrDOM.render函数
- 通过requestIdleCallback实现任务的分段执行
- 因为直接分段的形式会使得用户看到不完整的页面。通过fiber架构,双向特殊链表,深度优先遍历,实现页面dom结构创建的分层和缓存,最后一次性递归append到页面
- 调和阶段对比oldFiber和newFiber的不同,实现新增,修改,删除三种页面常见的结构变化操作
- 支持函数式组件
- 支持建议的hooks