概述
这篇文章主要实现一个简单的React。主要争对下面8个点来实现。需要先使用
npx create-react-app my-app来创建一个运行的环境
- Step I: 实现createElement函数
- Step II: 实现render函数
- Step III: 并发模式
- Step IV: Fibers (虚拟dom)
- Step V: Render and Commit 阶段(渲染和提交阶段)
- Step VI: Reconciliation 阶段(dom diff)
- Step VII: Function Components
- Step VIII: Hooks
最基本的React是什么样子的?
const element = <h1 title="foo">Hello</h1>
React会通过createElement将element转化为虚拟dom
const container = document.getElementById("root")
ReactDOM.render(element, container)
大概如上图,就是babel解析后会调createElement将 Jsx转化为虚拟dom,将虚拟dom转化为真实dom,将真实dom挂载到root跟节点这一过程。
React会通过createElement将element转化为虚拟dom过程
const element = <h1 title="foo">Hello</h1> //bable解析后调用createElement
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
最终得到
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
1、实现createElement函数
React.createElement函数有3个参数,第一个是dom节点类型,第二个是dom节点的属性,第三个是其他子节点
/**
*
* @param {*} type dom节点类型
* @param {*} props dom节点的属性
* @param {...any} children //其他子节点
* @returns
*/
function createElement(type,props,...children){
return {
type,
props:{
...props,
//如果子节点是一个数值型或者字符串就创建一个text节点
children:children?.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
}
}
function createTextElement(text){
return {
type:'TEXT_ELEMENT',//自己定义一个常量
props:{
nodeValue: text,
children: [],
}
}
}
2、实现render函数
我们得render函数是渲染的入口函数,一般2个参数,第一个是要渲染的虚拟dom,还有一个就是需要挂载的节点
function render(element, container) {
const { type, props } = element
const dom = type === 'TEXT_ELEMENT' //是否是文本节点
? document.createTextNode("")
: document.createElement(type)
const isProperty = key => key !== 'children'
//将元素props分配给节点
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
//我们递归地为每个children做同样的事情
element.props?.children?.forEach(child => {
render(child, dom)
});
container.appendChild(dom)
}
通过render函数,我们已经实现了一个可以渲染的库。但是离实现一个React我们才开始。
3、并发模式
上面的render是采用递归的方式更新,不能终端,一旦dom多了,更新必然会卡顿,我们就需要一个可中断的异步更新来代替同步更新。想要实现异步更新,我们需要把更新拆分得更细,比如之前需要渲染整棵dom树,现在我们就渲染树的每个节点。我们将把工作分成几个小单元,在完成每个单元后,如果有其他需要做的事情,我们将让浏览器中断渲染。
let nextUnitOfWork = null //全局定义下一个需要更新的单元
function performUnitOfWork(fiber){
//得到下一个需要更新的fiber
}
function workLoop(deadline){
let shouldYield = false //优先级有关的
while(nextUnitOfWork && !shouldYield){//遇到高优先级的就中断
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)//第一次主动调度
requestIdleCallback类似于setTimeout函数,不过它是在浏览器空闲的时候调度。
现在大家可能会猜到,我们定义了一个nextUnitOfWork,他会在调用render的时候第一次赋值。我们改造一下render
4、fiber
fiber是React里面存储数据的结构,如下图
每个节点基础的一些属性就是【cildren,parent,sibing,props】等等
react里面使用的是双缓存机制,一个是currentFiber表示展示在屏幕上domTree的结构,还有一个workInProgressFiber表示更新渲染过程中的domeTree,最后用更新好的workInProgressFiber 替换currentFiber就更新完成。
他们通过alternate
属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
//react中采用双缓存
let currentFiber = null
let workInProgressFiber = null
function render(element, container){
workInProgressFiber = {
dom:container,
alternate:currentFiber,
props:{
children:[element]
}
}
nextUnitOfWork = workInProgressFiber
}
创建节点的函数我们就新取一个名createDom
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
}
这样在调render函数的时候,我们就找到了第一个更新的单元,然后会在浏览器空闲的时候调用requestIdleCallback,就正式开启了React的渲染
5、Render and Commit 阶段
React中有render阶段和commit阶段,render阶段是创建得到最后的fiber数据,commit阶段就是把fiber数据渲染到页面。React中的diff比较是在render阶段。下面先看看render阶段干了些什么
6、Reconciliation 阶段
这个阶段主要是在第一次新增的时候是创建render tree,在后面update的时候进行的是diff 比较,将没有修改的部分直接使用原先的数据替换。这个部分主要在performUnitOfWork中经行。
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (
index < elements.length ||
oldFiber != null
) {
const element = elements[index]
let newFiber = null
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
后面继续更新