React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。架构模式基于 MVC, UI 系统基于 JSX
🌄 安装
1、外部引入
//部署时,将 "development.js" 替换为 "production.min.js"
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
//引入 jsx(不适用于生产环境)
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
//使用 jsx(不适用于生产环境)
<script type="text/babel">
ReactDOM.render(<h1>Hello, world!</h1>,document.getElementById('root'));
</script>
- React:React顶级 API
- React DOM:添加 DOM 特定的方法
- Babel:一种JavaScript编译器,可让我们在开发环境旧版浏览器中使用ES6 +
2、CLI
- npx安装cra:
npx create-react-app my-app
=>cd my-app
=>npm start
- yarn 安装 typescript+cra:
yarn create react-app my-app --template typescript
=>cd my-app
=>yarn start
- 安装storybook:
- 在cra基础上安装:
yarn add -D @storybook/preset-create-react-app
- 安装story:
npx -p @storybook/cli sb init --type react_scripts
- 在cra基础上安装:
🔰 JSX 及 React 元素
1、JSX
JSX 是 JavaScript 的语法扩展,描述 UI 呈现出它应有交互的本质形式。可拆分为多行,内容包裹在括号中。
- JSX 表示对象:Babel 会把 JSX 转译成
React.createElement(component, props, ...children)
函数调用,创建对象被称为 “React 元素”。 - JSX 可防止注入攻击;JSX 可嵌入表达式
- JSX 是一个表达式:if 语句、for 循环不是表达式不能在 JSX 中直接使用。可使用 conditional (三元运算) 表达式来替代
1.1 JSX 类型
- 使用点语法:在一个模块中导出许多 React 组件时,使用点语法来引用
import React from "react";
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
};
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
- 运行时选择类型:通过通用表达式来(动态)决定元素类型,需将类型赋值给一个大写字母开头的变量。根据 prop 来渲染不同组件:
import React from "react";
import { PhotoStory, VideoStory } from "./stories";
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
1.2 JSX 特定属性
使用 camelCase(小驼峰命名)来定义 props 属性的名称,Props 默认值为 “True”
- 字符串字面量:
const element = <div tabIndex="0"></div>;
- JS 表达式:
const element = <img src={user.avatarUrl}></img>;
- 属性展开:易将不必要的 props 传递给不相关的组件。建议谨慎使用
const Button = props => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button className={className} {...other} />;
};
1.3 JSX 中的子元素
- 字符串字面量:
<Component>Hello world!</Component>
- JSX 子元素:可与字符串字面量同使用。组件能返回存储在数组中的一组元素
- JS 表达式:
{todos.map(msg => (<Item key={msg} msg={msg} />))}
- 函数:
{index => <div key={index}>This is item {index} </div>}
- true、false、null、undefined 是合法的子元素但不会被渲染:
//确保 && 之前的表达式总是布尔值:
<div>
{props.messages.length > 0 && <MessageList messages={props.messages} />}
</div>
2、 React 元素
React 元素是不可变对象,代表某个特定时刻的 UI。React 只更新它需要更新的部分
ReactDOM.render(<Clock />, document.getElementById("root"));
🍎 组件及 Props、State
1、基本概念
组件 | Vue | React |
---|---|---|
定义 | 可复用的 Vue 实例 | Props 作为参数返回 React 元素的 JavaScript 函数 |
命名 | 推荐使用 kebab-case | 必须以大写字母开头 |
VirtualDOM 挂载 | 实例 render 函数创建虚拟节点 VNode,如render: h => h(App) ,el 选项或 vm.$mount() 再将其挂载在 DOM 节点 |
函数 render 方法返回 React 元素,再由 ReactDOM 的 render 方法将其挂载到 DOM 节点 |
2、组件分类
2.1 class 组件
- 定义:作为 React.Component 子类,通过 props 从父组件向子组件传递数据
- 渲染方法:render()方法是类组件中唯一需要的方法,用于渲染DOM节点
- 构造函数:初始化 state 或进行方法绑定,一定需要调用 super(props)
- 通过给
this.state
赋值对象来初始化内部 state。 - 为事件处理函数绑定实例
- 通过给
- 状态维护:state 用于组件状态维护,被视为一个组件的私有属性
- 每次定义子类的构造函数时,都需要调用 super 方法。即 super(props) 开头
- 每次在组件中调用 setState 时,React 都会自动更新其子组件
- 状态提升:需共享 state 向上移动到最近共同父组件中的 state 中用作“数据源”,即“状态提升”。任何可变数据应当只有一个相对应的唯一“数据源”。
- 受控组件:由 React 控制并且所有的表单数据统一存放的组件。响应数据改变时,子组件调用 this.props.onChange() 而非 this.setState()。
- 不可变性:不直接修改原数据/底层数据以便跟踪数据的改变,确定在 React 中何时重新渲染
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => this.props.onClick()}>
{this.props.value}
</button>
);
}
}
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null)
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = "X";
this.setState({ squares: squares });
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
return (
<div>
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
);
}
}
2.2 函数组件
- 定义:不需定义一个继承于 React.Component 的类,可定义一个接收 props 作为参数的函数,然后返回需要渲染的元素
//把两个 this.props 都替换成了 props,注意两侧括号不再有
function Square(props){
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
)
}
3、Props 的只读性
组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。所有 React 组件都必须像纯函数一样保护其 props 不被更改。但状态 state 允许组件随时间更改其输出以响应用户操作,网络响应以及其他任何情况。
4、State
state 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
- 不要直接修改 State,应该拷贝一份再使用 setState()
- this.props 和 this.state 可能会异步更新,不要依赖他的值来更新下一个状态。可以让 setState() 接收一个函数而不是一个对象
- state 的更新会被合并。调用 setState() 的时候,React 会把你提供的对象合并到当前的 state
- 自上而下数据流:任何状态始终归某个特定组件所有,且从该状态派生的任何数据或UI都只影响树中“下方”的组件。
- 如果某些数据可以由 props 或 state 推导得出,那么它就不应该存在于 state 中
5、生命周期
😎 事件处理
1、与原生区别
- 使用小驼峰:onClick={handleClick}、onChange等等…
- 只能显示阻止默认行为:handleClick(e){ e.preventDefault();}
- 注意:在JavaScript中,默认情况下不绑定类方法。如果忘记绑定 this.handleClick 并将其传递给onClick,则在实际调用该函数时将无法定义
- 通常,如果引用的方法后面没有(),例如onClick = {this.handleClick},则应绑定该方法
- 箭头函数绑定:onClick={() => this.handleClick()}
- 构造函数绑定:this.handleClick = this.handleClick.bind(this);
- 在大多数情况下,箭头函数很好。但如果将此回调作为对较低组件的支持传递,则这些组件可能会进行额外的重新渲染。建议在构造函数中绑定或使用类字段语法,以避免此类性能问题。
- 通常,如果引用的方法后面没有(),例如onClick = {this.handleClick},则应绑定该方法
2、TypeScript 分类事件
- 合成事件:MouseEvent、KeyboardEvent…
🌮 条件渲染
1、变量存储组件
2、与运算符
true && expression 总返回 expression,而 false && expression 总返回 false。
3、三目运算符
三目运算符condition ? true : false
可用于较复杂的表达式。若条件变得过于复杂,应考虑如何提取组件
4、阻止组件渲染
需要隐藏组件,render 方法中直接返回 null,不影响组件的生命周期,如 componentDidUpdate仍将被调用。
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
💥 列表组件及 Key
1、列表组件
map 方法把数组转化为元素列表。在数组 map() 方法中的元素需要设置 key 属性,以便识别哪些元素被改变。
2、key属性
key属性必须保证其在同级中 (siblings) 是唯一的,但不要求全局唯一
- 索引作为键:深入了解使用索引作为键的负面影响。案例:使用key=id、使用key=index
- 使用场景:当以下所有这些都满足时,您以安全地将索引用作键
- 列表和项目是静态的——它们不会被计算并且不会更改;
- 列表中的项目没有ID;
- 该列表永远不会重新排序或过滤
- 不建议使用场景:项目的顺序可能更改,不建议使用
- 使用场景:当以下所有这些都满足时,您以安全地将索引用作键
录入ID 属性:
将抽象中的编号向上移动一级。使用全局索引可确保任何两个项目具有不同的ID
todoCounter = 1;
function createNewTodo(text) {
return {
completed: false,
id: todoCounter++,
text
}
}
生产解决方案建议使用shortid。它可以快速生成“简短的,非顺序的,URL友好的唯一” ID
var shortid = require('shortid');
function createNewTodo(text) {
return {
completed: false,
id: shortid.generate(),
text
}
}
传递给子组件:key 只是React的 Hint,不会传递给您的组件。如果在组件中需要相同的值,则将其作为 props以不同的名称显式传递。如
key={id} id={id}
,使用 props.id 而不是 props.key
🎭 表单
React 中,表单状态通常保存在组件的 state 属性中,且只能通过 setState() 来更新
1、受控组件
state“唯一数据源”。被 React 控制取值的表单输入元素叫做“受控组件”。input等value属性始终由 state 驱动
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
}
handleChange=(event)=>{
this.setState({ value: event.target.value });
}
handleSubmit=(event)=>{
alert("提交的名字: " + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>名字:
<input type="text" value={this.state.value}
onChange={this.handleChange}
/>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
2、textarea 及 select 标签
React 使用 value 属性而非 selected 属性,可传入数组以支持多选。如<select multiple={true} value={['B', 'C']}>
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = { value: "coconut" };
}
handleChange=(event)=> {
this.setState({ value: event.target.value });
}
handleSubmit=(event)=> {
alert("你喜欢的风味是: " + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
3、文件 input 标签
<input type=“file” />
允许用户从存储设备中选择多个文件上传到服务器。非受控组件,value 只读
4、处理多个输入
每个元素添加 name 属性,根据
**event.target.name**
的值选择要执行的操作。 ```jsx class Reservation extends React.Component { constructor(props) { super(props); this.state = {isGoing: true,
numberOfGuests: 2
}; }
handleInputChange=(event)=> { const target = event.target; const value = target.type === “checkbox” ? target.checked : target.value; const name = target.name; this.setState({
[name]: value
});
}
render() { return (
); } }
- 计算属性:
```javascript
let param = 'size'
let config = {
[param]: 12,
['mobile' + param.charAt(0).toUpperCase() + param.slice(1)]: 4
}
console.log(config) // {size: 12, mobileSize: 4}
😺 组合与继承
1、包含关系
- 使用特殊的 children prop 来将子组件传递到渲染结果中预留位置。
```javascript
function FancyBorder(props) {
return (
); }
{props.children}
function WelcomeDialog() {
return (
Welcome
Thank you for visiting our spacecraft!
); }
- 可不使用 children,自行约定,同“槽”slot
```javascript
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
2、特例关系
组件看作是其他组件的特殊实例。“特殊”组件可以通过 props 定制并渲染“一般”组件:
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">{props.title}</h1>
<p className="Dialog-message">{props.message}</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.state = { login: "" };
}
handleChange=(e)=> {this.setState({ login: e.target.value });}
handleSignUp=()=> {alert(`Welcome aboard, ${this.state.login}!`);}
render() {
return (
<Dialog
title="Mars Exploration Program"
message="How should we refer to you?"
>
<input value={this.state.login} onChange={this.handleChange} />
<button onClick={this.handleSignUp}>Sign Me Up!</button>
</Dialog>
);
}
}
☂ 其他
1、代码分割
1.1 React.lazy
用于处理动态引入的组件。Suspense 组件可包裹多个 lazy 组件并置于之上任何位置,fallback 属性接受组件加载过程中任何可展示的 React 元素
const OtherComponent = React.lazy(() => import("./OtherComponent"));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
1.2 异常捕获边界
模块加载失败(如网络问题)可通过异常捕获边界(Error boundaries)技术来处理,以显示良好的用户体验并管理恢复事宜
import MyErrorBoundary from "./MyErrorBoundary";
const OtherComponent = React.lazy(() => import("./OtherComponent"));
const AnotherComponent = React.lazy(() => import("./AnotherComponent"));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section><OtherComponent /><AnotherComponent /></section>
</Suspense>
</MyErrorBoundary>
</div>
);
错误边界作为组件,可捕获并打印发生在其子组件树任何位置的 JS 错误,且渲染出备用 UI。在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
- 当抛出错误后,使用
static getDerivedStateFromError()
渲染备用 UI ,使用componentDidCatch()
打印错误信息 错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
1.3 基于路由的代码分割
使用 React.lazy 和 React Router 这类的第三方库,来配置基于路由的代码分割 ```javascript import { BrowserRouter as Router, Route, Switch } from “react-router-dom”; import React, { Suspense, lazy } from “react”;
const Home = lazy(() => import(“./routes/Home”)); const About = lazy(() => import(“./routes/About”));
const App = () => (
<a name="37d994ee"></a>
### 2、Refs 及其转发
> Refs 提供一种方式,允许访问 DOM 节点或在 render 方法中创建的 React 元素
> Ref 转发是将 ref 自动地通过组件传递到其一子组件,常用于可重用的组件库
Ref 转发是可选特性,其允许某些组件接收 ref,并将其向下传递给子组件。FancyButton 使用 `React.forwardRef` 来获取传递给它的 ref,然后转发到它渲染的 DOM button
- 调用 React.createRef 创建一个 React ref 并将其赋值给 ref 变量。
- 指定 ref 为 JSX 属性,将其向下传递给 `<FancyButton ref={ref}>`。
- React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
- 向下转发该 ref 参数到 `<button ref={ref} />`,将其指定为 JSX 属性。
- ref 挂载完成,ref.current 将指向 `<button>` DOM 节点
```javascript
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
3、Fragments
React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。类似 Vue 中的 slot
- 短语法
<> </>
也可声明 Fragments,但不支持 key 或属性 - 带 key 的 Fragments:key 是唯一可以传递给 Fragment 的属性
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// 没有`key`,React 会发出一个关键警告
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
4、高阶组件
高阶组件(HOC)是参数为组件,返回值为新组件的函数。编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。
- 不要改变原始组件,使用组合。可以将 HOC 视为参数化容器组件
- HOC 应该透传与自身无关的 props
- 常见的 HOC 签名如下:
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
- 不要在 render 方法中使用 HOC,务必复制静态方法,Refs 不会被传递
function withSubscription(WrappedComponent, selectData) {
// ...并返回另一个组件...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ...负责订阅相关的操作...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
5、Portals
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。ReactDOM.createPortal(child, container)
- 一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框
当在使用 portal 时, 管理键盘焦点尤为重要
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode
);
}
通过 Portal 进行事件冒泡:一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先
🎒 Hooks
1、Hooks简介
Hook 让你在函数组件里“钩入” React state 及生命周期等特性的函数,不能在 class 组件中使用。
- 优化组件间复用状态逻辑:无需修改组件结构的情况下复用状态逻辑
- 更好理解复杂组件:将组件中相互关联的部分拆分成更小的函数,非强制按照生命周期划分。使用 reducer 来管理组件的内部状态,使其更加可预测
- class问题:非 class 的情况下可以使用更多的 React 特性,可渐进式地使用
2、Hooks概览
2.1 State Hook——useState()
- 参数调用:在函数组件里调用,参数为初始状态 initState
- 返回:数组解构为 state、setState
- 更新 state 函数 setState:若 state 更新取决于以前的 state,需始终在 setState 中使用一个函数
- 重新渲染 reRender:每次更新 state 函数调用会触发 reRender 重新执行函数组件,因此 state 必须单独存储在useState外作为全局变量,以免重新渲染时使用useState中重置的 state
惰性初始化 state
useEffect :副作用钩子,使函数组件操作副作用。与 class 组件中的componentDidMount 、componentDidUpdate、componentWillUnmount 功能类似
const Chart = ({ data }) => {
useEffect(() => {
// when Chart mounts, do this
// when data updates, do this
return () => {
// when data updates, do this
// before Chart unmounts, do this
}
}, [data])
return (
<svg className="Chart" />
)
}
参数调用: callback 和 dependencies 数组。callback 即副作用,为异步执行的回调函数,不能为async 函数
- 如果 dependencies 不存在,那么 callback 每次 render 都会执行
- 如果 dependencies 存在,只有当它发生了变化, callback 才会二次执行
- Q:为什么第二个参数是空数组,相当于
componentDidMount
?- A:因为依赖一直不变化,callback 不会二次执行
- callback 返回:分为无需清除的 effect 和 需要清除的 effect
- 无返回:无需清除的 effect,如发送网络请求,手动变更 DOM,记录日志等。
- 返回清除函数:需清除的 effect,如订阅外部数据源,以防止引起内存泄露。将添加和移除订阅的逻辑放在一起,React 会在组件卸载时执行清除操作
- dependencies 依赖
- 诚实告知依赖:在依赖中包含所有effect中用到的组件内的值;修改effect内部的代码以确保它包含的值只会在需要的时候发生变更
- 移除不必要依赖:使用类似 setState(state=>state+1); 使用useReducer,可以把更新逻辑 reducer 和描述发生了什么 action 分开,相比于直接在effect里面读取状态,它dispatch了一个action来描述发生了什么。使得 effect 和 step 状态解耦 ```javascript const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state;
useEffect(() => { const id = setInterval(() => { dispatch({ type: ‘tick’ }); // Instead of setCount(c => c + step); }, 1000); return () => clearInterval(id); }, [dispatch]);
- 函数作为依赖:把query添加到useCallback 的依赖中,任何调用了getFetchUrl 或 fetchData 的effect在query改变后都会重新运行。useCallback本质上是添加了一层依赖检查。使函数本身只在需要的时候才改变,而不是去掉对函数的依赖
```javascript
function SearchResults() {
const [query, setQuery] = useState('react');
// ✅ Preserves identity until query changes
const getFetchUrl = useCallback(() => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]); // ✅ Callback deps are OK
useEffect(() => {
const url = getFetchUrl();
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
// ...
}
}
//通过属性从父组件传入的函数
function Parent() {
const [query, setQuery] = useState('react');
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={fetchData} />
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}
- 建议:把不依赖props和state的函数提到你的组件外面,并且把那些仅被effect使用的函数放到effect里面。effect 若需用到组件内的函数(包括通过props传进来的函数),可以在定义它们的地方用useCallback包一层
与 class 组件生命周期对比:useEffect 保证自身 state 的 data 值 与 props 的 dataRange 变量同步
const Chart = ({ dateRange }) => {
const [data, setData] = useState()
useEffect(() => {
const newData = getDataWithinRange(dateRange)
setData(newData)
}, [dateRange])
return (
<svg className="Chart" />
)
}
转换场景:
- 将数据存储在状态中,以防止每次组件更新时都重新计算数据。可不使用 state 而使用 useMemo( ),它将仅在其依赖项数组更改时重新计算数据,常用于大量数据或复杂计算
const Chart = ({ dateRange }) => {
const data = useMemo(() => (
getDataWithinRange(dateRange)
), [dateRange])
return (
<svg className="Chart" />
)
}
- 将数据存储在状态中,以防止每次组件更新时都重新计算数据。可不使用 state 而使用 useMemo( ),它将仅在其依赖项数组更改时重新计算数据,常用于大量数据或复杂计算
竞态与异步请求:
function Article({ id }) {
const [article, setArticle] = useState(null);
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;//在清除函数中取消异步请求
};
}, [id]);
// ...
}
自定义异步请求 Hook:useDataApi ```javascript const dataFetchReducer = (state, action) => { switch (action.type) { case ‘FETCH_INIT’:
return {
...state,
isLoading: true,
isError: false
};
case ‘FETCH_SUCCESS’:
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
};
case ‘FETCH_FAILURE’:
return {
...state,
isLoading: false,
isError: true,
};
default:
throw new Error();
} };
const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, });
useEffect(() => { let didCancel = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
return [state, setUrl]; };
//使用 useDataApi const AnyComponent = props => { const [{ data, isLoading, isError }, doFetch] = useDataApi( ‘http://hn.algolia.com/api/v1/search?query=redux‘, null, );
… };
<a name="473c4484"></a>
#### 2.3 Hooks 复用问题
对于多个 Hooks,为保证各自 state、deps 相互独立。可以使用数组来解决 Hooks 的复用问题。在 React 中数据存在 fiber node 上,每个组件都有自己的 memoizedState。
- 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中
- **reRender 时会重新执行函数组件,从 memoizedState 中按顺序查询 state、deps 上次记录的值,同时忽略Hooks 传入的 initState 参数**
![](https://cdn.nlark.com/yuque/0/2020/png/401207/1590817089375-f7f378c1-45d2-40b5-8c3f-edf3e869b1f0.png#crop=0&crop=0&crop=1&crop=1&height=1148&id=Pmrql&originHeight=1148&originWidth=460&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=none&title=&width=460)
```javascript
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
// 如果当前没有 state,说明是第一次执行,把 initialValue 复制给它
// 不严谨bug:init为false时
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
cursor = 0;
render();//简化reRender
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];// deps 记录 useEffect 上一次的 依赖
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
2.4 Hook 使用规则
- 只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用。为什么?
- 确保 Hooks 执行顺序一致:memoizedState 数组是按 hook定义的顺序来放置数据,若 hook不在最外层被调用,则重新渲染时会找不到相应数据而报错
只能在 React 的函数组件、自定义的 Hook 中调用 Hook。不要在其他 JavaScript 函数中调用
2.5 自定义 Hook
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。用于共享组件之间的状态逻辑(另有高阶组件、render props)
自定义 Hook 是一种重用状态逻辑的机制,每次使用自定义 Hook 时,所有 state 和副作用都是完全隔离的
- Hook 的每次调用都有一个完全独立的 state
- useInterval ```javascript import React, { useState, useEffect, useRef } from ‘react’;
//Hook function useInterval(callback, delay) { const savedCallback = useRef();
// Remember the latest callback. useEffect(() => { savedCallback.current = callback; }, [callback]);
// Set up the interval. useEffect(() => { function tick() { savedCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); }
//Use Hook function Counter() { let [count, setCount] = useState(0);
useInterval(() => { // Your custom logic here setCount(count + 1); }, 1000);
return
{count}
; }
<a name="6dVsf"></a>
#### 2.6 重新渲染过程
- 每一次渲染都有它自己的 Props 和 State,在单次渲染的范围内,props和state始终保持不变
- 每一次渲染都是独立的闭包,每一次渲染都有它自己的事件处理函数,有它自己的 Effects
- 每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state
- 在 effect 的回调函数中获取最新的 state :使用 ref
```javascript
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
3、useReducer
action 钩子,用于复杂 state 管理且包含多个子值,或下一 state 依赖于之前 state 等。当你写类似setSomething(something => …)这种代码的时候,也许就是考虑使用reducer的契机。const [state, dispatch] = useReducer(reducer, initState,init);
- 参数:reducer、initState、init
- reducer 纯函数:接收状态 state、行为 action,返回新状态 newState
- initState 初始状态:用于首次渲染的状态取值,同 useState 中的状态一样
- init 惰性初始化函数:可选,()=>(initState)
- 返回:state、dispatch
- state 状态:同 useState中的状态一样
- dispatch 分发函数:用于传入 action 并分发给相应的 reducer
- 与useState 区别: 将 setState 函数细拆分为 dispatch、reducer。组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 reducer 函数算出新的状态
```javascript
const todoReducer = (state, action) => {
//action 至少有一个类型 type 属性,还有一个有效负载 payload,涉及行为变动的数据
switch (action.type) {
case ‘DO_TODO’:
case ‘UNDO_TODO’:return state.map(todo => {
if (todo.id === action.id) {
return { ...todo, complete: true };
} else {
return todo;
}
});
default:return state.map(todo => {
if (todo.id === action.id) {
return { ...todo, complete: false };
} else {
return todo;
}
});
} }; const initialTodos = [ { id: ‘a’, task: ‘Learn React’, complete: false, }, { id: ‘b’, task: ‘Learn Firebase’, complete: false, }, ];return state;
const App = () => { const [todos, dispatch] = React.useReducer( todoReducer, initialTodos );
const handleChange = todo => { dispatch({ type: todo.complete ? ‘UNDO_TODO’ : ‘DO_TODO’, id: todo.id, }); };
return (
-
{todos.map(todo => (
- ))}
<a name="ubahh"></a>
### 4、useContext
共享状态钩子。如果需要在组件之间共享状态,可以使用useContext()。useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
- 参数:一个 context 对象(React.createContext 的返回值)
- 返回: context 的当前值,由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
- 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
```javascript
const AppContext = React.createContext({});
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}
const Messages = () => {
const { username } = useContext(AppContext)
return (
<div className="messages">
<h1>Messages</h1>
<p>1 message for {username}</p>
</div>
)
}
5、useRef
useRef 返回一个可变的 ref 对象,其 current
属性被初始化为传入的参数(initialValue)。
- useRef 返回的 ref 对象在组件的整个生命周期内保持不变,即每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref) ```javascript import React, { useState, useEffect, useRef } from ‘react’; import ReactDOM from ‘react-dom’;
function Parent() {
let [number, setNumber] = useState(0);
return (
<>
- forwardRef
- **forwardRef 可以在父组件中操作子组件的 ref 对象**
- **forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上**
- **子组件接受 props 和 ref 作为参数**
- **配合useImperativeHandle 将forwarfRef传入的ref元素进行绑定,指定该子组件对外暴露的方法或属性**
```javascript
function InputWithLabel(props) {
const { label, myRef } = props;
const [value, setValue] = useState("");
const _innerRef = useRef(null);
const handleChange = e => {
const value = e.target.value;
setValue(value);
};
const getValue = () => {
return value;
};
useImperativeHandle(myRef, () => ({
getValue,
focus() {
const node = _innerRef.current;
node.focus();
}
}));
return (
<div>
<span>{label}:</span>
<input
type="text"
ref={_innerRef}
value={value}
onChange={handleChange}
/>
</div>
);
}
const RefInput = React.forwardRef((props, ref) => (
<InputWithLabel {...props} myRef={ref} />
));
function App() {
const myRef = useRef(null);
const handleFocus = () => {
const node = myRef.current;
console.log(node);
node.focus();
};
return (
<div className="App">
<RefInput label={"姓名"} ref={myRef} />
<button onClick={handleFocus}>focus</button>
</div>
);
}