「这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战」
前言
从今天开始讲react系列。 react想必大家都用了很久了,可是react源码可能大部分同学还没有写过,主要的痛点就是找不到入手的地方,不知道该如何下手。我写这篇文章的目的也是希望能帮助想探索react源码,但是又无从下手的同学一些帮助,一起来探索react源码
准备环境
首先搭建一个简单的webpack环境
要安装的包
@babel/core // babel核心包
@babel/preset-react //支持jsx预发
@babel/preset-env // 支持最新的js语法
babel-loader // 把es6转化为es5
webpack // 这个大家都认识
webpack-cli // 可以在命令行运行webpack
webpack-dev-server // webpack服务器
html-webpack-plugin //生成html并自动引入打包文件的插件
clean-webpack-plugin //每次打包清除旧的dist里面的文件
jsx
我们知道react一大特点就是使用了虚拟dom,那么虚拟dom是如何生成的呢?
我们在写react的UI的时候,都使用jsx语法,jsx是js的一种扩展,可以让你在js中写html
const element = `<h1 title="foo">Hello<span>React</span></h1>`; //jsx
虽然jsx是js的一种扩展,但是浏览器并不能识别jsx,需要将jsx转化为js代码
那么jsx代码是如何转化为js代码的呢?
答案是由Babel转译为对createElement方法的调用
//引用babel核心包
const babel = require("@babel/core");
const element = `<h1 title="foo">Hello<span>React</span></h1>`;
// 使用@babel/preset-react预处理对字符串进行转义
// 此处是把jsx代码通过@babel/preset-react预设进行转化
const result = babel.transform(element, {
presets: ["@babel/preset-react"],
});
console.log(result.code);
//输出,此时输出的就是jsx经过babel转义后的js代码
/*#__PURE__*/
React.createElement(
"h1",
{
title: "foo"
},
"Hello",
/*#__PURE__*/
React.createElement("span", null, "React")
);
虚拟dom
那现在我们可以写自己的MyReact了
从上面的转义结果我们可以看出,createElement应该有三个参数
type->元素类型->对应"h1"
props->元素上面的属性->对应对象{title: "foo"}
children->对应子元素->"Hello"和React.createElement("span", null, "React"),所以我们children可以写成扩展运算符的形式
class React {
// 三个参数type类型,props元素上面的属性,为什么children是...children,因为children里面有可能包括很多子元素,所以是一个数组
createElement(type, props, ...children) {
return {
type,
props: {
// props是一个对象,所以我们展开
...props,
// 还记得this.props.children拿到子元素么,所以把children放到props中
children,
},
};
}
}
module.exports = new React();
// 使用我们自己的react
const React = require("./MyReact/react");
const babel = require("@babel/core");
const element = `<h1 title="foo">Hello<span>React</span></h1>`;
const result = babel.transform(element, {
presets: ["@babel/preset-react"],
});
// 转化上面的代码的时候,解析的代码里面会自动调用React.createElement
// TODO:所以为什么我们的jsx中没有使用React,也要引用react,原因就在这里
// 因为result.code是字符串,所以调用eval进行执行
const virtualDom = eval(result.code);
console.log(virtualDom);
输出
{
type: 'h1',
props: { title: 'foo', children: [ 'Hello', [Object] ] }
}
这就是我们的虚拟dom,是不是感觉很简单。 为什么叫虚拟dom,因为他不是真实的dom,他只是描述dom的js对象
文本节点的处理
和文本节点相关,我们可以看出我们的virtualDOM的文本节点是以文本字符串的形式存在VirtualDOM当中的,这明显不符合我们的要求。我们的要求是即使是文本节点,也要以节点对象的形式表示出来。
class React {
createElement(type, props, ...children) {
return {
type,
props: {
...props,
// createElement会返回一个字符串,所以我们来通过typeof child来区分是元素节点还是文本节点
children: children.map((child) =>
typeof child === "object" ? child : this.createTextElement(child)
),
},
};
}
createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
// 文本节点的children依然要写,就是一个空字符串
children: [],
},
};
}
}
module.exports = new React();
在JSX中有js表达式他的值呢是true或者false,根据我们之前学习了解到,virtualDOM中不展示true, flase ,null这三个值的,我们应该将这三个值清除
优化
在讲第二节之前,我们先把我们现在写的东西优化一下,主要是要使用webpack进行编译,这样我们就不用每个jsx都要用babel转化,而是webpack会给我们完成这个操作,还有就是html不支持模块化(require,import),所以也需要使用webpack来进行打包
如果对建立webpack有什么不太懂的,可以看我的另一个系列---驾驭webpack传送门
建立我们的webpack
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
mode: "development",
// 入口
entry: path.join(__dirname, "/src/index.js"),
// 出口
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.[hash:8].js",
},
module: {
rules: [
{
// 对以js结尾的文件进行es6转es5
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/,
},
],
},
plugins: [
// html生成插件
new HtmlWebpackPlugin({
// 生成html参考的模板
template: "./src/index.html",
}),
],
// 服务器
devServer: {
static: path.join(__dirname, "/dist"),
open: true,
},
};
建立.babelrc
// 预设,可以解析js最新预发和jsx语法
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
在src下创建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
在package.json中创建script
// 配置命令行
"scripts": {
"start": "webpack-dev-server --config webpack.config.js"
},
我们的react代码现在为
// 把我们的react定义为一个类
class React {
// 生成虚拟dom的createElement方法
createElement(type, props, ...children) {
return {
type,
props: {
...props,
// 有人可能要问,为什么要把children放到props中,这是因为,我们在写组件的时候,通常可以使用props.children拿到子组件,所以要把children放到props中
children: children.map((child) =>
typeof child === "object" ? child : this.createTextElement(child)
),
},
};
}
// 文本节点处理的createTextElement方法
createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
// 有人可能说这里为什么叫nodeValue,我记不住呀,这是节点值的意思https://www.runoob.com/jsref/prop-node-nodevalue.html
nodeValue: text,
children: [],
},
};
}
}
export default new React();
我们的jsx代码为
// 使用我们自己的react
import React from "../MyReact/React";
const App = (
<h1 title="foo">
Hello<span style="color:blue">React</span>
</h1>
);
export default App;