学习点
- 实现createElement
- 实现render
文件路径
src/react.js
:存放 createElemnetsrc/common.js
:存放REACT_ELEMENTsrc/react-dom.js
:存放ReactDOM的 rendersrc/inde.js
:入口文件
createElement
因为我们是使用官方的脚手架创建的,所以默认是使用新的转换形式,为了接近 react 的原始形态,所以后续的源码采用旧的转换形式
在 package.json 中进行如下设置,然后重启项目即可
json
"scripts": {
"start": "DISABLE_NEW_JSX_TRANSFORM=true react-scripts start"
}
然后看看是否正常
javascript
import ReactDOM from 'react-dom/client';
import React from 'react';
const root = ReactDOM.createRoot(document.getElementById('root'));
let element = <div>Hello</div>
React.createElement("div", null, "Hello")
root.render(element);
console.log(<div>Simple React</div>);
- 旧版本的初始化代码
javascript
import ReactDOM from "react-dom";
ReactDOM.render(<div>Simple React</div>, document.getElementById("root"));
所以这里需要实现两个方法,一个是 render,还有一个 createElement
render(React.createElement(元素), DOM 元素)
具体实现
由于 createElement 返回的是一个对象,这里写个基本架构
javascript
function createElement() {
return {
}
}
这里先看看如下的 JSX 会创建怎么样的对象
javascript
const element =
<div class="red-color" kk='vv'>
Hi
<span>xx1</span>
<span>xx2</span>
</div>
转换后
javascript
const element = /*#__PURE__*/React.createElement("div", {
class: "red-color",
kk: "vv"
}, "Hi", /*#__PURE__*/React.createElement("span", null, "xx1"), /*#__PURE__*/React.createElement("span", null, "xx2"));
由此可知这个函数接收三个参数,分别是 type,properties,children
再看看返回值,返回值就是一个虚拟的 DOM 对象,直接去 copy 之前打印出来的
定义一个常量,在 common.js当中(这里随意文件都可以)
javascript
export const REACT_ELEMENT = Symbol('react.element')
一个基本的返回值就构建好了
javascript
import { REACT_ELEMENT } from './common'
function createElement(type, properties, children) {
return {
$$typeof: REACT_ELEMENT,
type,
ref, // 用于操作 DOM
key, // 用于更新时做比较,diff
props
}
}
但是在打印的时候,还看到了其他的属性,这些属性实际上和 react 的逻辑是没有关联的,是babel 带来的,因此可以删掉,然后对刚刚没有定义变量进行处理
javascript
import { REACT_ELEMENT } from 'react'
function createElement(type, properties, children) {
let ref = properties.ref || null;
let key = properties.key || null;
['key','ref','_self', '_source'].forEach(key => {
delete properties[key]
})
let props = {...properties}
return {
$$typeof: REACT_ELEMENT,
type,
ref, // 用于操作 DOM
key, // 用于更新时做比较,diff
props
}
}
打印如下的对象再看看
js
console.log(<div class="red-color" kk='vv'>Hi<span>xx1</span><span>xx2</span></div>);
可以看到 props 里面是一个数组,所以需要对 props 再进行处理
如何处理?先看看这个 arguments 里面是什么
如果传进来的参数长度大于 3,那么获取从索引为2开始到末尾的所有参数,并将它们转换为一个数组,并且赋值给 children,如果不是大于 3 则正常赋值即可
javascript
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2);
} else {
props.children = children;
}
最后导出这个对象
javascript
const React = {
createElement,
};
export default React;
完整代码
js
import { REACT_ELEMENT } from "./common";
function createElement(type, properties, children) {
let ref = properties.ref || null;
let key = properties.key || null;
["key", "ref", "_self", "_source"].forEach((key) => {
delete properties[key];
});
let props = {...properties};
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2);
} else {
props.children = children;
}
return {
$$typeof: REACT_ELEMENT,
type,
ref, // 用于操作 DOM
key, // 用于更新时做比较,diff
props,
};
}
const React = {
createElement,
};
export default React;
运行结果
js
import React from "./react";
console.log(
<div class="red-color" kk="vv">
Hi<span>xx1</span>
<span>xx2</span>
</div>
);
ReactDOM render 实现
创建 react-dom.js
js
function render(VNode, containerDOM) {
// 将虚拟 DOM 转换为真实 DOM
// 将得到的真实 DOM 挂载到containerDOM
mount(VNode, containerDOM)
}
function mount(VNode, containerDOM){
}
const ReactDOM = {
render
}
export default ReactDOM
为什么 render 中的挂载需要独立使用一个函数,实际上,render 是初始化渲染,并不等于挂载,挂载只是其中的一件事情
挂载函数
负责两件事情,第一个是根据虚拟 DOM 创建真实的DOM,第二个是将这个 DOM 对象加入到 DOM 容器当中
js
function mount(VNode, containerDOM){
let newDOM = createDOM(VNode);
newDOM && containerDOM.appendChild(newDOM);
}
虚拟 DOM 到真实 DOM
再来看看 props 里面的内容,然后总结一下每个虚拟 DOM 的特点
- type: div
- props
- children: Array
- Hi
- ReactElement
- class
- kk
- ...其他的 key-value 使用 ts 描述如下
- children: Array
- props
ts
interface ReactElement {
children: (string | ReactElement)[];
class: string;
kk: string;
...其他 key: value
}
如此就很清晰了,开始编写 createElement,重点在于如何处理这个 props 中的 children 数组,有如下几个情况:
- children 只是一个普通的对象,这时候证明只有一个子元素
xml
<div class="red-color" kk="vv">
<span>xx1</span>
</div>
- children 是一个数组,这时候有多个子元素
js
console.log(
<div class="red-color" kk="vv">
Hi
<span>xx1</span>
<span>xx2</span>
Hello
</div>
);
- children 是一个字符串,这时候代表这个元素的一个文本
js
<div class="red-color" kk="vv">
Hi
</div>
因此处理如下
js
function createDOM(VNode) {
// 创建真实 DOM
let {type, props} = VNode;
let dom;
if (type && VNode.$$typeof === REACT_ELEMENT){
// 根据虚拟 DOM 创建真实 DOM
dom = document.createElement(type);
}
// 处理子元素和属性值
if (props) {
// 如果是个普通的对象子节点
if (typeof props.children === 'object' && props.children.type){
mount(props.children, dom);
}else if(Array.isArray(props.children)){
// 对子节点进行处理
mountArray(props.children, dom);
}else if(typeof props.children === 'string'){
// 如果是个文本
dom.appendChild(document.createTextNode(props.children));
// TODO 处理属性值
}
}
return dom;
}
对于是个数组的情况需要单独处理,采用递归的思想
js
function mountArray(children, parent){
if(!Array.isArray(children)) return
for (let i = 0; i < children.length; i++){
if (typeof children[i] === 'string'){
parent.appendChild(document.createTextNode(children[i]));
}else {
mount(children[i], parent);
}
}
}
此时在页面上已经可以基本处理了,index.js
js
ReactDOM.render(<div>Simple React</div>, document.getElementById("root"));
属性处理
我们需要实现将如下的属性进行处理
js
ReactDOM.render(<div style={{color: "red", backgroundColor: "green"}}>Simple React</div>, document.getElementById("root"));
处理 style 的逻辑很简单,style 是一个对象,只需要依次遍历然后挂载即可
js
function setPropsForDOM(dom, VNodeProps = {}){
console.log(VNodeProps);
if (!dom) return;
for (let key in VNodeProps){
console.log(key);
if (key === 'children') continue;
if (/^on[A-Z].*/.test(key)){
// TODO 事件处理
}else if(key === 'style'){
// 样式处理
Object.keys(VNodeProps[key]).forEach(styleName => {
dom.style[styleName] = VNodeProps[key][styleName]
})
}else{
dom[key] = VNodeProps[key];
}
}
}
完整代码
js
import { REACT_ELEMENT } from "./common";
function render(VNode, containerDOM) {
// 将虚拟 DOM 转换为真实 DOM
// 将得到的真实 DOM 挂载到containerDOM
mount(VNode, containerDOM)
}
function mount(VNode, containerDOM){
let newDOM = createDOM(VNode);
newDOM && containerDOM.appendChild(newDOM);
}
function createDOM(VNode) {
// 创建真实 DOM
let {type, props} = VNode;
let dom;
if (type && VNode.$$typeof === REACT_ELEMENT){
// 根据虚拟 DOM 创建真实 DOM
dom = document.createElement(type);
}
// 处理子元素和属性值
if (props) {
// 如果是个普通的对象子节点
if (typeof props.children === 'object' && props.children.type){
mount(props.children, dom);
}else if(Array.isArray(props.children)){
// 对子节点进行处理
mountArray(props.children, dom);
}else if(typeof props.children === 'string'){
// 如果是个文本
dom.appendChild(document.createTextNode(props.children));
}
setPropsForDOM(dom, props);
}
return dom;
}
function mountArray(children, parent){
if(!Array.isArray(children)) return
for (let i = 0; i < children.length; i++){
if (typeof children[i] === 'string'){
parent.appendChild(document.createTextNode(children[i]));
}else {
mount(children[i], parent);
}
}
}
function setPropsForDOM(dom, VNodeProps = {}){
console.log(VNodeProps);
if (!dom) return;
for (let key in VNodeProps){
console.log(key);
if (key === 'children') continue;
if (/^on[A-Z].*/.test(key)){
// TODO 事件处理
}else if(key === 'style'){
// 样式处理
Object.keys(VNodeProps[key]).forEach(styleName => {
dom.style[styleName] = VNodeProps[key][styleName]
})
}else{
dom[key] = VNodeProps[key];
}
}
}
const ReactDOM = {
render
}
export default ReactDOM;