一步一个脚印搭建简易React(第二步✌️)
实现自定义组件的挂载
用过React的同学会发现,我们在React中经常会用到自定义的组件,而我们的乞丐版React目前肯定是不支持渲染自定义的标签的,我们可以尝试着将之前的渲染方式修改一下
// main.js
class MyComponent {
constructor() {
}
}
const customComponent = <MyComponent />
// 注意 一定要使用一下customComponent 不然webpack在打包的时候会会略只声明没有使用的变量
console.log(customComponent);
其实也不奇怪,我们可以看到我们的MyComponent这个类型最后通过createElement去创建的时候是传了一个函数对象,原生的API肯定是不支持的。
那我们要怎么做呢?「思考一下,我们在做React的时候是不是所有自定义的Class都会从React.Component继承?」
所以我们顺着这个思路,写一个能被createElement识别的基类Component,然后我们所有自定义的Component再继承它。
那么接下来,我们的任务就是「封装一个能渲染到原生页面上的Component类」
- props: 首先按照经验,每个Component都能接受一个props的参数
- children:可以接受子组件作为children
- appendChild: 能往根节点添加子节点
- root: 根节点(实DOM)
- 有一个render方法去创建「虚拟树」(这个地方我们就用jsx自带的render方法,不重新定义),render方法本质上调用的是React.createElement方法(
plugin-transform-react-jsx
帮我们做了这些工作)
「所以!!!我们还需要将我们的createElement方法做一下修改,之前的createElement方法返回的都是实DOM(不信你可以回去看一下),这不符合React的设计原则,我们需要让createElement都返回一个虚拟DOM」
方法: 原来的createElement中创建DOM的逻辑用类来封装一下,其中调用原生Web API的document.createElement和document.createTextNode,用root变量来存储,createElement中返回的只是我们封装类的实例,想要拿到实DOM需要再调用root方法。
新建my-react.js
文件,将React.Component的逻辑单独放在一个文件
my-react
│
│
└───dist
│ │ main.js
└───package.json
|
└───src
│ │ main.js
│ │ my-react.js
└───package.json
|
└───webpack.config.js
my-react.js
// my-react.js
export let React = {
createElement: (tagName, attributes, ...children) => {
let ele;
if (typeof tagName === 'string') {
// 原生标签的创建方法
ele = new ElementWrapper(tagName, attributes);
} else {
ele = new tagName(attributes);
}
// 由于我们的children也是走的createElement逻辑,所以也是一个虚拟DOM,把child改成vchild方便理解
children.forEach(vchild => {
if (vchild === null) {
return;
}
if (typeof vchild === "string") {
vchild = new TextWrapper(vchild);
}
ele.appendChild(vchild);
})
return ele;
},
};
class ElementWrapper {
constructor(type, attributes) {
this.root = document.createElement(type);
Object.keys(attributes || {}).forEach(key => {
if (key.match(/^on/)) {
let eventType = key.replace(/^on/, '').toLocaleLowerCase();
this.root.addEventListener(eventType, attributes[key]);
return
}
this.root.setAttribute(key, attributes[key]);
});
}
appendChild(vchild) {
// appendChild 本质上是在实DOM上进行操作,所以需要取createElment返回的实例的实DOM
this.root.appendChild(vchild.root);
}
}
class TextWrapper {
constructor(content) {
this.root = document.createTextNode(content);
}
}
class Component {
constructor(props) {
this.props = props;
this.children = [];
}
// 由于我们渲染的时候需要拿到实DOM所以需要一个获取root的属性
get root() {
// 拿到渲染后的虚拟DOM再去获取实DOM
return this.render().root;
}
}
React.Component = Component;
在main.js中测试我们的React
// main.js
import { React } from "./my-react";
class MyComponent extends React.Component {
constructor() {}
render() {
return (
<div id="id" style="background: red">
<span
onClick={() => {
console.log("add event success!");
}}
>
zaoren1
</span>
<span>zaoren2</span>
</div>
);
}
}
const customComponent = <MyComponent />;
document.body.appendChild(customComponent.root);
好了,现在我们已经能加载自定义的组件了,为了使用起来更接近我们真正的React,我们新建一个react-dom.js文件来写一个render方法
my-react
│
│
└───dist
│ │ main.js
└───package.json
|
└───src
│ │ main.js
│ │ my-react.js
│ │ react-dom.js
└───package.json
|
└───webpack.config.js
react-dom.js
// react-dom.js
export const render = function(vElement, parentDOM) {
parentDOM.appendChild(vElement.root);
}
main.js
// main.js
+ import { render } from "./react-dom";
...
+ render(<MyComponent />, document.body);
- document.body.appendChild(customComponent.root);
实现setState来更新组件状态
// my-react.js
class Component {
constructor(props) {
this.props = props;
this.children = [];
this._root = null;
}
setState(state) {
this.state = state;
let oldRoot = this._root;
if (oldRoot && oldRoot.parentNode) {
oldRoot.parentNode.replaceChild(this.root, oldRoot);
}
}
// 由于我们渲染的时候需要拿到实DOM所以需要一个获取root的属性
get root() {
// 拿到渲染后的虚拟DOM再去获取实DOM
// 每次获取root的时候都将root在_root中保存一份???
return this._root = this.render().root;
}
}
// main.js
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'zaoren1'
}
}
render() {
const { name } = this.state;
return (
<div id="id" style="background: red">
<span
onClick={() => {
this.setState({
name: 'setState success!'
})
}}
>
{name}
</span>
<span>zaoren2</span>
</div>
);
}
}
然后点击我们的zaoren1
,内容变成setState success!
同时我们可以看到,React中的setState只是调用了replaceChild替换了某个父节点下的子节点,并不需要重新渲染整个DOM。
本文使用 mdnice 排版