前言
jsx和虚拟dom一直都是react面试中老生常谈的问题,但面试题背归背,只有把问题弄懂了才能转换成自己的真正的实力。
什么是JSX?
JSX 是一个 JavaScript 的语法扩展,在react项目中可以像这样声明一个变量。
const element = <div class='a'>hello world!</div>
// 在babel中会被编译为
const element = /*#__PURE__*/React.createElement("div", {
class: "a"
}, "hello world!");
所以JSX其实是React.createElement()的语法糖,JSX在编译时会被Babel编译为React.createElement方法。
这也是为什么在每个使用JSX的JS文件中,你必须显式的声明
import React from 'react'
不过,React 17 在 React 的 package 中引入了两个新入口,这些入口只会被 Babel 和 TypeScript 等编译器使用。新的 JSX 转换不会将 JSX 转换为React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。
// 假设你的源代码如下
function App() {
return <h1>Hello World</h1>;
}
// 下方是新 JSX 被转换编译后的结果:
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
React.createElement
React.createElement内部会调用ReactElement函数最后返回一个对象。
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// 将 config 处理后赋值给 props
// ...省略
}
const childrenLength = arguments.length - 2;
// 处理 children,会被赋值给props.children
// ...省略
// 处理 defaultProps
// ...省略
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 标记这是个 React Element
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return element;
};
ReactElement最终会返回一个包含组件数据的js对象,这就是经常说的虚拟dom对象,也是一个react元素,其中$$typeof属性被赋值为REACT_ELEMENT_TYPE的常量标记这个对象是一个合法的react元素。
并且react提供了全局API用于校验对象是否为合法的react元素
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
$$typeof
$$typeof 属性也起到了防止 XSS 攻击的作用。
如果服务器允许用户储存任意的 JSON 数据的情况下。那么就可以手动构建 React Element 对象传入到元素中。例如:
// Server could have a hole that lets user store JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* put your exploit here */'
},
},
// ...
};
let message = { text: expectedTextButGotJSON };
// Dangerous in React 0.13
<p>
{message.text}
</p>
REACT_ELEMENT_TYPE值的定义
if (typeof Symbol === 'function' && Symbol.for) {
const symbolFor = Symbol.for;
REACT_ELEMENT_TYPE = symbolFor('react.element');
REACT_PORTAL_TYPE = symbolFor('react.portal');
REACT_FRAGMENT_TYPE = symbolFor('react.fragment');
...
}
使用 Symbol 类型是因为 JSON 中无法传递 Symbol。React 会检查 element.$$typeof 然后拒绝处理非法的元素,这样就可以规避这个问题。
React Component和 React Element
使用class component或者function component的时候component都会被当作React.createElement函数中的第一个参数type传入
class ClassComp {
render() {
return <div>ClassComp</div>
}
}
const FuncComp = () => {
return <div>FuncComp</div>
}
const element1 = <ClassComp />
const element2 = <FuncComp />
// 在babel中会被编译为
class ClassComp {
render() {
return /*#__PURE__*/React.createElement("div", null, "ClassComp");
}
}
const FuncComp = () => {
return /*#__PURE__*/React.createElement("div", null, "FuncComp");
};
const element1 = /*#__PURE__*/React.createElement(ClassComp, null);
const element2 = /*#__PURE__*/React.createElement(FuncComp, null);
注意点
如果自定义组件的命名不是大写开头的话,babel只会转换成普通的标签,也称为HostComponent.
const funcComp = () => {
return <div>FuncComp</div>
}
const element1 = <funcComp />
// 在babel中会被编译为
const funcComp = () => {
return /*#__PURE__*/React.createElement("div", null, "FuncComp");
};
const element1 = /*#__PURE__*/React.createElement("funcComp", null);
结果type会变成"funcComp"而不是funcComp组件,这也是react自定义组件为什么要大写开头的原因。
JSX与Fiber节点
从上面的内容我们可以发现,JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
比如如下信息就不包括在JSX中:
- 组件在更新中的优先级
- 组件的state
- 组件被打上的用于Renderer的标记
- 这些内容都包含在Fiber节点中。
所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。
在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。