一、是什么
JSX是react的语法糖,它允许在HTML中写js,不能被浏览器直接识别,需要通过webpack、babel之类的编译工具转换为js执行。
JSX的特点
可以将HTML语言直接写在JavaScript语言之中,不加任何引号,这就是JSX的语法,它允许HTML与JavaScript的混写。
JSX允许直接在模板插入JavaScript变量。如果这个变量是一个数组,则会展开这个数组的所有成员。
防注入攻击
在JSX中嵌入用户输入是安全的;
React DOM在渲染之前默认会过滤所有传入的值。它可以确保应用不会被注入攻击。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止XSS(跨站脚本攻击)
Babel转译器会把JSX转换成一个名为React.createElement()的方法调用。
react通过将组件编写的JSX映射到屏幕,以及组件中的状态发生了变化之后 React会将这些「变化」更新到屏幕上
JSX通过babel最终转化成React.createElement这种形式,例如:
<div>
< img src="avatar.png" className="profile" />
<Hello />
</div>
会被bebel转化成如下:
React.createElement(
"div",
null,
React.createElement("img", {
src: "avatar.png",
className: "profile"
}),
React.createElement(Hello, null)
);
在转化过程中,babel在编译时会判断 JSX 中组件的首字母:
- 当首字母为小写时,其被认定为原生
DOM标签,createElement的第一个变量被编译为字符串 - 当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象
最终都会通过RenderDOM.render(...)方法进行挂载,如下:
ReactDOM.render(<App />, document.getElementById("root"));
二、过程
在react中,节点大致可以分成四个类别:
- 原生标签节点
- 文本节点
- 函数组件
- 类组件
如下所示:
class ClassComponent extends Component {
static defaultProps = {
color: "pink"
};
render() {
return (
<div className="border">
<h3>ClassComponent</h3>
<p className={this.props.color}>{this.props.name}</p >
</div>
);
}
}
function FunctionComponent(props) {
return (
<div className="border">
FunctionComponent
<p>{props.name}</p >
</div>
);
}
const jsx = (
<div className="border">
<p>xx</p >
< a href=" ">xxx</ a>
<FunctionComponent name="函数组件" />
<ClassComponent name="类组件" color="red" />
</div>
);
这些类别最终都会被转化成React.createElement这种形式
React.createElement其被调用时会传⼊标签类型type,标签属性props及若干子元素children,作用是生成一个虚拟Dom对象,如下所示:
function createElement(type, config, ...children) {
if (config) {
delete config.__self;
delete config.__source;
}
// ! 源码中做了详细处理,⽐如过滤掉key、ref等
const props = {
...config,
children: children.map(child =>
typeof child === "object" ? child : createTextNode(child)
)
};
return {
type,
props
};
}
function createTextNode(text) {
return {
type: TEXT,
props: {
children: [],
nodeValue: text
}
};
}
export default {
createElement
};
createElement会根据传入的节点信息进行一个判断:
- 如果是原生标签节点, type 是字符串,如div、span
- 如果是文本节点, type就没有,这里是 TEXT
- 如果是函数组件,type 是函数名
- 如果是类组件,type 是类名
虚拟DOM会通过ReactDOM.render进行渲染成真实DOM,使用方法如下:
ReactDOM.render(element, container[, callback])
当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 diff算法进行高效的更新
如果提供了可选的回调函数callback,该回调将在组件被渲染或更新之后被执行
render大致实现方法如下:
function render(vnode, container) {
console.log("vnode", vnode); // 虚拟DOM对象
// vnode _> node
const node = createNode(vnode, container);
container.appendChild(node);
}
// 创建真实DOM节点
function createNode(vnode, parentNode) {
let node = null;
const {type, props} = vnode;
if (type === TEXT) {
node = document.createTextNode("");
} else if (typeof type === "string") {
node = document.createElement(type);
} else if (typeof type === "function") {
node = type.isReactComponent
? updateClassComponent(vnode, parentNode)
: updateFunctionComponent(vnode, parentNode);
} else {
node = document.createDocumentFragment();
}
reconcileChildren(props.children, node);
updateNode(node, props);
return node;
}
// 遍历下子vnode,然后把子vnode->真实DOM节点,再插入父node中
function reconcileChildren(children, node) {
for (let i = 0; i < children.length; i++) {
let child = children[i];
if (Array.isArray(child)) {
for (let j = 0; j < child.length; j++) {
render(child[j], node);
}
} else {
render(child, node);
}
}
}
function updateNode(node, nextVal) {
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => {
if (k.slice(0, 2) === "on") {
let eventName = k.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[k]);
} else {
node[k] = nextVal[k];
}
});
}
// 返回真实dom节点
// 执行函数
function updateFunctionComponent(vnode, parentNode) {
const {type, props} = vnode;
let vvnode = type(props);
const node = createNode(vvnode, parentNode);
return node;
}
// 返回真实dom节点
// 先实例化,再执行render函数
function updateClassComponent(vnode, parentNode) {
const {type, props} = vnode;
let cmp = new type(props);
const vvnode = cmp.render();
const node = createNode(vvnode, parentNode);
return node;
}
export default {
render
};
三、总结
在react源码中,虚拟Dom转化成真实Dom整体流程如下图所示:
其渲染流程如下所示:
- 使用React.createElement或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(…) ,Babel帮助我们完成了这个转换的过程。
- createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
- ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM
四、react/jsx-runtime介绍
react/jsx-runtime是React提供的一个JSX运行时库,用于实现JSX元素到React元素的转换。 在v17版本中,React官方提供了一个新的工具包react/jsx-runtime,它提供了JSX转换的运行时函数供开发者使用。使用react/jsx-runtime,如:
import { jsx } from 'react/jsx-runtime';
function MyComponent() {
return jsx('div', null, jsx('h1', null, 'Hello World'));
}
可以看到,用jsx函数替代了原来的HTML标签语法,同时需要传入两个参数,第一个参数是元素类型,第二个参数是元素的属性。如果元素有子元素,可以在第二个参数后面添加任意数量的参数,这些参数将作为元素的子元素。这样,使用react/jsx-runtime提供的JSX运行时库,就可以在不再需要import React的情况下使用JSX语法了。
1、react中有了React.createElement为什么还要有react/jsx-runtime?
React.createElement是用于构建虚拟DOM的方法,它需要手动指定元素的类型、属性和子元素等,使用起来比较繁琐。而JSX语法则可以更直观地描述UI组件的结构和属性,更符合开发者的思维模式。
在React之前,使用JSX语法需要通过Babel等工具将JSX编译为React.createElement的形式,这一过程相对比较繁琐。React v17中引入了jsx-runtime,使得开发者可以在不依赖于Babel等编译工具的情况下直接使用JSX语法,节省构建和调试的时间成本。
在使用jsx-runtime后,JSX语法会被直接转换为React.createElement的调用,相比手动编写React.createElement,使用JSX语法更加简洁和易读。同时,jsx-runtime还提供了一些额外的语法糖,如Fragment、jsxDEV等,可以帮助开发者更方便地构建React应用。
因此,react/jsx-runtime是为了方便开发者使用JSX语法而诞生的,它简化了React应用的开发流程,提高了开发效率。
注意:
如果不需要使用jsx方法创建dom的话,不用手动引入
import { jsx } from 'react/jsx-runtime';
如下代码一样可以正常运行:
import ReactDOM from 'react-dom/client'
const Root = ReactDOM.createRoot(document.getElementById('root'))
let element = <h1 className='title' style={{color:'red'}}>hello</h1>
Root.render(
element
)