相信使用react的大家对于jsx已经游刃有余了,可是你真的了解jsx的原理吗?

让我们由浅入深,来一层一层揭开jsx的真实面目。

React.createElement

react官方中讲到,关于jsx语法最终会被babel编译成为React.createElement()方法。

我们来看看这段jsx

<div className="wang.haoyu">hello</div>
复制代码

经过babel编译后它变成这样的代码:

React.createElement("div", {
    className:'wang.haoyu'
}, "hello");
复制代码

jsx中存在多个节点元素时,比如:

<div>hello<span>world</span></div>
复制代码

它会将多个节点的jsxchildren属性变成多个参数进行传递下去:

React.createElement("div", null, "hello", React.createElement("span", null, "world"));
复制代码

可以看到,外层的div元素包裹的children元素依次在React.createElement中铺平排列进去,并不是树型结构排列。

需要注意的是,旧的react版本中,只要我们使用jsx就需要引入react这个包。而且引入的变量必须大写React,因为上边我们看到babel编译完jsx之后会寻找React变量。

新版本中,不再需要引入React这个变量了。有兴趣的同学可以去看看打包后的react代码,内部会处理成为Object(s.jsx)("div",{ children: "Hello" }),而老的版本是React.createElement('div',null,'Hello')

这两种方式效果和原理是一模一样的,只是新版额外引入包去处理了引入。所以不需要单独进行引入React

React元素

React之中元素是构建React的最小单位,其实也就是虚拟Dom对象。

本质上jsx执行时就是在执行函数调用,是一种工厂模式通过React.createElement返回一个元素。

const element = <div>Hello</div>
console.log(element,'element')
复制代码

image.png

先忽略掉一些ref/key之类的属性,这个时候来看我们发现它其实就是一个js对象,记录了type表示元素类型。props表示元素的接受的prop,注意这里会将jsx内部标签内容插入到propschildren属性中。

需要注意的是这里的children属性,如果内部标签元素存在多个子元素时候。children会是一个数组。因为这里仅仅只有文本节点,所以只有一个Hello

在我们平常使用react项目的时候,index.tsx中总是会存在这样一段代码:

ReactDOM.render(<App />, document.getElementById('root'));
复制代码

结合上边我们所讲的React.createElement,我们不难猜出ReactDOM.render这个方法它的作用其实就是按照React.createElement生成的虚拟DOM节点对象,生成真实DOM插入到对应节点中去,这就是简单的渲染过程。

元素的更新

react中元素本身是不可变的。

比如:

const element = <h1 title="hello" >Hello</h1>
console.log(JSON.stringify(element,null,2))
复制代码

image.png

当我们想将它的内容改成world时,如果直接通过

element.props.children = 'world'
复制代码

这样是不可以的,react会提示:

Uncaught TypeError: Cannot assign to read only property 'children' of object '#<Object>'
复制代码

无法给一个只读属性children进行赋值,修改其他属性比如type之类同理也是不可以的。

当我们通过这种方式给react元素增加属性时,也是增加的。

Cannot add property xxx, object is not extensible
复制代码

not extensiblereact17之后才进行增加的。通过Object.freeze()将对象进行处理元素。

需要注意Object.freeze()是一层浅冻结,在react内部进行了递归Object.freeze()

所以在react中元素本身是不可变的,当元素被创建后是无法修改的。只能通过重新创建一个新的元素来更新旧的元素。

你可以这样理解,在react中每一个元素类似于动画中的每一帧,都是不可以变得。

当然在react更新中仅仅会更新需要更新的内容,内部会和Vue相同的方式去进行diff算法,高效更新变化的元素而不是更新重新渲染所有元素。

jsx原理分析

需要注意我们这里使用旧的React.createElement方法,如果是^17版本下,需要在环境变量中添加DISABLE_NEW_JSX_TRANSFORM=true

上边我们已经分析过React.createElement这个方法的返回值,接下来我们就尝试自己来实现jsx的渲染。

先来看看原本React中createElement方法的返回值:


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

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

console.log(JSON.stringify(element, null, 2), 'element');
复制代码

image.png

接下来我们就根据结果来推写法,实现一个简单的createElement方法

实现React.crateElement方法-原生DOM元素的渲染

  • 实现utils/react.js

// 这里之所以额外书写一个 wrapToDom元素 是为了方便对比 react源码中没有这段方法是特殊处理的
// 我们为了方便 将普通类型 也统一处理成为Object

