这篇文章会带领读者从零构建一个简单的 react,一共大概 300 行代码。其中包含了 react 的核心理念,例如:fibers、hooks、reconciliation。
这边文章专注实现 React的方法组件 ,所以没有 Class组件相关的实现代码。
从头开始,以下是我们逐一添加到 React 版本中的所有内容:
- 第一步: createElement Function
- 第二步: render Function 渲染函数
- 第三步: Concurrent Mode 并发模式
- 第四步: Fibers
- 第五步: Render and Commit 阶段
- 第六步: Reconciliation 调和
- 第七步: Function Components 函数组件
- 第八步: Hooks
众所周知,React
的一大特色是 JSX
语法,它本质上是一个语法糖,类似 XML
的风格让 JSX
拥有更丰富的表现力。
例如下面的代码:
const element = <div title='foo'>Hello</div>
// 等同于
const element = React.createElement(
'div',
{ title: 'foo' },
'Hello'
)
再查看一个嵌套的例子:
const element = (<div title='foo'>
<span id='a'>hello</span>
<span id='b'>world</span>
</div>)
// 等同于
const element = React.createElement(
'div',
{ title: 'foo' },
React.createElement(
'span',
{ id: 'a' },
'hello'
),
React.createElement(
'span',
{ id: 'b' },
'world'
)
);
可以看到与使用原生 JS 相比, JSX
的表达更加简约直接,上面的例子可以直接运行,查看如下代码:
import React from 'react';
import ReactDOM from 'react-dom';
const element = React.createElement(
'div',
{ title: 'foo' },
React.createElement(
'span',
{ id: 'a' },
'hello'
),
React.createElement(
'span',
{ id: 'b' },
'world'
)
);
const container = document.getElementById('root');
ReactDOM.render(element, container);
运行 yarn demo11
可以查看效果。
解析
JSX
不在这篇文章要将的范畴,我们会借助类似babel
这样的转换工具帮我们转换JSX
。
上面我们谈论 JSX
的时候,我们用到了 React.createElement
方法,它是干什么的呢?
我们在 第一步 的例子中打印个日志看一下它的数据结构,如下图:
其中 props
和 type
这两个属性比较关键,这两个属性会帮助我们构建完整的虚拟dom树。其中:
type
是一个字符串,我们用它来创建不同类型的 DOM 节点,例如 div
、span
、h1
。
props
是一个对象,它具有 JSX 属性中的所有 key 和 value。另外,它还有一个特殊的属性:children
,它又是与上面一样类型的对象,这也是为什么整个对象其实是一棵树。
如果用 TypeScript
来表示这个对象,可以写为:
export interface IPropsFiber {
type: string;
props: {
children?: IPropsFiber[];
[props: string]: any
};
}
注意到我们的类型定义里有 fiber
这个单词,这一个重要的概念,但是这里先不展开讲,我们后续会更详细介绍。
言归正传,createElement
要做的事情很简单,就是根据入参转换出一个树结构出来,例如:
const element = (<div title='foo'>
<span id='a'>hello</span>
<span id='b'>world</span>
</div>)
// 等同于
const element = MyReact.createElement(
'div',
{ title: 'foo' },
MyReact.createElement(
'span',
{ id: 'a' },
'hello'
),
MyReact.createElement(
'span',
{ id: 'b' },
'world'
)
);
// 等同于
const element = {
type: 'div',
props: {
title: 'foo',
children: [
{
type: 'span',
props: {
id: 'a',
children: [
{
type: 'TEXT_ELEMENT',
props: {
nodeValue: 'hello',
children: []
}
}
]
}
},
{
type: 'span',
props: {
id: 'b',
children: [
{
type: 'TEXT_ELEMENT',
props: {
nodeValue: 'world',
children: []
}
}
]
}
}
]
}
};
注意到 createElement
会有三个或者更多入参,并且第三个参数后(包括第三个参数)都是其子元素,所以 createElement
的形参可以定义为:
createElement(type, props, ...children){
// ...
}
这样 children
其实就是包含所有子元素的数组。另外要注意,children
有可能包含原始数据类型,例如字符串,因此,我们需要将所有不是对象的 element 也包装成 element 对象,并为其创建特殊类型: TEXT_ELEMENT
。
于是我们的 createElement
方法可以定义为:
function createElement(type: string, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
};
}
function createTextElement(text: string) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
};
}
运行 yarn demo12
可以查看效果:
可看到一个树状的数据结构。
细心的同学会在 demo12 中看到 @jsx
的注释,如下:
/** @jsx createElement */
const element = (<div title='foo'>
<span id='a'>hello</span>
<span id='b'>world</span>
</div>);
解释一下,加上这行注释 parcel 就会用 babel 相应的插件来解析 JSX 语法。
总结一下:在第二步中,我们借助了 babel 的能力解析了 JSX 语法,然后通过我们自定义的 createElement
实现了树状结构。
实现 render 方法其实非常简单:
function render(element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
是的,就是这个可用的 render
方法。
但是,我们需要考虑递归的问题,因为 render 的第2个参数是一个树状的结构。
于是我们优化如下:
function render(element, container) {
const dom = document.createElement(element.type)
+ element.props.children.forEach(child => render(child, dom))
container.appendChild(dom)
}
考虑到 element 可能是原始数据类型,我们还需要继续优化:
function render(element, container) {
- const dom = document.createElement(element.type)
+ const dom =
+ element.type === 'TEXT_ELEMENT'
+ ? document.createTextNode('')
+ : document.createElement(element.type)
element.props.children.forEach(child => render(child, dom))
container.appendChild(dom)
}
另外,我们还要将树中属性写到 Dom 上:
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)
}
注意到这里过滤了 children
属性,因为在 react 中 children
表示子元素。
先阶段完整的 react
代码如下:
function createElement(type: string, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
};
}
function createTextElement(text: string) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
};
}
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);
}
/** 以下是业务代码 **/
/** @jsx createElement */
const element = (<div title='foo'>
<span id='a'>hello</span>
<span id='b'>world</span>
</div>);
render(element, document.getElementById('root'));
我们可以运行 yarn demo13
查看它的运行效果,和 react 是一样的效果。
Build your own React react-fiber-architecture React docs 烤透 React Hook