本文主要记录阅读Build your own react一些记录,虽然该博客构建的ReactV16.8
(所以博客中react
代码都是V16
的写法),和现在ReactV19
版本有一定差别,但V16.8
开始包含hooks
组件,主体渲染流程是基本一致的,原文最后目的是通过砍掉react优化策略内容,保留最本质重要思想,并实现didact框架(200多行代码)模拟react
基本渲染过程。
我们可以收获:
- 知道
react
写法对应的原生JS
实现都做了哪些事情; - 知道
react
背后渲染流程步骤有哪些,以及这么做理由; - 明白上面这些,自己再实现一个简约版
React(useState)
,加深对React的理解;
react写法对应原生JS实现
react
的jsx
是一种声明式写法,开发者无需直接操作dom
,只需在函数组件返回中写jsx
需要渲染dom
结构,然后react
帮助我们去更新dom
,所以react
是做了一层抽象封装,最后react
代码也是会被编译为对dom
操作,所以我们的角度(从react代码,看其背后原生js做的事情)。
tsx
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container) //V18之前写法
注:V18使用
ReactDOM.createRoot(root).render(<App />)
来渲染root节点以支持并发模式。
上面的jsx
写法const element = <h1 title="foo">Hello</h1>
通过babel
调用React
内置的createElement
方法,实际上会被转义一个js
对象记录节点的type类型
,props
和children
等基本信息 而ReactDOM.render(element, container)
,实际上做就是根据传入的React Element
对象生成对应dom元素并挂载到容器root节点上。
js
// jsx 对应js对象
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
}
},
const container = document.getElementById("root")
// ReactDOM.render对应做的事情
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
createElement
React.createElement
为了复刻一个react
,我们要做就是实现createElement
,在编译时让babel
调用我们实现createElement
转为js对象,对着react.createElement
实现看看参数列表,基本都是包含节点的type类型
,props
和children
信息,不同时返回节点类型不同,并且把input
单拎出来定义。
tsx
createElement('img',{src:'xxxx'})
自定义版createElement
自定义版不需要考虑很多,保留基本逻辑就行。
tsx
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
function createTextElement(text) {
return {
// 这里将文本类型,定义TEXT_ELEMENT
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
tsx
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<span>图片描述</span>
<img src='xxxx'/>
</div>)
// 上面jsx会转义成下面的babel调用
const element = Didact.createElement(
"div",
{ id:"foo"},
Didact.createElement("span", null, "图片描述"),
Didact.createElement("img")
),
// 对应js对象
const element= {
type: 'div',
props:{
id:"foo",
children:[{
type: 'span',
props:{
children:[{
type:'TEXT_ELEMENT',
props: {
nodeValue: '图片描述',
children: [],
},
}]
},
{
type: 'img',
props:{
children:[]
}
}
}]
}
}
render函数
有了需要确认渲染结构elements
对象,我们只需要传入render
函数执行document.createElement
等原生相关方法
ts
function render(element, container) {
const dom = element.type == "TEXT_ELEMENT"
? document.createTextNode("") : document.createElement(element.type)
const isProperty = key => key !== "children"
Object.keys(element.props).filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
小结
有了createElement
和render
,我们似乎可以通过写jsx
代码到渲染真正的dom
元素,但是render
里只有添加dom
,更新和删除并没有实现,并且由于render
是递归调用的,这意味着一旦开始渲染,就不会停止执行,如果元素树很大,可能会阻塞主线程太长时间。如果浏览器需要执行高优先级的操作,例如处理用户输入或保持动画流畅,则必须等到渲染完成。 所以还需要进行优化实现并发模式渲染 和fiber比较渲染提交。