一、「深入React源码」--- 手写JSX渲染

732 阅读4分钟

前言:想要彻底了解react框架的原理及实现,学习源码是必经之路。在此学习期间,要最大程度的吸收编码思想、设计思想等。如果你也处于想要提升自己的阶段,那我们就一起交流一起学习吧~

一、什么是JSX

  • JSX是一种语法糖,在编译过程中会通过babeljs转译成React.createElement语法
  • React.createElement会返回一个React元素
  • React元素事实上是普通的JS对象,也就是虚拟DOM,用来描述展现在屏幕上的内容
  • ReactDOM来确保浏览器中的真实DOM数据和React元素保持一致
  • ReactDOM.render把虚拟DOM转成真实DOM,插入到容器

二. JSX编译过程

// JSX:
<h1 className="title" style={{color:'red'}}>hello</h1>

// React17以前的编译结果:
React.createElement("h1", {
    className: "title",
    style: { color: 'red' } 
}, "hello")

// React17以后的编译结果:
// 如果使用jsx,会自动引入'react/jsx-runtime'包
// 用来解除React17之前 import React from 'react'引发的:开发时eslint错误,运行时React undefined
// React.createElement ==> _jsx
import { jsx as _jsx } from 'react/jsx-runtime';
let element = _jsx("h1", {
    className: "title",
    style: { color: 'red' } 
}, "hello")

// 返回的结果
{ type:'h1', props:{ className: "title", style: { color: 'red' } }, children:"hello" }

三.实现思路

image.png

1. 实现React.createElement()

1)参照React提供的createElement方法,我们创建自己的createElement方法。 入参为:元素类型type(div/span)、配置config(key/ref)、子元素children。children可能存在多个,因此对children进行判断,当子元素有多个时,应存放进数组。

2)为方便后续进行dom-diff比较,将基本类型包装为对象。创建工具函数wrapToVdom,入参为:elemment。此时考虑入参为string/number类型时,包装为对象再被返回。

3)包装原本createElement中返回的元素

2. 实现ReactDOM.render()

1)创建渲染函数render,入参:vdom、container。函数内部要进行挂载

2)创建挂载函数mount,入参:vdom、container。

3)创建createDOM方法,入参:vdom。把虚拟DOM转为真实DOM。首先判断vdom不存在的情况,其次对根据虚拟DOM类型的不同做不同的处理:

  • REACT_TEXT:创建文本节点 createTextNode

  • 普通元素:创建元素createElement。处理内部属性。属性后续都可能更新。因此需要更新属性方法updateProps。判断属性

  • react元素:直接挂载

  • 数组:遍历数组,把每一项挂载

四. 实现JSX

1.创建项目

create-react-app react-basic
cd react-basic
yarn add cross-env

2. src/index.js

import React from "./react"; 
import ReactDOM from "./react-dom"; 
let element1 = ( 
   <div className="title" style={{ color: "red" }}> 
       <span>hello</span>world
   </div> 
 ); 
console.log(JSON.stringify(element1, null, 2)); 

// ReactDOM.render负责渲染,把虚拟的react元素变成真实DOM渲染/插入到DOM容器内
// bable并没有把JSX编译成虚拟DOM,而是把JSX编译成了React,createElement的方法调用
// 在浏览器执行的时候才会执行React.createElement,才会生成虚拟DOM
ReactDOM.render(element1, document.getElementById("root"));

3. src/constants.js

export const REACT_TEXT = Symbol('REACT_TEXT');
export const REACT_ELEMENT = Symbol('react.element');

4. src/utils.js

import { REACT_TEXT } from "./constants";
export function wrapToVdom(element) {
    return typeof element === "string" || typeof element === "number" 
        ? { type: REACT_TEXT, props: { content: element } } 
        : element; }

5. src/react.js

import { wrapToVdom } from "./utils";
import { REACT_ELEMENT } from "./constants";

/**
 * 创建一个虚拟DOM == React元素
 * @param {*} type 元素的类型 span div p
 * @param {*} config 配置对象 className style
 * @param {*} children 子元素,单个-对象/多个-数组
 */
function createElement(type, config, children) {
    let ref; // 可以通过ref获取引用此元素
    let key; // 子元素的key唯一标识
    if (config) {
        delete config.__source; // 删除暂时没用的属性 source:bable编译时产生的属性
        delete config.__self;
        ref = config.ref; // 取出ref key
        delete config.ref; // 删除config中的ref key
        key = config.key; 
        delete config.key;
     } 
     let props = { ...config }; 
     if (arguments.length > 3) { 
         // 如果入参多余3个,说明有多个子元素,截取后以数组形式保存
         props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom); 
     } else { 
         // 可能是React元素对象,也可能是string/number/null/und
         props.children = wrapToVdom(children); 
     } 
     return { 
         $$typeof: REACT_ELEMENT,
         type,
         ref,
         key,
         props, 
     }; 
 } 
 const React = { createElement, };
 export default React;

6. src/react-dom.js

import { REACT_TEXT } from "./constants";

/**
 * 把虚拟DOM变成真实DOM插入容器
 * @param {*} vdom 虚拟DOM
 * @param {*} container 容器
 */
function render(vdom, container) {
    mount(vdom, container);
}

export function mount(vdom, container) {
    let newDOM = createDOM(vdom);
    container.appendChild(newDOM);
} 

/** 把虚拟DOM转为真实DOM */
export function createDOM(vdom) {
    if (!vdom) return null; // null、und也是合法的dom
    let { type, props } = vdom;
    let dom; // 真实DOM
    if (type === REACT_TEXT) {
        // 如果元素为文本,创建文本节点
        dom = document.createTextNode(props.content);
    } else { 
        dom = document.createElement(type);
    }
    // 处理属性
    if (props) { 
        updateProps(dom, {}, props); 
        if (typeof props.children == "object" && props.children.type) {
            mount(props.children, dom);
        } else if (Array.isArray(props.children)) {
            reconcileChildren(props.children, dom); 
        } 
    } 
    vdom.dom = dom;
    return dom;
} 

/**
 * 把新的属性更新到真实DOM上
 * @param {*} dom 真实DOM
 * @param {*} oldProps 旧的属性对象
 * @param {*} newProps 新的属性对象
 */
function updateProps(dom, oldProps={}, newProps={}) {
    for (let key in newProps) { 
        if (key === 'children') { 
            continue;  // 子节点另外处理
        } else if (key === 'style') {
            let styleObj = newProps[key];
            for (let attr in styleObj) {
                dom.style[attr] = styleObj[attr];
            } 
        } else { 
            dom[key] = newProps[key];
        }
    } 
    for(let key in oldProps){ 
        if(!newProps.hasOwnProperty(key)){
            dom[key] = null;
        }
    }
} 

function reconcileChildren(childrenVdom, parentDOM) { 
    for (let i = 0; i < childrenVdom.length; i++) { 
       let childVdom = childrenVdom[i];
        mount(childVdom, parentDOM);
    }
}

const ReactDOM = { 
    render,
};
    
export default ReactDOM;