react源码之createElement(step 1)

896 阅读5分钟

「这是我参与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;

参考: