React学习-渲染的奥秘

135 阅读4分钟

React学习-渲染的奥秘

相信深入学习过 ReactVue 的友友们或多或少都知道所谓框架的渲染,其实就是 Render 函数的作用,本文章就是针对该函数怎么实现的展开说明。

引入 jxs 背后的秘密

为什么这里要先说明jsx呢?

  • 现在的 React 很多写法都是跟 jsx 一起的,然后再利用 babel 插件将 jsx 编译成浏览器懂得的js代码。

还不清楚jsx是什么的朋友可以接着往下看,有说明的。

创建一个 React 项目

终端输入
create-react-app react_demo

当你打开你创建的项目的时候,其实里面的很多文件都是没啥用的,你大可将 src 目录下的全部文件删除到只剩下 index.js 文件,这个文件才是整个项目的入口文件。

说明 index.js 中的细节,并拆解jsx

此时的 index.js 文件

// src/index.js
import React from "react";
import ReactDOM from "react-dom/client";

const root = ReactDOM.createRoot(domcument.getElementById("root"));
root.render(<h1>Hello World</h1>)

想必大家发现了,为什么在 render 函数里面可以传一个html式的实参呢?其实这就是 jsx 语言了。 里面的 实参 其实会等价于:

root.render(<h1>Hello World</h1>);
// 等价于
const element = React.createElement("h1",{},"Hello World");
root.render(element); // 等下我们会重写 render 函数

// 也就是说
<h1>Hello World<h1>
// 会被解析成
const Vnode = {
    type:"h1",
    props:{},
    children
}
// 这也是大家耳熟能详的虚拟dom

上面引入了 React.createElement 方法,这是 react 内部实现的一个实现的一个办法,现在我们就来实现一下这个办法。

实现 React.createElement

确认函数的参数

在 src 目录下创建 react.js 文件。

// src/index.js
const element = React.createElement("h1",{},"Hello World");

由上述代码可以知道 createElement 函数传进去了三个实参,所以:

// src/react.js
function createElement(type,config = {}, children){
...
}

函数中的细节

// src/react.js
function ReactElement(type,props){
    const element = {type,props}
    return element;
}

function createElement(type,config = {}, children){
    let proName;
    
    // React 源码中的虚拟dom是props字段
    const props = {};
    for(proName in config) {
        props[propName] = config[propName];
    }
    
    // aguments.length 获取参数的数量,减二获取children的数量
    const childrenLength = aguments.length - 2;
    if(childrenLength === 1){
        props.children = children;
    }else if{
        props.children = Array.from(aguments).slice(2);
    }
    
    return ReactElement(type,props) // 返回出虚拟dom的格式
}

export default {creatElement};

渲染类组件

// src/react.js
class Component {
    static isReactCompont = true; 
    constructor(props) {
        this.props = props;
    }
}
...
exprot default {createElement,Component};

上述代码中 isReactCompont 起到至关重要的作用,在下面重写的 render 函数中,它用来识别是不是类组件。

实现 render 函数

在此之前,请允许我修改一下 index.js 内的代码:

// src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
const element = React.createElement("h1",{ className: "title" }, "hello World");
ReactDOM.render(element,document.getElementById("root"))

大家可以对比一下这跟React脚手架生成的代码有什么区别,颇有异曲同工之妙。

创建 react-dom.js 文件,确定参数

在src目录下创建 react-dom.js 文件

// src/react-dom.js
function render(element,parentNode){
...
}
export default { render }

观察 index.js 中的render函数可以知道,实际上传给该函数的参数是 渲染元素父元素

render 函数中的细节

相信熟悉react的友友们都知道,我们在所写的要渲染的代码,无疑就是 字符串、数字、函数式组件、类组件。其实 render 函数也就是分情况来编写代码逻辑,根据要渲染的东西类型的不同来进行不同的加工。

// react-dom.js
function render(element, parentNode) {
      if (typeof element == "string" || typeof element == "number") {
        return parentNode.appendChild(document.createTextNode(element));
      }
      let type, props;
      type = element.type; //Welcome1
      props = element.props;
      if (type.isReactComponent) {
        let returnedElement = new type(props).render();
        type = returnedElement.type; 
        props = returnedElement.props; 
      } else if (typeof type == "function") {
        let returnedElement = type(props);
        type = returnedElement.type; 
        props = returnedElement.props;
      }
      let domElement = document.createElement(type); //span
      for (let propName in props) {
        if (propName == "className") {
          domElement.className = props[propName];
        } else if (propName == "style") {
          let styleObj = props[propName]; 
          let cssText = Object.keys(styleObj)
            .map((attr) => {
              return `${attr.replace(/([A-Z])/g, function () {
                return "-" + arguments[1].toLowerCase();
              })}:${styleObj[attr]}`;
            })
            .join(";");
          domElement.style.cssText = "color:red;font-size:50px";
        } else if (propName === "children") {
          let children = Array.isArray(props.children)
            ? props.children
            : [props.children];
          children.forEach((child) => render(child, domElement));
        } else {
          domElement.setAttribute(propName, props[propName]);
        }
      }
      parentNode.appendChild(domElement);
}
export default { render };

分情况讨论:

  • 如果所渲染的是 字符串数字,直接渲染。
  • 如果所渲染的是 类组件isReactComponent,new 一个实例进行渲染
  • 如果所渲染的是 函数式组件:则将参数直接传给 type(因为它是一个函数)
  • 接下来就是 children 们的处理了,因为 jsx 中书别的类名(className)跟传统的 class 有所不同,所以先转换一下。在 jsx 中样式用的懂是小驼峰命名,所以也要用正则表达式转换一下。最后万一children里面还有children,很明显递归它!
  • 最后把所处理好的 domElement 插入刚刚传进来的父节点就万事大吉了。

待续