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