• 手写一个react,看透react运行机制


    适合人群

    本文适合0.5~3年的react开发人员的进阶。

    讲讲废话:

    react的源码,的确是比vue的难度要深一些,本文也是针对初中级,本意让博友们了解整个react的执行过程。

    写源码之前的必备知识点

    JSX

    首先我们需要了解什么是JSX。

    网络大神的解释:React 使用 JSX 来替代常规的 JavaScript。JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。

    是的,JSX是一种js的语法扩展,表面上像HTML,本质上还是通过babel转换为js执行。再通俗的一点的说,jsx就是一段js,只是写成了html的样子,而我们读取他的时候,jsx会自动转换成vnode对象给我们,这里都由react-script的内置的babel帮助我们完成。

    简单举个栗子:

    return (
      <div>
        Hello  Word  </div>
    )
    
    实际上是:
    
    return React.createElement(
      "div",
      null,
      "Hello"
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面。

    虚拟Dom

    这里说明一下react的虚拟dom。react的虚拟dom跟vue的大为不同。vue的虚拟dom是为了是提高渲染效率,而react的虚拟dom是一定需要。很好理解,vue的template本身就是html,可以直接显示。而jsx是js,需要转换成html,所以用到虚拟dom。

    我们描述一下react的最简版的vnode:

    function createElement(type, props, ...children) {
      props.children = children;
      return {
        type,
        props,
        children,
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里的vnode也很好理解,
    type表示类型,如div,span,
    props表示属性,如{id: 1, style:{color:red}},
    children表示子元素
    下边会在createElement继续讲解。

    原理简介

    我们写一个react的最简单的源码:

    import React from 'react'
    import ReactDOM from 'react-dom'
    function App(props){
         return <div>你好</div>
     </div>
    }
    ReactDOM.render(<App/>,  document.getElementById('root'))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • React负责逻辑控制,数据 -> VDOM
      首先,我们可以看到每一个js文件中,都一定会引入import React from ‘react’。但是我们的代码里边,根本没有用到React。但是你不引入他就报错了。

    为什么呢?可以这样理解,在我们上述的js文件中,我们使用了jsx。但是jsx并不能给编译,所以,报错了。这时候,需要引入react,而react的作用,就是把jsx转换为“虚拟dom”对象。

    JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面。而引入React,就是为了时限这个过程。

    • ReactDom渲染实际DOM,VDOM -> DOM

    理解好这一步,我们再看ReactDOM。React将jsx转换为“虚拟dom”对象。我们再利用ReactDom的虚拟dom通过render函数,转换成dom。再通过插入到我们的真是页面中。

    这就是整个mini react的一个简述过程。相关参考视频讲解:进入学习

    手写react过程

    1)基本架子的搭建

    react的功能化问题,暂时不考虑。例如,启动react,怎么去识别JSX,实现热更新服务等等,我们的重点在于react自身。我们借用一下一下react-scripts插件。

    有几种种方式创建我们的基本架子:

    • 利用 create-react-app zwz_react_origin快速搭建,然后删除原本的react,react-dom等文件。(zwz_react_origin是我的项目名称)

    • 第二种,复制下边代码。新建package.json

        {
          "name": "zwz_react_origin",
          "scripts": {
            "start": "react-scripts start"
          },
          "version": "0.1.0",
          "private": true,
          "dependencies": {
            "react-scripts": "3.4.1"
          },
        }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      然后新建public下边的index.html

        DOCTYPE html>
        <html lang="en">
          <head>
          head>
          <body>
            <div id="root">div>
          body>
        html>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      再新建src下边的index.js

      这时候react-scripts会快速的帮我们定为到index.html以及引入index.js

        import React from "react";
        import ReactDOM from "react-dom";
      
        let jsx = (
          <div>
            <div className="">react启动成功</div>
          </div>
        );
        ReactDOM.render(jsx, document.getElementById("root"));
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      这样,一个可以写react源码的轮子就出来了。

    2) React的源码

    let obj = (
      <div>
        <div className="class_0">你好</div>
      </div>
    );
    console.log(`obj=${ JSON.stringify( obj) }`);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    首先,我们上述代码,如果我们不import React处理的话,我们可以打印出:
    ‘React’ must be in scope when using JSX react/react-in-jsx-scope
    是的,编译不下去,因为js文件再react-script,他已经识别到obj是jsx。该jsx却不能解析成虚拟dom, 此时我们的页面就会报错。通过资料的查阅,或者是源码的跟踪,我们可以知道,实际上,识别到jsx之后,会调用页面中的createElement转换为虚拟dom。

    我们import React,看看打印出来什么?

    + import React from "react";
    let obj = (
      <div>
        <div className="class_0">你好</div>
      </div>
    );
    console.log(`obj:${ JSON.stringify( obj) }`);
    
    结果:
    jsx={"type":"div","key":null,"ref":null,"props":{"children":{"type":"div","key":null,"ref":null,"props":{"className":"class_0","children":"你好"},"_owner":null,"_store":{}}},"_owner":null,"_store":{}}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    由上边结论可以知道, babel会识别到我们的jsx,通过createElement并将其dom(html语法)转换为虚拟dom。从上述的过程,我们可以看到虚拟dom的组成,由type,key,ref,props组成。我们来模拟react的源码。

    此时我们已经知道react中的createElement的作用是什么,我们可以尝试着自己来写一个createElement(新建react.js引入并手写下边代码):

    function createElement() {
      console.log("createElement", arguments);
    }
    
    export default {
      createElement,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时的打印结果:



    我们可以看出对象传递的时候,dom的格式,先传入type, 然后props属性,我们根据原本react模拟一下这个对象转换的打印:

    function createElement(type, props, ...children) {
      props.children = children;
      return {
        type,
        props,
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样,我们已经把最简版的一个react实现,我们下边继续看看如何render到页面

    3) ReactDom.render

    import React from "react";
    + import ReactDOM from "react-dom";
    let jsx = (
      <div>
        <div className="class_0">你好</div>
      </div>
    );
    // console.log(`jsx=${ JSON.stringify( jsx) }`);
    + ReactDOM.render(jsx, document.getElementById("root"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果此时,我们引入ReactDom,通过render到对应的元素,整个简版react的就已经完成,页面就会完成渲染。首先,jsx我们已经知道是一个vnode,而第二个元素即是渲染上页面的元素,假设我们的元素是一个html原生标签div。
    我们新建一个reactDom.js引入。

    function render(vnode, container) {
      mount(vnode, container);
    }
    
    function mount(vnode, container){
        const { type, props } = vnode;
        const node = document.createElement(type);//创建一个真实dom
        const { children, ...rest } = props;
        children.map(item => {//子元素递归
            if (Array.isArray(item)) {
              item.map(c => {
                mount(c, node);
              });
            } else {
              mount(item, node);
            }
        });
        container.appendChild(node);
    }
    
    
    //主页:
    - import React from "react";
    - import ReactDOM from "react-dom";
    + import React from "./myReact/index.js";
    + import ReactDOM from "./myReact/reactDom.js";
    let jsx = (
      <div>
        <div className="class_0">你好</div>
      </div>
    );
    ReactDOM.render(jsx, document.getElementById("root"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    此时,我们可以看到页面,我们自己写的一个react渲染已经完成。我们优化一下。

    首先,这个过程中, className="class_0"消失了。我们想办法渲染上页面。此时,虚拟dom的对象,没有办法,区分,哪些元素分别带有什么属性,我们在转义的时候优化一下mount。

     function mount(vnode, container){
        const { type, props } = vnode;
        const node = document.createElement(type);//创建一个真实dom
        const { children, ...rest } = props;
        children.map(item => {//子元素递归
            if (Array.isArray(item)) {
              item.map(c => {
                mount(c, node);
              });
            } else {
              mount(item, node);
            }
        });
    
        // +开始
        Object.keys(rest).map(item => {
            if (item === "className") {
              node.setAttribute("class", rest[item]);
            }
            if (item.slice(0, 2) === "on") {
              node.addEventListener("click", rest[item]);
            }
          });
        // +结束  
    
        container.appendChild(node);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    4) ReactDom.Component

    看到这里,整个字符串render到页面渲染的过程已完成。此时入口文件已经解决了。对于原始标签div, h1已经兼容。但是对于自定义标签呢?或者怎么完成组件化呢。

    我们先看react16+的两种组件化模式,一种是function组件化,一种是class组件化。

    首先,我们先看看demo.

    import React, { Component } from "react";
    import ReactDOM from "react-dom";
     class MyClassCmp extends React.Component {
    
      constructor(props) {
        super(props);
      }
    
      render() {
        return (
        <div className="class_2" >MyClassCmp表示:{this.props.name}</div>
        );
      }
    
    }
    
    function MyFuncCmp(props) {
      return <div className="class_1" >MyFuncCmp表示:{props.name}</div>;
    }
    let jsx = (
      <div>
        <h1>你好</h1>
        <div className="class_0">前端小伙子</div>
        <MyFuncCmp />
        <MyClassCmp  />
      </div>
    );
    ReactDOM.render(jsx, document.getElementById("root"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    先看简单点一些的Function组件。暂不考虑传递值等问题,Function其实跟原本组件不一样的地方,在于他是个函数,而原本的jsx,是一个字符串。我们可以根据这个特点,将函数转换为字符串,那么Function组件即跟普通标签同一性质。

    我们写一个方法:

    mountFunc(vnode, container);
    
    function mountFunc(vnode, container) {
      const { type, props } = vnode;
      const node = new type(props);
      mount(node, container);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时type即是函数体内容,我们只需要实例化一下,即可跟拿到对应的字符串,即是普通的vnode。再利用我们原来的vnode转换方法,即可实现。

    按照这个思路,如果我们不考虑生命周期等相对复杂的东西。我们也相对简单,只需拿到类中的render函数即可。

    mountFunc(vnode, container);
    
    function mountClass(vnode, container) {
      const { type, props } = vnode;
      const node = new type(props);
      mount(node.render(), container);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里可能需注意,class组件,需要继承React.Component。截图一下react自带的Component

    可以看到,Component统一封装了,setState,forceUpdate方法,记录了props,state,refs等。我们模拟一份简版为栗子:

    class Component {
      static isReactComponent = true;
      constructor(props) {
        this.props = props;
        this.state = {};
      }
      setState = () => {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    再添加一个标识,isReactComponent表示是函数数组件化。这样的话,我们就可以区分出:普通标签,函数组件标签,类组件标签。

    我们可以重构一下createElement方法,多定义一个vtype属性,分别表示

      1. 普通标签
      1. 函数组件标签
      1. 类组件标签

    根据上述标记,我们可改造为:

    function createElement(type, props, ...children) {
      props.children = children;
      let vtype;
      if (typeof type === "string") {
        vtype = 1;
      }
      if (typeof type === "function") {
        vtype = type.isReactComponent ? 2 : 3;
      }
      return {
        vtype,
        type,
        props,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    那么,我们处理时:

    function mount(vnode, container) {
      const { vtype } = vnode;
      if (vtype === 1) {
        mountHtml(vnode, container); //处理原生标签
      }
    
      if (vtype === 2) {
        //处理class组件
        mountClass(vnode, container);
      }
    
      if (vtype === 3) {
        //处理函数组件
        mountFunc(vnode, container);
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    至此,我们已经完成一个简单可组件化的react源码。不过,此时有个bug,就是文本元素的时候异常,因为文本元素不带标签。我们优化一下。

    function mount(vnode, container) {
      const { vtype } = vnode;
      if (!vtype) {
        mountTextNode(vnode, container); //处理文本节点
      }
      //vtype === 1
      //vtype === 2
      // ....
    }
    
    //处理文本节点
    function mountTextNode(vnode, container) {
      const node = document.createTextNode(vnode);
      container.appendChild(node);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    简单源码:

    package.json:

    {
      "name": "zwz_react_origin",
      "version": "0.1.0",
      "private": true,
      "dependencies": {
        "react": "^16.10.2",
        "react-dom": "^16.10.2",
        "react-scripts": "3.2.0"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject"
      },
      "eslintConfig": {
        "extends": "react-app"
      },
      "browserslist": {
        "production": [
          ">0.2%",      "not dead",      "not op_mini all"    ],    "development": [      "last 1 chrome version",      "last 1 firefox version",      "last 1 safari version"    ]  }}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    index.js

    import React from "./wzReact/";
    import ReactDOM from "./wzReact/ReactDOM";
    
    class MyClassCmp extends React.Component {
      constructor(props) {
        super(props);
      }
    
    render() {
        return (
        <div className="class_2" >MyClassCmp表示:{this.props.name}</div>
        );
      }
    }
    
    function MyFuncCmp(props) {
      return <div className="class_1" >MyFuncCmp表示:{props.name}</div>;
    }
    
    let jsx = (
      <div>
        <h1>你好</h1>
        <div className="class_0">前端小伙子</div>
        <MyFuncCmp name="真帅" />
        <MyClassCmp name="还有钱" />
      </div>
    );
    
    ReactDOM.render(jsx, document.getElementById("root"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    /wzReact/index.js

    function createElement(type, props, ...children) {
      console.log("createElement", arguments);
      props.children = children;
      let vtype;
      if (typeof type === "string") {
        vtype = 1;
      }
      if (typeof type === "function") {
        vtype = type.isReactComponent ? 2 : 3;
      }
      return {
        vtype,
        type,
        props,
      };
    }
    
    class Component {
      static isReactComponent = true;
      constructor(props) {
        this.props = props;
        this.state = {};
      }
      setState = () => {};
    }
    
    export default {
      Component,
      createElement,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    /wzReact/ReactDOM.js

    function render(vnode, container) {
      console.log("render", vnode);
      //vnode-> node
      mount(vnode, container);
      // container.appendChild(node)
    }
    // vnode-> node
    function mount(vnode, container) {
      const { vtype } = vnode;
      if (!vtype) {
        mountTextNode(vnode, container); //处理文本节点
      }
      if (vtype === 1) {
        mountHtml(vnode, container); //处理原生标签
      }
    
      if (vtype === 3) {
        //处理函数组件
        mountFunc(vnode, container);
      }
    
      if (vtype === 2) {
        //处理class组件
        mountClass(vnode, container);
      }
    }
    
    //处理文本节点
    function mountTextNode(vnode, container) {
      const node = document.createTextNode(vnode);
      container.appendChild(node);
    }
    
    //处理原生标签
    function mountHtml(vnode, container) {
      const { type, props } = vnode;
      const node = document.createElement(type);
    
      const { children, ...rest } = props;
      children.map(item => {
        if (Array.isArray(item)) {
          item.map(c => {
            mount(c, node);
          });
        } else {
          mount(item, node);
        }
      });
    
      Object.keys(rest).map(item => {
        if (item === "className") {
          node.setAttribute("class", rest[item]);
        }
        if (item.slice(0, 2) === "on") {
          node.addEventListener("click", rest[item]);
        }
      });
    
      container.appendChild(node);
    }
    
    function mountFunc(vnode, container) {
      const { type, props } = vnode;
      const node = new type(props);
      mount(node, container);
    }
    
    function mountClass(vnode, container) {
      const { type, props } = vnode;
      const cmp = new type(props);
      const node = cmp.render();
      mount(node, container);
    }
    
    export default {
      render,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    至此,本文mini简单版本源码结束,代码将在文章最后段送出。
    因本文定位初中级, 没有涉及react全家桶。
    下一篇,fiber,redux, hooks等概念或者源码分析,将在新文章汇总出。如对你有用,关注期待后续文章。

  • 相关阅读:
    unixbench cpu 性能测试
    1.5-16:买房子
    【docker系列】逐行解析Nginx镜像Dockerfile(学习经典)
    电子印章怎么弄?三步教你电子印章在线生成免费教程!
    Spring Boot中发送邮件时,如何让发件人显示别名
    宇视摄像机防水施工方法
    小型目标检测中的Transformer:一个基准和最先进技术的综述
    Flink CDC引起的Mysql元数据锁
    java的Integer中也会有缓存
    V8垃圾回收
  • 原文地址:https://blog.csdn.net/It_kc/article/details/127781642