3. 可以看到 createElement 的第一个参数已经不再是 string ,而是我们定义的 Class 类,于是可以进行第一步改造,根据 type 进行对应的处理
function createElement(type, attributes, ...children) { let currentElement; if (typeof type === "string") { // 创建 dom 实例 currentElement = document.createElement(type); }else { // 获取对应的 dom 实例 currentElement = new type().render(); } // 处理属性 if (attributes) { for (const name in attributes) { currentElement.setAttribute(name, attributes[name]); } } // 处理子节点 if (children.length) { for (let child of children) { // 处理文本节点 if (typeof child === "string") { child = document.createTextNode(child); } // 往当前元素中插入子节点 currentElement.appendChild(child); } } return currentElement; } 复制代码
这样一来,我们就可以成功渲染 Class 组件
五、抽离逻辑实现 Toy-React
尽管上面我们实现了对 JSX 的渲染,但所有操作都在 main.jsx 中进行,包括 createElement 方法也是直接在该文件中声明和实现的,既然我们要实现 Toy-React , 那么我们应该要保证其在使用上要和 React 保持一致.
1. createElement 中要实现的功能有:
- 获取或创建 dom 实例
- 为 dom 实例设置 attribute
- 创建文本节点
- 为 dom 实例添加子节点
- 返回最终的 dom 实例
2. 为了让 createElement 中所有的 type 都能拥有正常调用 DOM API 的能力,我们需要给所有的 type 定义一个通用 ElmentWrapper,同时也为文本节点定义一个对应的 TextWrapper.
3. 同样的,为了让所有的 Class 组件拥有共同的一些功能特性,我们需要实现 Component 这个类,来保证所有 Class 组件拥有统一性.
4. 在 main.jsx 中最后是通过 document.body.appendChild(JSX) 的方式,把 JSX 转换后的结果最终渲染在页面上的,因此,在这里我们要实现 render 方法去替换这种方式.
toy-react.js 最终实现如下:
// ElementWrapper class ElementWrapper { constructor(type) { this.root = document.createElement(type); } setAttribute(name, value) { this.root.setAttribute(name, value); } appendChild(component) { this.root.appendChild(component.root); } } // TextWrapper class TextWrapper { constructor(content) { this.root = document.createTextNode(content); } } // Component export class Component { constructor() { this._root = null; this.props = {}; this.children = []; } setAttribute(name, value) { this.props[name] = value; } appendChild(component) { this.children.push(component); } get root() { if (!this._root) { this._root = this.render().root; } return this._root; } } // createElement export function createElement(type, attributes, ...children) { // 1. 获取 dom 实例 let currentElement; if (typeof type === "string") { currentElement = new ElementWrapper(type); } else { currentElement = new type(); } // 2. 处理 dom 实例属性 if (attributes) { for (const name in attributes) { currentElement.setAttribute(name, attributes[name]); } } // 3. 处理子节点 const insertChildren = (children) => { if (children.length) { for (let child of children) { // 处理文本节点 if (typeof child === "string") { child = new TextWrapper(child); } // 当子节点拥有子节点时,递归处理 // 即在组件中使用了 { this.children } 表达式 if (typeof child === "object" && child instanceof Array) { insertChildren(child); } else { currentElement.appendChild(child); } } } }; // 初始化调用 insertChildren(children); return currentElement; } // render export function render(component, parentElement) { parentElement.appendChild(component.root); } 复制代码
在 main.jsx 中使用如下:
import { createElement, render, Component } from './toy-react'; class MyComponent extends Component { render() { return (<div id="MyComponent"> <h1>i am MyComponent</h1> { this.children } </div>); } } const JSX = ( <div id="jsx"> <h1>i am Jsx</h1> <MyComponent> <h1>i am MyComponent child</h1> </MyComponent> </div> ); render(JSX, document.querySelector("#app")); 复制代码
渲染结果