一步一个脚印搭建简易React

一步一个脚印搭建简易React

准备环境:

  1. node环境
  2. vscode编辑器

初始化项目

// 创建一个文件夹,当然你也可以手动创建
mkdir my-react
// 进入到项目目录
cd my-react
// 生成pakeage.json文件,这个文件主要是用来记录这个项目的详细信息的,它会将我们在项目开发中所要用到的包,以及项目的详细信息等记录在这个项目中
npm init

搭建项目环境

由于我们的目的是实现一个简易的React,为了能把更多的重心花在React上,我们需要借助一些工具来帮助我们处理一些React之外的问题。

  1. webpack: 我们需要使用webapck将我们的项目打包,最终生成一个js文件
  2. babel:能帮助我们将高级的ES语法向下转化成浏览器识别的低版本的ES语法
webpack
cnpm install --save-dev webapck webapck-cli

然后在我们项目的根目录新建webapck.config.js文件来告诉webapck如何打包我们的js文件

module.exports = {
  entry: './src/main.js',
};

同时创建src目录,并且新建main.js作为项目的入口文件

// main.js
const array = [1, 2, 3, 4, 5];
array.find((item) => item === 1);
babel

babel需要安装的包比较多

cnpm install --save-dev babel-loader @babel/core @babel/preset-env
  1. babel-loader: 使用babel-loader处理js文件,会将es5以上的语法进行转义
  2. @babel/core: 封装了babel-loader需要用到的api
  3. @babel/preset-env: babel 内部经历了「解析 - 转换 - 生成」三个步骤。而 @babel/core 这个库则负责「解析」,具体的「转换」和「生成」步骤则交给各种插件(plugin)和预设(preset)来完成

tips1: 使用@babel开头是为了声明作用域

tips2: @babel/preset-*实际上就是各种插件的打包组合,也就是说各种转译规则的统一设定,目的是告诉loader要以什么规则来转化成对应的js版本

好了,回到我们的项目本身,我们需要告诉webapck,用babel-loader去打包我们的js文件,以及babel-loader对应的配置。

module.exports = {
  entry: {
    main: "./src/main.js",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
};

第一次打包编译

在终端执行npx webpack,就可以在dist文件加下查看打包出来的文件了

tips: 使用npx可以保证我们执行的是当前项目

my-react
│ 
│
└───dist
│   │   main.js
└───src
│   │   main.js
└───package.json
|
└───webpack.config.js

引入babel插件支持jsx语法

现在我们尝试在main.js中声明一个不一样的变量,然后再执行npx webpack

const array = [1, 2, 3, 4, 5];

array.find((item) => item === 1);

const ele = <div id="id" class="mr5" >
  <span> zaoren </span>
</div>;
console.log(ele);

我们看到报错了,原因是我们不能解析jsx语法

因此,我们引入另一款babel插件来帮助我们解析jsx语法 - @babel/plugin-transform-react-jsx

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

然后在我们的webapck.config.js中引入插件

{
    test: /\.js$/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: ["@babel/preset-env"],
        plugins: ['@babel/plugin-transform-react-jsx']
      },
    },
  },

然后再用webapck打包一下,发现没有报错,打包成功了!

然后我们尝试着用一个main.html来引入我们打包后的main.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./main.js"></script>
</body>
</html>

但是当我们在浏览器中运行的时候发现报了个错误

咦?React未定义,想想我们这篇文章的主题是什么?不就是实现一个简易的React吗?所以,到现在为止,我们不进一步使用工具来帮助我们简化工作量。完全靠自己了!!!

开始动手写React

从打包后的代码可以看出,首先,我们需要一个React变量,React变量的createElement方法接受了三个参数

  1. DOM节点类型
  2. DOM节点上的属性对象
  3. DOM上的子节点children

我们来简单动手实现一下React这个对象,并且把创建好的DOM对象挂载到html页面的body上

