8月更文挑战|react手写实现渲染JSX普通版

386 阅读3分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

前言

React是用于构建用户界面的JavaScript库, 起源于Facebook的内部项目,目前在前端社区也是非常的流行作者也用了很久,这段时间阅读了部分react渲染的核心源码,今天一起来写一下jsx渲染的原理

介绍

本次是一边写代码一边进行逻辑讲解,大量的注释会写在代码中可能全部操作下来要耗费一些时间,所以我尽量把展示的效果也一起贴出来本次的渲染组件是没有fiber的逻辑也就是React16版本之前的逻辑,之后下一篇会使用16版本以后的fiber在写一篇

开始

使用create-react-app创建一个应用,重新构建index.js文件,这是我们的基础结构包含ClassComponent和FunctionConponent组件,目的是全部正常渲染到页面

import React from 'react';
import ReactDOM from './react-dom'
import Component from './Component'
import './index.css' //样式只有一个boder颜色就不用贴了class ClassComponent extends Component {
  render() {
    return (
      <div className="box">
        <p>{this.props.name}</p>
      </div>
    );
  }
}
function FunctionComponent(props) {
  return (
    <div className="box">
      <p>{props.name}</p>
    </div>
  );
}
​
const JSX = (
  <div className='box'>
    <h1>飞雪连天射白鹿,笑书神侠倚碧鸳</h1>
    <a href='https://juejin.cn/post/6995918802546343973' >my world</a>
    <FunctionComponent name='函数组件' />
     <ClassComponent name='类组件'/>
  </div>
)
​
ReactDOM.render(
  JSX,
  document.getElementById('root')
);
​

Component.js

function Component(props) {
  this.props = props; //将props挂载到实例上
}
​
Component.prototype.isReactComponent = {}; //主要区分函数组件还是类组件,源码中也是这么干的
export default Component;
​

创建react-dom.js,之后我们会按照函数调用顺序编写我们的渲染逻辑

实现render函数

//vnode 虚拟dom节点
//node 真实dom节点
​
​
/**
 * @vnode 虚拟节点
 * @container 真实容器节点
*/
function render(vnode, container) {
    //step1 将vnode转为node
    let node = createNode(vnode)
    //step2 将节点插入到容器中
    container.appendChild(node)
}
​
export default { render }

createNode创建真实dom

/**
 * @vnode 虚拟节点
*/
function createNode(vnode) {
    let node
    const { type } = vnode
    if (typeof type === 'string') {
        //生成标签节点 div h1 等
        node = updateHostComponent(vnode)
    } else if (isStringOrNumber(vnode)) {
        //生成文本节点只判断数字或者字符串
        node = updateTextComponent(vnode)
    } else if (typeof type === 'function') {
        //区分渲染函数组件还是类组件
        node = type.prototype.isReactComponent ?
            updateClassComponent(vnode) :
            updateFunComponent(vnode)
    }
    return node
}

updateHostComponent更新标签节点

/**
 * @vnode 虚拟节点
*/
function updateHostComponent(vnode) {
    const { type, props } = vnode
    //step1 创建标签节点
    let node = document.createElement(type)
    //step2 更新节点上的属性 props传的参数等
    updateNode(node, props)
    //step3 遍历渲染子节点
    reconcilChildren(node, props.children)
    return node
}

updateTextComponent更新文本节点

/**
 * @vnode 虚拟节点
*/
function updateTextComponent(vnode) {
    //创建文本节点
    const node = document.createTextNode(vnode)
    return node
}

updateClassComponent更新类组件

/**
 * @vnode 虚拟节点
*/
function updateClassComponent(vnode) {
    const { type, props } = vnode
    //step1 实例化类组件传递props
    const instance = new type(props)
    //step2 执行类组件中的render函数获取vnode
    const child = instance.render()
    //step3 创建真实节点
    const node = createNode(child)
    
    return node
}

updateFunComponent更新函数组件

/**
 * @vnode 虚拟节点
*/
function updateFunComponent(vnode) {
    const { type, props } = vnode
    //step1 执行函数组件传递props
    const child = type(props)
    //step2 创建真实节点
    const node = createNode(child)
​
    return node
}

updateNode更新节点上的属性

/**
 * @node 真实节点
 * @value props值 
*/
function updateNode(node, nextVal) {
    //step1 遍历props,需要筛选出children因为这里是将props值放到节点上不需要children
    //step2 将值存储到节点上
    Object.keys(nextVal).filter(v => {
        return v !== 'children'
    }).forEach(key => {
        node[key] = nextVal[key]
    })
  
    return node
}

reconcilChildren遍历子节点

/**
 * @container 真实容器节点
 * @children 节点元素
*/
function reconcilChildren(container, children) {
    //step1 整合一下children,方便遍历
    const newChildren = Array.isArray(children) ? children : [children]
​
    //step2 将节点插入到真实dom中,直接调用我们写好的render即可
    for (let i = 0; i < newChildren.length; i++) {
        let child = newChildren[i]
        render(child, container)
    }
}

流程图

我们画个流程图串一下可以更加方便大家理解整个渲染流程

react.png

结语

本次渲染流程到这里就结束了之后我会继续编写下一篇使用fiber的渲染逻辑,感谢观看☕️