JavaScript 的语法扩展。在 JS 中使用类似 html 的结构。JSX 代码本身不能被浏览器读取,必须使用 Babel 和 webpack 等工具将其转换为传统的 JS。JSX 中可以嵌入 JS 表达式,用 { }
包裹,字符串插值 `${ }` 。
const element = <h1>Hello, world!</h1>; // 字符串插值 `Hello, ${str}`
React 通过 props 实现父子组件之间的通讯。由于 React 采用单向数据流的模式,所以子组件只能读取从父组件传递的 props 的值,而不能直接更改。所以说 props 是不可变的,子组件要想改变数据,就需要父组件传递新的 props 对象。
// 除了可以传递 props 给子组件之外(Avatar),还可以将 JSX 作为子组件传递(Card)
// 可以将带有 children prop 的组件看作有一个“洞”,可以由其父组件使用任意 JSX 来“填充”
const Card = ({ children }) => {
return (
<div className="card">
{children}
</div>
);
}
const Avatar = ({size}) => {
return (
<img
width={size}
height={size}
src=""
/>
)
}
export default function Profile() {
return (
<Card>
<Avatar size={100} />
</Card>
);
}
React 通过 state 来实现响应式页面,当 state 的值发生变化时,React 会自动重新渲染组件,并更新与该 state 相关的部分。
由 React 元素构成的轻量级 JavaScript 对象。当组件的 state 或 props 发生变化时,React 会重新渲染组件,生成新的虚拟 DOM树,通过比较前后两个虚拟 DOM 树的差异,React 会确定需要更新的部分,生成一系列更新操作指令,应用于实际的 DOM 元素。
Hook 是 React16.8 新增的特性,在此之前,只有类组件可以使用 state 管理状态,Hook 使函数组件可以使用 state 和其他 React 特性。
Hook 只能在函数顶层或者自定义 Hook 中调用。
https://react.docschina.org/learn/state-as-a-snapshot
import React, { useState } from 'react';
export default () => {
const [count, setcount] = useState(0);
const increase = () => setCount(count + 1);
return (
<Button onClick={increase}>{count}</Button>
)
}
useState 返回两个值,[ count, setcount ] 其实是数组的解构赋值。
setCount 只能在事件处理函数(onChange 等)和生命周期方法(useEffect)中调用。
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。 设置 state 并不会改变现有的 state 值,而是会触发重新渲染,重新渲染后的 state 值是计算后的结果。
const increase = () => {
setCount(count + 1);
setCount(count + 2);
setCount(count + 1); // 页面展示 1,由于批量更新,最后只会执行最后一条setCount,使count加一
console.log(count); // 后台输出 0,count的值在一次渲染中始终固定为0
}
const increase = () => {
setCount(count + 2);
setCount(n => n + 1); // 前端展示为 3
}
注意比较上下两段代码的区别,搞明白它们的展示结果为什么不同。
const increase = () => {
setCount(n => n + 1);
setCount(n + 2); // 前端展示为 2
}
更改 state 中的对象,需要拷贝创建一个新的对象并把它传给 state 的设置函数。
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
const updateCity = (e) => {
setPerson({
...person,
artwork: {
...person.artwork,
city: e.target.value,
}
})
}
除此之外,还可以使用 Immer 更加简洁的更新。
npm install use-immer
import { useImmer } from 'use-immer'
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
const updateCity = (e) => {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
const [ products, setProducts ] = useState([
{ id: 0, name: 'Baklava', count: 1},
{ id: 1, name: 'Cheese', count: 5},
{id: 2, name: 'Spaghetti', count: 2},
])
const increase = (productId) => {
const newList = products.map(product => {
if (product.id === productId){
return { ...product, count: product.count + 1}
} else {
return product
}
})
setProducts(newList);
}
避免使用 (会改变原始数组) | 推荐使用 (会返回一个新数组) | |
---|---|---|
添加元素 | push,unshift | concat,[…arr] 展开语法 |
删除元素 | pop,shift,splice | filter,slice |
替换元素 | splice,arr[i] = … 赋值 | map |
排序 | reverse,sort | 先将数组复制一份 |
import React, { useEffect } from 'react';
useEffect(() => {
setup();
return () => {
cleanup();
};
}, [dependencies?]); // 如果不传递依赖项数组,Effect会在组件每次重新渲染后运行
/*
1. 组件挂载到页面时运行 setup 代码
2. 依赖项dependencies发生变化触发重新渲染
首先,使用旧的props和state运行cleanup代码
然后使用新的props和state运行setup代码
3. 组件从页面卸载后,最后运行一次 cleanup 代码
*/
import React, { useState, useRef } from 'react';
export default function Stopwatch = () => {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null); // 由此计算的secondsPassed需要在浏览器渲染,所以使用useState
const intervalRef = useRef(null); // intervalID只在事件处理中使用,不需要渲染,所以使用useRef
const handleStart = () => {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => { // 可以修改current的值
setNow(Date.now());
}, 10);
}
const handleStop = () => {
clearInterval(intervalRef.current);
}
// secondsPassed 可以根据现有的state计算得出,所以不需要使用useEffect,而是在渲染的过程中直接计算
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
开始
</button>
<button onClick={handleStop}>
停止
</button>
</>
);
}
import React, { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus(); // 可以使用任意浏览器 API
}
return (
<>
<input ref={inputRef} /> // 传递ref使React将ref的current属性设置为相应的DOM节点()
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
import React, { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
import { forwardRef, useRef } from 'React';
const MyInput = forwardRef((props, ref) => {
const handleSearch = () => {
// ...
}
useImperativeHandle(ref, ()=> {
handleSearch,
});
return <input onchange={handleSearch}/>; // 注意,ref 已不再被转发到 中
});
export default function Form() {
const inputRef = useRef(null);
return (
<>
<MyInput ref={inputRef} />
<button onClick={inputRef.current.handleSearch()}>
搜索
</button>
</>
);
}
通常来说,父组件通过 props 向子组件传递数据。但是,如果必须通过很多的中间组件向下传递props,会使代码变得十分冗长,这时可以使用 useContext。
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null); // 1. 创建context
export default function MyApp() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}> // 2. 把子组件用 context provider 包裹起来,以提供ThemeContext给它们,如果任意子组件中请求ThemeContext,就给它们theme的值
<SignUp />
<Checkbox
checked={theme === 'dark'}
onChange={(e) => {
setTheme(e.target.checked ? 'dark' : 'light')
}}
>暗黑模式</Checkbox>
</ThemeContext.Provider>
)
}
const SignUp = ({ children }) => {
return (
<Panel title="Welcome">
<Btn>注册</Btn>
<Btn>登录</Btn>
</Panel>
);
}
const Panel = ({ title, children }) => {
const theme = useContext(ThemeContext); // 3. 使用context,使用上层最近的传递过来的值
const className = 'panel-' + theme;
return (
<div className={className}>
<h1>{title}</h1>
{children}
</div>
)
}
const Btn = ({ children }) => {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
<Button className={className}>
{children}
</Button>
);
}
import { useMemo } from 'React';
const TodoList = ({todos, theme, tab }) => {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
return (
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}> { todo.text } </li>
))}
</ul>
)
}
React 首次渲染时会调用 filterTodos 函数,在之后的渲染中,如果依赖列表没有发生变化,React 将直接返回相同值。否则将再次调用 filterTodos 函数返回最新结果,然后缓存该结果以便下次重复使用。
import React, { memo, useMemo } from 'react';
const List = memo(function List({ items }) {
// ...
});
export default function TodoList({todos, theme, tab}){
const visibleTodos = useMemo( // 此处需要使用useMemo是因为引用数据类型每次计算都会得到一个不用的数组,即使子组件使用memo包裹也会重新渲染
() => filterTodos(todos, tab),
[todos, tab]
);
return (
<List items={visibleTodos} />
)
}
useCallback 和 useMemo 的用法类似,也是一个性能优化的 hook。不同之处在于,useMemo缓存函数调用的结果,而 useCallback 缓存函数本身。
在父组件将函数作为 props 传递给子组件的过程中,父组件每次重复渲染都会生成一个不同的函数(引用数据类型,引用地址变动),即使子组件使用 memo 包裹,也会重复渲染。这时就需要 useCallback 和 memo 配合来使依赖项不变时跳过此次渲染。
import React, { useCallbck, memo } from 'react';
const ShippingForm = memo(function ShippingForm(onSubmit){
// ...
});
export default function ProductPage({ productId, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
orderDetails,
});
}, [productId]);
return (
<ShippingForm onSubmit={handleSubmit} />
)
使用 useCallback 时,如果没有将依赖数组指定为第二个参数,那么每次渲染都会返回一个新的函数。依赖数组为空数组时,会在组件首次渲染时创建一次函数,之后重复使用。