// main.js
let React = {
  createElement: (tagName, attributes, ...children) => {
    let ele =  document.createElement(tagName);
      
    Object.keys(attributes || {}).forEach(key => {
      ele.setAttribute(key, attributes[key]);
    });
    
    children.forEach(child => {
      ele.appendChild(child);
    })

    return ele;
  },
};

const ele = <div id="id" style="background: red" >
  <span>zaoren1</span>
  <span>zaoren2</span>
</div>;

document.body.appendChild(ele);

执行npx webpack之后,我们打开浏览器发现报错了!

调试后发现,我们这个t为文本“zaoren”,并且原生的Web API中createElement这个方法是不支持添加文本节点的,需要使用createTextNode方法(详情见MDN)

因此在创建子节点的时候,需要对是否是文本节点进行判断。

children.forEach(child => {
  if (typeof child === "string") {
    child = document.createTextNode(child);
  }
  ele.appendChild(child);
})

重新npx webpack,查看效果

可以看到我们的<div>子节点<span>以及我们的一些属性都能成功设置啦!恭喜你,已经完成了第一步!

与属性相对应对的是函数,但目前我们的React还是没有处理事件的能力的,那么只需要在attribute中过滤出函数属性,然后增加事件监听器(这里简单用正则去匹配on字符串)

... 省略部分
Object.keys(attributes || {}).forEach(key => {
  if (key.match(/^on/)) {
    let eventType = key.replace(/^on/, '').toLocaleLowerCase();
    ele.addEventListener(eventType, attributes[key]);
    return
  }
  ele.setAttribute(key, attributes[key]);
});

... 省略部分
const ele = <div id="id" style="background: red" >
  <span onClick={() => {console.log('add event success!')}}>zaoren1</span>
  <span>zaoren2</span>
</div>;

点击zaoren1,可以看到我们已经成功添加click事件啦!

实现自定义组件的挂载

用过React的同学会发现,我们在React中经常会用到自定义的组件,而我们的乞丐版React目前肯定是不支持渲染自定义的标签的,我们可以尝试着将之前的渲染方式修改一下

// main.js
class MyComponent {
  constructor() {

  }
}

const customComponent = <MyComponent />

// 注意 一定要使用一下customComponent 不然webpack在打包的时候会会略只声明没有使用的变量
console.log(customComponent);

其实也不奇怪,我们可以看到我们的MyComponent这个类型最后通过createElement去创建的时候是传了一个函数对象,原生的API肯定是不支持的。

那我们要怎么做呢?思考一下,我们在做React的时候是不是所有自定义的Class都会从React.Component继承?

所以我们顺着这个思路,写一个能被createElement识别的基类Component,然后我们所有自定义的Component再继承它。

那么接下来,我们的任务就是封装一个能渲染到原生页面上的Component类

  1. props: 首先按照经验,每个Component都能接受一个props的参数
  2. children:可以接受子组件作为children
  3. appendChild: 能往根节点添加子节点
  4. root: 根节点(实DOM)
  5. 有一个render方法去创建虚拟树(这个地方我们就用jsx自带的render方法,不重新定义),render方法本质上调用的是React.createElement方法(plugin-transform-react-jsx帮我们做了这些工作)

所以!!!我们还需要将我们的createElement方法做一下修改,之前的createElement方法返回的都是实DOM(不信你可以回去看一下),这不符合React的设计原则,我们需要让createElement都返回一个虚拟DOM

方法: 原来的createElement中创建DOM的逻辑用类来封装一下,其中调用原生Web API的document.createElement和document.createTextNode,用root变量来存储,createElement中返回的只是我们封装类的实例,想要拿到实DOM需要再调用root方法。

新建my-react.js文件,将React.Component的逻辑单独放在一个文件

my-react
│ 
│
└───dist
│   │   main.js
└───package.json
|
└───src
│   │   main.js
│   │   my-react.js
└───package.json
|
└───webpack.config.js

my-react.js

