tiny-react
开始
jsx
// 定义一个 React 元素
const element = <h1 title="foo">Hello</h1>; // 第 1 行代码
// 获取一个 DOM 节点, 作为容器
const container = document.getElementById("root"); // 第 2 行代码
// 将 React 元素渲染到容器中
ReactDOM.render(element, container); // 第 3 行代码
替换第 1 行代码
jsx
// const element = React.createElement(type, props, ...children)
// 类似于
//! const element = Object.assign({ type }, { props: Object.assign(props, { children }) });
// 替换第 1 行代码
const element = React.createElement(
"h1" /** type */,
{ title: "foo" } /** props */,
"Hello" /** children */,
);
// 得到
const element = {
type: "h1",
// type: 创建的 DOM 节点的类型
// 创建 HTML 元素时, 传递给 document.createElement(tagName, options?) 中的 tagName
// type 也可以是一个函数
props: {
title: "foo",
// props: props 有一个特殊属性 children
// children: 这里 children 是一个字符串, 但通常 children 是一个元素数组
children: "Hello",
},
};
继续替换第 3 行代码
React 使用 render 方法更新 DOM
js
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
};
const container = document.getElementById("root");
// 继续替换第 3 行代码
// 使用 React 元素的 type 创建一个 DOM 节点
// Web API: document.createElement(tagName, options);
const node = document.createElement(element.type);
// 将 React 元素的所有 props 分配给该 DOM 节点
// Object.keys(element.props)
// // 先过滤 props 中的 children 属性
// .filter((propName) => propName !== "children")
// .forEach((propName) => (node[propName] = element.props[propName]));
node["title"] = element.props.title;
// 对于 props 中的 children 属性, 为 node 创建子节点
// 这里的 children 是一段文本, 所以创建一个文本节点
// Web API: document.createTextNode(data);
const text = document.createTextNode("");
// 使用 document.createTextNode 而不是设置 innerText 允许后续以相同方式处理所有元素
text["nodeValue"] = elements.props.children;
// 最后, 将 textNode (这里的 text) 添加到 h1 (这里的 node)
// 并将 h1 (这里的 node) 添加到 container
node.appendChild(text);
container.appendChild(node);
对比
jsx
const element = <h1 title="foo">Hello</h1>;
const container = document.getElementById("root");
ReactDOM.render(element, container);
js
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
};
const container = document.getElementById("root");
const node = document.createElement(element.type);
node["title"] = element.props.title;
const text = document.createTextNode("");
text["nodeValue"] = elements.props.children;
node.appendChild(text);
container.appendChild(node);
step 1: 实现 createElement 函数
观察 createElement
的调用
jsx
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
js
const element = React.createElement(
"div", // element.type
{ id: "foo" }, // element.props
// element.children
React.createElement(
"a" /** type */,
null /** props */,
"bar" /** children */,
),
React.createElement("b" /** type */),
);
js
function createElement(type, props, ...children /** 剩余参数 */) {
return {
type,
props: {
...props, // 扩展运算符
children,
},
};
}
children 数组可以包含数字或字符串等基本类型, 像这样:
<div>416</div> --> createElement("div", null, 416)
<div>foo</div> --> createElement("div", null, "foo")
需要将所有基本类型包装在自定义元素中, 指定自定义类型 type = TEXT_ELEMENT
js
function createElement(type, props, ...children /** 剩余参数 */) {
return {
type,
props: {
...props, // 扩展运算符
children: children.map((child) => typeof child === "object")
? child
: createTextElement(child),
},
};
}
function createTextElement(text /** number, string, ... */) {
return {
type: "TEXT_ELEMENT", // 自定义类型
props: {
nodeValue: text,
children: [],
},
};
}
js
const MyReact = {
createElement,
};
const element = MyReact.createElement(
"div", // element.type
{ id: "foo" }, // element.props
// element.children
MyReact.createElement(
"a" /** type */,
null /** props */,
"bar" /** children */,
),
MyReact.createElement("b" /** type */),
);
const container = document.getElementById("root");
ReactDOM.render(element, container);
jsx
// 使用以下注释, 当 babel 转换 JSX 时, babel 将使用 MyReact.createElement
/** @jsx MyReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
const container = document.getElementById("root");
ReactDOM.render(element, container);