(二)手写 React 源码之初始化渲染

学习点

  • 实现createElement
  • 实现render

代码仓库:github.com/kjhuanhao/j...

文件路径

  • src/react.js:存放 createElemnet
  • src/common.js:存放REACT_ELEMENT
  • src/react-dom.js:存放ReactDOM的 render
  • src/inde.js:入口文件

createElement

因为我们是使用官方的脚手架创建的,所以默认是使用新的转换形式,为了接近 react 的原始形态,所以后续的源码采用旧的转换形式

在 package.json 中进行如下设置,然后重启项目即可

json 复制代码
 "scripts": {
    "start": "DISABLE_NEW_JSX_TRANSFORM=true react-scripts start"
  }

然后看看是否正常

javascript 复制代码
import ReactDOM from 'react-dom/client';
import React from 'react';


const root = ReactDOM.createRoot(document.getElementById('root'));
let element = <div>Hello</div>
React.createElement("div", null, "Hello")
root.render(element);

console.log(<div>Simple React</div>);
  • 旧版本的初始化代码
javascript 复制代码
import ReactDOM from "react-dom";

ReactDOM.render(<div>Simple React</div>, document.getElementById("root"));

所以这里需要实现两个方法,一个是 render,还有一个 createElement

render(React.createElement(元素), DOM 元素)

具体实现

由于 createElement 返回的是一个对象,这里写个基本架构

javascript 复制代码
function createElement() {
  return {
    
  }
}

这里先看看如下的 JSX 会创建怎么样的对象

javascript 复制代码
const element = 
<div class="red-color" kk='vv'>
Hi
    <span>xx1</span>
    <span>xx2</span>
</div>

转换后

javascript 复制代码
const element = /*#__PURE__*/React.createElement("div", {
  class: "red-color",
  kk: "vv"
}, "Hi", /*#__PURE__*/React.createElement("span", null, "xx1"), /*#__PURE__*/React.createElement("span", null, "xx2"));

由此可知这个函数接收三个参数,分别是 type,properties,children

再看看返回值,返回值就是一个虚拟的 DOM 对象,直接去 copy 之前打印出来的

定义一个常量,在 common.js当中(这里随意文件都可以)

javascript 复制代码
export const REACT_ELEMENT = Symbol('react.element')

一个基本的返回值就构建好了

javascript 复制代码
import { REACT_ELEMENT } from './common'

function createElement(type, properties, children) {
  return {
    $$typeof: REACT_ELEMENT,
    type,
    ref, // 用于操作 DOM
    key, // 用于更新时做比较,diff
    props
  }
}

但是在打印的时候,还看到了其他的属性,这些属性实际上和 react 的逻辑是没有关联的,是babel 带来的,因此可以删掉,然后对刚刚没有定义变量进行处理

javascript 复制代码
import { REACT_ELEMENT } from 'react'

function createElement(type, properties, children) {
  let ref = properties.ref || null;
  let key = properties.key || null;

  ['key','ref','_self', '_source'].forEach(key => {
    delete properties[key]
  })
  let props = {...properties}
  return {
    $$typeof: REACT_ELEMENT,
    type,
    ref, // 用于操作 DOM
    key, // 用于更新时做比较,diff
    props
  }
}

打印如下的对象再看看

js 复制代码
console.log(<div class="red-color" kk='vv'>Hi<span>xx1</span><span>xx2</span></div>);

可以看到 props 里面是一个数组,所以需要对 props 再进行处理

如何处理?先看看这个 arguments 里面是什么

如果传进来的参数长度大于 3,那么获取从索引为2开始到末尾的所有参数,并将它们转换为一个数组,并且赋值给 children,如果不是大于 3 则正常赋值即可

javascript 复制代码
  if (arguments.length > 3) {
    props.children = Array.prototype.slice.call(arguments, 2);
  } else {
    props.children = children;
  }

最后导出这个对象

javascript 复制代码
const React = {
  createElement,
};
export default React;

完整代码

js 复制代码
import { REACT_ELEMENT } from "./common";

function createElement(type, properties, children) {
  let ref = properties.ref || null;
  let key = properties.key || null;

  ["key", "ref", "_self", "_source"].forEach((key) => {
    delete properties[key];
  });
  let props = {...properties};
  if (arguments.length > 3) {
    props.children = Array.prototype.slice.call(arguments, 2);
  } else {
    props.children = children;
  }
  return {
    $$typeof: REACT_ELEMENT,
    type,
    ref, // 用于操作 DOM
    key, // 用于更新时做比较,diff
    props,
  };
}
const React = {
  createElement,
};
export default React;

运行结果

js 复制代码
import React from "./react";
console.log(
  <div class="red-color" kk="vv">
    Hi<span>xx1</span>
    <span>xx2</span>
  </div>
);

ReactDOM render 实现

创建 react-dom.js

js 复制代码
function render(VNode, containerDOM) {
  // 将虚拟 DOM 转换为真实 DOM

  // 将得到的真实 DOM 挂载到containerDOM
  mount(VNode, containerDOM)
}
function mount(VNode, containerDOM){

}
const ReactDOM = {
  render
}

export default ReactDOM

为什么 render 中的挂载需要独立使用一个函数,实际上,render 是初始化渲染,并不等于挂载,挂载只是其中的一件事情

挂载函数

负责两件事情,第一个是根据虚拟 DOM 创建真实的DOM,第二个是将这个 DOM 对象加入到 DOM 容器当中

js 复制代码
function mount(VNode, containerDOM){
  let newDOM = createDOM(VNode);
  newDOM && containerDOM.appendChild(newDOM);
}

