深入浅出搞定React系列(一):JSX代码是如何“摇身一变”成为DOM的?

内容来源于拉勾教育专栏课程——深入浅出搞定React

普通开发者“逆袭”机会:一个好的框架,就是最好的老师

  1. 深挖一个优质的前端框架,吃透其底层原理
  2. 跟框架作者学架构思想、学编码规范、学设计模式

学习的本质是重复
性能决定用户体验,设计模式决定研发效率

01 JSX代码是如何“摇身一变”成为DOM的?

思考以下三个问题:

  1. JSX的本质是什么,它和JS之间到底是什么关系?
  2. 为什么要用JSX?不用会有什么后果?
  3. JSX背后的功能模块是什么,这个功能模块都做了那些事情?

JSX的本质:JavaScript的语法扩展

JSX是JavaScript的一种语法扩展,它和模板语言很接近,但是它充分具备JavaScript的能力。

1.JSX语法是如何在JavaScript中生效的?

JSX会被编译为React.createElement(),React.createElement()将返回一个叫作“React Element”的JS对象

编译是由Babel完成的,Babel是一个工具链,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

image.png
React选用JSX语法的动机:
JSX语法糖允许前端开发者使用我们最为熟悉的类HTML标签语法来创建DOM,在降低学习成本的同时,也提升了研发效率与研发体验。

2.createElement源码解读

分析之前可以尝试阅读追加进源码中的逐行代码解析,大致理解createElement中每一行代码的作用:

/**
 101. React的创建元素方法
 */
export function createElement(type, config, children) {
  // propName 变量用于储存后面需要用到的元素属性
  let propName; 
  // props 变量用于储存元素属性的键值对集合
  const props = {}; 
  // key、ref、self、source 均为 React 元素的属性,此处不必深究
  let key = null;
  let ref = null; 
  let self = null; 
  let source = null; 

  // config 对象中存储的是元素的属性
  if (config != null) { 
    // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
    if (hasValidRef(config)) {
      ref = config.ref;
    }

    // 此处将 key 值字符串化
    if (hasValidKey(config)) {
      key = '' + config.key; 
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
    for (propName in config) {
      if (
        // 筛选出可以提进 props 对象里的属性
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName) 
      ) {
        props[propName] = config[propName]; 
      }
    }
  }

  // childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
  const childrenLength = arguments.length - 2; 
  // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
  if (childrenLength === 1) { 
    // 直接把这个参数的值赋给props.children
    props.children = children; 
    // 处理嵌套多个子元素的情况
  } else if (childrenLength > 1) { 
    // 声明一个子元素数组
    const childArray = Array(childrenLength); 
    // 把子元素推进数组里
    for (let i = 0; i < childrenLength; i++) { 
      childArray[i] = arguments[i + 2];
    }
    // 最后把这个数组赋值给props.children
    props.children = childArray; 
  } 

  // 处理 defaultProps
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) { 
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}
复制代码
  1. 入参解读:创建一个元素需要知道哪些信息
export function createElement(type, config, children)
复制代码

createElement有3个入参,这3个入参囊括了React创建一个元素所需要知道的全部信息.

  • type: 用于标识节点的类型。它可以是类似“h1”“div”这样的标准HTML标签字符串,也可以是React组件类型或React fragment类型。
  • config:以对象形式传入,组件所有的属性都会以键值对的形式存储在config对象中
  • children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”
React.createElement("ul", 
  // 传入属性键值对
  className: "list"
   // 从第三个入参开始往后,传入的参数都是 children
}, React.createElement("li", {
  key: "1"
}, "1"), React.createElement("li", {
  key: "2"
}, "2"));
复制代码

这个调用对应的DOM结构如下:

<ul className="list">
  <li key="1">1</li>
  <li key="2">2</li>
</ul>
复制代码
  1. createElement函数体拆解

createElement在逻辑层面的任务流转图示:
image.png
createElement中并没有十分复杂的涉及算法或真实DOM的逻辑,它的每一个步骤几乎都是在格式化数据。更直白点儿,createElement就像是开发者和ReactElement调用之间的‘转换器’、‘一个数据处理层’。它可以从开发者处接收相对简单的参数,然后将这些参数按照ReatElement的预期做一层格式化,最终通过调用ReactElement来实现元素的创建。整个过程如下图所示:
image.png
3. 出参解读:初识虚拟DOM
createElement执行到最后会return一个针对ReactElement的调用。

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
    $$typeof: REACT_ELEMENT_TYPE,
    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录创造该元素的组件
    _owner: owner,
  };
  if (__DEV__) {
    // 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
  }
  return element;
};
复制代码

ReactElement其实只做了一件事情,就是“创建”,或者说是“组装”:ReactElement把传入的参数按照一定的规范,“组装”进了element对象里,并把它返回给React.createElement,最终React.createElement又把它交回到开发者手中。整个过程如下图所示:
image.png
输出示例中的App组件的JSX部分:

const AppJSX = (<div className="App">
  <h1 className="title">I am the title</h1>
  <p className="content">I am the content</p>
</div>)

console.log(AppJSX)
复制代码

会发现它确实是一个标准的ReactElement对象示例,如下图所示:
image.png
这个ReactElement对象实例,本质上是以JavaScript对象形式存在的对DOM的描述,也就是老生常谈的“虚拟DOM”,虚拟DOM与真实DOM之间的距离就是由大家喜闻乐见的ReactDOM.render方法来填补的。在每一个React项目的入口文件中,都少不了对React.render函数的调用。

ReactDOM.render
    // 需要渲染的元素(ReactElement)
    element, 
    // 元素挂载的目标容器(一个真实DOM)
    container,
    // 回调函数,可选参数,可以用来处理渲染结束后的逻辑
    [callback]
)
复制代码

ReactDOM.render方法可以接收3个参数,其中第二个参数就是一个真实的DOM节点,这个真实的DOM节点充当的是“容器”的角色,React元素最终都会被渲染到这个‘容器’里面去。比如,示例中的App组件,它对应的render调用是这样的:

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码

注意,这个真实DOM一定是确实存在的,比如,在App组件对象的index.html文件中,已经提前预置了id为root的根节点:

<body>
    <div id="root"></div
</body>
复制代码

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享