React Hooks
Hook是React v16.8的新特性,可以用函数的形式代替原来的继承类的形式,可以在不编写
class
的情况下使用state
以及其他React特性
React 设计原理
- React认为,UI视图是数据的一种视觉映射,
UI = F(Data)
,这里的F
主要负责对输入数据进行加工,对数据变更做出相应 - 公式里的
F
在React里抽象成组件,React是以组件为粒度编排应用的,组件是代码复用的最小单元 - 在设计上,React采用
props
属性来接收外部数据,使用state
属性来管理组件自身产生的数据,而为了实现(运行时)对数据变更做出相应需要,React采用基于类的组件设计 - 除此之外,React认为组件是有生命周期的,因此提供了一系列API供开发者使用
我们所熟悉的React组件长这样
import React, { Component } from "react";
// React基于Class设计组件
export default class Button extends Component {
constructor() {
super();
// 组件自身数据
this.state = { buttonText: "Click me, please" };
this.handleClick = this.handleClick.bind(this);
}
// 响应数据变更
handleClick() {
this.setState({ buttonText: "Thanks, been clicked!" });
}
// 编排数据呈现UI
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
组件类的缺点
上面实例代码只是一个按钮组件,但是可以看到,它的代码已经很重了。真实的React App由多个类按照层级,一层层构成,复杂度成倍增长。再加入 Redux + React Router,就变得更复杂
很可能随便一个组件最后export
出去就是酱紫的:
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
一个4层嵌套HOC,嵌套地狱
同时,如果你的组件内事件多,那么你的constructor
就是酱紫的
class MyComponent extends React.Component {
constructor() {
// initiallize
this.handler1 = this.handler1.bind(this)
this.handler2 = this.handler2.bind(this)
this.handler3 = this.handler3.bind(this)
this.handler4 = this.handler4.bind(this)
this.handler5 = this.handler5.bind(this)
// ...more
}
}
而Function Component编译后就是一个普通的function,function对js引擎是友好的,而Class Component在React内部是当做Javascript Function类来处理的,代码很难被压缩,比如方法名称
还有this
啦,稍微不注意就会出现因this
指向报错的问题等。。。
总结一下就是:
- 很难复用逻辑,会导致组件树层级很深
- 会产生巨大的组件(很多代码必须写在类里面)
- 类组件很难理解,比如方法需要
bind
,this
的指向不明确 - 编译size,性能问题
Hooks
State Hook
Hook是什么?
可以先通过一个例子来看看,在class中,我们通过在构造函数中设置this.state
初始化组件的state:
this.state = {
n: 0
}
而在函数组件中,我们没有this
,所以我们不能分配或读取this.state
,但是可以在组件中调用useState
Hook
import React, {useState} from 'react';
function xxx() {
const [n, setN] = useState(0);
}
在上面代码中,useState
就是Hook
Hook是一个特殊的函数,它可以让你“钩入”React的特性。例如
useState
是允许你在React函数组件中添加state
的Hook。
如果你在编写函数组件并意识到需要向其添加一些state
,以前的做法是必须将其转化为Class。现在你可以在现有的函数组件中使用Hook
让函数组件自身具备状态处理能力,且自身能够通过某种机制触发状态的变更并引起re-render,这种机制就是Hooks
走进useState
示例代码:
import React, { useState } from 'react';
function App() {
// 声明一个叫 "n" 的 state 变量
// useState接收一个参数作为初始值
// useState返回一个数组,[state, setState]
const [n, setN] = useState(0);
return (
<div>
{/* 读取n,等同于this.state.n */}
<p>{n}</p>
{/* 通过setN更新n,等同于this.setN(n: this.state.n + 1) */}
<button onClick={() => setN(n + 1)}>
+1
</button>
</div>
);
}
运行一下(代码1)
- 首次渲染 render
<App />
- 调用
App
函数,得到虚拟DOM对象,创建真实DOM - 点击buttno调用
setN(n + 1)
,因为要更新页面的n
,所以再次render<App />
- 重复第二步,从控制台打印看出每次执行
setN
都会触发App
函数运行,得到一个新的虚拟DOM,DOM Diff更新真实DOM
那么问题来了,首次运行App
函数和setN
时都调用了App
,两次运行useState
是一样的吗?setN
改变n
的值了吗?为什么得到了不一样的n
,useState
的时候做了什么?
分析:
- setN
- setN一定会修改数据x,将n+1存入x
- setN一定会触发
<App />
重新渲染(re-render)
- useState
- useState肯定会从x读取n的最新值
- x
- 每个组件都有自己的数据x,我们将其命名为state
尝试实现React.useState(代码2)
// 和useState一样,myUseState接收一个初始值,返回state和setState方法
const myUseState = initialValue => {
let state = initialValue
const setState = newValue => {
state = newValue
// 重新渲染
render()
}
return [state, setState]
}
const render = () => {
// 鸡贼暴力渲染法
ReactDOM.render(<App />, rootElement)
}
function App() {
const [n, setN] = myUseState(0)
...
}
点击button,n没有任何变化
原来每次state
都变成了初始值0
,因为myUseState
会将state
重置
我们需要一个不会被myUseState
重置的变量,那么这个变量只要声明在myUseState
外面即可
let _state;
const myUseState = initialValue => {
// 如果state是undefined,则赋给初始值,否则就赋值为保存在外面的_state
_state = _state === undefined ? initialValue : _state;
const setState = newValue => {
_state = newValue;
render();
};
return [_state, setState];
};
还有问题,如果一个组件有俩state咋整?由于所有数据都放在_state,产生冲突:
function App() {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
...
}
解决:
- 把_state做成对象
- 不可行,没有key,
useState(0)
只传入了一个参数0
,并不知道是n
还是m
- 不可行,没有key,
- 把_state做成数组
- 可行,
_state = [0, 0]
- 可行,
let _state = [];
let index = 0;
const myUseState = (initialValue) => {
const currentIndex = index;
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
render();
};
index += 1;
return [_state[currentIndex], setState];
};
const render = () => {
// 重新渲染要重置index
index = 0;
ReactDOM.render(<App />, rootElement);
};
解决了存在多个state的情况,但是还有问题,就是useState
调用顺序必须一致!
- 如果第一次渲染时n是第一个,m是第二个,k是第三个
- 则第二次渲染时必须保证顺序一致,因为数组根据调用顺序存储值
- re-render时会从第一行代码开始重新执行整个组件
- 所以React不允许出现如下代码
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
最后一个问题:
App用了_state和index,那其他组件用什么?放在全局作用域重名怎么解决?
运行App后,React会维护一个虚拟DOM树,每个节点都有一个虚拟DOM对象(Fiber),将_state,index存储在对象上
额外扩展一下Fiber对象,它的数据结构如下:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance 实例
this.tag = tag;
this.key = key;
// JSX翻译过来之后是React.createElement,他最终返回的是一个ReactElement对象
// 就是ReactElement的`?typeof`
this.elementType = null;
// 就是ReactElement的type,他的值就是<MyClassComponent />这个class,不是实例,实例是在render过程中创建
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
// 用来存储state
// 记录useState应该返回的结果
this.memoizedState = null;
this.firstContextDependency = null;
// ...others
}
总结:
- 每个函数组件对应一个React节点(FiberNode)
- 每个节点保存着
_state
(memorizedState)和index
(实际是链表) -
useState
会读取对应节点的state[index] -
index
是由useState
的调用顺序决定 -
setState
会修改_state
,并触发更新
搞清楚useState
干了啥以后,回过头再看setN
改变n
了吗,为什么得到了不一样的n
(代码3)
- 先+1,后log => 1
- 先log,后+1 => 0
- 为什么log出了旧数据
分析:
- 先点击log,
log(0)
三秒后执行,此时n
是0
,n
不会变 - 再点击+1,此时调用的是一个新的函数,生成了新的
n
,re-render -
n=0
和n=1
同时存在内存中
结论:因为有多个n
,setN
并不会改变n
,React函数式编程决定了n的值不会被改变,只会被回收
注意事项:
- 不可局部更新(代码4)
- 地址要变:
setState(obj)
如果obj
地址不变,那么React就认为数据没有变化 - useState接受函数:函数返回初始state,且只执行一次
- setState接收函数:setN(i => i + 1),优先使用这种形式
useReducer
React本身不提供状态管理功能,通常需要使用外部库,最常用的库是
Redux
Redux的核心概念是,将需要修改的state都存入到store里,发起一个action用来描述发生了什么,用reducers描述action如何改变state,真正能改变store中数据的是store.dispatch API
Reducer是一个纯函数,只承担计算 State 的功能,函数的形式是(state, action) => newState
Action是消息的载体,只能被别人操作,自己不能进行任何操作
useReducer()
钩子用来引入Reducer功能(代码5)
const [state, dispatch] = useReducer(reducer, initial)
上面是useReducer
基本用法
- 接受Reducer函数和一个初始值作为参数
- 返回一个数组,数组
[0]
位是状态当前值,第[1]
位是dispatch
函数,用来发送action
似曾相识的感觉
const [n, setN] = useState(0)
// n:读
// setN:写
总的来说useReducer就是复杂版本的useState,那么什么时候使用useReducer,什么时候又使用useState呢?
看一个代码6
当你需要维护多个state,那么为什么不用一个对象来维护呢,对象是可以合并的
需要注意的是,由于Hooks可以提供状态管理和Reducer函数,所以在这方面可以取代Redux。但是,它没法儿提供中间件(midddleware)和时间旅行(time travel),如果你需要这两个功能,还是要用Redux。
中间件原理:封装改造store.dispatch,将其指向中间件,以实现在dispatch和reducer之间处理action数据的逻辑,也可以将中间件看成是dispatch方法的封装器
有没有代替Redux的方法呢?
Reducer + Context
useContext
什么是上下文?
- 全局变量是全局的上下文
- 上下文是局部的全局变量
使用方法:
// 创建上下文
const c = createContext(null)
function App() {
const [n, setN] = useState(0)
return (
// 使用<c.Provider>圈定作用域
<c.Provider value={n, setN}>
<Father />
</ c.Provider>
)
}
function Father() {
return (
<div>我是爸爸
<Son />
</div>
)
}
function Son() {
// 在作用域中使用useContext(c)来获取并使用上下文
// 要注意这里useContext返回的是对象,不是数组
const {n, setN} = useContext(c)
const onClick = () => {
setN( i => i + 1)
}
return (
<div>我是儿子,我可以拿到n:{n}
<button onClick={onClick}>我也可以更新n</button>
</div>
)
}
注意事项:
- 使用useContext时,在一个模块改变数据,另一个模块是感知不到的
-
setN
会重新渲染<App />
,自上而下逐级通知更新,并不是响应式,因为响应式是监听数据变化通知对应组件进行更新
useEffect
useEffect钩子会在每次render后运行
React保证了每次运行useEffect的同时,DOM 都已经更新完毕
应用:
- 作为
componentDidMount
使用,[]作第二个参数 - 作为
componentDidUpdate
使用,可指定依赖 - 作为
componentWillUnmount
使用,通过return - 以上三种可同时存在
function App() {
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
const afterRender = useEffect;
// componentDidMount
useEffect(() => {
console.log('第一次渲染之后执行这句话')
}, [])
// componentDidUpdate
useEffect(() => {
console.log('每次次都会执行这句话')
})
useEffect(() => {
console.log('n变化就会执行这句话,包含第一次')
}, [n])
// componentWillUnmount
useEffect(() => {
const id = setInterval(() => {
console.log('每一秒都打印这句话')
}, 1000)
return () =>{
// 如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
console.log('当组件要挂掉了,打印这句话')
window.clearInterval(id)
}
}, [])
return (
<div>
n: {n}
<button onClick={onClick}>+1</button>
</div>
)
}
Hook 允许我们按照代码的用途分离他们,而不是像生命周期函数那样
React将按照effect声明的顺序依次调用组件中的每一个effect
对应的,另一个effect钩子,useLayoutEffect
- useEffect在浏览器渲染之后执行,useLayoutEffect在渲染前执行(代码7)
- useLayoutEffect在渲染前执行,使用它来读取 DOM 布局并同步触发重渲染
// 伪代码
App() -> 执行 -> VDOM -> DOM -> useLayoutEffect -> render -> useEffect
特点:
- useLayoutEffect性能更好,但是会影响用户看到页面变化的时间(代码7)
- useLayoutEffect总是比useEffect先执行
- useLayoutEffect里的任务最好是影响了layout
- 还是推荐优先使用useEffect(如果不涉及操作dom的操作)
为什么建议将修改DOM的操作放到useLayoutEffect里,而不是useEffect呢,是因为当DOM被修改时,浏览器的线程处于被阻塞阶段(js线程和浏览器线程互斥),所以还没有发生回流、重绘。由于内存中的DOM已经被修改,通过useLayoutEffect可以拿到最新的DOM节点,并且在此时对DOM进行样式上的修改。这样修改一次性渲染到屏幕,依旧只有一次回流、重绘的代价。
注意:
由于useEffect是在render之后执行,浏览器完成布局和绘制后,不应在函数中执行阻塞浏览器更新屏幕的操作
useMemo
React默认有多余的render(修改n,但是依赖m的组件却自动刷新了),如果props不变就没有必要再执行一次函数组件,先从一个例子来理解memo(代码8)
这里有一个问题,如果给子组件一个方法,即使prop没有变化,子组件还是会每一次都执行
const onClickChild = () => {}
<Child data={m} onClick={onClickChild} />
这是因为在App重新渲染时,生成了新的函数,就像一开始讲的多个n的道理一样,新旧函数虽然功能一样,但是地址不一样,这就导致props还是变化了
那么对于子组件的方法,如何重用?
使用useMemo钩子(代码9)
const onClickChild = useMemo(() => {
return () => {
console.log(m)
}
}, [m])
特点:
- useMemo第一个参数是
() => value
(value可以是函数、对象之类的),第二个参数是依赖数组[m]
- 只有当依赖变化时,才会重新计算新的value
- 如果依赖没有变化,就重用之前的value
- 这不就是vue中的
computed
吗?
注意:
- 如果你的value是个函数,那么你要写成
useMemo(() => x => console.log(x))
- 这是一个返回函数的函数
- 这么难用的话,用用useCallback
// useMemo
const onClickChild = useMemo(() => {
return () => {
console.log(m)
}
}, [m])
// useCallback
const onClickChild = useCallback(() => {
console.log(m)
})
// 伪代码
useCallback(x => log(x), [m]) 等价于 useMemo(() => x => log(x), [m])
useMemo
和useCallback
作用完全一样,语法糖而已
useRef
一直用到的这个例子,每点击一下就会重新渲染一下App
function App() {
console.log('App 执行');
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
return (
<div>
<button onClick={onClick}>update n {n}</button>
</div>
)
}
假如我要知道这个App执行了多少次,我怎么记录?
如果我需要一个值,在组件不断render的时候也能够保持不变怎么做?
function App() {
// count的值通过useRef记录了下来
// 初始化
const count = useRef(0)
useEffect(() => {
// 读取 count.current
count.current += 1
})
}
同样的,useRef
也是通过它所对应的fiberNode对象来保存
为什么需要current?
- 为了保证两次useRef是同一个值,只有引用才能做到
- useRef存储的实际上是一个对象
{currnt: 0}
,对象对应的是同一个地址(内存) - 每次改变只是改变对象中的值,而不是改变对象,新旧组件必须引用同一个对象
讲了useRef
就不得不讲讲forwardRef
了
在函数组件中怎么使用ref,尝试一下(代码10)
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
说明,props无法传递ref属性
所以,函数组件用ref的话,需要用forwardRef
包装做一下转发,才能拿到ref
自定义Hook
通过自定义Hook,可以将组件逻辑提取到可重用的函数中
自定义Hook是一个函数,其名称以 “use” 开头(符合 Hook 的规则),函数内部可以调用其他的Hook
每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的(每次调用 Hook,它都会获取独立的 state)
代码
网友评论