【react进阶】react的jsx渲染理解以及简易版的react实现

309 阅读6分钟

一、渲染过程

我们知道react是把Virtual DOM(虚拟dom),渲染为真实的dom。

1、先看什么是虚拟dom:

具体的虚拟dom格式类似如下:

vdom: {
  type: 'ul',
  props: {
    className: 'list'
  },
  children: [
    {
      type: 'li',
      props: {
        className: 'item',
        style: {
          background: 'blue',
          color: '#cd0303'
        },
        onClick: function () {
          alert(1);
        }
      },
      children: 'aaaa'
    },
    {
      type: 'li',
      props: {
        className: 'item',
        onDoubleClick: () => {
          console.log(111)
        }
      },
      children: 'bbbb'
    }
  ]
}

其中:type代表节点类型,props表示节点属性,children代表该节点下的子节点信息。

2、转换渲染过程

从jsx到浏览器上面展示的真实dom过程,需要经过以下阶段:

注意:下面的render是ReactDOM.render而非组件component内的render

2022.02.14_肖国垚&a9502e8364892e3e9a21bf0e38c73ec4.png

如这段代码:

function hello() {
  return <div>
    <span>Hello world!</span>
    <div style={{color:'red',background:'blue'}}>aaaa</div>
    <div onClick={()=>alert(111)}>
         11111111
    </div>
    <div onDoubleClick={()=>console.log(222) }>2222</div>
  </div>;
}

首先Babel(点击在线查看转换)会将jsx语法转换为,React语法糖(不使用jsx的react)React.createElement(component, props, ...children)

看下Babel的转换结果(去掉注释后如下):

function hello(){
  return React.createElement("div", null,
    React.createElement("span", null, "Hello world!"),
    React.createElement("div", {
      style: {
        color: "red",
        background: "blue"
      }
    }, "aaaa"),
    React.createElement("div", {onClick: () => alert(111)}, "11111111"),
    React.createElement("div", {onDoubleClick: () => console.log(222)}, "2222")
  );
}

接着React.createElement()会创建并且返会虚拟DOM,最后React.render()会将虚拟dom渲染并且挂在到真实dom上面。

二、简易版React渲染实现

我们已经知道jsx是由Babel编译转换为react语法,所以要实现react的渲染我们只需要拿转换过后的代码格式来实现即可。 以上面的转换代码为例,我们需要实现必要有两个主要方法:

  • React.createElement(component, props, ...children)

  • React.render(vdom,document.getElementById('root'))

1、对React.createElement方法的实现

/**
 * createElement方法,用来创建虚拟dom数据
 * @param type 
 * @param props
 * @param children
 * @returns {{children: *[], type, props}}
 */
myCreateElement = (type, props, ...children) => {
  const extendsChildren = [...children]
  const newChildren = extendsChildren.map(item => {
    return item
  })
  return {
    type: type,
    props: props,
    children: newChildren,
  }
}

type是组件的类型(dom),props是一些dom的属性,最后一个是子节点。因为createElement存在父子级的话是链式调用,比如说:

return React.createElement("div", null,
        React.createElement("div",  {onDoubleClick: () => console.log(222)}, "2222")
    );

所以必须返回children为数组。

我们对比自己写的myCreateElementreact.createElement创建出来的虚拟dom数据:

// Babel编译后的原生react语法,创建虚拟Dom的写法
const hello = () => {
  return React.createElement("div", null,
    React.createElement("span", null, "Hello world!"),
    React.createElement("div", {
      style: {
        color: "red",
        background: "blue"
      }
    }, "aaaa"),
    React.createElement("div", {onClick: () => alert(111)}, "11111111"),
    React.createElement("div", {onDoubleClick: () => console.log(222)}, "2222")
  );
}

// 我们自己写的
const hello2 = () => {
  return this.myCreateElement("div", null,
    this.myCreateElement("span", null, "Hello world!"),
    this.myCreateElement("div", {
      style: {
        color: "red",
        background: "blue"
      }
    }, "aaaa"),
    this.myCreateElement("div", {onClick: () => alert(111)}, "11111111"),
    this.myCreateElement("div", {onDoubleClick: () => console.log(222)}, "2222")
  );
}
console.log('hello()',hello())
console.log('hello222()',hello2())

结构并无多大差异,只是简陋了: 2022.02.14_肖国垚&c8bfd62efc215bbcd8ed5c23605b4ec7.png

2、对ReactDom.render方法的实现

