[笔记1]手写react——ToyReact jsx的实现

456 阅读4分钟

1. 项目初始化

// 1.创建toyReact文件夹
mkdir toyReact

// 2.进入toyReact文件夹
cd toyReact

// 3.创建项目
npm init -y

2. 配置webpack环境

1.安装webpack

npm install webpack webpack-cli --save-dev

2.配置webpack

建立webpack.config.js

//node的标准export
module.exports = {
	// 入口文件
	entry: {
		main: './main.js'
	}
    // 增加build后文件可读性,不压缩打包后文件
    mode: "development",
    optimization: {
    	minimize: false
    }
}

3.配置babel

安装babel

// babel-loader webpack将babel打包到main.js
// @babel/core  babel核心
// @babel/preset-env 将babel转义到所需js版本
// https://www.babeljs.cn/docs/babel-preset-env
npm install babel-loader @babel/core @babel/preset-env --save-dev

在webpack.config.js中配置babel-loader

module.exports = {
    // modules 打包规则
    module: {
    	rules: [
        // js 文件需经过babel
        	{
            	test: /\.js$/,
                use: {
                	loader: 'babel-loader',
                    // 配置babel-loader
                    options: {
                    	presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    }
}

安装babel的jsx插件

npm install @babel/plugin-transform-react-jsx --save-dev

配置babel的jsx插件

module.exports = {
    // modules 打包规则
    module: {
    	rules: [
        // js 文件需经过babel
        	{
            	test: /\.js$/,
                use: {
                	loader: 'babel-loader',
                    // 配置babel-loader
                    options: {
                    	presets: ['@babel/preset-env'],
                        plugins: ['@babel/plugin-transform-react-jsx']
                    }
                }
            }
        ]
    }
}

因为是手写的react借助react-jsx的塑料react,所以我们可以重新配置@babel/plugin-transform-react-jsx,让它看起来没有那么react

module.exports = {
    module: {
    	rules: [
        	{
            	test: /\.js$/,
                use: {
                	loader: 'babel-loader',
                    // 配置babel-loader
                    options: {
                    	presets: ['@babel/preset-env'],
                        // 这样打包后的‘React.createElement就会变成‘createElement’
                        plugins: [['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]
                    }
                }
            }
        ]
    }
}

3.实现toyReact

使用@babel/plugin-transform-react-jsx打包之后jsx是这个样子

// 打包前的jsx
let a = <div id="a" class="c">
	<div></div>
    <div></div>
</div>

// 打包后的jsx
var a = React.createElement("div", 
{id: "a", "class": "c"}, 
React.createElement("div", null), 
React.createElement("div", null))

所以我们可以知道,React.createElement()会传入三个参数tagName,tag的属性列表,tag的子元素

React.createElement(tagName, attributes, ...children)

那么我们可以实现最简单的createElement

function createElement (tagName, attributes, ...children) {
	return document.createElement(tagName)
}

现在为我们的createElement增加属性和子节点的配置

function createElement (tagName, attributes, ...children) {
	let e = document.createElement(tagName)
    // 增加属性
    for (let i in attributes) {
    	e.setAttribute(i, attributes[i])
    }
    // 增加子节点
    // 扩展运算符将children包装为一个数组
     for (let child of children) {
    	e.appendChild(child)
    }
	return e
}

我们的createElement已经可以实现使用,但是尚未考虑文本节点

function createElement (tagName, attributes, ...children) {
	let e = document.createElement(tagName)
    for (let i in attributes) {
    	e.setAttribute(i, attributes[i])
    }
  
     for (let child of children) {
     // 将child创建为文本节点
     	if (typeof child === "string") {
        	child = document.createTextNode(child)
        }
    	e.appendChild(child)
    }
	return e
}

目前,我们的createElement可以支持基础的dom操作,是合格的语法糖了🍬。

但问题依旧存在:在react的jsx中,小写的tagName对应生成原生dom对象,而大写的tagName则对应自定义组件tagName参数支持传入的,不仅仅是字符串,还支持class组件以及函数组件

故而,tagName其实是tagType参数,我们需要根据tagType生成不同的dom对象 (目前只支持class组件)

3.1 普通节点的wrapper实现

class ElementWrapper {
	constructor (type) {
    	// 创建根元素
    	this.root = document.createElement(type)
    }
    // 配置属性
    setAttribute (name, value) {
    	this.root.setAttribute(name, value)
    }
    // 添加子元素
    // 添加的是component,所以要取出传入的component的root
    appendChild (component) {
    	this.root.appendChild(component.root)
    }

}

//文本节点不需要设置属性及添加子元素
class TextWrapper {
	constructor (content) {
    	this.root = document.createTextNode(content)
    }
}

3.2 实现Component类

自定义组件需要继承Component类,限定它的默认行为

class MyComponent extends Component {
	render () {
    	return <div>
          <h1>myComponent</h1>
          {this.children}
        </div>
    }
}

那么模仿react的实现,我们的Component类大概是这个样子:

class Component {
	constructor () {
    	// 不需要有什么行为
        // 取到props
        this.props = Object.create(null)
        this.children = []
        // 初始化root
        this._root = null
    }
    // 把Component的属性存起来
    setAttribute (name, value) {
    	this.props[name] = value
    }
    // 添加子元素
    appendChild (component) {
    	this.children.push(component)
    }
    // 设置 root 的getter
    get root () {
    	if (!this._root) {
        // 渲染组件,调用组件的render方法
        // 如果render之后是component,则会递归调用,直至其成为elementWrapper或者textWrapper
        	this._root = this.render().root
        }
        return this._root
    }
}

3.3 实现render

function render (component, parentElement) {
    // parentElement为实际dom
	parentElement.appendChild(component.root)
}

3.4 重构createElement

function createElement (tagType, attributes, ...children) {
	let e
    
    if (typeOf tagType === "string") {
        // 如果是小写的tagName,则生成ElementWrapper对象
    	e = new ElementWrapper(type)
    } else {
        // 如果是组件,则生成对应的组件对象
    	e = new tagType
    }
    
    for (let i in attributes) {
        // 调用元素的setAttribute方法
    	e.setAttribute(i, attributes[i])
    }
    
    let insertChildren = (children) => {
    
    for (let child of children) {
     	// 如果child是文本节点
     	if (typeof child === "string") {
            // 构造文本节点元素
        	child = new TextWrapper(child)
        	}
        
        // 当child是数组的时候,即component中的children,需要展开child
          if (typeof child === "object" && child instanceof Array){
              // 递归调用
              insertChildren(child)
          } else {
              // 调用元素的appendChild方法
              e.appendChild(child)
          }
    	}
    }
    insertChildren(children)
  
	return e
}

3.5 main.js文件

import { Component, createElement, render } from "./toy-react";

class MyComponent extends Component {
    render () {
        return <div>
            <h1>myComponent</h1>
            {this.children}
        </div>
    }
}

render(<MyComponent id="a" class="c">
    <h2>ssss</h2>
</MyComponent>, document.body)

4. 一些笔记

通过阅读Babel官网,了解到@babel/plugin-transform-react-jsx插件有两种编译jsx的方式:

  • 运行时编译方式(React Automatic Runtime)
  • 手动引入React.createElement的方式(React Classic Runtime)

这解释了困扰我的两个问题:

1、为什么定义了createElement方法却没有调用?

因为babel jsx转换插件是“运行时编译”且pagram参数为createElement,所以代码编译时会自动解析jsx并调用createElement方法。

2、为什么render方法里父节点要接收一个component.root作为参数而不是component?

同样是由于babel jsx插件的“运行时”编译,调用createElement方法后会实例化一个Component对象,该对象初始root为null,从而会调用render方法(即MyComponent中的render方法),该方法返回一个JSX,从而又会调用createElement方法,此时是一个真实的DOM节点,所以会初始化一个ElementWrapper对象,该对象包含一个root属性,这时root就不为null了,而是一个div