简单实现react的渲染

68 阅读6分钟

jsx介绍

jsx 是 React.createElement(component, props, ...children) 函数的语法糖。最终会通过babel转换变成React.createElement的形式。

可以使用babel进行转化看看,如下:

image.png

react的元素渲染

react的render函数执行流程大致如下:

  1. 接收的vdom变成真实dom
  2. 把vdom的属性更新到dom上
  3. 将vdom的儿子变成真实dom挂载到自己的dom上,dom.appendChild
  4. 将自己挂载到容器root上

看看下面这段代码

import React from "react";
import ReactDOM from "react-dom/client";

let element = (
  <div className="active" style={{ color: "red" }}>
    <span>hello world</span>
  </div>
);

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

它的执行过程如下:

  1. 通过babel将element变成React.createElement的形式
  2. React.createElement内部会返回vdom(虚拟dom,是一个js对象)
  3. 将这个vdom传递给render函数
  4. 然后执行render内部的步骤(上面讲过)

可以直接打印element看到vdom的形式

console.log(element);

image.png

也可以通过json.stringfy看的清楚点

console.log(JSON.stringify(element, null, 4));//4指定的缩进

image.png

React.createElement实现

简单实现React.createElement函数

//react.js

/**
 * 创建vdom
 * @param {*} type 标签类型
 * @param {*} config 传入的配置,例如类名,行内样式
 * @param  {...any} children 孩子
 * @returns 
 */
function createElement(type, config, ...children) {
   //如果孩子只有一个节点,那么vdom的children是对象形式,而不是数组形式,例如上面的json.strinfy的结果
  if (children.length === 1) {
    children = children[0];
  }
  let props = { ...config };
  props.children = children;

  return {
    type,
    props,
  };
}

const React = { createElement };
export default React;

校验,将上面通过babel转换得到的React.createElement用自己实现的createElement实现

import React from "./react";

let element2 = React.createElement(
  "div",
  {
    className: "active",
    style: {
      color: "red",
    },
  },
  React.createElement("span", null, "hello world")
);

console.log(JSON.stringify(element2, null, 4));

结果如下:

image.png

render函数实现

简单实现render函数,render函数内部流程:

  1. 接收的vdom变成真实dom
  2. 把vdom的属性更新到dom上
  3. 将vdom的儿子变成真实dom挂载到自己的dom上,dom.appendChild
  4. 将自己挂载到容器root上

render函数的四步会变成如下函数的实现过程

首先通过createDOM函数接收到的vdom转成真实dom,然后createDOM内部通过updateProps将dom上面的属性更新到刚刚创建的dom上,接着需要通过reconcileChildren将儿子变成真实dom挂载到父dom上。代码如下:

//ReactDOM.js
//渲染流程
// *  1.接收的vdom变成真实dom
// *  2.把vdom的属性更新到dom上
// *  3.将vdom的儿子变成真实dom挂载到自己的dom上,dom.appendChild
// *  4.将自己挂载到容器root上

/**
 * 将vdom渲染成真实dom并挂载容器上面
 * @param {*} vdom 虚拟dom对象
 * @param {*} container 挂载容器
 */
function render(vdom, container) {
  const dom = createDOM(vdom);
  container.appendChild(dom);
}

/**
 * 虚拟dom变成真实dom
 * @param {*} vdom
 */
function createDOM(vdom) {
  //是数字或字符串,直接返回一个文本节点
  if (typeof vdom === "string" || typeof vdom == "number") {
    return document.createTextNode(vdom);
  }
  //1.是一个vdom对象,变成真实dom
  let { type, props } = vdom;
  const dom = document.createElement(type);

  //2.更新vdom上面的属性到dom上
  updateProps(dom, props);

  //3.将vdom的儿子变成真实dom挂载到自己的dom上
  if (
    typeof props.children === "string" ||
    typeof props.children === "number"
  ) {
    dom.textContent = props.children;
  } else if (typeof props.children === "object" && props.children.type) {
    render(props.children, dom);
  } else if (Array.isArray(props.children)) {
    reconcileChildren(props.children, dom);
  } else {
      //到这里都是抛出错误,
      throw new Error();
  }

  return dom;
}

/**
 *  更新vdom上面的属性到dom上
 * @param {*} dom 真实dom
 * @param {*} newProps vdom上面的属性
 */
function updateProps(dom, newProps) {
  for (let key in newProps) {
    //遇到儿子先跳过,在后面处理
    if (key === "children") continue;

    if (key === "style") {
      let styleObj = newProps[key];
      for (let style in styleObj) {
        dom.style[style] = styleObj[style];
      }
    } else {
      dom[key] = newProps[key];
    }
  }
}

/**
 * 将儿子变成真实dom并挂载到parentDom上面
 * @param {*} childrenVdom 儿子的vdom数组
 * @param {*} parentDom 儿子需要挂载的位置,父容器
 */
function reconcileChildren(childrenVdom, parentDom) {
  for (let i = 0; i < childrenVdom.length; i++) {
    render(childrenVdom[i], parentDom);
  }
}

const ReactDOM = {
  render,
};

export default ReactDOM;

校验

//index.js
//引入自己创建的render
import React from "./react";
import ReactDOM from "./react-dom";


let element = (
  <div className="active" style={{ color: "red" }}>
    <span>hello world</span>
  </div>
);

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

