React 用于构建 Web 和原生交互界面的库 。
都是构建UI的框架,但关注点不同:
因此,Vue在构建页面上更便捷,而React在构建页面时可以使用全部的JS能力。
与Vue.js一样,有CDN链接;也可以通过npm独立安装;当然也有对应的脚手架工具。
为了更快下载必要js文件,将CDN链接上的js下载本地。
<!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>Hello</title>
<script src="../babel.min.js"></script>
<script src="../react.development.js"></script>
<script src="../react-dom.development.js"></script>
</head>
<body>
<div id="root"></div>
<!-- 这个script不能是普通的脚本 -->
<!-- 通过type属性告诉babel这里的代码 是需要它转译 -->
<script type="text/babel">
// 0 创建一个简单 UI:
const element = <h1>Hello, world.</h1>; // 这就是jsx
// 创建root实例,提供一个render方法 可以将构建的UI 渲染到页面中
// 参数 为 一个DOM对象
// 1. 获取容器的dom对象
const containerEle = document.getElementById('root');
// 2. 创建root实例,并指定对应容器DOM
const root = ReactDOM.createRoot(containerEle);
// 3. 将 element 渲染到 指定容器DOM下
root.render(element);
</script>
</body>
</html>
形似
<h1>Hello, World.</h1>
这样的语法,看着 既不是 HTML 标签, 也不是 JS中数据类型。它就是jsx
.
在React中,可以使用{}
将一个JS表达式的值 绑定到 JSX 中。
let username = 'guoguo';
// 在React中可以使用 "{}" 去 在jsx中渲染数据
// 在"{js表达式}"中,编写的是 JS表达式。类似与 Vue的插值语法"{{}}"
let element = <h3>Hello, {username}.</h3>; // <h3>Hello, daxia.</h3>
在开发UI时,jsx的操作会像HTML标签那样去指定各种属性。
由于 JSX 最终会被 babel 转译成 JS对象。因此JSX的属性命名规则要遵从JS规范。
实现时注意以下几点:
剩下那些就无需改变,直接可以使用。
// let element = (
// <h3 id="hello" className="h3-red">
// Hello, React.
// </h3>
// );
/**
*! 1. jsx元素必须闭合
*! 2. 在jsx中注释内容的话 需要使用 绑定语法
*/
let element = (
<div>
<label htmlFor="name">姓名:</label>
{/* jsx元素必须闭合*/}
<input id="name" type="text" placeholder="输入姓名..." tabIndex="1" />
<br />
<label htmlFor="xueli">学历:</label>
<input
id="xueli"
type="text"
placeholder="输入学历..."
tabIndex="3"
/>
<input type="text" placeholder="干扰..." tabIndex="2" />
</div>
);
ReactDOM.createRoot(document.getElementById('root')).render(element);
所有UI界面都需要通过事件来完成交互的。
不同与Vue,有v-on指令来处理事件,react是通过jsx相对应的事件属性来实现的
React在处理事件时,和原生DOM对象类似。可以通过对应事件相关属性来注册监听器。
不同的是:
原生DOM事件绑定的值类型为 string;而React JSX 事件绑定的值类型为 function。
<!-- 原生事件注册 -->
<button id="btn">点我试试</button>
<script>
// 比如注册点击事件
document.getElementById('btn').onclick = function () {
alert('你敢点我???');
};
</script>
// React中的实现
function handleClick() {
alert('你敢点我???');
}
const element = <button onClick={handleClick}>点我试试</button>;
ReactDOM.createRoot(document.querySelector('#root')).render(element);
在实际开发中,当某一事发生后开发者需要根据当前事件获取相关的信息数据。此时就需要找到事件对象。不同的事件,会创建不同事件对象,并且不同的事件对象包含的数据也不尽相同。
那么怎么获取 事件对象 呢?默认情况下,JSX中注册所有监听器函数都会接收一个 事件对象 作为 唯一入参。因此在需要时在事件监听器函数中定义形参接收即可。
// 事件监听的第一个参数 就是 事件对象
function handleClick(e) {
console.log(e);
alert('你敢点我???');
}
function handleChage(e) {
// 每次文本改后将其打印出来
console.log('changed:', e.target.value);
}
function handleSubmit(e) {
e.preventDefault(); // 取消默认行为
alert('submited.');
}
const element = (
<div>
{/**1 点击事件 **/}
<button onClick={handleClick}>点我试试</button>
{/**2 change事件 **/}
{/* input元素的change事件 和原生的input事件 是一样的 */}
<input type="text" onChange={handleChage} />
{/**3 submit事件 **/}
<form onSubmit={handleSubmit}>
<p>这旮瘩是表单</p>
<button>提交</button>
</form>
</div>
);
在React16之前,事件的监听器中this可能会被频繁使用。因此我们需要记住this的绑定值。
但是在React18之后,即推出Hook之后,开发者就可以绕过 JS 中难以理解的this指向。
原生事件属性绑定的监听器中的this
<button onclick="onClick1()">原生事件1</button>
<button id="btn">原生事件2</button>
<script type="text/babel">
function onClick1() {
console.log(this); // undefined
}
function onClick2() {
console.log(this); // button元素
}
document.getElementById('btn').onclick = onClick2;
</script>
但是在React中 监听器的this 与HTML属性注册事件是一样的,即监听器中的this不会绑定任何事。
<div id="root"></div>
<script type="text/babel">
function onClick() {
console.log(this);// undefined 不会绑定任何值
}
let element = <button onClick={onClick}>click me</button>;
ReactDOM.createRoot(document.querySelector('#root')).render(element);
</script>
如果这些监听器函数中的this需要有一个有效值:
<div id="root"></div>
<script type="text/babel">
var obj = {name: 'big fat girl baby'};
function onClick() {
console.log(this);// undefined 不会绑定任何值
}
let element = <button onClick={onClick.bind(obj)}>click me</button>;
ReactDOM.createRoot(document.querySelector('#root')).render(element);
</script>
默认监听器中只会接收一个参数,即事件对象。
如果想要传入额外参数,像这样
function deleteListItem(e, id) {
console.log(e, id);
}
let element = (
<ul>
<li>
<div>
<p>list - 1</p>
{/* 使用箭头函数套壳 */}
<button
onClick={(e) => {
deleteListItem(e, 1);
}}
>
删除
</button>
</div>
</li>
<li>
<div>
<p>list - 2</p>
{/* 也可以使用bind函数 */}
<button onClick={deleteListItem.bind(null, 2)}>删除</button>
</div>
</li>
</ul>
);
ReactDOM.createRoot(document.querySelector('#root')).render(element);
React组件更像是一个函数,返回组件最终渲染后的效果(UI)。这里函数的名字就是组件名。为了和其他普通函数区分,组件的名称 遵循 大坨峰命名法(PascalCase)。
// 定义组件
function Greeting(){
return <div className="greeting"><h3>大家,下午好。</h3></div>
}
// 组件的渲染
let element = <Greeting />; // 将组件 => jsx(React元素)
ReactDOM.createRoot(document.querySelector('#root')).render(element);
所有函数定义的组件 也都会接收一个唯一参数,即props。和Vue中一样,都是单向数据流。推荐数据是从父组件向其后代流动。
本质上 props 是一个 object对象。存储父组件给绑定的所有属性集合。
function Parent(props) {
console.log('parent', props);
return (
<div
className="parent"
style={{ border: '1px solid red', padding: '10px' }}
>
<p>谁是爹》</p>
<Child name="tom" age={18} />
</div>
);
}
function Child(props) {
console.log('child', props);
return (
<div
style={{ border: '1px solid green', padding: '10px' }}
className="parent"
>
这是儿子,它叫 {props.name}。今年 {props.age} 岁。
</div>
);
}
let element = <Parent />; // 将组件 => jsx(React元素)
ReactDOM.createRoot(document.querySelector('#root')).render(element);
props 是 组件与外部数据交互的接口。同时还是响应式,即当props中数据发生变化后,组件会及时重新渲染。
==注意:props 是 "只读的"!,当在使用props的时候,虽然语法上是可以修改其属性,但是React会检查该操作,一旦发现就会抛出一个类型异常: Uncaught TypeError: Cannot assign to read only property 'age' of object。==
state 是 组件内部自己拥有的状态数据。不同于 props ,props 是 只读的,但是 state 是可以修改的。同时state变化后,组件会重新渲染。
如果想要组件内部拥有状态state,在不使用hooks时,必须通过es6中的类来定义组件,这样在类组件渲染时,会创建组件实例。通过组件实例的state属性 去存储 所有内部的状态(state类型为object对象)。
其中,类名就是组件名,也遵循PascalCase命名法。
// 定义类Greeting 继承自 React.Component
// 此时,即为Greeting 组件
class Greeting extends React.Component {
// 定义render实例方法
render() {
return (
<div>
<h3>你好,兄弟!</h3>
</div>
);
}
}
上述代码要求:
React.Component
才会是组件接下来,看一个计数器案例:
class Counter extends React.Component {
// 添加状态
state = {
name: '计数器',
count: 0, // 定义一个计数的状态值
};
reduceOne() {
// console.log(this);
// 类Vue,如果直接修改state可以吗?
// this.state.count--;// wrong
// 正确做法
this.setState({
count: this.state.count - 1,
});
}
addOne = () => {
// console.log(this);
this.setState({
count: this.state.count + 1,
});
};
render() {
// 1 在类组件中 通过this先拿到组件实例,然后通过访问state属性得到所有的状态。
return (
<div>
<h3>{this.state.name}</h3>
<p>当前值:{this.state.count}</p>
<p>
<button onClick={this.reduceOne.bind(this)}>-</button>
<button onClick={this.addOne}>+</button>
</p>
</div>
);
}
}
上面代码小结:
this
先拿到组件实例;再访问state属性获取到所有的状态。setState方法
才可以。setState(newState | (prevState, props) => newState)
该方法在修改状态时,会将旧的state与setState传入新的state合并后产生的新状态对象 替换 旧状态对象的方式来实现响应式的。因此在调用setState时,要么直接传入一个新的状态对象,或者传入一个函数,该回调函数返回一个新state对象。使用React对应脚手架工具==Creat-React-App(CRA)==来搭建一个React单页应用程序的开发环境。
npx create-react-app first-react-demo
该项目中的package.json
文件中包含以下几个NPM脚本:
npm start:Starts the development server.
npm run build:Bundles the app into static files for production.
npm test:Starts the test runner.
npm run eject:Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back!
分析实现过程:
基于组件系统。我们去实现一个时钟组件--Clock。
Clock组件 是 函数组件 亦或是 类组件?
该组件需要一个当前时间的数据。并且该数据会每隔一秒发生变化。因此该数据是变化的,可能就不是props。这样就确定Clock是一个类组件。
业务上来看,时钟就应该包含有一个自身需要维护的时间数据!
实现Clock组件的静态版本
添加state数据,
开启一个定时器,每隔一秒后修改state。此时需要ComponentDidMount生命周期钩子
在组件卸载前清除定时器
import { Component } from 'react';
import './Clock.css';
class Clock extends Component {
// 添加state
state = {
now: new Date(),
};
// 组件挂载后,开启定时器
componentDidMount() {
this.timer = setInterval(() => {
this.setState({
now: new Date(),
});
}, 1000);
}
// 组件更新后
componentDidUpdate() {
console.log(new Date());
}
// 组件卸载前
componentWillUnmount() {
clearInterval(this.timer);
}
// 必须实现 render. 也是生命周期钩子;对应constructor也是
render() {
return (
<div className="clock">
<span>当前时间为: {this.state.now.toLocaleString()}</span>
</div>
);
}
}
export default Clock;
不同于Vue,React没有指令,需要开发者自己手动通过原生JS来实现。
实现时通常会用到下面语句或者表达式:
if
语句 或者 switch
语句?:
三目运算符&&
逻辑与
// props.user = {name: 'daxia'}
export function Greeting({ user }) {
//! 1 if语句的实现
// let element; // 元素变量:根据不同条件存储不同的jsx元素
// if (user && user.name) {
// element = <h3>你好,{user.name}。</h3>;
// } else {
// element = <h3>你好,访客。</h3>;
// }
// return element;
//! 2 ?:三目运算符
// return user && user.name ? (
// <h3>你好,{user.name}。</h3>
// ) : (
// <h3>你好,访客。</h3>
// );
//! 3 逻辑与&&
let element = <h3>你好,访客。</h3>;
user?.name && (element = <h3>你好,{user.name}。</h3>);
return element;
}
Array.forEach()
function SomeLoop() {
var str = '';
var arr = [1, 2, 3];
// var i = 0;
// var l = arr.length;
// while (i < l) {
// str += arr[i] + ',';
// i++;
// }
// for (var i = 0, l = arr.length; i < l; i++) {
// str += arr[i] + ',';
// }
//! 数组的forEach方法 可以遍历数组每一个元素,但是无法终止(除非是自然停止)
//! 当每遍历到一个元素时,都会执行传入的回调函数,并且将当前元素值,索引值,以及遍历的数组对象作为参数传入进去。
arr.forEach((v, i, arr) => {
str += v + ',';
});
return <div>Some Loop Case@@{str.slice(0, str.length - 1)}</div>;
}
export default SomeLoop;
map()
方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。
const array1 = [1, 4, 9, 16];
// Pass a function to map
const map1 = array1.map((x) => x * 2);
console.log(map1);
在React中没有指令,只能开发者自己使用数组的forEach或者map或者for循环等方式来实现列表的渲染。
class ListRender extends Component {
state = {
list: [1, 2, 3, 4, 5, 6],
};
renderList() {
let result = [];
let list = this.state.list;
// var i = 0;
// var l = list.length;
// for (; i < l; i++) {
// result.push(<li>{list[i]}</li>);
// }
list.forEach((d, i) => {
result.push(<li key={i}>{d}</li>);
});
return result;
}
render() {
return (
<ul>
{/* <li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li> */}
{/* 数组对象 它是有效的react元素 */}
{/* <li>{this.state.list}</li> */}
{/* jsx 本质 就是js对象。数组也可以存储任意js对象 */}
{/* {[<li>1</li>, <li>2</li>, <li>3</li>]} */}
{/* 将原数据数组 变成包含 jsx元素的数组 */}
{/* {this.state.list.map((d) => (
<li>{d}</li>
))} */}
{/* 普通循环构建 */}
{this.renderList()}
</ul>
);
}
}
Hook 译为 『钩子』。因此 Hooks 就是 钩住 react 特性的意思。
在 React16.8 之后引入Hooks。可以使函数组件 像类组件那样 拥有自身状态以及生命周期等特性。
本质上,这些Hook都是具有一定功能特性的函数。
useState
is a React Hook that lets you add a state variable to your component.useState 是一个 React Hook,可以让开发者在函数组件中添加状态变量。
const [state, setState] = useState(initialState);
调用useState时,需要传入一个 初始状态值。返回 一个 数组值。数组中第一个元素为状态变量;第二个元素是修改状态变量的函数。当该函数调用时会修改状态,同时会更新组件。
import { useState } from 'react';
function TestUseState() {
let [name, setName] = useState('daxia'); // 姓名
// hooks都可以多次调用
let [age] = useState(18); // 年龄
return (
<div>
<h3>你好,{name}。这小子今年 {age} 岁。</h3>
<button
onClick={() => {
setName('xiaoxia');
}}
>
修改name
</button>
</div>
);
}
export default TestUseState;
在useState传入的参数initialState
可以 是一个 函数,返回初始状态值。
比如,在需要一定计算量才能得出某状态变量的初始值时,可以将一个函数传入给 initialState
。
同时,要注意修改状态的函数 与 类组件的setState方法 实现不完全一样。该修改函数 会 直接将新状态 替换 旧的状态。而不是像setState那样 先 合并旧状态 在替换。
因此在对象类型的状态修改时,要小心丢失某些状态对象 的 属性。
function TestUseState() {
let [user, setUser] = useState({
name: 'daxia',
age: 18,
});
return (
<div>
<h3>
你好,{user.name}。这小子今年 {user.age} 岁。
</h3>
<button
onClick={() => {
// 此处修改 会丢失 age 属性
setUser({ name: '小霞' });
}}
>
修改name
</button>
</div>
);
}
另外,所有状态变量的修改函数 在修改状态时可能是异步的。因此在修改函数中 直接获取状态 用作上一次状态的值 可能是错误的
import { useState } from 'react';
export default function Counter() {
let [count, setCount] = useState(0);
function addOne() {
setCount(count + 1);
}
function reduceOne() {
setCount(count - 1);
}
return (
<div>
<h3>计数器</h3>
<p>值为:{count}</p>
<p>
<button onClick={addOne}>+</button>
<button onClick={reduceOne}>-</button>
<button
onClick={() => {
// setCount 这些修改函数 可能是异步的。
// 因此下面代码不能将count设置为 3
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}}
>
+3
</button>
</p>
</div>
);
}
上面 +3 按钮会失效。因此在连续调用setCount时参数count每一次都是0,也就是说 你调用三次setCount(1)
。
为了 能够在异步更新状态时准确获取到上一次状态,可以给所有修改函数传入一个 函数类型值,它可以接收两个参数:上一个状态值preState,以及组件属性props,然后该函数返回新状态。
上面代码中,将setCount调用方式改为如下即可:
setCount((preCount) => preCount + 1);
setCount((preCount) => preCount + 1);
setCount((preCount) => preCount + 1);
小结
Effect 译为 『副作用』。
useEffect hook 就是在函数组件中执行副作用(和渲染无关)。
useEffect是一个React Hook,可以让你将一个组件与外部系统同步。
其本质,就是用来实现一些生命周期的钩子。
useEffect(setup, dependencies?)
那么,useEffect 能帮助开发者实现那些生命周期钩子的效果呢?
每一次组件渲染时,都希望执行一些副作用
useEffect(() => {
// 编写一些副作用代码。注意: 别随意修改状态
// 组件每次渲染 都会执行
})
仅当组件首次渲染后执行一些副作用
useEffect(() => {
// 仅会在组件首次渲染后执行一次,可以在这里发送http请求
// 模拟的就是componentDidMount钩子
console.log('===组件渲染后===');
}, []);
在组件卸载前执行一些副作用
useEffect(() => {
// 返回一个回调函数
return () => {
// 这里返回的回调 会在组件卸载前执行
// 可以在这里清除一些『垃圾』
console.log('MsgItem卸载了');
};
}, []);
监听state或者props,只要有一发生变化就会执行一些副作用
// 当组件首次渲染后 以及 title发生变化时 会执行
useEffect(() => {
console.log('title发生变化');
}, [title]);
useMemo
is a React Hook that lets you cache the result of a calculation between re-renders.> const cachedValue = useMemo(calculateValue, dependencies) > ``` **参数** - calculateValue:函数类型,返回一个想要缓存的值。React会在组件首次渲染时调用此函数或者当依赖发生变化时,其他情况都会读取缓存值。 - dependencies:计算缓存值所依赖的state或者props。 **返回值** 在最初的渲染中,useMemo返回调用calculateValue的结果。 在接下来的渲染过程中,它要么返回上次渲染时已经存储的值(如果依赖关系没有改变),要么再次调用calculateValue,并返回calculateValue已经返回的结果。
jsx function TestUseMemo() { let [c, setC] = useState(10);
let doubleC = useMemo(() => c * 2, [c]);
return (
<div>
<p>{c}</p>
<p>{doubleC}</p>
</div>
); }
export default TestUseMemo;
**小结**
1. useMemo 的使用 是为了提高性能的
2. 如果 依赖 频繁发生变化的话,useMemo 其实并不能提升多少性能,甚至可能还会损耗性能
3. 可以在 需要一定计算量的情况下 使用useMemo
4. 可以在 父子组件 数据传递中,通过 useMemo 去缓存给子组件传递的数据
### 8.4 useCallback
> 类似与 useMemo,useCallback也是用来缓存的。但是useCallback 只能缓存一个函数。
js import { useCallback, useEffect, useState } from 'react';
// props 包含children,存储后代React元素 function MyButton({ children, onClick }) { useEffect(() => {
console.log('my button');
}); return {children}; }
function TestUseCallback() { let [msg, setMsg] = useState('');
function handleClick() {
setMsg('heheda');
}
return (
<div>
<p>{msg}</p>
<MyButton onClick={handleClick}>按钮文字</MyButton>
</div>
); }
export default TestUseCallback;
上面代码,父组件TestUseCallback只要重新渲染,handleClick函数就会重建,那么会导致两次MyButton组件绑定的onClick属性不一致,进而出现多次非必要的重新渲染逻辑。
因此,在组件TestUseCallback多次重新渲染中,MyButton组件是不必一起重新渲染的。那么需要将handleClick函数缓存起来即可。
jsx import { useCallback, useEffect, useState } from 'react';
// props 包含children,存储后代React元素 function MyButton({ children, onClick }) { useEffect(() => {
console.log('my button');
}); return {children}; }
function TestUseCallback() { let [msg, setMsg] = useState('');
// 当然这里也可以使用useMemo来缓存。 const handleClick = useCallback(() => {
setMsg('heheihei');
}, []);
return (
<div>
<p>{msg}</p>
<MyButton onClick={handleClick}>按钮文字</MyButton>
</div>
); }
export default TestUseCallback;
## 9. refs & dom
> refs 是在组件中 连接 HTML元素的一种推荐方式。
`useRef` 是一个 React Hook,它能让你引用一个不需要渲染的值。
const ref = useRef(initialValue)
#### 参数
- `initialValue`:ref 对象的 `current` 属性的初始值。可以是任意类型的值。这个参数会首次渲染后被忽略。
#### 返回值
`useRef` 返回一个只有一个属性的对象:
- `current`:最初,它被设置为你传递的 `initialValue`。之后你可以把它设置为其他值。如果你把 ref 对象作为一个 JSX 节点的 `ref` 属性传递给 React,React 将为它设置 `current` 属性。
在后续的渲染中,`useRef` 将返回同一个对象。
下面案例,是用ref对象保存定时器id:
jsx import { useEffect, useState, useRef } from 'react';
// let timer;
function Clock({ title }) { let [now, updateNow] = useState(new Date());
let timer = useRef(undefined);
useEffect(() => {
// 启动定时
timer.current = setInterval(() => {
updateNow(new Date());
}, 1000);
// 返回函数,在组件卸载前清除定时器
return () => {
// console.log(timer);
clearInterval(timer.current);
};
}, []);
useEffect(() => {
console.log(timer.current);
}, [title]);
return (
<div>
{title || '时间为'} : {now.toLocaleString()}
</div>
); }
export default Clock;
下面案例 是 用ref对象 保存DOM对象
jsx import { useCallback, useRef } from 'react';
function RefsDOM() { const handleUpload = useCallback(() => {
console.log('你要上传的文件是:', inputRef.current?.value);
}, []);
let inputRef = useRef(null);
return (
<div>
<input type="file" ref={inputRef} />
<button onClick={handleUpload}>上传</button>
</div>
); }
export default RefsDOM;
`forwardRef` lets your component expose a DOM node to parent component with a [ref.](https://react.docschina.org/learn/manipulating-the-dom-with-refs)
jsx const SomeComponent = forwardRef(render)
`useImperativeHandle` 是 React 中的一个 Hook,它能让你自定义由 [ref](https://react.docschina.org/learn/manipulating-the-dom-with-refs) 暴露出来的句柄。
useImperativeHandle(ref, createHandle, dependencies?)
jsx import { forwardRef, useImperativeHandle, useRef } from 'react';
function FileInput(props, ref) { let inputRef = useRef(); useImperativeHandle(
ref,
() => {
// 返回值 是给父组件指定暴露内容
return {
getFile() {
return inputRef.current?.value;
},
start() {},
stop() {},
};
},
[]
); return ; }
export default forwardRef(FileInput); // 父组件 import FileInput from './FileInput'; import { useRef } from 'react';
export default function TestForwardRef() { let fileRef = useRef(null);
return (
<div>
<FileInput ref={fileRef} />
<button
onClick={() => {
console.log(fileRef.current.getFile());
// console.log(fileRef.current?.value);
}}
>
上传
</button>
</div>
); } ```