React
React由Facebook来更新和维护,它是大量优秀程序员的思想结晶,React的流行不仅仅局限于普通开发工程师对它的认可
-
React可以说是前端的先驱者,它总是会引领整个前端的潮流 -
大量流行的其他框架借鉴
React的思想,比如Vue Composition API学习了React Hooks的思想 -
官方对
React的解释:用于构建用户界面的JavaScript库 -
React的官网文档:zh-hans.reactjs.org/
特点
- 声明式编程:它允许我们只需要维护自己的状态,当状态改变时,React可以根据最新的状态去渲染我们的UI界面
- 组件化开发:组件化开发页面是前端的流行趋势,会将复杂的界面拆分成一个个小的组件
- 多平台适配:
-
2013年,
React发布之初主要是开发Web页面 -
2015年,
Facebook推出了ReactNative,用于开发移动端跨平台;(虽然目前Flutter非常火爆,但是还是有很多公司在使用ReactNative) -
2017年,
Facebook推出ReactVR,用于开发虚拟现实Web应用程序;
-
依赖
开发React必须依赖三个库:
-
react:包含react所必须的核心代码 -
react-dom:react渲染在不同平台所需要的核心代码 -
babel:将jsx转换成React代码的工具,-
默认情况下开发
React其实可以不使用babel -
但是前提是我们自己使用
React.createElement来编写源代码,非常繁琐可读性差 -
可以直接编写
jsx(JavaScript XML)的语法,并且让babel帮助我们转换成React.createElement
-
这三个库是各司其职的,目的就是让每一个库只单纯做自己的事情; 在React的0.14版本之前是没有react-dom这个概念的,所有功能都包含在react里;
后来为什么要进行拆分呢?原因就是react-native
-
react包中包含了react web和react-native所共同拥有的核心代码 -
react-dom针对web和native所完成的事情不同:-
web端:react-dom会将jsx最终渲染成真实的DOM,显示在浏览器中 -
native端:react-dom会将jsx最终渲染成原生的控件(比如Android``中的Button,iOS中的UIButton)
-
依赖引入
-
方式一:直接
CDN引入 -
方式二:下载后,添加本地依赖
-
方式三:通过
npm管理(脚手架使用) -
暂时直接通过
CDN引入:-
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin> -
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin> -
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"> -
这里有一个
crossorigin的属性,这个属性的目的是为了拿到跨域脚本的错误信息
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>第一个React程序</title> </head> <body> <div id="root"></div> <!-- cdn引入 crossorigin解决跨域--> <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <!-- babel引入 --> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> <script type="text/babel"> //type="text/babel"这时才会babel才会解析 /* 18之前用render在这里用会报错 ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17.*/ // ReactDOM.render(<h2>Hello World</h2>,document.querySelector('#root')) /* 18之后 createRoot只需要写渲染到哪里就可以 */ let root = ReactDOM.createRoot(document.querySelector('#root')) root.render(<h2>Hello World</h2>) </script> </body> </html> -
组件化
定义
在React中,如何封装一个组件呢?这里暂时使用类的方式封装组件:
-
定义一个类(类名大写,组件的名称是必须大写的,小写会被认为是
HTML元素),继承自React.Component -
实现当前组件的
render函数,render当中返回的jsx内容,就是之后React会帮助我们渲染的内容
数据依赖
组件化问题一:数据在哪里定义?
- 在组件中的数据,可以分成两类:
-
参与界面更新的数据:当数据变化时,需要更新组件渲染的内容
-
不参与界面更新的数据:当数据变化时,不需要更新将组件渲染的内容
-
- 参与界面更新的数据可以称之为是参与数据流,这个数据是定义在当前对象的
state中-
可以通过在构造函数中
this.state = {定义的数据} -
当数据发生变化时,可以调用
this.setState来更新数据,并且通知React进行update操作 -
在进行
update操作时,就会重新调用render函数,并且使用最新的数据来渲染界面
-
事件绑定
组件化问题二:事件绑定中的this
- 在类中直接定义一个函数,并且将这个函数绑定到元素的
onClick事件上,当前这个函数的this指向的是谁呢?-
默认情况下是
undefined,很奇怪居然是undefined -
因为在正常的
DOM操作中,监听点击函数中的this其实是节点对象(比如说是button对象) -
这次因为
React并不是直接渲染成真实的DOM,编写的button只是一个语法糖,它的本质是React的Element对象 -
那么在这里发生监听的时候,
react在执行函数时并没有绑定this,默认情况下就是一个undefined
-
- 在绑定的函数中,可能想要使用当前对象,比如执行
this.setState函数,就必须拿到当前对象的this-
需要在传入函数时,给这个函数直接绑定
this -
可以在构造函数里提前给点击函数绑定
this:this.changeText = this.changeText.bind(this) -
也可以在按钮上绑定
this:<button onClick={this.changeText.bind(this)}>改变文本</button>
-
整体代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
<!-- cdn引入 crossorigin解决跨域 -->
<script
src="https://unpkg.com/react@18/umd/react.development.js"
crossorigin
></script>
<script
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
crossorigin
></script>
<!-- babel引入 -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
const root = ReactDOM.createRoot(document.getElementById("root"));
// let message = 'Hello World'
// function btnClick() {
// message = 'Hello React'
// rootRender() // 修改react不会自动重新刷新页面需要手动修改
// }
// rootRender() // 函数有空间提升
// function rootRender() {
// root.render(
// <div>
// <h2>{message}</h2>
// <button onClick={btnClick}>修改文本</button>
// </div>
// )
// }
/* 使用类组件方式重构 */
class App extends React.Component {
constructor() {
super();
this.state = {
message: "Hello World",
};
// 另一种写法提前给方法绑定this
// this.btnClick = this.btnClick.bind(this)
}
btnClick() {
console.log(this); // undefined 因为babel转换
// 修改message值,自动执行render函数
this.setState({ message: "Hello React" });
// 现在这个this不一定代表这个组件实例
}
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={this.btnClick.bind(this)}>修改文本</button>
</div>
);
/* bind(this)表示把render的this绑定过this,都是组件实例 错误写法:表示方法的调用,this.btnClick()默认返回undefined,把undefined绑定给onClick肯定报错 <button onClick={this.btnClick()}>修改文本</button> */
}
}
/* const app = new App()
const foo = app.btnClick
foo() // 这里this是默认绑定,默认指向window,但严格模式下指向undefined,es6 class类定义的默认是严格模式,babel转换默认undefined
function fo() {
console.log(this); // 这里this指向undefined,因为用了babel,关闭babel指向window
}
fo() */
root.render(<App />); // app根组件
</script>
</body>
</html>
JSX
-
JSX是一种JavaScript的语法扩展(extension),也在很多地方称之为JavaScript XML,因为看起就是一段XML语法 -
它用于描述我们的
UI界面,并且其完全可以和JavaScript融合在一起使用 -
它不同于
Vue中的模块语法,你不需要专门学习模块语法中的一些指令(比如v-for、v-if、v-bind)
规范
-
JSX的顶层只能有一个根元素,所以很多时候会在外层包裹一个div元素(或者使用后面学习的Fragment) -
JSX的外层需要包裹一个() 方便阅读,可以进行换行书写 -
JSX的标签可以是单标签,也可以是双标签,单标签必须以<xxx/>结尾 -
JSX中的写注释{/* JSX注释 */} -
JSX嵌入变量作为子元素-
情况一:当变量是
Number、String、Array类型时,可以直接显示 -
情况二:当变量是
null、undefined、Boolean类型时,内容为空如果希望显示
null、undefined、Boolean则需要转成字符串,转换的方式有很多,比如toString方法、和空字符串拼接,String(变量)等方式 -
情况三:
Object对象类型不能作为子元素(not valid as a React child)
-
-
JSX嵌入表达式:运算表达式、三元运算符、执行一个函数
事件绑定
如果原生DOM原生有一个监听事件,可以如何操作呢?
-
方式一:获取
DOM原生,添加监听事件 -
方式二:在
HTML原生中,直接绑定onclick
在React中是如何操作呢?我们来实现一下React中的事件监听,这里主要有两点不同
-
React事件的命名采用小驼峰式(camelCase),而不是纯小写 -
再需要通过
{}传入一个事件处理函数,这个函数会在事件发生时被执行 -
在事件执行后,可能需要获取当前类的对象中相关的属性,这个时候需要用到
this -
解决
this的问题三种方案:-
bind给btnClick显式绑定this:this.changeText = this.changeText.bind(this) -
使用
ES6 class fields语法:<button onClick={this.changeText}>{message}</button> | changeText = () => {} -
事件监听时传入箭头函数(推荐):
<button onClick={() => this.changeIsHave()}>切换</button>
-
参数传递
在执行事件函数时,有可能我们需要获取一些参数信息:比如event对象、其他参数
- 情况一:获取
event对象-
很多时候需要拿到
event对象来做一些事情(比如阻止默认行为) -
那么默认情况下,
event对象有被直接传入,函数就可以获取到event对象
-
- 情况二:获取更多参数
- 有更多参数时,最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数
条件渲染
某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容,在vue中会通过指令来控制比如v-if、v-show;在React中,所有的条件判断都和普通的JavaScript代码一致,常见的条件渲染的方式有哪些呢?
-
方式一:条件判断语句:适合逻辑较多的情况
-
方式二:三元运算符:适合逻辑比较简单
-
方式三:与运算符
&&:适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染 -
v-show的效果:主要是控制display属性是否为none
列表渲染
在React中并没有像Vue模块语法中的v-for指令,而且需要通过JavaScript代码的方式组织数据转成JSX,React中的JSX正是因为和JavaScript无缝的衔接,让它可以更加的灵活
-
在
React中,展示列表最多的方式就是使用数组的map高阶函数 -
很多时候在展示一个数组中的数据之前,需要先对它进行一些处理
-
比如过滤掉一些内容:
filter函数 -
比如截取数组中的一部分内容:
slice函数
-
-
在
vue中使用v-for进行列表渲染时需要绑定key值,在react中也需要添加key,否则会报错,key主要的作用是为了提高diff算法时的效率
JSX知识代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JSX语法相关</title>
</head>
<body>
<div id="root"></div>
<style>
.active {
border: 1px red solid;
}
</style>
<script src="./lib/babel.js"></script>
<script src="./lib/react.js"></script>
<script src="./lib/react-dom.js"></script>
<script type="text/babel">
// 1.定义App根组件
class App extends React.Component {
constructor() {
super();
this.state = {
message: "Hello React",
// JSX中String/Number/Array可以直接展示
a: "11",
b: 222,
c: ["aa", "bb"],
// JSX中null/undefined/Boolean内容为空,若展示需要转化为字符串
aa: null,
bb: undefined,
cc: true,
// JSX中对象不能作为子元素
obj: { name: "哈哈哈" },
currentIndex: 0,
isHave: true,
baidu: "https://www.baidu.com",
style1: { color: "red" },
list: [
{ id: 11, name: "哈哈", score: 90 },
{ id: 12, name: "啦啦", score: 78 },
{ id: 13, name: "哒哒", score: 65 },
{ id: 14, name: "你你", score: 59 },
{ id: 15, name: "塔塔", score: 73 },
],
};
}
render() {
const { message, list } = this.state;
const { a, b, c } = this.state;
const { aa, bb, cc } = this.state;
const { obj } = this.state;
const { currentIndex } = this.state;
const { isHave, baidu, style1 } = this.state;
const className1 = `aa bb ${isHave && a}`;
const className2 = ["aa", "bb"];
if (isHave) className2.push(a);
// 基本使用
// return (
// // JSX只有一个根元素
// <div>
// {/* JSX注释:渲染 */}
// <h2>{message}</h2>
// <h2>{a}</h2>
// <h2>{b}</h2>
// <h2>{c}</h2>
// <h2>{aa}</h2>
// <h2>{bb}</h2>
// <h2>{cc}</h2>
// {/* null和undefined没有toString方法,可以使用String(aa)和bb+'' */}
// <h2>{String(aa)}</h2>
// <h2>{bb + ""}</h2>
// <h2>{cc.toString()}</h2>
// {/* 对象会报错:Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead. */}
// {/* <h2>{obj}</h2> */}
// {/* JSX可以嵌入表达式比如电影列表例子/三元表达式/执行函数 */}
// <h2>{isHave ? "有好事" : "没好事"}</h2>
// <h2>{this.getData()}</h2>
// <a href={baidu}>{this.getData()}</a>
// {/* 绑定class */}
// {/* <h2 class={`aa bb ${a}`}>{this.getData()}</h2> 报错react-dom.js:73 Warning: Invalid DOM property `class`. Did you mean `className`?*/}
// <h2 className={`aa bb ${isHave && a}`}>{this.getData()}</h2>
// <h2 className={className1}>{this.getData()}</h2>
// <h2 className={className2.join(" ")}>{this.getData()}</h2>
// {/* 还有一种就是使用第三方库classnames -> npm install classnames */}
// {/* 绑定style */}
// <h2 style={{ color: "red" }}>{style1}</h2>
// <h2 style={style1}>style </h2>
// </div>
// );
// this绑定 第一种参考计数器
// return (
// <div>
// {/* 第二种绑定箭头函数 */}
// <button onClick={this.click1}>{message}</button>
// {/* 第三种绑定(推荐掌握) */}
// <button onClick={(e) => this.click2(e, 11)}>{message}</button>
// </div>
// );
// 条件渲染两种实现
let showEle = null;
if (isHave) {
showEle = <h1>条件渲染1</h1>;
} else {
showEle = <h2>条件渲染2</h2>;
}
// return <div>{showEle}</div>;
// return <div>{isHave ? <h1>条件渲染1</h1> : <h2>条件渲染2</h2>}</div>;
// return <div>{isHave && <h1>条件渲染1</h1>}</div>;
// return (
// <div>
// {isHave && <h1>条件渲染1</h1>}
// <button onClick={() => this.changeIsHave()}>切换</button>
// </div>
// );
// v-show效果
// return (
// <div>
// <h1 style={{ display: isHave ? "block" : "none" }}>
// 条件渲染v-show
// </h1>
// <button onClick={() => this.changeIsHave()}>切换</button>
// </div>
// );
// 列表渲染
// const ele = list
// .filter((f) => f.score > 70)
// .slice(0, 2)
// .map((m) => {
// return (
// <div key={m.score}>
// <h1>学号:{m.id}</h1>
// <h2>分数:{m.score}</h2>
// <h3>名字:{m.name}</h3>
// </div>
// );
// });
return (
<div>
{/* ele */}
{list
.filter((f) => f.score > 70)
.slice(0, 2)
.map((m, index) => {
return (
<div
key={m.id}
className={currentIndex == index && "active"}
onClick={(e) => this.changeIndex(index)}
>
{/* 必须有一个包裹 */}
<h1>学号:{m.id}</h1>
<h2>名字:{m.name}</h2>
<h3>分数:{m.score}</h3>
</div>
);
})}
</div>
);
}
click1 = () => {
console.log("click1", this);
this.setState({ message: "hello1" });
};
click2 = (e, num) => {
console.log("click1", this, e, num);
this.setState({ message: "hello2" });
};
changeIsHave() {
this.setState({ isHave: !this.state.isHave });
}
changeIndex(currentIndex) {
this.setState({
currentIndex,
});
}
getData() {
return !this.state.isHave ? "有好事" : "没好事";
}
}
// 创建root并渲染App组件
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
</script>
</body>
</html>
本质
实际上jsx 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖,所有的jsx最终都会被转换成React.createElement的函数调用,createElement需要传递三个参数:
- 参数一:
type-
当前
ReactElement的类型 -
如果是标签元素,那么就使用字符串表示 “div”
-
如果是组件元素,那么就直接使用组件的名称
-
- 参数二:
config-
所有
jsx中的属性都在config中以对象的属性和值的形式存储 -
比如传入
className作为元素的class
-
- 参数三:
children-
存放在标签中的内容,以
children数组的方式进行存储 -
当然如果是多个元素呢?
React内部有对它们进行处理,处理的源码在下方
-
默认jsx是通过babel进行语法转换的,所以之前写的jsx代码都需要依赖babel,可以在babel的官网中快速查看转换的过程:babeljs.io/repl/#?pres…
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JSX语法相关</title>
</head>
<body>
<div id="root"></div>
<script src="./lib/babel.js"></script>
<script src="./lib/react.js"></script>
<script src="./lib/react-dom.js"></script>
<script type="text/babel">
// 1.定义App根组件
class App extends React.Component {
constructor() {
super();
this.state = {
message: "Hello React",
count: 100,
};
}
render() {
const { message } = this.state;
/*
JSX的本质仅仅是React.creatrElement(type, config, children)的语法糖
type:是类型,比如div
config:是对象比如:{title: 'Header', class: 'Header', src: ''},
children:是子元素
2023.07.31使用bebal转换如下:
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
_jsxs("div", {
className: "app",
children: [_jsx("div", {
title: "Header",
children: "Header"
}), _jsxs("div", {
children: [_jsx("button", {
onClick: e => this.changeCount(1),
children: "+1"
}), _jsxs("h2", {
children: ["\u5566\u5566\u5566\u5566", this.state.count]
}), _jsx("button", {
onClick: e => this.changeCount(-1),
children: "-1"
})]
}), _jsx("div", {
children: "Footer"
})]
});
*/
const ele = React.createElement(
"div",
null,
/*#__PURE__*/ React.createElement(
"div",
{
className: "header",
},
"Header"
),
/*#__PURE__*/ React.createElement(
"div",
{
className: "Content",
},
/*#__PURE__*/ React.createElement("div", null, "Banner"),
/*#__PURE__*/ React.createElement(
"ul",
null,
/*#__PURE__*/ React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E1"
),
/*#__PURE__*/ React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E2"
),
/*#__PURE__*/ React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E3"
),
/*#__PURE__*/ React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E4"
),
/*#__PURE__*/ React.createElement(
"li",
null,
"\u5217\u8868\u6570\u636E5"
)
)
),
/*#__PURE__*/ React.createElement(
"div",
{
className: "footer",
},
"Footer"
)
);
console.log(ele);
// return (
// <div className="app">
// {/* JSX注释:渲染 */}
// <div title="Header">Header</div>
// <div>
// <button onClick={(e) => this.changeCount(1)}>+1</button>
// <h2>啦啦啦啦{this.state.count}</h2>
// <button onClick={(e) => this.changeCount(-1)}>-1</button>
// </div>
// <div>Footer</div>
// </div>
// );
return ele;
}
changeCount(num) {
this.setState({
count: this.state.count + num,
});
}
}
// 创建root并渲染App组件
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
</script>
</body>
</html>
虚拟DOM
通过 React.createElement 最终创建出来一个 ReactElement 对象,这个ReactElement对象是什么作用呢?React为什么要创建它呢?
-
原因是
React利用ReactElement对象组成了一个JavaScript的对象树 -
JavaScript的对象树就是虚拟DOM(Virtual DOM),上面代码打印的ele就可以看到结构 -
而
ReactElement最终形成的树结构就是Virtual DOM
为什么使用虚拟DOM,⽽不是直接修改真实的DOM呢?
-
React官方的说法:Virtual DOM是一种编程理念 -
在这个理念中,
UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象 -
可以通过
ReactDOM.render让虚拟DOM和真实DOM同步起来,这个过程中叫做协调(Reconciliation) -
这种编程的方式赋予了
React声明式的API,只需要告诉React希望让UI是什么状态,React来确保DOM和这些状态是匹配的 -
不需要直接进行
DOM操作,就可以从手动更改DOM、属性操作、事件处理中解放出来
-
原有的开发模式,很难跟踪到状态发⽣的改变,不⽅便针对应⽤程序进⾏调试
-
操作真实
DOM性能较低:传统的开发模式会进⾏频繁的DOM操作,⽽这⼀的做法性能⾮常的低 -
虚拟
DOM帮助我们从命令式编程转到了声明式编程的模式 -
虚拟
DOM有利于实现跨平台的能⼒,即⼀套代码可以打包出各个平台的应⽤
- 关于虚拟
DOM的一些其他内容,在后续的学习中还会再次讲到