前言
在面试的时候,面试官经常会问到Diff算法,React中的Diff算法就会涉及到虚拟DOM,那么这个虚拟DOM是什么呢?
在React诞生的时候,它提出了一个新的语法JSX,而现在我们在开发中使用的React,也是基于JSX语法进行开发的。
那么JSX语法是什么呢?它是一种类似于HTML的语法,但它并不是HTML,它是一种JavaScript的语法扩展,它可以让我们在JavaScript中编写类似于HTML的代码,但是它并不是HTML,它最终会被编译成JavaScript代码。
其实浏览器是不能直接识别JSX语法的,所以我们需要使用Babel来将JSX语法编译成JavaScript代码,这样才能保证JSX语法能在浏览器中正常运行。而这个编译器就是@babel/preset-react。
@babel/preset-react是一个Babel预设,它可以将JSX语法编译成JavaScript代码,它的最终目的是将JSX语法编译成React.createElement。
在React中,createElement是一个核心API,它用于创建一个React元素,但其实我们在开发中很少会直接使用createElement,更多的是使用JSX语法,因为JSX语法更加简洁易读。
其实我们学习怎么使用React的时候,都是从JSX语法开始的,所以即使我们不知道createElement是什么,也不影响我们使用React,但是如果你想要深入学习React,那么你就需要了解createElement。
createElement做的事情很简单,它接收三个参数,type, props, children,然后它会返回一个React元素,这个React元素就是一个普通的JavaScript对象,这个普通的JavaScript对象就是我们所说的虚拟DOM。
有了这些虚拟DOM,React就可以通过这些虚拟DOM来进行渲染真实的DOM,而当我们修改了虚拟DOM之后,React也会通过新旧虚拟DOM的对比来计算出最小的修改,然后再去修改真实的DOM,这个过程就是React的核心算法虚拟DOM Diff算法。
万丈高楼平地起,要了解React的原理,我们就需要先了解createElement。
__DEV__是什么?
它是一个全局变量,用来表明当前是否是开发环境,如果是开发环境,那么它的值就是true,如果是生产环境,那么它的值就是false。
而源码中大量使用了__DEV__,这是因为我们在开发环境中,经常有代码调试的需求,而为了满足这些需求,React就需要在开发环境中提供一些额外的功能,例如在开发环境中,React会对一些常见的错误进行警告,由于开发环境存在了这些额外的功能,所以它的体积会比生产环境大很多,而性能也会比生产环境差一些。
如果只是为了了解React的原理,那么我们在阅读源码的时候,可以直接忽略包裹在__DEV__中的代码,这样可以让我们更加容易理解源码。
createElement
从createElement源码,可以看到它接收三个参数,type, props, children。
这三个参数分别是什么呢?
- type:元素的类型。
- props:元素的属性。
- children:元素的子元素。
type
它可以是一个字符串,也可以是一个函数,如果是一个字符串,那么它就是一个普通的HTML标签,如果是一个函数,那么它就是一个组件,这个组件可以是一个类组件,也可以是一个函数组件。
如果是一个类组件,那么它必须继承自React.Component,如果是一个函数组件,那么它必须是一个函数,这个函数接收一个参数props,然后返回一个React元素。
它还有一些特殊的值,例如Fragment,StrictMode,Portal,Context,Profiler,Suspense,Lazy,这些值都是React中的特殊类型,它们都是React中的组件。
在源码中,这些特殊类型都是通过Symbol来进行标识的,例如Fragment的值是Symbol.for("react.fragment"),具体可以直接看ReactSymbols源码。
props
传入的props中有两个特殊的属性:key,ref。
至于这两个属性的作用,我们后面再慢慢讲。
children
对于children,它是该元素的子元素,它可以是一个字符串,也可以是一个React元素,也可以是一个数组,也可以是一个函数,也可以是一个布尔值,也可以是一个数字,也可以是一个对象,也可以是一个Symbol,也可以是一个undefined,也可以是一个null。
在源码中,通过判断arguments的长度,来判断children的类型,如果长度为3,那么直接将children赋值给props,如果长度大于3,那么就将children赋值给一个数组,然后将这个数组赋值给props。
在经过一系列的处理之后,会调用ReactElement方法,这个方法会返回一个React元素。
React.createElement
当然,你也可以直接使用React.createElement进行创建子元素,我们随便找两个例子来改一改,因为我觉得实际编写项目的过程中,应该不会有人直接使用React.createElement这个方法。
import React from 'react';
export default function TestComponent() {
return React.createElement(
'h1',
null,
'这是使用React.createElement创建的元素',
);
}
渲 染结果:

到目前为止,上面的内容可能是大多数文章中都会提到的,JSX仅仅只是一个语法糖,其实它会被转换成一个方法:React.createElement,但我想说,下面的内容才是你在面试中可以和面试官battle的东西。
$$typeof
需要值得注意的是,这个函数中有一个$$typeof属性,这个属性是用来标识这个对象是一个React元素的,它的值是Symbol.for("react.element")。
$$typeof它的作用是用来标识这个对象是一个React元素的,因为在JavaScript中,对象是没有类型的,所以React需要通过这个属性来标识这个对象是一个React元素,其实它是为了防止XSS攻击,因为JSON中是不支持Symbol类型的,所以React会检测element.$$typeof,如果元素丢失或者无效,会拒绝处理该元素。
这里就得提到React提供的另一个全局API React.isValidElement。
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
可以发现,React在验证一个元素是不是合法元素时,会去验证$$typeof这个值。
React 17
React17的发布并没有包含什么新特性,但是它提供了一个全新的JSX转换,意思就是JSX 不会再被转换为 React.createElement。
- 使用全新的转换,你可以单独使用 JSX 而无需引入 React。
- 根据你的配置,JSX 的编译输出可能会略微改善 bundle 的大小。
- 它将减少你需要学习 React 概念的数量,以备未来之需。
那么新的JSX转换器又会将JSX语句编译成什么呢?
如下所示:
function App() {
return <h1>Hello World</h1>;
}
编译结果:
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
根据文档中所说:jsx这个函数的属性即参数如下:
function jsx(type, props, key) {
return {
$$typeof: ReactElementSymbol,
type,
key,
props,
};
}
与React.createElement最大的不同是,现在children需要放在第二个参数,也就是props中。
至于这部分内容,React官方在介绍全新的 JSX 转换中有详细的讲解。
了解了上面这些内容后,我们就可以开始了解React中的一个重要的概念Fiber。
Fiber
在React中,Fiber是一个重要的概念,它是React16中引入的一个新的架构,它的目的是为了解决React在处理大量数据时出现的卡顿问题。
在React16之前,React的架构是一个递归架构,当我们在渲染一个组件时,它会递归的渲染子组件,这样就会形成一个递归调用的过程,当组件层级很深时,这个递归调用的过程就会很长,这样就会导致浏览器的主线程被长时间的占用,这样就会导致页面出现卡顿的现象。
而在React16中,React引入了一个新的架构Fiber,它的目的就是为了解决React在处理大量数据时出现的卡顿问题。
听到Fiber这个词,你可能并不理解它到底指的是什么,其实说白了,它就是虚拟DOM的一种新的实现,它是一个链表结构,它可以将递归调用变成循环调用,这样就可以将大量的任务拆分成一个个小任务,然后通过循环调用的方式来执行这些小任务。
再用白话来表达,其实它就是指的将虚拟DOM转化成真实DOM并且渲染到页面上的过程(当然,这个说法可能有误)。