目标
本文从 createRoot 开始,将一步一步实现原生组件的初步渲染,废话不多说,直接开始
开始
1. 实现 createRoot
使用方法:
const root = document.getElementById('root')
React.createRoot(root).render(<App/>)
实现过程:
export default createRoot(container: HTMLElement) {
const root = {
// 源码中这里包裹了一层
containerInfo: container
}
return new ReactDOMRoot(root)
}
function ReactDOMRoot(internalRoot) {
this._internalRoot = inernalRoot
}
// 给 ReactDOMRoot 的原型链上添加一个 render 方法
ReactDOMRoot.prototype.render = function(children) {
const root = this._internalRoot
// 更新节点
updateContainer(children, root)
}
function updateContainer(element, container) {
const { containerInfo } = container
// 创建 Fiber 节点
const fiber = createFiber(element, {
// 这里的 nodeName 会返回一个大写的标签,例如 DIV,这里我们转换成小写
type: containerInfo.nodeName.toLowerCase(),
// 保存当前节点的 DOM 或 实例(类组件)
stateNode: containerInfo
})
// 初次渲染或更新节点
scheduleUpdateOnFiber(fiber)
}
ReactFiberWorkLoop
// 全程 work in process 表示当前正在工作中的
let wip = null
// 用来记录根节点
let wipRoot = null
// 这个函数作用:初次渲染以及执行更新
export function scheduleUpdateOnFiber(fiber) {
wip = fiber
wipRoot = fiber
}
function performUnitOfWork() {
const { tag } = wip
switch(tag) {
// 原生标签
case HostComponent:
updateHostComponent(wip)
break
// 函数组件
case FunctionComponent:
updateFunctionComponent(wip)
break
// 类组件
case ClassComponent:
updateClassComponent(wip)
break
// 空节点组件
case Fragment:
updateFragmentComponent(wip)
break
// 文本节点
case HostText:
updateHostText(wip)
break
default:
break
}
}
function workLoop(IdleDeadLine) {
// 执行所有任务
while(wip && IdleDeadLine.timeRemaining() > 0 ) {
performUnitOfWork()
}
// 任务完成,进行 Commit 阶段
if(!wip && wipRoot) {
commitRoot()
}
}
function commitRoot() {
commitWorker(wipRoot)
wipRoot = null
}
function commitWorker(wip) {
// 这个函数做三件事
// 递归终止条件
if(!wip) {
return
}
// 1. 提交自己本身
const { flags, stateNode } = wip
// 原生标签的 parentNode 即 wip.return
const parentNode = wip.return
// 如果是类组件或者函数组件,写法会发生变化
if(flags & Placement && stateNode) {
parentNode.appendChild(stateNode)
}
// 2. 提交子节点
commitWorker(wip.child)
// 3. 提交兄弟节点
commitWorker(wip.sibling)
}
// 这个函数是 window 上的,不懂的可以去 mdn 查找一下
requestIdleCallback(workLoop)
ReactFiberReconciler
export function updateHostComponent(wip) {
if(!wip.stateNode) {
wip.stateNode = document.createElement(wip.type)
// 处理属性值
updateNode(wip.stateNode, wip.props)
}
// 处理原生标签
reconcileChildren(wip, wip.props.children)
}
// 处理属性值
export function updateNode(node, nextVal) {
Object.keys(nextVal).forEach(key => {
if(key === 'children') {
if(isStringOrNumber(nextVal[key])) {
node.textContent = nextVal[key]
}
} else {
node[key] = nextVal[key]
}
})
}
// 处理原生标签
function reconcileChildren(wip, children) {
if(isStringOrNumber(children)) {
return
}
// 确保 children 是一个 数组
const newChildren = Array.isArray(children) ? children : [children]
let previousNewFiber = null
for(let i = 0, n = newChildren.length, i < n;i ++) {
const newChild = newChildren[i]
if(newChild === null) {
continue
}
const newFiber = createFiber(newChild, wip)
if(previousNewFiber === null) {
// 挂载头结点
wip.child = newFiber
} else {
// 上一个节点的兄弟节点是 newFiber
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
}
}
总结
本文实现了 React 中对原生标签的初步渲染流程,当我们使用 createRoot 创建节点调用 render 方法之后,React 内部会逐一执行
- updateContainer(更新节点)
- scheduleUpdateOnFiber(初步渲染以及更新节点)
- workLoop(执行当前所有的任务,之后进入
Commit阶段) - performUnitOfWork (按照当前节点执行任务,初始化节点等)
- updateNode(处理属性值)
- reconcileChildren(处理,渲染原生标签)
- commitRoot(从自身开始更新,按照 自身 -> 子 -> 兄弟 的顺序进行更新)
至此,我们的原生标签就能渲染出来啦