结果成功渲染

image.png

添加背景颜色校验

let element = (
  <div className="active" style={{ color: "red", backgroundColor: "skyblue" }}>
    <span>hello world</span>
  </div>
);

image.png

函数组件渲染

函数组件转成vdom对象跟普通元素有点不同,它的type值是一个函数,所以需要在上面的创建真实dom需要做进一步判断

函数组件的vdom对象如图:


function Content(props) {
  return (
    <div className="active">
      <span style={{ color: "red" }}>name:</span>
      <span>{props.name}</span>
      {props.children}
    </div>
  );
}

let element = (
  <Content name="zs">
    <div>
      <span style={{ color: "red" }}>age</span>:18
    </div>
  </Content>
);

console.log(element);

image.png

createDOM做如下修改


    ...
  //1.是一个vdom对象,变成真实dom
  let { type, props } = vdom;
  let dom;
  //如果是一个函数组件
  if (typeof type === "function") {
    return renderFunctionComponent(vdom);
  } else {
    dom = document.createElement(type);
  }
    ...
    

renderFunctionComponent函数

/**
 * 将一个函数组件的vdom转成真实dom并返回
 * @param {*} vdom
 */
function renderFunctionComponent(vdom) {
  let { type, props } = vdom;
  let renderVdom = type(props); //函数执行后通过return返回的vdom;

  return createDOM(renderVdom); //renderVdom进行渲染并返回
}

校验

function Content(props) {
  return (
    <div className="active">
      <span style={{ color: "red" }}>name:</span>
      <span>{props.name}</span>
       {/* 当外部只有一个子孩子时,children不是一个数组,可以直接使用props.children */}
      {props.children}
    </div>
  );
}

let element = (
  <Content name="zs">
    <div>
      <span style={{ color: "red" }}>age</span>:18
    </div>
  </Content>
);

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

image.png

类组件渲染

类组件的vdom的type值是一个类,所以通过typeof获得的是'function',所以需要在createDOM里面进一步修改

打印查看类组件的vdom

class Count extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  render() {
    return (
      <div>
        <div>
          <span>count</span>
          <span>{this.state.count}</span>
        </div>
      </div>
    );
  }
}
let element = <Count name="zs"></Count>;
console.log(element);

image.png

从上图可以看到type是一个类

createDOM修改如下


    ...
  //如果是一个函数组件或函数组件
  if (typeof type === "function") {
    //判断是类组件还是函数组件
    if (type.isReactComponent) {
      return renderClassComponent(vdom);
    } else {
      return renderFunctionComponent(vdom);
    }
  } else {
    dom = document.createElement(type);
  }
  ...
  

类组件的type是一个类,这个isReactComponent是类的一个静态变量,用来区分是函数组件还是类组件。renderClassComponent实现类组件变成一个真实dom

updateProps也需要做相应的改变,因为类组件可能绑定事件,需要多加一个else if判断,将这个事件绑定到dom元素上,这里只是简单是实现一下,而react内部真正使用的是合成事件,会将事件代理到document上面。

/**
 *  更新vdom上面的属性到dom上
 * @param {*} dom 真实dom
 * @param {*} newProps vdom上面的属性
 */
function updateProps(dom, newProps) {
  for (let key in newProps) {
    //遇到儿子先跳过,在后面处理
    if (key === "children") continue;

    if (key === "style") {
      let styleObj = newProps[key];
      for (let style in styleObj) {
        dom.style[style] = styleObj[style];
      }
    } else if (key.startsWith("on")) {
      //绑定一个事件
      dom[key.toLocaleLowerCase()] = newProps[key];
    } else {
      dom[key] = newProps[key];
    }
  }
}

renderClassComponent函数实现

/**
 * 将一个类组件转成真实dom并返回
 * @param {*} vdom
 */
function renderClassComponent(vdom) {
  let { type, props } = vdom;
  let classInstance = new type(props);
  //获取vdom
  let renderVdom = classInstance.render();
  //转成真实dom
  let dom = createDOM(renderVdom);
  //为了后面更新组件需要用到真实dom
  classInstance.dom = dom;
  return dom;
}

React.Component实现

//Component.js
class Component {
  //用于标识组件是类组件
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
  }

  setState(partialState) {
    let oldstate = this.state;
    this.state = { ...oldstate, ...partialState };
    //重新调用render方法获取新的vdom
    let newVdom = this.render();
    //更新dom节点
    updateClassComponent(this, newVdom);
  }
}

//这里直接替换掉元节点,没有使用diff,也没有异步更新
function updateClassComponent(classInstance, newVdom) {
    //这里就可以拿到旧的dom
  let oldDom = classInstance.dom;
  let newDom = createDOM(newVdom);
  oldDom.parentNode.replaceChild(newDom, oldDom);
  classInstance.dom = newDom;
}

export default Component;

校验

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

class Count extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count);
  };

  render() {
    return (
      <div>
        <div>
          <span>count:</span>
          <span>{this.state.count}</span>
        </div>
        <button onClick={this.handleClick}>+</button>
        <div>name:{this.props.name}</div>
      </div>
    );
  }
}

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

image.png

当点击了“+”后,会触发handleClick函数,因为这里没有使用异步更新,所以这里结果是同步的。