这部分主要是将上面生成的虚拟dom数据转换为真实的Dom过程,核心是利用html原生的document.createTextNode()生成纯文本document.createElement()创建真实dom这两个方法。 其中生成真实dom比较复杂一点,需要把事件和属性赋值添加到dom上面。

/**
 * render方法,用来渲染虚拟dom
 * @param vdom
 * @param relaDom
 * @returns {*|ActiveX.IXMLDOMNode}
 */
myRender = (vdom, relaDom) => {
  const mount = relaDom ? el => relaDom.appendChild(el) : el => el  // 挂载虚拟dom到真实dom上面
  if (typeof vdom === 'string' || typeof vdom === "number") { // 如果是文本:字符串或者数字
    return mount(document.createTextNode(vdom))
  }
  if (vdom && (typeof vdom === 'object')) {
    let createTypeDom = ''
    if (typeof vdom.type === 'function') {
      // 如果type本身就是一个组件,这里先不做
    } else if (typeof vdom.type === 'string') {
      createTypeDom = document.createElement(vdom.type)
    }
    if (vdom.children?.length > 0) {
      for (let child of vdom.children) {
        this.myRender(child, mount(createTypeDom))
      }
    }
    return mount(createTypeDom)
  }
}

经过我们写的方法渲染的效果如图:

2022.02.16_肖国垚&59271227f242af00248a60f17790899c.png

但是这还不够,还需要给dom添加属性。

3、给真实dom设置属性

例如虚拟dom上面有input的type='button'、className或者其他属性也需要绑定到真实dom上面。

  • 普通属性我们可以通过原生的setAttribute来实现

element.setAttribute(attributename,attributevalue)

attributename表示要添加的属性名称,attributevalue表示属性值

  • 样式使用原生的setproperty或者直接Object.assign()把样式对象连接来实现。

object.setProperty(propertyname, value)

propertyname表示样式名称,value表示样式值。

/**
 * 给真实dom设置属性
 * @param relaDom
 * @param key
 * @param propsParams
 */
mySetAttribute = (relaDom, key, propsParams) => {
  if (Object.prototype.toString.call(propsParams) === "[object Function]") { //如果是事件方法
   // this.myEvent(relaDom, key, propsParams)  见下面第4点:事件处理方法
  } else if (typeof propsParams === 'object' && (key === 'style')) { // 样式
    for (let cssKey in propsParams) {
      relaDom.style.setProperty(cssKey, propsParams[cssKey])
    }
    // Object.assign(relaDom.style, propsParams) // 也可以使用
  } else if (typeof propsParams === 'string') { // 其他属性
    for (let otherKey in propsParams) {
      relaDom.setAttribute(otherKey, propsParams[otherKey])
    }
  }
}

在render的时候调用这个方法。

myRender = (vdom, relaDom) => {
  const mount = relaDom ? el => relaDom.appendChild(el) : el => el 
  if (typeof vdom === 'string' || typeof vdom === "number") {
    return mount(document.createTextNode(vdom))
  }
  if (vdom && (typeof vdom === 'object')) {
    let createTypeDom = ''
    if (typeof vdom.type === 'function') {
      // 如果type本身就是一个组件,这里先不做
    } else if (typeof vdom.type === 'string') {
      createTypeDom = document.createElement(vdom.type)
    }
    if (vdom.children?.length > 0) {
      for (let child of vdom.children) {
        this.myRender(child, mount(createTypeDom))
      }
    }
    for (let key in vdom.props) {  // 使用循环,props可能有多个属性
      this.mySetAttribute(createTypeDom, key, vdom.props[key]) // 这里调用我们的mySetAttribute
    }
    return mount(createTypeDom)
  }
}

此时样式等其它属性已经生效了,看看效果:

2022.02.16_肖国垚&9c044227050d26978ce969d3a4831239.png

但是props中的事件还没有给真实dom绑定上去,接下来我们就要处理事件。

4、事件处理

虚拟dom上面有一些属性是事件属性,例如onClick,在生成真实dom中我们需要给他绑定到上面并且对应事件可执行。

在React中事件有自己的一套事件机制(react的事件都是合成事件,例如onClick等等),顺序与原生的事件一致,他们是先捕获进行事件收集,再分发事件后执行。

原生事件执行如下草图: 2022.02.15_肖国垚&144c01c5ca312dd7a61aac2d2f8f39ec.png

所以我们这里实现事件用原生的addEventListener来实现,给dom添加事件监听。

