1. createElemnt && render(草稿)
发布于 2022年 02月 16日 07:08
#react
1. createElemnt && render
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
通过babel
把JSX转换成JS
删除冗余信息得到
const element = React.createElement(
"div",
{
id: "foo",
},
React.createElement("a", null, "bar"),
React.createElement("b", null)
);
const container = document.getElementById("root");
ReactDOM.render(element, container);
(React)Node 和(React)Element
抛去JSX及虚拟DOM的概念不谈,我们先来回顾下HTML中真实的DOM是怎么划分的呢?
具体的到 Node properties: type, tag and contents看,这个网站一级棒!
那我们现在如果需要做个虚拟DOM,即使用对象来映射上面的DOM关系,你会怎么思考呢?
看图知道个大概意思就好了,图的细节不重要,细节在代码中呈现,如下代码:
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactNode = ReactChild | ReactNodeArray | boolean | null | undefined;
interface Attributes extends Record<string, any> {
children?: ReactNode;
}
interface ReactElement<P extends Attributes = any, T = string> {
type: T;
props: P;
}
createElemnt
现在,我们开始实现React.createElement
function createTextElement(text: ReactText): ReactElement {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
};
}
function createElement(
type: string,
props: Attributes | null,
...children: ReactChild[]
) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
)
}
};
}
console.log(`==============>createElement("div", null, a)`);
console.log(createElement("div", null, "a"));
// { type: 'div', props: { children: [ 'a' ] } }
console.log(`==============>createElement("div", null, a, b)`);
console.log(createElement("div", null, "a", "b"));
// { type: 'div', props: { children: [ 'a', 'b' ] } }
函数的作用就是返回一个ReactElement
类型,比较困惑的可能就是以下这段函数
children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
);
根据TS类型判断我们很容易知道child
的类型是ReactChild
,
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactElement<P extends Attributes = any, T = string> {
type: T;
props: P;
}
所以当child !== "object"
的时候,child
的类型就是ReactText
,即string
或者number
。
为了后面的代码简单和统一,我们使用createTextElement
进行包裹,具体缘由在后面说。
render
创建好虚拟DOM后,我们就需要把这些DOM映射到真实的环境中,这里我们仅实现ReactDOM
,即web环境。
哈哈!别的环境我也不会,又没有文章可以抄,唉!希望有大佬写个
react native
借我抄
function render(
element: ReturnType<typeof createElement>,
container: HTMLElement | null
): void {
if (!container) {
console.error("获取不到容器,请确保根节点是否存在");
return;
}
// 把虚拟DOM,即ReactElement 转换成真实的dom
const dom = document.createElement(element.type);
// 递归创建真实dom
element.props.children.forEach((child) => render(child, dom));
// 把dom插入到页面对应的容器中
container.appendChild(dom);
}
比较难理解的就两个
- 递归:
element.props.children.forEach((child) => render(child, dom))
- 类型:
element: ReturnType<typeof createElement>
根据以下两种写法,我们来回答刚刚前面问的为什么在child !== 'object'
时对ReactNode
进行包装?
因为这样可以确保返回的类型children
的ReactElement[]
,而获取该类型是为了方便以下函数的递归。
element.props.children.forEach((child) => render(child, dom));
在理解递归后,我们需要对上面的代码进行分类处理:
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
还有把属性添加到dom上
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
const isProperty = (key: Key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {
// @ts-ignore
dom[name] = element.props[name];
});
主呀!感谢万能的
@ts-ignore
然后代码就可以跑了!
全部代码
/* 类型-------------------------------------------------------------------------- */
type Key = string | number;
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactNode = ReactChild | ReactNodeArray | boolean | null | undefined;
interface Attributes extends Record<string, any> {
children?: ReactNode;
}
interface ReactElement<P extends Attributes = any, T = string> {
type: T;
props: P;
}
/* 源码实现-------------------------------------------------------------------------- */
function createTextElement(text: ReactText): ReactElement {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
};
}
function createElement(
type: string,
props: Attributes | null,
...children: ReactChild[]
) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
)
}
};
}
function render(
element: ReturnType<typeof createElement>,
container: Node | null
): void {
if (!container) {
console.error("获取不到容器,请确保根节点是否存在");
return;
}
// 把虚拟DOM,即ReactElement 转换成真实的dom
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
const isProperty = (key: Key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {
// @ts-ignore
dom[name] = element.props[name];
});
// 递归创建真实dom
element.props.children.forEach((child) => render(child, dom));
// 把dom插入到页面对应的容器中
container.appendChild(dom);
}
const React = {
createElement
};
const ReactDom = {
render
};
/*用例 -------------------------------------------------------------------------- */
const element = (
<div style="background: salmon">
<h1>Hello World</h1>
<h2 style="text-align:right">——忘尘</h2>
</div>
);
const rootElement = document.getElementById("root");
ReactDom.render(element, rootElement);