React 实现render和createElement函数

1,951 阅读3分钟

引言

我们平时使用react开发一般都是这个结构:

import React from 'react';
import ReactDOM from 'react-dom';

const dom = <div>test source</div>;
ReactDOM.render(dom, document.getElementById("root"));

dom:JSX代码,需要用babel解析成虚拟dom,其实就是babel里调用了react的createElement方法,将jsx解析成虚拟dom(可能描述不准确,只是个人浅见)

ReactDOM.render: 将虚拟dom转换成真实dom并挂到根节点root

先介绍一下react的相关包,我们平时引入的React都是引自react.js,主要就是关于react的一些函数,比如React、Component、Context等;我们class调用的render()引用自 react-dom 顾名思义,就是操作dom相关的都在这里。 所以我们主要就是实现 ReactcreateElementReactDOMrender()

代码实现

测试用例:

// import React from 'react';
// import ReactDOM from 'react-dom';
// 调用自己写的方法
import React from "./wReact";
import ReactDOM from "./wReact/ReactDOM";

const dom = (
  <div>
    <p>111111</p>
  </div>
);

ReactDOM.render(dom, document.getElementById("root"));

实现createElement(type, props, ...children)

有人可能要问,我怎么没看到哪里调用了这个方法? 我当时也有这个疑问,于是我看了很多文章,发现,我们写的代码里确实没有调用痕迹,但是其实是调用了,在babel编译jsx的时候,调用了React的createElement方法,生成虚拟dom,生成的虚拟dom再由render函数渲染到真实dom节点。

function createElement(type, props, ...children) {
  // type: 必填; 表示元素的类型,比如:h1, div 等
  // props: 必填;表示该元素上的属性,使用 JavaScript 对象方式表示
  // children: 非必填;表示该元素内部的内容,可以是文字,可以继续嵌套另外一个React.createElement
  
  let defaultProps = {};
  // 处理默认属性,props优先级高于defaultProps
  if(type && type.defaultProps){
    defaultProps = type.defaultProps;
  }

  return {
    type,
    props: {
      ...defaultProps,
      ...props,
      // child(item) 可能是数组或者字符串,此处只是简单处理
      children: children.map(item => typeof item === 'object' ? item : createTextNode(item))
    }
  }
}

function createTextNode(value) {
  return {
    type: 'TEXT',
    props: {
      nodeValue: value,
      children: []
    },
  }
}

export default {
  createElement
}

实现 ReactDOM.render(vnode)

// vnode => node
function render(vnode, container) {
  const node = createNode(vnode);
  container.appendChild(node)
}

// 创建节点
function createNode(vnode){
  const {type, props} = vnode;
  let node;
  
  if(typeof type === 'function'){
    node = type.isReactComponent ? updateClassComponent(vnode) : updateFunctionComponent(vnode);
  }else if(type === 'TEXT'){
    // 创建文本节点
    node = document.createTextNode("");
  }else if(type){
    // 通过指定名称创建一个元素
    node = document.createElement(type);
  } else {
    // 创建了一虚拟的节点对象,节点对象包含所有属性和方法。
    // 就是创建一个空的节点,渲染的时候渲染此节点的所有子孙节点
    node = document.createDocumentFragment();
  }

  // 处理defaultProps
  if(type && type.defaultProps){
    const defaultProps = type.defaultProps;
    for (const key in defaultProps) {
      if(props[key] === undefined){
        props[key] = defaultProps[key]
      }
    }
  }

  updateNode(node, props);
  reconcilerChildren(props.children, node);
  return node;
}

// 对vnode的children进行操作
function reconcilerChildren(children, node){

  for (let i = 0; i < children.length; i++) {
    const child = children[i];

    // 遍历 创建元素
    // 判读child类型 当前child 可能是数组和字符串
    if(Array.isArray(child)){
      for (let j = 0; j < child.length; j++) {
        render(child[j], node);
      }
    }else{
      render(child, node);
    }
  }
}

// 更新节点上的属性和方法
function updateNode(node, nextVal) {
  
  Object.keys(nextVal).filter(item => item !== 'children').forEach(key => {
    // 以on开头 说明是个事件
    if(key.slice(0, 2) === 'on'){
      const eventName = key.slice(2).toLocaleLowerCase();
      node.addEventListenter(eventName, nextVal[key]);
    }else{
      node[key] = nextVal[key];
    }
  });
}

// class组件返回node
function updateClassComponent(vnode){
  // type: class本身
  const {type, props} = vnode;
  const cmp = new type(props); // 实例化
  const vvnode = cmp.render(); // class 的render函数里返回了jsx(虚拟dom)
  const node = createNode(vvnode); // vnode 转换为 node
  return node;
}

// function组件返回node
function updateFunctionComponent(vnode){
  const {type, props} = vnode;
  const vvnode = type(props);
  const node = createNode(vvnode);
  return node;
}

export default {
  render
}

简单实现,欢迎指正