1. React 的思想
文章涉及到的在线事例
用密:admin/admin
-
虚拟 DOM
-
标签就是函数,标签的属性就是函数的参数
<Parent name={this.state.name}/> -
我们自定义组件的时候必须要大写开头,因为只有这样
createElement把组件当成一个变量而不是字符串。

-
组件名不能是表达式,应该是以大写字母开头的变量。
-
在
react中一切皆可props-
包括基本数据类型,React 元素以及函数。
-
props的默认值为true<Com loading /> // 二者等价 <Com loading={true} />
-
-
jsx可以进行字符串字面量转移,防止xss(跨站脚本攻击) 。
1.1 react 的创建更新过程
react 中有三种创建更新的方式
-
通过
ReactDom.render()||hydrate初次渲染 -
通过
setState更新 -
通过
forceState更新<div class='container'> parent <p><span>span</span></p> <div>div</div> </div> // 转换为 每一个节点都需要 React.createElement 进行创建 React.createElement("div", { class: "container" }, "parent", React.createElement("p", null,React.createElement("span", null, "span")), React.createElement("div", null, "div"));

1.1.1 ReactDom.render 的过程
-
先创建一个
ReactRoot顶点对象app.js
// 根据给 createElement 传了一个 APP 类 ReactDOM.render(<App />, document.getElementById('root'));// 源码 ReactDOM.js function ReactRoot( container: DOMContainer, isConcurrent: boolean, hydrate: boolean, ) { // 这个 root 指的是 FiberRoot // createContainer 平台、任务调度的东西 // createContainer => return createFiberRoot(containerInfo, isConcurrent, hydrate); const root = createContainer(container, isConcurrent, hydrate); this._internalRoot = root; }到这里就创建了一个 root 对象,react 在创建对象的时候会把根节点内的所有元素都移除。我们一般都写成
<div id="root"></div>没有子元素 -
再创建
FiberRoot和RootFiber,二者是非常重要的apifiber和 Fiber 是两个不一样的东西,前者代表着数据结构,后者代表着新的架构。- 每一个
dom节点对应着一个fiber对象 FiberRoot就是整个fiber树的根节点
function ReactRoot( container: DOMContainer, isConcurrent: boolean, hydrate: boolean, ) { // 这个 root 指的是 FiberRoot 这里创建了一个 FiberRoot const root = createContainer(container, isConcurrent, hydrate); this._internalRoot = root; }- 因为有 react 16 有任务优先级的概念,同一时间有任务优先级不同的任务。
1.1.2 FiberRoot
- FiberRoot 是整个应用的起点
- 包含应用挂载的目标节点(
<APP/>) - 记录整个应用更新过程的各种信息
- 在 react 里面,通过 this.props.child 拿到子节点
// react fiberRoot 中的数据结构
type BaseFiberRootProperties = {
// 容器,也就是 render 的第二个参数
containerInfo: any,
// Used only by persistent updates.
// 只在持续更新中使用
pendingChildren: any,
// The currently active root fiber. This is the mutable root of the tree.
// 当前的 fiber 对象,也就是 root fiber
current: Fiber, // 指向 RootFiber
// 用来存放 update,也就是用来记录改变状态的
updateQueue: UpdateQueue <any> | null,
...
}
1.1.3 RootFiber
fiber会组成一个树结构,内部使用了单链表树结构,每个节点及组件都会对应一个fiber
- 父节点只有一个子节点,而每个子节点又有兄弟节点(
sibling),还有一个return指回了父节点。

- 对应
Dom结构如下

1.1.4 render
updateContainercurrentTime代表react初始化的时间expirationTime代表更新的优先级- 指一个任务的过期时间,
expirationTime大于当前时间react就会延迟执行这个任务sync的数字是最大的,所以优先级也是最高的- 交互事件 优先级较高
- 异步事件 优先级最低
- 高优先级可以打断低优先级的执行
- 这样就造成了某些生命周期函数的多次执行
- 指一个任务的过期时间,
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
// 这里指 FiberRoot
// document.querySelector('#root')._reactRootContainer 可进行查看
const root = this._internalRoot;
// ReactWork 的功能就是为了在组件渲染或更新后把所有传入
// ReactDom.render 中的回调函数全部执行一遍
const work = new ReactWork();
callback = callback === undefined ? null : callback;
// 如果有 callback,就 push 进 work 中的数组
if (callback !== null) {
work.then(callback);
}
// work._onCommit 就是用于执行所有回调函数的,就是我们传进来的 callback
// updateContainer(<App/>,'root节点',parentComponent,callback)
// 这里的 parentComponent react采用了硬编码直接为 null
updateContainer(children, root, null, work._onCommit);
return work;
};
// 非常重要的 api
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
// 取出容器的 fiber 对象,也就是 fiber root
const current = container.current;
// 计算时间 (react 初始化用了多少时间)
const currentTime = requestCurrentTime();
// expirationTime 代表优先级,数字越大优先级越高(指的是一个任务的过期时间)
// sync 的数字是最大的,所以优先级也是最高的
// 交互事件 优先级较高
// 异步事件 优先级最低
const expirationTime = computeExpirationForFiber(currentTime, current);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback,
);
}
1.1.5 update (和 setState 相关)
export function createUpdate(expirationTime: ExpirationTime): Update<*> {
// 这里指的是 React.render(payload,callback)
return {
expirationTime: expirationTime,
tag: UpdateState,
// setState 的第一二个参数
payload: null,
callback: null,
// 用于在队列中找到下一个节点
next: null,
nextEffect: null,
};
}
- 用于记录组件状态的改变
- 存放于
updateQueue中 - 多个 update 可以同时存在 (串联)
1.1.5 进入调度
- 最后创建更新,进入更新调度阶段。
- 主要靠 expirationTime
// 进入任务调度 有更新产生 需要更新
scheduleWork(current,expirationTime)
2. state、props、setState
state 和 props 之间最重要的区别是:props 由父组件传入,而 state 由组件本身管理。组件不能修改 props,但它可以修改 state。
构造函数是唯一可以给 this.state 赋值的地方
- 建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。
2.1 props & 组件通信
-
父传子
- 将父组件的方法以函数的形式传递给子组件,在子组件中调用
-
子传父
- 父组件中通过 ref 得到子组件的标签对象
- 通过 this.myRef.current.xxx() 得到子组件的方法
// 父组件
class Parent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
test(){
const childMethodResult = this.myRef.current.xxx() // 'child methods'
}
render() {
return <Children ref={this.myRef} />;
//return <Children ref={ el => this.myRef = el} />; 推荐这样创建 ref
}
}
// 子组件
class Child extends React.Component {
constructor(props) {
super(props);
}
xxx(){
return 'child methods'
}
render() {
return null;
}
}
2.2 setState 为什么是异步的
- 第一:出于性能考虑,React 可能会把多个
setState()调用合并成一个调用,state的更新可能是异步的。 - 第二:为了保持内部一致性:
props的更新是异步的,因为re-render父组件的时候,传入子组件的props才变化;为了保持数据一致,state也不直接更新,都是在flush的时候更新。 - 要解决这个问题,可以让
setState()接收一个函数而不是一个对象。这个函数用上一个state作为第一个参数,将此次更新被应用时的props做为第二个参数:
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
2.3 setState 何时异步
- 在 `react 的监听回调中是异步的( React 中的事件监听不是用的原生的事件监听,用的是合成的自定义的事件监听)
- 在
react的生命周期钩子函数中是异步的
2.4 setState 何时同步
-
定时器中
-
元素的
DOM事件(ref获取到原生的dom) -
Promise(下面的 data 是实时更新的)Promise.resolve().then(data=>{ console.log(this.state.data) this.setState({data}) console.log(this.state.data) }
3. 受控和非受控组件
-
用
props传入数据的话,组件可以被认为是受控。受控标签都接受一个value属性- react
input输入框,由 react 控制表单的输入。 - 使 react 的
state成为唯一的数据源
this.setState({ value: event.target.value }) render(){ return ( <input type="text" value={this.state.value} onChange={this.handleChange} /> ) } - react
-
数据只保存在组件内部
state上的是非受控组件(因为外部没办法直接控制state)。- 下面
input它的value只读,所以它是 React 中的一个非受控组件
<input type="file" />- 非受控组件数据交给
Dom来处理- 使用 ref 来从 DOM 节点中获取表单数据。
- 数据都存在真实的 DOM 节点上
- 下面
4. virtual DOM
4.1 怎么进行 dom diff
-
采用广度优先,层层比较的方式,算法时间复杂度 o(n)。
- 只比较同层节点,从上到下,从左到右。

情况一:A B 节点的属性、位置发生了变化,进行位置交换。
情况二:节点类型发生变化会直删除节点,重新渲染新节点。
-
React 为什么要这样进行 DOM diff
- 是基于组件的 DOM 结构是非常稳定的(一般 HTML 树是非常稳定的)
- 是基于类型相同的兄弟节点可以被唯一标识 key
4.2 虚拟 DOM 的优势
它优化了触发浏览器 reflow 和 repaint 的步骤,把众多页面节点改动集中到一次来进行触发。在用 setState 顺利触发了component 的 render 后,react 会对 Virtual DOM 进行操作,而这些操作并不会触发浏览器的 reflow 和 repaint ,因为 Virtual DOM 它只是存在内存中的一个有着 DOM层级关系的数据而已。 当最终形成了新的 Virtual DOM 后,转换成真实的 DOM这一步才会触发浏览器的 reflow 和 repaint。
5. 高阶组件(HOC)
它是一种设计模式,不是 React 独有。
-
普通组件是将 props 转换为 UI
-
高阶组件是将组件转换为另一个组件
- 借助高阶组件(一个函数)去实现一些逻辑,高阶组件自身并不包含任何 UI 展现。
- 接收一个参数(参数是一个组件),再返回一个组件。目的是将组件转换为另一个组件。
- 接收的组件就是用于返回的组件,目的是挂载一些
props
-
适应场景
例如一个时钟的显示,封装成一个高阶函数组件
<A/>进行导出,然后在组件 B 中使用。<B/>就可以获取到高阶组件<A/>的props// 高阶组件 A export default function A (WrapComponent){ return class extends Component { state = {time:new Date()} } render(){ return ( <WrapComponent time={this.state.time} ...this.props /> ) } } // 组件 B import A from 'xxx' class B extends Component { render(){ return ( <div>{this.props.time}</div> ) } }
export default A(B) // 这样就可以获得 A 组件的属性
上面可以看到,并没有复用 A 组件。
6. createContext API 及使用场景
6.1 基本用法
出现的意义是为了解决组件间通信的问题,因为组件间数据的层层传递非常麻烦。Redux 就依赖此 API。来共享全局状态。
用场景:主题、语言切换等。
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
// 如果有其它的 context 同理从新写一遍即可
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
render(){
return (
<ThemeContext.Consumer>
{/* 以函数作为子组件,放到 Consumer 里面才会生效 */}
{
theme => <Button theme={theme} {...props}/> // theme === 'dark'
}
</ThemeContext.Consumer>
)
}
}
如果增加一些功能,点击切换主题,道理是一样的,切换的时候传给 Context 的 Provider 传入主题的值让其层层传递就可以。这样就做到了实时更新数据。
为什么不采用写一个外部配置文件的方式进行主题更改?
如果写成一个配置文件去切换,那么你还需要监听数据的变化然后再进行更新(forceUpdate)的操作。因为外部的数据并不属于组件内部的一个状态。
6.2 contextType
项目只有一个 createContext的情况下可以采用简写的形式,上面中我们获取 provider 提供的 value 需要在 render 中的函数里得到,利用 contextType 我们可以不写函数也能得到
class ThemedButton extends React.Component {
static contextType = ThemeContext; // 静态方法
render(){
const theme = this.context // 调用 context 直接返回
return (
<Button theme={theme} {...props}/> // theme === 'dark'
)
}
}
7. Redux
Redux 的原则 :
- 单一数据源
- 状态不可变
- 纯函数修改状态
应⽤中所有的 state 都以⼀个对象树的形式储存在⼀个单⼀的 store 中。唯一改变 state 的办法是触发 action ⼀个描述发⽣了什么的对象。
为了描述 action 如何改变 state 树,你需要编写 reducers。
-
React 的模式是 state 到 DOM,是组件内部的状态。
-
Redux 的模式是把状态移动到了组件之外,全局的状态,放到一个唯一的 store 树上。
-
Redux 的特性是纯函数更新 store,reducer 根据传入的 state 和 action 生成新的 state 。
7.1 createStore()
- 接收的参数为
reducer函数,返回store。这样就产生了一个 store 。
store的方法getState()返回值为store内部保存的数据dispatch()参数为action对象,触发reducer,更新store触发subscribe监听,更新视图subscribe()参数为监听内部state更新的回调函数
7.3 action
7.3.1 bindActionCreators(actionCreators, dispatch)
actionCreators(Function or Object): 一个 action creator,或者一个 value 是 action creator 的对象。dispatch(Function): 一个由Store实例提供的dispatch函数。
// action
function plus(){
return { type:'PLUS',payload:{count:1} }
}
// 用store 分发一个 action
store.dispatch(plus())
用 bindActionCreators 进行改写
// action
function plus(){
return { type:'PLUS',payload:{count:1} }
}
function minus(){
return { type:'MINUS',payload:{count:1} }
}
const action = bindActionCreators({plus,minus},store.dispatch)
// 内部会执行 dispatch
action.plus()
action.minus()
7.3.2 异步 action 流程

流程说明:
在 View 进行 click 触发一个异步 action 然后被中间件(redux-thunk)截获,处理完成后,进行dispatch 到reducer 然后到到 state 进行处理,更新 state 触发 View 更新。
中间件是一些函数用于定制对特定请求的处理过程
function middleWare({dispatch,getState}){
return function(next){
return function(action){
//do...
return next(action)//处理完后调用下一个中间件
}
}
}
// es6写法
({dispatch,getState})=>next=>action=>next(action)
// store
import {createStore,applyMiddleware} from 'redux'
import ReduxThunk from 'redux-thunk'
export default createStore(appReducer,(applyMiddleware(ReduxThunk)))
redux-thunk 截获异步请求的依据就是判断这个 action 是不是一个函数 (Promise)是的话就执行这个函数,执行后再 diapatch 这个 action
7.4 reducer
reducer 是不允许有副作用的。你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅是 —— 初始化和计算新的 state。就是根据老的 state 和传入的 action 生成一个新的 state。一般在一个项目中会有好多个 reducer 函数。
例如:一个获取登录用户信息的 reducer
export function loginUserInfo (previousState = {}, action) {
if (action.type === GET_LOGIN_USER_INFO || action.type === LOGIN) {
return action.data.userInfo || {}
} else if (action.type === LOGOUT) {
return {}
} else {
return previousState
}
}
7.4.1 combineReducers()
- 接收包含
n个reducer的对象,返回一个新的reducer函数
8. react-redux
8.1 connect()
connect() 是一个高阶函数,执行后返回一个高阶组件,接收一个UI组件,返回一个容器组件。
从左到右看。

参数为:
mapStateToProps:一个函数,指定向 UI 组件传递哪些一般属性,必须返回一个对象。mapDispatchToProps:可以是对象,对象所定义的方法名将作为属性名;也可以是函数通过dispatch显式分发action,每个方法将返回一个新的带有(dispatch,getState)参数的函数,目的是向UI组件传递方法。
8.2 Provider
Provider 组件,接收 store 属性让所有组件都看到 store,通过 store 读取、更新状态。
export default App extends Component {
render(){
return (
<Provider store = { store } >
</Provider>
)
}
}
9. 如何组织 redux
- 单个
action和reducer放在同一个文件,利于改写不用来回切换文件。 - 统一在
action.js和reducer.js导入所有的action和reducer
9.1 actionType、action 方法和 reducer 方法的命名
actionType常量采用下划线命名法action、reducer采用驼峰命名法,适当可使用下划线actionType名称与action的名称,结构都动宾结构:‘V+N’reducer方法的命名为action名去掉动词部分的宾语(名词)部分
以获取登录用户信息为例子:
// actionType常量
GET_LOGIN_USER_INFO = 'GET_LOGIN_USER_INFO'
// action方法
export function getLoginUserInfo (params) {
return async (dispatch,getState)=>{
const res = await reqLogin(loginInfo)
if (res.status === 0) {
// 分发一个同步action
dispatch(
{
type:LOGIN_USER_INFO,
data:res.data
}
)
}
}
}
//reducer
export function loginUserInfo (previousState = {}, action) {
if (action.type === GET_LOGIN_USER_INFO || action.type === LOGIN) {
return action.data.userInfo || {}
} else if (action.type === LOGOUT) {
return {}
} else {
return previousState
}
}
10.不可变数据
10.1 redux 运行的基础

如上图:我们更新左边图的一个点,无论你怎么更新都需要复制一份(深、浅拷贝)包含修改的部分,右图绘制绿色的点需要更新的点,其它没有改变的点无需更新。没有改变的点就是不可变数据。
10.2 redux 需要不可变数据的原因
- 为了性能优化
reducer根据传入的state和action生成了一个新的state而不是修改原有的state。- 这个时候就可以比较新旧
state而不需要比较值,继而不需要进行深层次的遍历,只比较它们的引用就可以,从而确定是否需要更新组件。
- 为了易于调试跟踪
- 可以追踪到新旧
state。
- 可以追踪到新旧
- 为了易于推测
- 推测
action的状态 ,是否正确。
- 推测
10.3 如何操作不可变数据
-
原生的两种方法,
Object.assign()和{...}。 -
Immutability-helper(节点很深可以用这个,用法类似Object.assign()) -
ImmerImmer 的用法,性能没有原生的
Object.assign()好。import produce from 'immer' // 接收旧的 state ,回调函数里可以直观的进行修改,类似在原有的 state 上进行修改,采用代理的方式。 produce(state,drafState=>{ drafState.todos.push('xxx') })
11. react-router-dom
11.1 为什么需要路由
- 单页应用需要来回切换
- 通过
URL可以定位到页面 - 可以清晰(语义化)的划分资源
11.2 实现方式
-
URL 模式
- 通过
BrowserRouter实现,HTML5 中historyAPI 。
<BrowserRouter> <Switch>{/* 该组件只渲染第一个与当前访问地址匹配的 <Route> 或 <Redirect> */} <Route exact path={'/login'} component={Login}></Route> <Route path={'/'} component={Admin}></Route> </Switch> </BrowserRouter> - 通过
-
hash 模式
- 通过
HashRouter实现 - 支持低版本浏览器。使用 window.location.hash 来保持 UI 和 url 的同步。
- 通过
-
内存模式
- 通过
MemoryRouter实现。 - 服务端渲染的时候用,存在内存中,URL 地址无变化 。
- 通过
11.3 参数定义、路由嵌套
-
通过 URL 传递参数
- 应用场景:一般需要复制链接到别处仍可以看到当前组件内部的数据。不需要根据 URL 传递到组件,组件再请求数据,复制 URL 后直接就去请求了。
<Link to = {'/user/123'}> render user123 </Link> <Router path = {'/user:id'} .../> -
如何获取参数
{/* 获取上面的 id */} this.props.match.params.id
12. 单元测试
12.1 Jest
Facebook 开源的 JS 单元测试框架
…
13. 理想的架构
-
易于开发
- 开发工具是否完善
- 生态圈、社区是否活跃繁荣
-
易于维护
- 代码是否容易上手
- 文档是否齐全
-
易于构建
- 构建工具的选择
-
易于测试
- 功能分层是否清晰
- 副作用少,尽量使用纯函数
-
易于扩展
14. 性能问题
14.1 常见的性能问题场景
- 键盘输入
- 出现卡顿,采用事件节流。
- 鼠标移动
- 滚动卡顿,采用防抖。
14.2 代码的性能
-
组件拆分
根据
React的虚拟 DOM DIFF 算法O(n)。组件根据情况拆分的越细,React性能越高。因为组件细了后就可以作为一个纯组件对待,可以整体看成是一个dom,组件状态未发生改变的时候,就不会更新这个dom节点,对应的也就不会进行 Dom diff 的操作。 -
网络优化,按需加载
-
借助 webpack
import进行动态加载。 -
使用 react-loadable库。
/** * @desc 借助 react-loadable 进行 code-splitting 时的loading组件 */ function Loading({ error }) { if (error) { return 'error'; } else { return <div></div>; } } /** * @desc 引入,注意是import()方法,不是import关键字 */ import Loadable from 'react-loadable' const Home = Loadable({ loader: () => import('./children/Home'), loading: Loading, }) /** * @desc 路由匹配成功的时候才引入,而不是一开始全部引入 */ <Route path="/home" exact component={Home} /> -
React 的
Suspenseapi
借助 webpack 、 import 动态导入。
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 = () => ( <Router> <Suspense fallback={<div>Loading...</div>}> <Switch> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </Switch> </Suspense> </Router> -
);
- 错误捕获
- 借助生命周期
componentDidCatch,进行render错误的渲染。
总结:
pureComponent,提供简单的对比算法比较新旧 props state 减少性能开销。pureComponent只能直接对比值,值的内部发生改变无法对比。- 函数组件没有
state可以借助memo达到同样的效果。
它们都是使用 Object.is(new,old) 进行对比。和 === 区别的
Object.is(+0,-0) // false
Object.is(Number.NaN,NaN) // true
+0===-0 // true
NaN===NaN // false
14.3 注意可重构代码
- 什么的代码可重构?
- 功能独立,不依赖(较少依赖)外部变量。
- 也被其它组件依赖
14.4 利用工具进行定位问题
- React dev-tool
15. Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
15.1 Hooks 出现的原因:
15.1.1 class 的不足之处
-
状态逻辑难以复用
render、props或者高阶组件(HOC)都在外层包裹了一层组件,无端增加代码层级。- 函数组件等到有自己状态的时候,要改成
class组件就会难以维护。
-
事件的绑定解绑在不同的生命周期
- 在
componentDidMount中注册事件以及其他的逻辑,在componentWillUnmount中卸载事件。
- 在
-
this指向问题-
构造函数显示
bind(this) -
render函数显式bind(this) -
箭头函数绑定
-
静态类属性
handleClick = () => { this.setState({ num: this.state.num + 1, }) }; render() { return (<div> <button onClick={this.handleClick}>click me</button> </div>) }
-
15.1.2 Hooks 的特点
-
解决了上述的几类问题。
-
可以复用状态逻辑(自定义 Hooks)。
-
新的
api使用范围广。 -
副作用关注点分离
- 每个
useEffet对应一个副作用,不用像class那样都写到对应的生命周期函数中去。
- 每个
15.1.3 使用注意事项
-
每一个 Hooks 相互独立
- 不同函数调用同一个 Hooks 他们的状态时各自独立的,互不影响。
-
只能在 React 的函数组件中调用 Hook
-
必须把 Hooks 写在函数的最外层不能写在
if...else等条件语句当中-
就是靠这样确保每次调用次序的一致性,而保证多个
useState是相互独立的。- 是根据调用的顺序而保证
useState是相互独立的 - 每次调用的次序、数量必须一致,不能多也不能少。
- 就是利用了全局唯一性。
- 是根据调用的顺序而保证
-
不然就会打乱
useState的调用次序
-
15.2 useState
15.2.1 useState 的概念
- 它是一个函数,接收一个唯一的参数
state,返回一个数组。 - 返回数组的第一项是
state第二项是操作state的方法setState - 与类组件中的
this.setState的区别是setState是直接用新的state替换旧的state。而this.setState是拿新的state和旧的state进行合并。 - 另外可以有多个
useState就像我们定义this.state那样有多个状态。相应的每个状态对应一个改变它的setState。
当你觉得现在的 state 满足不了需求的时候不防增加一个 state 变量试试
15.2.2 useState 的使用
点击按钮 count 就会自动更新,如果 count 的值没有变化就不会进行重新渲染。
setState 使用 Object.is(oldState,newState) 进行判断新旧值是否相等
和’=‘的区别是’='将数字值 -0 和 +0 视为相等,并认为 Number.NaN 不等于 NaN。
function HooksUseState() {
// 数组的解构 参数0是state的初始值 返回的变量
// 等价于 const _useState = useState(0)
// 等价于 const count = _useState[0]
// 等价于 const setCount = _useState[1]
const [count, setCount] = useState(0);
return (
<div>
<h1>{count}</h1>
<button onClick={()=>setCount(count+1)}>+</button>
</div>
);
}
合并更新对象的多个属性
function Counter() {
const [counter, setCounter] = useState({
name: "计数器",
number: 0,
count: 0
});
return (
<>
<p>
{counter.name}number:{counter.number}count:{counter.count}
</p>
<button
onClick={() =>
setCounter({
...counter,
number: counter.number + 1,
count: counter.count + 1
})
}
>
+
</button>
</>
);
}
- 如何强制渲染 Hooks 组件
- 声明一个无关的
useState - 更新这个
state就会强制渲染当前的 Hooks 组件。
- 声明一个无关的
15.2.3 优化 useState
Class 有 pureComponent,函数组件有 React.memo,它们通过比较新旧 state来减少组件的渲染次数。那么 useState该如何做呢?
上面我们已经说了如果 useState接收到的属性不变,则不会重新渲染函数。但是 useState 每次都会重新执行,接受到的 state 尽管是同一个它还是会把它当成新的 state
解决办法:
// 给初始值传入一个函数来执行 fn 这样 fn 就执行一次,否则直接传入 fn() 每次更新都会执行 fn
useState(()=> fn())
15.3 useEffect
用来处理副作用的操作,例如:事件绑定与解绑、发送网络请求、操作DOM。
useEffect 相当于 componentDidMount和 componentDidUpdate、componentWillUnmount
适用于组件挂载前、挂载后、更新后。如需清除上一次的副作用,可返回一个函数,函数内部执行清除副作用的操作。
15.2.1 如何使用
let timer;
function ajax() {
return new Promise((resolve, reject) => {
timer = setTimeout(() => {
resolve("我是后台返回的内容");
}, 2000);
});
}
function Hooks() {
const [content, setContent] = useState("loading");
// 异步请求 ,清除 timeout
useEffect(() => {
ajax().then(data => {
setContent(data);
});
// 清除定时器
return function clear() {
if (timer) {
clearTimeout(timer);
}
};
});
return (
<div>
<div style={{ padding: "30px" }}>
<Row gutter={16}>
<Col span={8}>
<Card title="useEffect" >
2s 后从后台获取 content:
{content}
</Card>
</Col>
</Row>
</div>
</div>
);
}
export default Hooks;
15.3.1 useEffect 的第二个参数
第二个参数是一个数组,只有数组的每一项都不变才会阻止 useEffect 的调用。
为什么要用第二个参数?
因为其它函数引起 Hooks 函数渲染的的时候 useEffect 每次都会执行,浪费性能。等于每执行一个其它操作都要请求一次。
// useEffect 每次都会执行
useEffect(() => {
document.title = `点击了${count}次`;
});
// 因为第一次执行的时候是空数组,第二次也是空数组他们一样,所以这个useEffect只更新一次
useEffect(() => {
ajax().then(data => {
setContent(data);
});
},[]);
手动触发更新:
// 异步请求 ,清除 timeout
useEffect(() => {
console.log('useEffect render')
const getData = async ()=>{
const res = await ajax(query)
console.log(res)
setContent(res)
}
getData()
},[query]); // 传空数组只会执行一次
// 手动改变 query 的值触发 useEffect 的更新调用渲染
<Button
size="small"
type="primary"
onClick={() => setQuery(query+1)}
>
要使用 useEffect 清除定时器,需要使用 useRef,具体请看 15.6 useRef
15.4 useContext
类似static contextType 的用法
const ThemeContext = React.createContext();
function Context() {
// 类似 contextType
const test = useContext(ThemeContext);
debugger
return <span>{test}</span>; // dark
}
export default function Hooks(){
return (
<ThemeContext.Provider value={'dark'}>
<Context />
</ThemeContext.Provider>
)
}
15.5 useMemo
15.5.1 useMemo vs memo
memo 和 PureComponent 一样是为了判断组件是否重复渲染,达到性能优化的目的。memo 主要用于函数组件 它没有 state 。PureComponent 用于类组件。
useMemo 是用于判断一段函数逻辑是否重复执行。
useMemo对应函数fnmemo对应函数组件<Foo/>- 都是为了性能优化,不做业务逻辑处理。
15.5.1 useMemo vs useEffect vs useCallback
-
useMemo是在渲染期间完成,且返回值可用作渲染。 -
useEffect是在渲染完成后执行有副作用的操作。 -
二者写法类似,第二次参数都是数组,用来对比。
-
如果
useMemo的返回值是一个函数可以用useCallback代替useMemoconst [testUseMemo,setTestUseMemo] = useState(0) const testUseCallBack = useMemo(()=>{ return function(){ setTestUseMemo(testUseMemo+1) } },[]) // 等价于上面的写法 const testUseCallBack = useCallback(()=>{ setTestUseMemo(testUseMemo+1) },[])
它们主要应用于给子组件传递参数的时候,是否渲染子组件,达到性能优化的效果
15.6 useRef
Ref 主要为了获取 dom 以及获取子组件。不支持传入函数参数进行初始化。
在函数组件中我们有三种方式创建 ref
- String ref 不推荐
- CreateRef
- Callback ref 推荐
注意:在函数组件上我们不能用上述方法创建 ref ,因为只有类组件才可以实例化。
但是在函数组件中,我们使用 useRef 创建 ref
15.6.1 useRef 的使用
-
获取
dom句柄或子组件 -
获取组件上次渲染的数据,同步到 ref 中在下次渲染的时候获得。(获取上次的 state)
- 当子组件 count 的值为 3 的时候清除定时器
const refTimer = useRef(); const [count, setCount] = useState(0); // 用 ref 保存变量 useEffect(() => { refTimer.current = setInterval(() => { setCount(count => count + 1); }, 1000); }, []); useEffect(() => { if (count >= 3) { clearTimeout(refTimer.current); } });
为什么不使用变量保存定时器然后清除?
因为组件每次都会重新渲染,拿到的变量无意义。
16. 自定义 Hooks
没什么好说的把原有的逻辑放到以函数名为use开头的自定义函数中。
目的就是为了复用代码
// 自定义 Hooks 函数名必须以 use 开头
function useNumber(defaultNumber) {
let [number, setNumber] = useState(defaultNumber);
const ref = useRef()
useEffect(() => {
ref.current = setInterval(() => {
setNumber(number => number + 1);
}, 1000);
}, []);
useEffect(()=>{
if(number>=10){
clearTimeout(ref.current)
}
})
return [number, setNumber];
}
function Counter() {
let [number, setNumber] = useNumber(0);
return (
<Button
type="primary"
onClick={() => {
setNumber(number + 10);
}}
>
点击加10 : {number}
</Button>
);
}
17. Hooks 使用注意事项
17.1 写法和配置上
-
Hooks 函数必须在函数顶层调用,不能在
if...else等条件语句中使用。- 确保执行
useState中state的唯一性,有序性。
- 确保执行
-
只能在函数组件,自定义 Hooks 中使用,为了确保能够在 Hooks 上下文中调用。
- 自定义 Hooks 函数,函数名要以
use开头。
- 自定义 Hooks 函数,函数名要以
-
安装官方推荐的 Hooks 插件eslint-plugin-react-hooks
- 配置 package.json
"eslintConfig": { "extends": "react-app", "plugins": [ "react-hooks" ], "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } }
17.2 class 组件的生命周期函数与 Hooks 的映射
import React,{useState,useEffect,memo, useRef} from 'react'
class ExampleComponent extends React.Component {
// 用于初始化 state
constructor() {}
useState()
// 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
// 因为该函数是静态函数,所以取不到 `this`
// 如果需要对比 `prevProps` 需要单独在 `state` 中维护
static getDerivedStateFromProps(nextProps, prevState) {}
useState()
static getDerivedStateFromError(){}
// Hooks 暂未实现组件错误处理的功能
// 判断是否需要更新组件,多用于组件性能优化
shouldComponentUpdate(nextProps, nextState) {}
memo() // memo 用于函数组件和 Hooks 关系不大
// 组件挂载后调用
// 可以在该函数中进行请求或者订阅
componentDidMount() {}
useEffect(()=>{
componentDidMount() {}
return ()=>{
componentWillUnmount() {}
}
},[])
// 用于获得最新的 DOM 数据
getSnapshotBeforeUpdate() {}
useEffect() 结合 useRef() 用 useRef() 保存上传的状态
// 组件即将销毁
// 可以在此处移除订阅,定时器等等
componentWillUnmount() {}
useEffect()
// 组件销毁后调用
componentDidUnMount() {}
// 组件更新后调用
componentDidUpdate() {}
useEffect()
// 渲染组件函数
render() {}
// 以下函数不建议使用
UNSAFE_componentWillMount() {}
UNSAFE_componentWillUpdate(nextProps, nextState) {}
UNSAFE_componentWillReceiveProps(nextProps) {}
}
文章涉及到的在线事例
用密:admin/admin
本文深入讲解React核心概念,包括组件、状态管理、虚拟DOM、性能优化及Hooks使用技巧,助你掌握React高级特性。

被折叠的 条评论
为什么被折叠?



