自己造轮子系列——实现一个简易的React框架?(一)
写在前面
今天就来造轮子,实现一个自己的React简易框架,深入学习一下React框架的实现原理,加深一下对React框架的理解和应用。
就程序而言,最好学习知识点的方法或许就是在项目中实战,再次一等也是要写个demo运行起来,最好看懂示例代码后自己实现一遍,往往会发现很多细节值得推敲,很多实现思路值得借鉴。
背景知识
要开发自己的React框架,就不能不提一些原生JS的DOM操作,我们现在大量使用第三方框架,一些基础的DOM操作可能都生疏了,这里汇总一些这次造轮子将要用的API:
document.getElementById
根据节点id获取真实dom;document.createElement
根据类型创建DOM,比如div,h1,span之类;document.createTextNode
创建文本节点;dom.appendChild
添加子节点;
先列举这几个马上要用到的,后面有用到新的API,再添加;
概述
React框架具体的实现细节十分庞杂,这里就挑几个最具代表性的特性——JSX
,Function Component
,Hooks
,当然每一个特性的实现细节也十分繁琐,我们的目标是实现核心功能,理解原理就好。另外对于React最新的Fibers树结构和并发模型,也简单探讨下。
总结下来,具体要实现的轮子:
React.createElement
函数——核心功能,解析JSX;ReactDOM.render
函数——核心功能;- Concurrent Mode并发模型——探讨;
- Fibers Tree——探讨;
Function Component
;Hooks
;
开始造轮子
React背后的渲染逻辑
我们先来看看React框架最基本的使用方式,核心只要三行就可以了:
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
先声明一个最简单JSX组件,然后获取到html上根节点的DOM对象,最后通过ReactDOM.render
方法把element组件渲染在container里;container的作用是appendChild这里就不再讨论;通过和原生JS对比,我们把主要差异聚焦在:element这个jsx组件最后被怎么添加到container里的。
首先JSX并不是一个合法JS表达式,需要通过babel编译成合法JS,具体实现就是通过调用React.createElement
方法,传入JSX里的tag,props和children。
JSX编译后结构如下:
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
React.createElement
方法的返回的对象,结构如下:
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
现在我们先跳过React.createElement
这个把JSX把转化为JS的实现,先来看看原生JS该怎么把element对象渲染,来感受下React.createElement
的内部的实现逻辑。
const container = document.getElementById("root")
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)
可以看到,对于一个简单的element对象,我们直接调用了document.createElement
方法,拿到node节点;然后给node节点添加了element对象里props属性;而对于”Hello“
这个文本children则通过document.createTextNode
来创建;通过appendChild
添加到node
节点中,最后再通把node节点添加到container中,渲染完成。
对于一个简单的H1标签我们这样实现没啥问题,对于一个页面或者一个项目也这样一点点通过原生JS就显然不太合理了。那前端框架本身就是对节点渲染逻辑进行一层抽象,React
框架创造性地适配了大家熟悉XML形式
的Dom树结构——也就是现在的JSX
。
当然我们需要理解JSX解析成JS对象,这就是我们接下来要讲的React.createElement
方法了。
React.createElement
我们先来回忆下JSX在React中的使用方式:
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root");
ReactDOM.render(element, container)
element这个JSX在传给ReactDOM.render方法,编译后的代码是这样的:
const element = React.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
那我们接下来实现一个自己的createElement:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
目前只处理了两层节点嵌套,文本也算作一层节点。
在我们自己的createElement
方法里,接收一个标签type,props属性,和children,然后创建了一个节点对象。
另外需要特别注意的是,JSX转化为JS是babel编译时完成的,所以就需要告诉babel,使用我们自己定义createElement,而不是使用React.createElement。
如何React区分开来?我们给自己的React轮子起名叫Didact
。
const Didact = {
createElement,
}
// babel编译后
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b")
)
如上面这段代码,我们希望babel编译时用自己定义的Didact.createElement
来解析JSX,
只需要在JSX
前面加一行babel注释
:/** @jsx Didact.createElement */
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
现在babel在编译JSX的时候,就会调用我们Didact.createElement
方法了,最后我们还是先调用ReactDOM.render方法进行渲染,运行代码,H1
能正常渲染
就OK了。
render
接下来我们来类比ReactDOM.render
来实现我们自己的Didact.render
渲染方法:
function render(element, container) {
// TODO create dom nodes
}
const Didact = {
createElement,
render,
}
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
Didact.render(element, container)
这里我们彻底抛弃了React框架,全部替换了自己的轮子Didact
,接下来我们来实现render
方法:
这里我们来回忆下babel编译之后的节点对象:
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
然后我们把这样的节点对象添加到container里:
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)
}
可以看到render
方法接受两个参数,第一个参数就是前面babel通过Didact.createElement
方法编译返回
的节点对象,而不是直接传入的JSX;第二个参数就是真实的顶层dom容器,用来appendChild
。
在render函数中,对文本节点调用了document.createTextNode
,而对非文本节点调用了document.createElement
;然后把props中除children的属性添加给新创建的dom;对于children属性则通过递归
调用render函数来进行添加props属性并添加到父节点里;最后把所有的节点通过appendChild
层层添加根节点上container。
后记
关于React框架中,JSX渲染涉及到的React.createElement
和React.render
方法做了基础的实现,并使用自己的Didact渲染了简单的JSX组件。
在实际开发中,JSX的嵌套层级不止两层,同时一个组件不止有JSX,还有很多业务逻辑,也就是我们后面要讲到的Function Component,另外后面我们还会讲到Fiber
和Hooks
的实现原理。
敬请期待吧!
转载自:https://juejin.cn/post/7222626198672621624