const React = {
  createElement: function (type, config, children) {
    const props = {
      ...config,
    };
    // 上边讲到babel编译jsx后
    // 如果参数大于3个 那么就有多个children props.children是一个数组
    if (arguments.length > 3) {
      props.children = Array.prototype.slice.call(arguments, 2);
    } else {
      props.children = children;
    }
    return {
      type,
      props,
    };
  },
};

export default React;
复制代码

这一步我们已经实现了基础的React.createElement方法。

  • index.tsx
import React from './utils/react';
import ReactDOM from 'react-dom';

// babel编译后的代码会引入 React.createElement
// 此时的React指向的是我们自己写的React
const element = (
  <div className="header" style={{ color: 'red' }}>
    <span>hello</span>world
  </div>
);

ReactDOM.render(element, document.getElementById('root'));
复制代码

实现ReactDOM.render方法-将react中源生DOM元素变成真实元素插入页面

  • 接着咱们先来实现一个对于children类型的判断方法
// utils.js 
// 常亮 判断文本类型
const REACT_TEXT = Symbol('REACT_TEXT')

// 无论以前是什么元素,都转成VDOM的对象形式
function transformVom(element) {
    // 额外处理文本节点 将文本节点输出和其他节点一样的Object类型
    if(typeof element === 'string' || typeof element === 'number') {
        return { type: REACT_TEXT, props: { content: element } }
    }
    return element
}
复制代码
  • 接下来我们改造一下我们之前写好的React.createElement方法
import { transformVNode } from './utils';

const React = {
  createElement: function (type, config, children) {
    const props = {
      ...config,
    };
    if (arguments.length > 3) {
      props.children = Array.prototype.slice
        .call(arguments, 2)
        .map(transformVNode);
    } else {
      props.children = transformVNode(children);
    }
    return {
      type,
      props,
    };
  },
};

export default React;

复制代码
  • 接下来我们已经拥有了对应的VDom对象,就可以开始实现React.render方法。

React.render核心思想就是将我们的Vdom对象编程浏览器可以识别的标签节点挂载在对应元素上

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {Object} vDom 虚拟DOM
 * @param {HTMLElement} el 元素
 */

import { REACT_TEXT } from './constant';

// 真正渲染方法
function render(vDom, el) {
  const newDom = createDom(vDom);
  el.appendChild(newDom);
}

// 先不考虑自定义组件
function createDom(vDom) {
  const { type, props } = vDom;
  let dom;
  // 文本节点
  if (type === REACT_TEXT) {
    dom = document.createTextNode(props.content);
  } else {
    dom = document.createElement(type);
  }
  // 更新属性
  if (props) {
    // 更新跟节点Dom属性
    updateProps(dom, {}, props);
    // 处理children 考虑undefined/null 不做任何处理
    // 考虑 children是一个数组 那么就表示他拥有多个儿子
    // 考虑children是一个Object 那么他就只有一个儿子节点
    if (typeof props.children === 'object' && props.children.type) {
      // 单个元素
      render(props.children, dom);
    } else if (Array.isArray(props.children)) {
      // 多个元素
      reconcileRender(props.children, dom);
    }
  }
  // 记录挂载节点
  vDom.__dom = dom;
  return dom;
}

// 挂载多个dom元素 React.createElement先不考虑递归
function reconcileRender(vLists, parentDom) {
  for (let node of vLists) {
    render(node, parentDom);
  }
}

/**
 * 把虚拟DOM变成真实DOM插入
 * @param {HTMLElement} dom 元素
 * @param {Object} oldProps 元素本身的props 用于更新这里暂时用不到
 * @param {Object} newProps 元素新的props
 */

function updateProps(dom, oldProps, newProps) {
  // 合并props 暂时没有老的 仅处理新的
  Object.keys(newProps).forEach((key) => {
    if (key === 'children') {
      // 单独处理children
      return;
    }
    if (key === 'style') {
      addStyleToElement(dom, newProps[key]);
    } else if (key === 'content') {
      // 文本不做任何操作
    } else {
      dom[key] = newProps[key];
    }
  });
}

function addStyleToElement(dom, styleObject) {
  Object.keys(styleObject).forEach((key) => {
    const value = styleObject[key];
    dom.style[key] = value;
  });
}

const ReactDOM = {
  render,
};

export default ReactDOM;
复制代码

其实这里的的核心思想就是通过render方法将虚拟DOM根据对应的属性转化成为真实DOM节点进行递归挂载,最终通过appendChild渲染到页面上。