前言
学了几天 React hook了,虽然还没有打过几行代码但是还是小有感触,记录一下。
作为一个 Vuer,深深感到 Vue 与 react hook 的思维太不一样了,它们完全是两种思维方式。
不过还是还是有不少 React 的 api 作用可以套用 Vue 的 api 来理解的。
理解 hooks —— useReducer
React 文档总是以 useState 开始介绍 hook
设置 hook 中的状态
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
<template>
<div>
<p>You clicked {{count}} times</p>
<!-- 我实际是 this.count++ -->
<button @click="count++">
Click me
</button>
</div>
</template>
<script>
export default{
data(){
return {
count:0
}
}
}
</script>
复制代码
如果你以 Vue 的思维理解这个 Example 组件,这个函数只会执行一次,而下面返回的 render 函数会被执行多次;
显然,函数执行一次是肯定不能完成 jsx 的状态随组件状态改变,比如 count,它设置为 0,这个 jsx 上的 count就是 0,如果只执行一次,jsx 上这里永远都是 0;而 Vue 的模板在编译时做了处理,让模板上绑定的 count 访问到 this.count,因为是引用的值,所以随时都可以访问到最新的值。
正确的理解是,函数组件每一轮渲染都会被执行一次,jsx 也就每一轮都能访问到最新值了。
React 哲学中有个很著名的公式 UI = fn(state) 状态经过处理展现 UI,又总是提及它的函数式特性和数据不可变的特性,看起来高深莫测,其实不然,当你理解了 useReducer 就大致理解了。
我们首先得明白,useState 是 useReducer 的简单版
import React, { useState , useReducer} from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
setCount(count + 1)
}
// 等价写法
function Example() {
// 用 useReducer 声明一个叫 "count" 的 state 变量
const [count, dispatch] = useReducer((state,action)=>action,0);
dispatch(count + 1)
}
复制代码
第一个参数接收一个函数,state 表示上一轮接收的值,action 表示上一轮调用 dispatch 传入的值,函数返回值作为当前状态值即 count,第二个参数为初始默认值。
不知道你发现没有,它简直与数组的 reduce 一模一样!
const [count, dispatch] = useReducer((state,action)=>action,0)
[1,2,3].reduce((preCount,curCount)=>curCount,0)
数组是有限的,而状态流是无限的,每一个状态依据上一个状态生成,状态生成后就无法改变了,而 setState 或者 setReducer 就是推进状态流动的动力,React会根据最新的状态重新渲染 UI
不知道你是否能意会 React 的函数式和不可变特性呢?
hooks API
useState,useReducer
useState 和 useReducer 已经提及,一般来说,在状态转移的逻辑比较繁杂时,使用 useReducer 更为合适
useState 和 useReducer 有缓存的特点,如果不调用 setState ,useState 始终返回最后的状态而并非新建一个状态(对于引用类型这很重要)
function Example(){
const [ref] = useState({ current:1 }) // useState 始终返回第一个传入的对象
ref.current = ref.current + 1
}
复制代码
useEffect,useMemo,useCallback
三个 api 都是需要依赖项,根据依赖项进行更新且都有缓存的特点。
判断依赖是否更新
react 的判断方式很简单,直接使用 ===
符号判断
如果 useState(obj) 时,遵循 react 的不可变特性,应该 setObj({ …obj,a: 1 }) ,而不是 obj.a = 1;setObj(obj)
function Example(prop){
const [obj,setObj] = useState(prop.obj)
obj.a = 1
setObj(obj) // 这不是依赖变更,不会触发更新
}
复制代码
因为 obj 始终是同一个,所以它并不会触发更新。
但是稍不注意,会引发不必要的更新或渲染
function Son(prop){
useEffect(callback,[prop.arr]) // callback 每次都会执行
}
function Father(){
Son({arr:[1,2,3]})
}
复制代码
arr 看起来每次传入的都是一样的,实际上它们是不同的数组,所以 Son 的 effect 回调每次都会执行。
useEffect
可以比拟 Vue 中的 watch,第一个参数是执行的函数 callback,第二个参数数组就是它的观察的依赖,当依赖项发生变化,重新执行 callback 并缓存 callback返回值。
同时,它远比 watch 强大,因为它蕴含了生命周期执行时机 ,useEffect 传入的函数一定会执行第一次,执行的生命周期就是 mouted;更新时,执行的生命周期就是 updated;在卸载时, 缓存的 callback 返回值也会被执行一次,执行的生命周期就是 destroyed。
// 传一个空依赖数组,它就可以用作生命周期函数
// 在 mouted 执行一次 callback
export const useMount = (callback: () => void) => {
useEffect(() => {
callback()
}, [])
}
// 在 unmouted 执行一次 callback
export const useUnMount = (callback: () => void) => {
useEffect(() => () => callback(), [])
}
// 可以围绕 a 写入在挂载和卸载中的逻辑
const DemoEffect = ({ a }) => {
/* 模拟事件监听处理函数 */
const handleResize =()=>{}
useEffect(()=>{
/* 定时器 延时器等 */
const timer = setInterval(()=>console.log(666),1000)
/* 事件监听 */
window.addEventListener('resize', handleResize)
/* 此函数用于清除副作用 */
return function(){
clearInterval(timer)
window.removeEventListener('resize', handleResize)
}
},[ a ])
return (<div >
</div>)
}
复制代码
补充:useLayoutEffect
useEffect
执行顺序: 组件更新挂载完成 -> 浏览器 dom
绘制完成 -> 执行 useEffect
回调。
useLayoutEffect
执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect
回调-> 浏览器dom
绘制完成。
所以说 useLayoutEffect
代码可能会阻塞浏览器的绘制 。我们写的 effect
和 useLayoutEffect
,react
在底层会被分别打上PassiveEffect
,HookLayout
,在commit
阶段区分出,在什么时机执行。
useMemo 和 useCallback
Memo 是 Memory 的缩写,顾名思义就是缓存的意思,useMemo 与 Vue 中的 computed 基本一模一样,用途也相同,封装一些逻辑或者避免大量的运算
function Example(){
const [count,useCount] = count(1)
const handleCount = useMemo(()=>{
...用count进行大量运算
return result
},[count]) // count 不变就返回上次运算的结果
}
复制代码
因为 jsx 的动态性,配合 useMemo,可以避免进行重复的 jsx 的运算
/* 用 useMemo包裹的list可以限定当且仅当list改变的时候才更新此list,这样就可以避免selectList重新循环 */
{useMemo(() => (
<div>{
selectList.map((i, v) => (
<span
className={style.listSpan}
key={v} >
{i.patentName}
</span>
))}
</div>
), [selectList])}
复制代码
useCallback 是用来缓存函数的,其实就是 useMemo 的函数专用版;
function Example(){
const [count,setCount] = useState(0)
useCallback(()=>count + 1,[count]) //等价于 useMemo(()=>()=>count + 1,[count])
}
复制代码
缓存注意点
三个 api 都有缓存的特性( useEffect 是缓存callback的返回值,即缓存 unmouted 回调函数),仅在依赖变更时,才更新缓存。既然是缓存,那么它使用的一定是缓存时的状态而并非当前的状态
function Example(){
const [count,useCount] = count(1)
useEffect(()=>{
return ()=>console.log(count)
},[])
const logCount = useCallback(()=>console.log(count),[])
setCount(count + 1)
}
复制代码
在 unmouted 阶段或者调用 logCount 会打印什么呢?如果你的答案不为 1,说明你对“缓存”的理解还不够透彻。它们都打印出 1,因为它们只在初始化执行过一次,此时的状态被函数的闭包保存,后续没有更新缓存,所以闭包上的 count 一直都是初始化的值 1。
一般情况下函数内部使用的依赖都应该写入依赖数组里。
如果函数内使用了依赖却没写入依赖数组,eslint 会报错以防止这种看起来疑惑的表现
Vue 的 computed 是在第一次执行函数时,自动收集使用到的所有依赖
useRef
我个人认为是一种以显示声明的方式摆脱不可变特性的一个 api ,useRef 会返回一个对象,对象上只有一个属性就是 current 属性。
useRef 初始化后,每次都返回第一次初始化的对象,所以在不同的状态下,我们都可以用 ref.current 访问到最新的 current 值,它挺像 Vue 中的 this
- 用法一:可以用来获取
dom
元素,或者class
组件实例 。
const DemoUseRef = ()=>{
const dom= useRef(null)
const handerSubmit = ()=>{
/* <div >表单组件</div> dom 节点 */
console.log(dom.current)
}
return <div>
{/* ref 标记当前dom节点 */}
<div ref={dom} >表单组件</div>
<button onClick={()=>handerSubmit()} >提交</button>
</div>
}
复制代码
ref 属性会自动将 div 的 dom节点(如果是类组件就是组件的实例,如果是函数组件需要使用 useImperativeHandle) 赋予 dom.current
- 用法二: 通过 useRef 保存一些不同于当前状态的数据。
// 上面函数的迷惑表现用 ref 就不会出现了
function Example(){
const countRef = useRef(1)
const {current:count} = countRef
useEffect(()=>{
return ()=>console.log(count)
},[])
const logCount = useCallback(()=>console.log(count),[])
countRef.current = current + 1
}
复制代码
// 这个组件作用是 初始化用 ref 保存初始 title,使用时传入新的 title,document.title 就更改
// 组件挂载时,恢复页面标题为初始 title
export const useDocumentTitle = (title: string, keepOnUnmount = true) => {
const oldTitle = useRef(document.title).current;
// 页面加载时: 旧title
// 加载后:新title
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => {
return () => {
if (!keepOnUnmount) {
// 如果不指定依赖,读到的就是旧title
document.title = oldTitle;
}
};
}, [keepOnUnmount, oldTitle]);
};
复制代码
不知道你是否记得第一个例子,其实原理上 useRef 并没有多大区别
function Example(){
const [ref] = useState({ current:1 })
ref.current = ref.current + 1
}
复制代码
为何不能用上面的方式呢?因为 state 在 React 是不可变的,useRef 就是显式表明了引用,React 开了一道口子
useImperativeHandle
使用 ref 引用某个组件时,class 组件引用到实例,dom 引用到真实的 dom 节点,而函数组件没有实体,无法用 ref 直接引用,useImperativeHandle 的出现就是解决这个问题,它给予函数组件自定义暴露给父组件 ref 对象的能力
useImperativeHandle
接受三个参数:
- 第一个参数ref: 接受
forWardRef
传递过来的ref
。 - 第二个参数
createHandle
:处理函数,返回值作为暴露给父组件的ref
对象。 - 第三个参数
deps
:依赖项deps
,依赖项更改形成新的ref
对象。
function Son (props,ref) {
const inputRef = useRef(null)
const [ inputValue , setInputValue ] = useState('')
useImperativeHandle(ref,()=>{
// 自定义 ref
const handleRefs = {
/* 声明方法用于聚焦input框 */
onFocus(){
inputRef.current.focus()
},
/* 声明方法用于改变input的值 */
onChangeValue(value){
setInputValue(value)
}
}
return handleRefs
},[])
return <div>
<input
placeholder="请输入内容"
ref={inputRef}
value={inputValue}
/>
</div>
}
复制代码
useContext
在 hook 时代新的状态管理方式
- 首先使用 React.createContext() 创建 Context
- Context 上的属性 Provider 和 Consumer,它们都是虚拟组件
- Provider 提供属性,Customer 接收属性
- useContext 可以代替 Customer 接收属性
// 创建上下文
const AuthContext = React.createContext(undefined)
AuthContext.displayName = 'AuthContext'
// 作为包装层
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState(null)
// 作为内部共享的值
return (
<AuthContext.Provider
children={children}
value={{ user }}
/>
)
}
export const useAuth = () => {
const context = React.useContext(AuthContext)
if (!context) {
throw new Error('useAuth 必须在 AuthProvider 中使用')
}
return context
}
// 某个子组件内
<AuthContext.Consumer>
{({user}) => /* 基于 context 值进行渲染*/}
</AuthContext.Consumer>
// 或者用 useAuth
const {user} = useAuth() // 后面 jsx 就可以用到这个数据了
复制代码
Provider 和 Customer 顾名思义,提供者和消费者,其实它与 Vue 的 provide 和 inject 非常相似,一个外层提供数据,内层消费数据,不过 React 是更为明显得将各个提供者,消费者也进行了划分。
hook 魔法
使用hook总是有些疑问,令我不安
-
useState 等 hook 是如何与状态一一对应起来的?
function Example(){ const [count1,setCount1] = useState(0) const [count2,setCount2] = useState(0) } 复制代码
同样的输入,为何第一个 useState 就是返回 count1 而 第二个 useState 返回 count2?
-
多处使用 Example 组件,如何做到每处都像类组件一样拥有自己的状态?
首先函数组件自己本身肯定是无状态的,但是别忘了多处使用的函数组件是由 React 调用的,所以它肯定为每处的函数组件生成了包含了此次函数组件信息对象,就是 workInProgress
React 执行函数组件的时,会为每个hook生成一个hook 节点,hook 节点的 next 指向下一个 hook 节点,这个 hook 链表头节点挂在 workInProgress.memoizedState 上
因为是按照顺序记录的,下一次函数组件执行时,某个位置就可以访问到的上次状态对应位置的值了。这也是 React 要求 hook 一定要放在最外层的原因,因为如果某个 hook 不执行,next 指针就会错乱,后续的 hook 访问值就可能错乱
Css in JS
react 如何编写 css 呢?一个很流行的方案就是 css in js,一个出名的实现库就是 emotion
import styled from "@emotion/styled";
export const Row = styled.div<{
gap?: number | boolean;
between?: boolean;
marginBottom?: number;
}>`
display: flex;
align-items: center;
justify-content: ${(props) => (props.between ? "space-between" : undefined)};
margin-bottom: ${(props) => props.marginBottom + "rem"};
> * {
margin-top: 0 !important;
margin-bottom: 0 !important;
margin-right: ${(props) =>
typeof props.gap === "number"
? props.gap + "rem"
: props.gap
? "2rem"
: undefined};
}
`;
// jsx 中使用时
<Row between={ true } marginBottom={ 11 } />
复制代码
可以看到使用很简单
- styled.div 被适用 css 的标签
- “` ` 是 es6 中的语法 “模板标签”
- 最大的特点就是复用静态的 css,通过传入 prop 定义动态css;Vue中的方案是切换 css 类名或者动态的行内 style
- 可以方便得复用伪类(style 中无法使用伪类)
- 缺点:没有高亮,没有 css 提示
我个人使用感觉,在使用 css in js 定义某些小型的通用组件是一种非常不错的方案,不过在业务组件等还是使用 Vue 的 <style scoped></style>
是最舒服的,不过目前 react 并没有官方方案