模拟react虚拟节点转换真实节点

2,672 阅读2分钟

1.主要涉及的API

  • ReactDom.render()
  • React.createElement()
  • React.Component

2.JSX

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。执行步骤:

ReactDom.render(vnode,container) => 
React.createElement(vnode) => // jsx转译
container.appendChild(node) // 转换为真实节点

接下来会做一个简单的模拟过程。

3.项目准备

  • 安装create-react-app
  • 新建kreact.js, kreact-dom.js文件放入k文件夹
  • 模拟改写index.js文件
  • index.js文件中引入我们自己的react,react-dom文件,并且设置三种类型的组件,函数组件,class组件,html标签;再准备一个子元素为数组类型的组件;调用ReactDom.render();
index.js如下:

import React, { Component } from "./k/kreact";
import ReactDOM from "./k/kreact-dom";
import "./index.css";

const list = [
  { id: 1, name: "第一行" },
  { id: 2, name: "第二行" },
  { id: 3, name: "第三行" },
];

// 函数组件
function FunComp() {
  return (
    <div
      name="函数组件"
      className="red"
      onClick={() => {
        console.log("click");
      }}
    >
      函数组件
      <div>
        {list.map((v) => (
          <div key={v.id}>{v.name}</div>
        ))}
      </div>
    </div>
  );
}

// jsx
const jsx = (
  <div name="dom组件" className="blue">
    dom组件
  </div>
);

// class组件
class ClassComp extends Component {
  render() {
    return (
      <div name="类组件" className="green">
        类组件
      </div>
    );
  }
}

ReactDOM.render(
  <div>
    {jsx}
    <FunComp />
    <ClassComp />
  </div>,
  document.getElementById("root")
);4.模拟React.createElement()

4.模拟React.createElement()

kreact.js文件:

// react jsx通过createElement方法转换为节点树
function createElement(type, props, ...children) {
  props.children = children;
  delete props.__source;
  delete props.__self;

  // 类组件和函数组件的type都返回function,
  // 因此可以在class组件继承的Component组件中设置静态属性以区分类组件和函数组件
  let vtype = ""; // 虚拟dom类型,1-html标签,2-类组件,3-函数组件
  if (typeof type === "string") {
    vtype = 1;
  } else if (type.isComponent) {
    vtype = 2;
  } else {
    vtype = 3;
  }
  return {
    vtype,
    type,
    props,
  };
}

export class Component {
  // class组件的标志,对象方便以后进行扩展,也可以是布尔值。
  static isComponent = {};
  constructor(props) {
    this.props = props;
    this.state = {};
  }
}
export default { createElement };

5.模拟ReactDom.render()

kreact-dom.js文件:

function render(vnode, container) {
  // 虚拟节点转为真实节点
  const node = mount(vnode);
  container.appendChild(node);
}

// 节点树转换为虚拟节点
function mount(vnode) {
  switch (vnode.vtype) {
    // html标签
    case 1:
      return createHtmlNode(vnode);
    // class组件类型
    case 2:
      return createClassNode(vnode);
    // 函数组件类型
    case 3:
      return createFunNode(vnode);
    // 文本类型(vnode为文本)
    default:
      return createTextNode(vnode);
  }
}

// 创建文本节点
function createTextNode(vnode) {
  return document.createTextNode(vnode);
}
// 创建html dom节点
function createHtmlNode(vnode) {
  const { type, props } = vnode;
  let node = document.createElement(type);
  // props把特殊的关键词单独拿出来,剩下的为props属性
  const { className, children, ...rest } = props;
  // className属性
  if (props.className) {
    node.setAttribute("class", props.className);
  }
  // 其余属性一一对应写上去就可以了
  Object.keys(rest).forEach((item) => {
    // 监听事件
    if (item.startsWith("on")) {
      node.addEventListener(
        item.toLocaleLowerCase().replace("on", ""),
        rest[item]
      );
    } else {
      node.setAttribute(item, rest[item]);
    }
  });

  children.forEach((item) => {
    // 子内容为数组时
    if (Array.isArray(item)) {
      item.forEach((v) => node.appendChild(mount(v, node)));
    } else {
      node.appendChild(mount(item, node));
    }
  });
  return node;
}
// class组件的vtype为class,需要new创建实例再调用实例的render方法获得返回的vnode,
// 再转换为html标签
function createClassNode(vnode) {
  const { type } = vnode;
  let instance = new type();
  return mount(instance.render());
}
// 函数组件的vtype为函数,需要调用方法获得返回的vnode,再转换为html标签
function createFunNode(vnode) {
  const { type } = vnode;
  return mount(type());
}

export default { render };