React从入门到出门
本文主要介绍函数组件及其常用hook
声明式
jsx
- 只能返回一个根元素(底层被转化为了对象,不能在一个函数中返回多个对象),可以在外层包裹Fragment(空标签)一般写为<></>,如果要给该标签传值,需调用:
import {Fragment} from 'react'
<Fragment key={}>
</Fragment>
- 混入JS表达式时使用{}。
- 标签必须闭合。
- 驼峰命名法给属性命名(className)。
- 换行用()包裹。
组件
封装起来具有独立功能的UI部件。 React的一切都是基于组件的,将页面划分为功能独立,逻辑完整,功能可复用的单元。可组合、可维护、可重用。
- React应用程序由组件构成,首字母大写。标签字母小写的React解析为HTML标签,标签首字母大写的才会被视为组件。
- 使用组件,通过重用同一组件,提高开发效率,通过小组件、大组件的使用进行开发。
组件的本质就是类和函数,但是与常规类和函数不同的是,组件承载了渲染视图的UI和更新视图的方法。
函数组件
函数组件的每一次更新,都是把函数重新执行:
- 产生一个新的闭包。
- 在闭包中所有创建函数的操作,都会:重新创建新的堆内存(也就是函数都会重新创建)。
- 不要尝试给函数组件prototype绑定属性或方法,即使绑定了也没有任何作用,因为React对函数组件的调用,是采取直接执行函数的方式,而不是new。
Props
- 通过Props进行组件通信。
- Props是组件的唯一参数。
- 可以使用展开语法转发props,但不要过度使用。
function Parent (props) {
return (
<>
<Child name={'A'} age={12}/>
<Child {...props}/>
</>
)
}
function Child ({name='a', age}) {
return (
<div>{name}-{age}</div>
)
}
function GrandParent () {
return (
<Parent name={"a"} age={10}/>
)
}
纯函数
仅执行计算操作,不做其他操作。
- 只负责自己的任务。
- 输入相同,则输出相同。
Hook
- 只在最顶层使用
useState
为组件添加状态,根据先前的state更新state。
import {useState} from 'react';
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
}
return (
<div>
<p>{count}</p>
<button onClick={incrementCount}>+</button>
</div>
)
-
State用于保存渲染间数据。
-
State setter 更新变量并触发React再次渲染组件。
-
state完全私有于声明它的组件。
-
将state视为只读。
- 对于对象,改变其state时对其结构然后进行属性覆盖。
- 对于数组,避免使用能够改变原数组的方法,(push、pop等)应对其进行拷贝后再操作。
const [student, setStudent] = setState({name:'A', age: 9})
function addAge () {
setStudent({
...student,
age: 19
})
}
- 状态提升:多个组件状态同步修改时,将状态移到最近的父级,通过props状态传递给组件。
- 调用set不会更新已经运行代码中的状态变量。更新函数可以获取待定状态并从中计算下一个状态。
function increment () {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
//只会+1
function increment () {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}
//会+3
Tip
在更新state时,如果传入了相同的state,视图不会更新。
- 处理逻辑中,会浅比较两次的state
如:当需要更改对象中的属性时,应该将对象浅拷贝
export default function App () {
const [wdy, setWdy] = useState({name: 'wdy'});
const handleClick = () => {
wdy.name = 'WDY';
setWdy(wdy) //×
setWdy({...wdy}) //√
}
return <button onClick={handleClick}>change</button>
}
类组件的setState和函数组件的useState的异同:
相同点:
- setState和useState更新视图,底层都调用了scheduleUpdateOnFiber方法,而且事件驱动情况下都有批量更新规则。
不同点:
- 在不是pureComponent组件模式下,setState不会浅比较两次state的值,只要调用setState,在没有其他优化手段的前提下,就会执行更新。但是useState中的dispatchAction会默认比较两次state是否相同,然后决定是否更新组件。
- setState有专门监听state变化的回调函数,可以获取最新的state;但是在函数组件中,只能通过useEffect来执行state变化引起的副作用。
- setState在底层处理逻辑上主要和老state进行合并处理,而useState更倾向于重新赋值。
useReducer
处理复杂状态。 参数一:纯函数(state,action)
参数二:初始化state
参数三(可选):用于计算初始值的函数
import {useReducer} from 'react';
function reducer (state, action) {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default :
throw new Error();
}
}
function double (x) {
return x*2;
}
function Counter ({a}) {
const [count, dispatch] = useReducer(reducer, a, double);
return (
<div>
<p>count: {count}</p>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</div>
)
}
useContext
将信息深入传递,无需手动将porps层层传递。
import {createContext} from 'react';
export const CountContext = createContext(0);
import {countContext, useContext} from '';
export default function Counter () {
const count = useContext(CountContext);
}
import {CountContext} from '';
export default function AnotherCounter(count) {
<CounterContext.Provider value={count}>
</CounterContext.Provieder>
}
- 正常传递porps,可以更清晰看到哪些组件用了哪些数据。
- 抽象组件并将jsx作为children传递。 以上两种方案均不适合,考虑useContext。
- 使用createContext来创建context对象,使用Provider修改其中的值,function组件使用useContext的hook来取值,class组件使用Consumer来取值。
useRef
引用一个不需要渲染的值,返回一个可变的ref对象,该对象可以在整个组件的生命周期内保留数据。
函数组件每一次render,函数上下文会重新执行,那么有一种情况就是,在执行一些事件方法改变数据或者保存新数据的时候,有没有必要更新视图,有没有必要把数据放到state中。如果视图层更新不依赖想要改变的数据,那么state改变带来的更新效果就是多余的。
useRef可以创建出一个ref原始对象,只要组件没有销毁,ref对象就一直存在。
参数一:ref对象的current属性的初始值。
import {useRef} from 'react';
funciton TextInput () {
const inputRef = useRef();
function focusInput = () {
inputRef.current.focus();
}
return (
<div>
<input type="text" ref={inputRef}/>
<button onClick={focusInput}>Fouc</button>
</div>
)
}
- 改变ref不会触发重新渲染
useEffect
脱围机制。组件与外部系统同步,执行副作用。 参数一:setup函数,当组件被添加到DOM时,setup函数将运行。 参数二(可选):setup代码中引用的所有响应式值的列表。
-
请求数据、定时器等异步逻辑,通常放入useEffect中。
-
根据传入依赖项的不同,会有不同的执行表现:
- 没有依赖项 组件初始渲染 + 组件更新执行
- 空数组依赖 只在初始渲染时执行一次
- 添加特定依赖项 组件初始渲染 + 特性依赖项发生变化时执行
import {useEffect} from 'react';
useEffect (() => {
// setup
return () => {
//cleanup 组件卸载时自动执行
}
}, [依赖项数组])
function App () {
const [count, setCount] = useState(0);
//没有依赖项 组件初始渲染 + 组件更新执行
useEffect (() => {
console.log('执行');
})
//空数组依赖 只在初始渲染时执行一次
useEffect (() => {
console.log('执行');
},[])
//添加特定依赖项 组件初始渲染 + 特性依赖项发生变化时执行
useEffect (() => {
console.log('执行');
},[count])
return (
<div>
<button onClick=(() => setCount(count + 1))>+{count}</button>
</div>
)
}
-
工作原理
- 调度副作用:当组件内部调用useEffect时,实际上是将一个副作用函数及其依赖项数组排队等待执行,这个函数并不会立即执行。
- 提交阶段(Commit Phase):React渲染组件并且执行了所有的纯函数组件或类组件的渲染方法后,会进入到所谓的提交阶段,在这个阶段,React将计算出的新视图(新的DOM节点)更新到屏幕上,一旦这个更新完成,React就知道现在可以安全地执行副作用函数了,因为这不会影响到正在屏幕上显示的界面。
- 副作用执行:提交阶段完成后,React会处理所有排队的副作用。如果组件是首次渲染,所有的副作用都会执行。如果组件是重新渲染,React会首先对比副作用的依赖项数组:如果依赖项未变,副作用不会执行;如果依赖项改变或没有提供依赖项:副作用再次执行。
- 清理机制:如果副作用函数返回了一个函数,那么这个函数将被视为清理函数。在执行当前副作用之前,以及组件卸载前,React会先调用上一次渲染中的清理函数。这样确保了不会有内存泄漏,同时能撤销上一次副作用导致的改变。
- 延迟副作用:尽管useEffect会在渲染之后执行,但是它是异步执行的,不会阻塞浏览器更新屏幕。这意味着React会等待浏览器完成绘制后,在执行副作用函数,以此来确保副作用处理不会导致用户可见的延迟。
- 这种机制,useEffect允许开发者以一种优化的方式来处理组件中可能存在的副作用,而不需要关心渲染的具体时机。退出清理功能确保了既是组件被多次快速创建和销毁,应用程序也能保持稳定和性能。
useLayoutEffect
参数一:setup函数。
参数二:setup代码中引用的所有响应式值的列表。
与useEffect区别:useEffect在DOM更新之后异步执行,useLayOutEffect在DOM更新之前同步执行。
useInsertionEffect
专为CSS-in-JS打造,只有在使用CSS-in-JS库并且需要注入样式才使用。
可以在布局副作用触发之前将元素插入到DOM中。
useMemo
防止重新计算的频率过高。每次重新渲染时能够缓存计算的结果。
const cachedValue = useMemo(create,deps)
参数一create:要缓存计算值的函数。
参数二deps:所有在参数一的函数中使用的响应式变量组成的数组。
返回值cachedValue:执行create的返回值。
-
函数组件每次更新,都是把函数重新执行:
- 产生新的闭包。
- 内部代码重新执行。
-
在函数每一次重新执行的时候,如果依赖的状态值没有发生改变,此操作逻辑不去执行,只有依赖值发生变化,我们再去执行即可。
- 第一次渲染组件的时候,callback会执行。
- 后期只有依赖的状态值发生改变,callback才会执行。
- 每次会把callback执行的返回结果赋值给xxx。
- 具备缓存效果(计算缓存),在依赖的状态值没有发生改变,callback没有触发执行的时候,xxx获取的是上一次计算出来的结果。
-
优化的Hook函数
- 如果函数组件中,有消耗性能的和时间的计算操作,则尽可能用useMemo缓存起来,设置对应的依赖。
- 这样可以保证,当非依赖的状态发生改变,不会去处理一些没必要的操作,提高组件的更新状态。
import {useMemo, useState} from 'react';
function Counter ({a, b}) {
const [count, setCount] = useState(0);
const value = useMemo(() => {
return a + b;
}, [a, b])
return (
<div>
<p>{a = ${a} b = ${b} count = ${count}}</p>
<p>{value = ${value}}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
function App () {
return (
<Counter a={1} b={1}/>
)
}
useMemo会记录上一次执行create的返回值,并把它绑定在函数组件对应的fiber对象上,只要组件不销毁,缓存值就一直存在,但是deps中如果有一项改变,就会重新执行create,返回值作为新的值记录在fiber对象上。
useCallback
减少组件重新渲染次数,防止回调函数被无限制地创建。
参数一:回调函数。
参数二:依赖项数组。
const xxx = useCallback(callback, [dependencies])
-
组件第一次渲染,useCallback执行,创建一个函数callback,赋值给xxx。
-
组件后续每一次更新,判断依赖的状态值是否改变:
- 如果改变,则重新创建新的函数堆,赋值给xxx。
- 如果没有改变或者没有设置依赖值,则xxx获取的一直是第一次创建的函数,不会创建新的函数出来。
- 或者说,基于useCallback,可以始终获取第一次创建函数的堆内存地址(或者说函数引用)。
-
useCallback不要滥用,并不是所有组件内部的函数,都拿其处理会更好。
- 虽然减少了堆内存的开辟。
- 但是useCallback本身也有自己的处理逻辑和缓存的机制,也会消耗时间。
-
使用范围:
- 父组件嵌套子组件,父组件要把一个内部的函数,基于属性传递给子组件,此时传递的这个方法,基于useCallback处理一下会更好。
- 函数组件用React.memo包起来,类组件使用React.PureComponent
import { useState, useCallback } from 'react';
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleClick}>Increment</button>
</div>
);
}
useImperativeHandle
自定义由ref暴露出来的句柄
参数一ref:接受forwardRef传递过来的ref
参数二createHandle:处理函数,返回值作为暴露给父组件的ref对象
参数三deps:依赖项deps,依赖项更改形成新的ref对象
自定义Hook
js函数,可以使用已经存在的Hooks,通常情况以use开头。
//自定义
import {useState} from 'react';
function useCounter (initialCount) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return {count, increment, decrement}
}
export default useCounter;
//使用
import useCounter from '';
function Counter () {
const [count, increment, decrement] = useCounter(0);
return
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
}
Api
e.stopPropagation
阻止事件转播
e.preventDefault
阻止浏览器默认行为
React.createElement
创建React元素
ReactDOM.createRoot
创建根容器,用来存放React元素
forwardRef
- 允许组件使用ref将DOM节点暴露给父组件
- 在多个组件中转发ref
- 暴露命令式句柄而非DOM节点
const SomeComponent = forwardRef(render);
高阶组件 HOC
高阶组件是一个函数,接收一个组件作为参数,并返回一个新的增强组件。这个增强组件可以包含一些共用逻辑和状态,用于实现代码复用和抽象。
解决大量的代码复用,逻辑复用问题
- 一般以with开头
- 不在render中使用HOC
- 尽可能透明地操作原始组件
import React from 'react';
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (!isLoading) return <Component {...props} />;
return <div>Loading...</div>;
}
}
function MyComponent({ data }) {
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
const MyComponentWithLoading = withLoading(MyComponent);
function App() {
const [isLoading, setIsLoading] = React.useState(true);
const [data, setData] = React.useState([]);
React.useEffect(() => {
//获取data isLoading
}, []);
return (
<div>
<MyComponentWithLoading isLoading={isLoading} data={data} />
</div>
);
}