前言:
笔者上一次系统学习React还要追溯到2020年下半年,当时还在读大四,学了基础部分写了个demo项目就没有后续了...实习的时候用的是Vue,毕业后大部分时间写的是Angular,但作为前端三大框架之一的React在国内外的影响力是有目共睹的,尤其是颇受国内一众大厂的青睐,故而掌握该框架对于一个前端工程师来说是相当有必要的。
1. JSX语法基础
JSX:javascript and xml(html),意即 js 和 html 混在一起,然后构建视图。
xml是可扩展的标记语言,html是xml的一种,叫超文本标记语言。html是按照W3C官方规范的文本标记来搭建的,xml是可以按照自己规定的标签来搭建一些结构。
下面是React18的语法:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<div></div>
</React.StrictMode>
);
ReactDOM.createRoot()的作用是指定哪个容器作为所有编译内容要渲染的那一个容器,和Vue中的$mounted一样的效果。它会返回一个实例,而这个实例会提供一个render方法,该方法可以将我们所写的所有的jsx语法经过虚拟DOM和dom diff最后变成真实DOM
React18以前的写法:
ReactDOM.render(视图,容器,回调函数)
ReactDOM里直接有render方法,有三个参数,对应分别是视图、容器和回调函数。当视图的内容全部编译完放到容器当中变成真实DOM,已经看到效果后会触发回调函数执行。而React18当中则是将指定容器和渲染分开了。
<React.StrictMode></React.StrictMode>让视图构建编译的时候,按照严格模式进行处理,会检测一些弃用/不建议的语法。
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div>
<div></div>
<div></div>
</div>
);
为了保证只能有一个根结点,要多一层div来包裹(👆),这样层级结构就多了一层,这种情况下是不利于seo优化的,也不利于视图的编译。
那怎样能做到鱼和熊掌兼得呢?我们可以用空标签来取代最外层包裹的div,这在React中叫做 Fragment。而在Vue3中,一个template视图当中可以出现多个根节点,其实现原理也是类似于React的 Fragment 在内部当中构建的。
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
<div></div>
<div></div>
</>
);
或者写成👇:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.Fragment>
<div></div>
<div></div>
</React.Fragment>
);
通过这两种做法,在查看元素的时候就会发现没有多出的一层html结构了。
要将数据放在视图中渲染的话,vue中用的是大胡子语法,即 {{}}。而在React中用的是小胡子语法,即{}。{}里只能放js表达式(执行有返回结果),判断的话一般写三元表达式,循环的话一般用map。
{} 中可以渲染出来的值:
- 基本数据类型:只渲染字符串和数字,其余类型的值渲染为空
- 对象数据类型:
- 数组对象:可以进行渲染,而且不是转换为字符串(每一项之间没有逗号分隔),它会逐一迭代数组每一项,把每一项都拿出来单独进行渲染
- 函数对象:可以作为函数组件进行渲染,要写成 <Component/ > 这种格式
- 其他对象:一般都是不可以直接进行渲染的
- 可以是一个JSX对象
- 如果设置的是style样式,则样式值必须写为对象格式
- ...
eg:
let title = 'hello world!';
let total = 10;
let arr = [
{
id: 1,
name: 'hebe'
},
{
id: 2,
name: 'Taylor Swift'
}
];
let obj = {
name: 'hebe',
age: 18
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
<div classname="box">{title}</div>
<div style={{
color: 'red',
fontSize: '14px'
}}>
{total > 10 ? 'YES' : 'NO'}
</div>
{ React.createElement('span') }
<ul>
{arr.map(item => {
let { id, name } = item;
return <li key={id}>
{name}
</li>;
})}
</ul>
{Reflect.ownKeys(obj).map((key, index) => {
let value = obj[key];
return <span key={index}>
{key} : {value}
</span>
})}
</>
);
和Vue一样,循环绑定的元素都要加唯一的key值,想绑定字符串类型的直接写字符串就行了,如果是一个变量or数字类型,就要用{}包裹起来。注意最好不要用索引作为唯一值。
那怎样去循环对象呢?这就要用到 Reflect 这个内置的对象,它是ES6中为了操作对象而提供的新 API。Reflect.ownKeys()中传入一个对象,可以获取该对象的所有私有属性并以数组形式返回。接下来在用map去循环这个数组就行了。
在JSX语法中,class这个属性是不被识别的,给元素设置类名要用 calssname。给元素设置样式,样式值对应的不是字符串,必须是一个对象,上面demo中关于style的两个大括号和Vue中的大胡子语法不一样,这里的最外层的大括号是胡子语法,用来渲染值的,里面的大括号是表示对象。
总结
JSX具有以下几个特点:
- 最外层只能有一个根元素节点
- <></> fragment空标记,即能作为容器把一堆内容包裹起来,还不占层级结构
- 动态绑定数据使用{},大括号中存放的是JS表达式 => 可以直接放数组:把数组中的每一项都呈现出来 => 一般情况下不能直接渲染对象 => 但是如果是JSX的虚拟DOM对象,是直接可以渲染的
- 设置行内样式,必须是 style={{color:'red'...}};设置样式类名需要使用的是className;
- JSX中进行的判断一般都要基于三元运算符来完成
- JSX中遍历数组中的每一项,动态绑定多个JSX元素,一般都是基于数组中的map来实现的=>和vue一样,循环绑定的元素要设置key值(作用:用于DOM-DIFF差异化对比)
另外,JSX语法具备过滤效果(过滤非法内容),有效防止XSS攻击。
2.JSX和template语法的对比
我们可以通过一个案例来比较下这两种语法的区别:
现有需求,需要通过一个数字类型的变量来创建标签,变量是几我们就创建h几标签。
这个需求在Vue的template语法中该怎么处理呢?
<template>
<div>
<h1 v-if="level === 1">标题1</h1>
<h1 v-else-if="level === 2">标题2</h1>
<h1 v-else-if="level === 3">标题3</h1>
<h1 v-else-if="level === 4">标题4</h1>
<h1 v-else-if="level === 5">标题5</h1>
<h1 v-else>标题6</h1>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
level: 2
}
}
}
</script>
这样写的话得要事先将所有的视图构建好,通过当前状态值控制哪一块元素渲染哪一块元素不渲染,写起来比较麻烦。那如果用JSX语法怎么来写这个需求呢?
let level = 2;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
{ React.createElement(`h${level}`, null, `标题${level}`) }
</>
);
这样写起来就比Vue的template语法更加简洁一些。
在构建更复杂的视图的时候,用JSX语法会更好一些
那面对这种场景,Vue就没有更好的方式去解决嘛?答案是有的!Vue 官方也知道 template 语法这种弱编程性的特点,所以在2.5版本之后Vue也融入了JSX语法。因此,现在在Vue项目中既可以使用template语法,也可以使用JSX语法来构建视图。
接下来,我们可以来尝试下在Vue中怎么去使用JSX语法:
首先,在 main.js 中注册一个全局组件,取名 TextDemo。这里构建js语法使用的是render这个方法,h 其实就是 createElement
Vue.component('TestDemo', {
data() {
return {
level: 2
};
},
render(h) {
return h(`h${this.level}`, null, [`标题${this.level}`)]);
}
});
然后,我们在App.vue的template中,用 TextDemo 这个组件去替换之前写的那些判断逻辑即可。
<template>
<div>
<TestDemo />
</div>
</template>
总结:Vue中的template语法构建视图,编程性比较弱,体现的特点就是灵活性比较差;React中的JSX语法具备很强的编程性,灵活性更好。
3.JSX底层渲染机制
使用JSX语法构建好视图之后,放在页面时肯定是变成了真实DOM来进行渲染的,那么具体是怎么实现的呢?这就需要了解JSX底层渲染机制。
(1) 基于 babel-preset-react-app 语法包,将 jsx 语法渲染解析为 React.creatElement 格式
在 babeljs.io/ 中写好视图,通过 babel-preset-react-app 语法包变成右边的 React.creatElement 格式
将👆右边编译过的代码copy下来并整理:
"use strict";
React.createElement(
"div",
{ classname: "box" },
React.createElement(
"h2",
{ className: "title" },
"\u6807\u9898\u4E8C"
),
React.createElement(
"ul",
{
className: "list",
style: {
color: "red",
},
},
React.createElement("li", null, "origin"),
React.createElement("li", null, "banana"),
React.createElement("li", null, "apple")
)
);
遇到 html 标签,就会变成 createElement 格式,其第一个参数就是标签名/组件,标签上设置的各种属性放到第二个参数对象当中(如果没有任何属性,值是 null),接下来的参数就是该节点的子节点的信息,以此类推...(ps: 文本也是子节点)
(2)执行 React.createElement() 方法生成一个对象,我们将这个对象称为 jsx元素对象,亦或虚拟DOM对象/React child
变为 creatElement 格式之后,执行 React.createElement() 方法,将返回结果用一个变量接收并打印后得到:
{
$$typeof: Symbol(react.element), // 证明当前是一个React元素
type: "div", // 标签名/组件
props: {}, // 含解析出来的各个属性,如果有子节点,则多一个 children 的属性,没有子节点及就没有这个属性。属性值可能是一个值或者是一个数组
ref: null, // 获取DOM元素的属性
key: null
}
(3)root.render() 把虚拟DOM对象转换为真实的DOM对象,放在浏览器中进行渲染
4.手写JSX底层渲染机制
这次手写主要实现上述JSX底层渲染机制中的(2)、(3)步即可
// 创建JSX元素对象
import React from "react";
const createElement = function createElement(type, props, ...children) {
let len = children.length,
virtualDOM = {
$$typeof: Symbol(React.element),
type,
props: {}
};
if (props !== null) virtualDOM.props = { ...props };
if (len === 1) virtualDOM.props.children = children[0];
if (len > 1) virtualDOM.props.children = children;
return virtualDOM;
};
// 迭代对象
const each = function each(obj, callback) {
if (typeof obj !== 'object') throw new TypeError('obj is not a object');
if (typeof callback !== 'function') throw new TypeError('callback is not a function');
let keys = Reflect.ownKeys(obj);
keys.forEach(key => {
let value = obj[key];
callback(value, key);
});
};
// 基于render渲染为真实DOM
const render = function render(virtualDOM, container) {
let { type, props } = virtualDOM;
// 如果type是一个字符串,渲染的是一个html标签
if (typeof type === 'string') {
let ele = document.createElement(type);
// 给标签设置属性
each(props, (value, key) => {
// className
if (key === 'className') {
ele.setAttribute('class', value);
return;
}
// style
if (key === 'style') {
// value ——> style对象
each(value, (styleVal, styleKey) => {
ele.style[styleKey] = styleVal;
});
return;
}
// children
if (key === 'children') {
// value ——> children值(一个值,也可以是数组)
let children = value;
if (!Array.isArray(children)) children = [children];
children.forEach(child => {
// child ——> 每一个字节点
// 如果是文本节点:直接插入进来
if (typeof child === 'string') {
let textNode = document.createTextNode(child);
ele.append(textNode);
return;
}
// 如果是元素节点:递归
render(child, ele);
});
return;
}
ele.setAttribute(key, value);
});
container.append(ele);
}
};