// my-react.js
export let React = {
  createElement: (tagName, attributes, ...children) => {
    let ele;
    
    if (typeof tagName === 'string') {
      // 原生标签的创建方法
      ele = new ElementWrapper(tagName, attributes);
    } else {
      ele = new tagName(attributes);
    }
    // 由于我们的children也是走的createElement逻辑,所以也是一个虚拟DOM,把child改成vchild方便理解
    children.forEach(vchild => {
      if (vchild === null) {
        return;
      }
      if (typeof vchild === "string") {
        vchild = new TextWrapper(vchild);
      }
      ele.appendChild(vchild);
    })
    
    return ele;
  },
};

class ElementWrapper {
  constructor(type, attributes) {
    this.root = document.createElement(type);

    Object.keys(attributes || {}).forEach(key => {
      if (key.match(/^on/)) {
        let eventType = key.replace(/^on/, '').toLocaleLowerCase();
        this.root.addEventListener(eventType, attributes[key]);
        return
      }
      this.root.setAttribute(key, attributes[key]);
    });
  }

  appendChild(vchild) {
    // appendChild 本质上是在实DOM上进行操作,所以需要取createElment返回的实例的实DOM
    this.root.appendChild(vchild.root);
  }
}

class TextWrapper {
  constructor(content) {
    this.root = document.createTextNode(content);
  }
}

class Component {
  constructor(props) {
    this.props = props;
    this.children = [];
    
  }
  
  // 由于我们渲染的时候需要拿到实DOM所以需要一个获取root的属性
  get root() {
    // 拿到渲染后的虚拟DOM再去获取实DOM
    return this.render().root;
  }

}

React.Component = Component;

在main.js中测试我们的React

// main.js
import { React } from "./my-react";

class MyComponent extends React.Component {
  constructor() {}

  render() {
    return (
      <div id="id" style="background: red">
        <span
          onClick={() => {
            console.log("add event success!");
          }}
        >
          zaoren1
        </span>
        <span>zaoren2</span>
      </div>
    );
  }
}

const customComponent = <MyComponent />;

document.body.appendChild(customComponent.root);

好了,现在我们已经能加载自定义的组件了,为了使用起来更接近我们真正的React,我们新建一个react-dom.js文件来写一个render方法

my-react
│ 
│
└───dist
│   │   main.js
└───package.json
|
└───src
│   │   main.js
│   │   my-react.js
│   │   react-dom.js
└───package.json
|
└───webpack.config.js

react-dom.js

// react-dom.js
export const render = function(vElement, parentDOM) {
  parentDOM.appendChild(vElement.root);
}

main.js

// main.js
+ import { render } from "./react-dom";
...
+ render(<MyComponent />, document.body);
- document.body.appendChild(customComponent.root);

实现setState来更新组件状态

// my-react.js
class Component {
  constructor(props) {
    this.props = props;
    this.children = [];
    this._root = null;
  }

  setState(state) {
    this.state = state;

    let oldRoot = this._root;

    if (oldRoot && oldRoot.parentNode) {
      oldRoot.parentNode.replaceChild(this.root, oldRoot);
    }
  }

  // 由于我们渲染的时候需要拿到实DOM所以需要一个获取root的属性
  get root() {
    // 拿到渲染后的虚拟DOM再去获取实DOM
    // 每次获取root的时候都将root在_root中保存一份???
    return this._root = this.render().root;
  }
}
// main.js
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'zaoren1'
    }
  }

  render() {
    const { name } = this.state;
    return (
      <div id="id" style="background: red">
        <span
          onClick={() => {
            this.setState({
              name: 'setState success!'
            })
          }}
        >
          {name}
        </span>
        <span>zaoren2</span>
      </div>
    );
  }
}

然后点击我们的zaoren1,内容变成setState success!

同时我们可以看到,React中的setState只是调用了replaceChild替换了某个父节点下的子节点,并不需要重新渲染整个

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值