虚拟 DOM 到真实 DOM

再来看看 props 里面的内容,然后总结一下每个虚拟 DOM 的特点

  • type: div
    • props
      • children: Array
        • Hi
        • ReactElement
      • class
      • kk
      • ...其他的 key-value 使用 ts 描述如下
ts 复制代码
interface ReactElement {
    children: (string | ReactElement)[]; 
    class: string; 
    kk: string; 
    ...其他 key: value
}

如此就很清晰了,开始编写 createElement,重点在于如何处理这个 props 中的 children 数组,有如下几个情况:

  1. children 只是一个普通的对象,这时候证明只有一个子元素
xml 复制代码
  <div class="red-color" kk="vv">
    <span>xx1</span>
  </div>
  1. children 是一个数组,这时候有多个子元素
js 复制代码
console.log(
  <div class="red-color" kk="vv">
    Hi
    <span>xx1</span>
    <span>xx2</span>
    Hello
  </div>
);
  1. children 是一个字符串,这时候代表这个元素的一个文本
js 复制代码
  <div class="red-color" kk="vv">
    Hi
  </div>

因此处理如下

js 复制代码
function createDOM(VNode) {
  // 创建真实 DOM
  let {type, props} = VNode;
  let dom;
  if (type && VNode.$$typeof === REACT_ELEMENT){
    // 根据虚拟 DOM 创建真实 DOM
    dom = document.createElement(type);
  }
  // 处理子元素和属性值
  if (props) {
    // 如果是个普通的对象子节点
    if (typeof props.children === 'object' && props.children.type){
      mount(props.children, dom);
    }else if(Array.isArray(props.children)){
      // 对子节点进行处理
      mountArray(props.children, dom);
    }else if(typeof props.children === 'string'){
      // 如果是个文本
      dom.appendChild(document.createTextNode(props.children));  
      // TODO 处理属性值
    }
  }
  return dom;
}

对于是个数组的情况需要单独处理,采用递归的思想

js 复制代码
function mountArray(children, parent){
  if(!Array.isArray(children)) return
  for (let i = 0; i < children.length; i++){
    if (typeof children[i] === 'string'){
      parent.appendChild(document.createTextNode(children[i]));
    }else {
      mount(children[i], parent);
    }

  }
}

此时在页面上已经可以基本处理了,index.js

js 复制代码
ReactDOM.render(<div>Simple React</div>, document.getElementById("root"));

属性处理

我们需要实现将如下的属性进行处理

js 复制代码
ReactDOM.render(<div style={{color: "red", backgroundColor: "green"}}>Simple React</div>, document.getElementById("root"));

处理 style 的逻辑很简单,style 是一个对象,只需要依次遍历然后挂载即可

js 复制代码
function setPropsForDOM(dom, VNodeProps = {}){
  console.log(VNodeProps);
  if (!dom) return;
  for (let key in VNodeProps){
    console.log(key);
    if (key === 'children') continue;
    if (/^on[A-Z].*/.test(key)){
      // TODO 事件处理
    }else if(key === 'style'){
      // 样式处理
      Object.keys(VNodeProps[key]).forEach(styleName => {
        dom.style[styleName] = VNodeProps[key][styleName]
      })
    }else{
      dom[key] = VNodeProps[key];
    }
  }
}

完整代码

js 复制代码
import { REACT_ELEMENT } from "./common";

function render(VNode, containerDOM) {
  // 将虚拟 DOM 转换为真实 DOM

  // 将得到的真实 DOM 挂载到containerDOM
  mount(VNode, containerDOM)
}
function mount(VNode, containerDOM){
  let newDOM = createDOM(VNode);
  newDOM && containerDOM.appendChild(newDOM);
}

function createDOM(VNode) {
  // 创建真实 DOM
  let {type, props} = VNode;
  let dom;
  if (type && VNode.$$typeof === REACT_ELEMENT){
    // 根据虚拟 DOM 创建真实 DOM
    dom = document.createElement(type);
  }
  // 处理子元素和属性值
  if (props) {
    // 如果是个普通的对象子节点
    if (typeof props.children === 'object' && props.children.type){
      mount(props.children, dom);
    }else if(Array.isArray(props.children)){
      // 对子节点进行处理
      mountArray(props.children, dom);
    }else if(typeof props.children === 'string'){
      // 如果是个文本
      dom.appendChild(document.createTextNode(props.children));  
    }
    setPropsForDOM(dom, props);
  }
  
  return dom;
}

function mountArray(children, parent){
  if(!Array.isArray(children)) return
  for (let i = 0; i < children.length; i++){
    if (typeof children[i] === 'string'){
      parent.appendChild(document.createTextNode(children[i]));
    }else {
      mount(children[i], parent);
    }

  }
}
function setPropsForDOM(dom, VNodeProps = {}){
  console.log(VNodeProps);
  if (!dom) return;
  for (let key in VNodeProps){
    console.log(key);
    if (key === 'children') continue;
    if (/^on[A-Z].*/.test(key)){
      // TODO 事件处理
    }else if(key === 'style'){
      // 样式处理
      Object.keys(VNodeProps[key]).forEach(styleName => {
        dom.style[styleName] = VNodeProps[key][styleName]
      })
    }else{
      dom[key] = VNodeProps[key];
    }
  }
}
const ReactDOM = {
  render
}

export default ReactDOM;
相关推荐
September_ning3 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人3 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0013 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking6 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫7 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull11 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet1 天前
那总结下来,react就是落后了
前端·react.js
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router