element.addEventListener(event, function, useCapture) event表示事件名称,function表示事件触发时执行的函数,useCapture表示在捕获阶段执行还是在冒泡阶段执行。

这样我们就可以实现虚拟dom上面的props携带的事件方法通过addEventListener去执行了.

/**
 * 事件处理
 * @param relaDom
 * @param eventName
 * @param event
 */
myEvent = (relaDom, eventName, event) => {
  const eventConfig = {
    onClick: 'click',
    onDoubleClick: 'dblclick',
    onKeyDown: 'keydown',
    onDragEnd: 'dragend',
    // 其他更多事件...
  }
  for (let key in eventConfig) {
    if (key === eventName) {
      relaDom.addEventListener(eventConfig[key], event)
    }
  }
}

三、实现效果

最后我把最初的babel转换后的代码拿来测试下,把react的方法替换为我们的实现方法:(完整代码)

import React, {Component} from 'react';

class Index extends Component {

  componentDidMount() {
    const myReactVDom = () => {
      // 最初由Babel转换过的代码
      return this.myCreateElement("div", null,
        this.myCreateElement("span", null, "Hello world!"),
        this.myCreateElement("div", {
          style: {
            color: "red",
            background: "blue"
          }
        }, "aaaa"),
        this.myCreateElement("div", {onClick: () => alert(111)}, "11111111"),
        this.myCreateElement("div", {onDoubleClick: () => console.log(222)}, "2222")
      );
    }
    this.myRender(myReactVDom(), document.getElementById('myRoot'))
  }

  /**
   * createElement方法,用来创建虚拟dom数据
   * @param type
   * @param props
   * @param children
   * @returns {{children: *[], type, props}}
   */
  myCreateElement = (type, props, ...children) => {
    const extendsChildren = [...children]
    const newChildren = extendsChildren.map(item => {
      return item
    })
    return {
      type: type,
      props: props,
      children: newChildren,
    }
  }

  /**
   * render方法,用来渲染虚拟dom
   * @param vdom
   * @param relaDom
   * @returns {*|ActiveX.IXMLDOMNode}
   */
  myRender = (vdom, relaDom) => {
    const mount = relaDom ? el => relaDom.appendChild(el) : el => el  // 挂载虚拟dom到真实dom上面
    if (typeof vdom === 'string' || typeof vdom === "number") {
      return mount(document.createTextNode(vdom))
    }
    if (vdom && (typeof vdom === 'object')) {
      let createTypeDom = ''
      if (typeof vdom.type === 'function') {
        // 如果type本身就是一个组件,这里先不做
      } else if (typeof vdom.type === 'string') {
        createTypeDom = document.createElement(vdom.type)
      }
      if (vdom.children?.length > 0) {
        for (let child of vdom.children) {
          this.myRender(child, mount(createTypeDom))
        }
      }
      for (let key in vdom.props) {
        this.mySetAttribute(createTypeDom, key, vdom.props[key])
      }
      return mount(createTypeDom)
    }
  }

  /**
   * 事件处理
   * @param relaDom
   * @param eventName
   * @param event
   */
  myEvent = (relaDom, eventName, event) => {
    const eventConfig = {
      onClick: 'click',
      onDoubleClick: 'dblclick',
      onKeyDown: 'keydown',
      onDragEnd: 'dragend',
      // 其他更多事件...
    }
    for (let key in eventConfig) {
      if (key === eventName) {
        relaDom.addEventListener(eventConfig[key], event)
      }
    }
  }

  /**
   * 给真实dom设置属性
   * @param relaDom
   * @param key
   * @param propsParams
   */
  mySetAttribute = (relaDom, key, propsParams) => {
    if (Object.prototype.toString.call(propsParams) === "[object Function]") { //如果是事件方法
      this.myEvent(relaDom, key, propsParams)
    } else if (typeof propsParams === 'object' && (key === 'style')) { // 样式
      for (let cssKey in propsParams) {
        relaDom.style.setProperty(cssKey, propsParams[cssKey])
      }
      // Object.assign(relaDom.style, propsParams) // 也可以使用
    } else if (typeof propsParams === 'string') { // 其他属性
      for (let otherKey in propsParams) {
        relaDom.setAttribute(otherKey, propsParams[otherKey])
      }
    }
  }

  render() {
    return (
      <div id={'myRoot'}>

      </div>
    );
  }
}

export default Index;

上面虚拟dom由代码的执行效果:

动画5555.gif

到这里我们就基本完成了非常简易版本的react渲染