[源码向] [1/4] 用小白的视角构建react库

写在前边

  • 创作本篇博客的初衷是,在浏览社区时发现了https://pomb.us/build-your-own-react/这篇宝藏文章,该博主基于react16之后的fiber架构实现了一套react的简易版本,非常有助于理解react工作原理。但是苦于只有英文版本,且偏向理论。
  • 本着提升自我、贡献社区的理念。在此记录下学习历程,并尽自己微薄之力对重点部分(结合自己理解)进行翻译整理。希望对大家有所帮助。
  • 内容比较多,所以篇幅会较分散,大家也可以主要看自己需要的部分。我会努力更新的!

零、准备工作

  1. 创建项目(自己命名),下载文件包

    $ mkdir xxx
    $ cd xxx
    $ yarn init -y / npm init -y
    $ yarn add react react-dom

  2. 建立如下目录结构

    • src/

      • myReact/
      • index.js
      • index.html
      • main.jsx
  3. 初始化文件内容

    //index.html





    React App








    // main.jsx
    import React from "./React/index";
    import React from "react";
    import ReactDom from "react-dom";
    const App = () => {
    return
    Hello
    ;
    };
    ReactDom.render(, document.getElementById("root"));

    // myReact/index.js
    export default {}

  4. 安装 parcel 用于打包和热更新

    $ yarn add parcel-bundler

一、createElement的功能

功不可没的babel

// main.jsx
const element = (


)

经过babel转译后的效果(使用plugin-transform-react-jsx插件,https://www.babeljs.cn/docs/babel-plugin-transform-react-jsx#both-runtimes):

const element = React.createElement(
"div", //type
{ id: "foo" }, //config
React.createElement("a", null, "bar"), //...children
React.createElement("span")
)

  • babel的 plugin-transform-react-jsx 做的事情很简单: 使用 React.createElement 函数来从处理.jsx文件中的jsx语法。
  • 这也就是为什么在.jsx文件中必须 import React from "react" 的原因啦,否则插件会找不到React对象的!

配置babel

tips:笔者本来也打算使用 plugin-transform-react-jsx 插件,但是在调试中遇到了问题。查找后才知道最新版本的插件已经不再是由

Hello World

React.createElement('h1', null, 'Hello world') 的简单转换了(具体见https://zh-hans.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html),故退而求其次选择了功能类似的 transform-jsx

$ touch .babelrc
$ yarn add babel@transform-jsx

// .babelrc
{
"presets": ["es2015"],
"plugins": [
[
"transform-jsx",
{
"function": "OllyReact.createElement",
"useVariables": true
}
]
]
}

$ parcel src/index.html

此时页面中可以看到Hello字样,说明我们配置成功了!

动手实现createElement

transform-jsx 插件会将参数封装在一个对象中,传入createElement。

// myReact/index.js
export function createElement(args) {
const { elementName, attributes, children } = args;
return {
type:elementName,
props: {
...attributes,
children
}
};
}

考虑到children中还可能包含基本类型如string,number。为了简化操作我们将这样的children统一使用 TEXT_ELEMENT 包裹。

// myReact/index.js
export function createElement(type, config, ...children) {
return {
type,
props: {
...attributes,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
}
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
export default { createElement }

React并不会像此处这样处理基本类型节点,但我们这里这样做:因为这样可以简化我们的代码。毕竟这是一篇以功能而非细节为主的文章。

看看效果

首先为我们自己的库起个名字吧!

//.babelrc
{
"presets": ["es2015"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"runtime": "automatic",
"importSource": "OllyReact"
}
]
]
}

引入时就使用自己写的名字吧!

// main.jsx
import OllyReact from "./myReact/index";
import ReactDom from "react-dom"
const element = (


Hello World


—Oliver



);
ReactDom.render(element, document.getElementById("root"));

此时页面上已经出现了Hello , 这证明我们的React.createElement已经基本实现了React的功能。

二、Render功能

接下来编写render函数。

目前我们只关注向DOM中添加内容。修改和删除功能将在后续添加。

// React/index.js
export function render(element, container) {}
export default {
//...省略
render
};

细节实现

注意:

本小节每一步内容主要参考思路即可,详细的逻辑顺序会在底部汇总。

  • 首先使用对应的元素类型创建新DOM节点,并把该DOM节点加入股container中

    const dom = document.createElement(element.type)
    container.appendChild(dom)

  • 然后递归地为每个child JSX元素执行相同的操作

    element.props.children.forEach(child =>
    render(child, dom)
    )

  • 考虑到TEXT节点需要特殊处理

    const dom =
    element.type == "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(element.type)

  • 最后将元素的props分配给真实DOM节点

    Object.keys(element.props)
    .filter(key => key !== "children") // children属性要除去。
    .forEach(name => {
    dom[name] = element.props[name];
    });

汇总:

export function render(element, container) {
const dom = element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
Object.keys(element.props)
.filter(key => key !== "children")
.forEach(name => {
dom[name] = element.props[name];
});
element.props.children.forEach(child =>
render(child, dom)
);
container.appendChild(dom);
}

看看效果

// main.jsx
import OllyReact from "./myReact/index";
const element = (


Hello World


—Oliver



);
OllyReact.render(element, document.getElementById("root"));

此时看到我们的render函数也可以正常工作了!

小结

就是这样!现在,我们有了一个可以将JSX呈现到DOM的库(虽然它只支持原生DOM标签且不支持更新 QAQ)。

参考https://pomb.us/build-your-own-react/

你可能感兴趣的:(前端react.js)