Hooks 各个击破

大纲链接 §

[toc]


React 内置的 Hooks 主要有以下几个:

  • useState 组件状态
  • useEffect 副作用
    • useLayoutEffect
  • useContext 上下文
  • useMemo 缓存
    • useCallback 回调
  • useReducer 统一状态管理(替代Redux
  • useRef 引用
    • useImperativeHandle
  • useDebugValue 自定义

1. useState

使用组件状态

  • const [n, setN] = React.useState(0) 原始类型
  • const [user, setUser] = React.useState({name: 'F'}) 复杂类型

注意事项1:不可局部更新属性,不自动合并

  • 如果state是一个复杂类型(对象等),禁止部分setState示例代码
    • 因为setState不会自动合并属性,缺失没有写入的属性
    • useReducer也不会自动合并
  • 解决方法:使用 ...,在对象中拷贝原状态的属性,再局部覆盖同名属性
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  const [user,setUser] = useState({name:'Franche', age: 18})
  const onClick = () => {
    setUser({
    ...user,
      name: 'Jack' // 同名覆盖原先的name属性
    })

    /*
    setUser({
      name: 'Jack' // 缺失 age 属性
    })
    */
  }
  return (
    <div className="App">
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <button onClick={onClick}>Click</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

注意事项2:地址要变

  • setState(obj) 如果obj不变,那么React中数据不变
  • React 只侦测对象地址变化,不侦测同对象的属性变化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  const [user,setUser] = useState({name:'Franche', age: 18})
  const onClick = () => {
    // 错误的演示:地址不变,React中数据不变
    user.name = 'Mick'
    setUser(user) // 不会更新UI
    /*
    setUser({
      ...user,
      name: 'Jack'
    }) // 传入的是一个新对象

    */
  }
  return (
    <div className="App">
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <button onClick={onClick}>Click</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

注意事项3 接受函数

useState接受函数:const [x, setX] = useState(()=>{})

  • const [state, setState] = useState( () => {return initialValue} )
  • const [state, setState] = useState( () => ({/*initialValue*/}) )
  • 该函数返回初始state,且只执行一次
    • 区别于useState传对象,JS每次会解析该对象
    • useState传工厂函数,减少内部多余的计算过程

setState接受函数:setState(()=>{})

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {useState} from 'react'
import ReacDOM from 'react-dom'

function App() {
  const [n, setN] = useState(0)
  const onClick = () => {
    setN(n + 1) // n 不会变
    setN(n + 2) // 只有最后一次有效
    /* 发现 n 不能 +1 再 +2 */

    // 改为传函数则符合预期
    setN(i => i + 1)
    setN(i => i + 1)
  }
  return (
    <div className="app">
      <h1>n: {n}</h1>
      <button onClick={onClick}>+2</button>
    </div>
  )
}
const rootElement = document.getElementById('root')
reactDOM.render(<App />, rootElement)
  • setN(i => i + 1)并不直接传值,而是传一个函数来操作,形参占位符i命名随便
  • 如果需要对setN多次操作,更推荐使用函数

2. useReducer

用来践行 Flux/Redux 的思想

  • 代码示例,共分4步走
    • 1.创建初始值initialValue
    • 2.创建所有操作reducer(state, action)
    • 3.传给useReducer(所有操作, 初始数据),获取 API
    • 4.调用 dispatch({type: '操作类型'}),不是直接操作state,而是通过reducer来操作
  • 总的来说useReducer就是useState的复杂版
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import {useReducer} from 'react'
import ReactDOM from 'react-dom'

// 1. 创建初始值 initialValue
const initialValue = {n: 0, m: 0}

// 2. 创建所有操作 reducer(state旧的状态, action操作含有自定义的属性)
/*
* @params({}) state
* @params({type: string}) action
* @return newState
*/
const reducer = (state, action) => {
  if(action.type === 'add') {
    // 不可以直接操作属性 state.n += 1; return state 同useState的规则
    // return {...state, n: state.n + 1}
    return {...state, n: state.n + action.number} // 还可以传额外属性参数
  } else if(action.type === 'multi2') {
    return {...state, n: state.n * 2}
  } else {
    throw new Error('unknown type')
  }
}

function App() {
  // 3. 传给 useReducer,获取 读 和 写 的 API
  const [state, dispatch] = useReducer(reducer, initialValue)
  // 也可以直接解构出属性 const {n} = state

  const onClick = () => {
    // 4. 调用 写dispatch({type: '操作类型'})
    // dispatch({type: 'add'}) // 调用`add`分支操作
    dispatch({type: 'add', number: 1}) // 还可以传额外属性参数
  }
  const onClickAdd2 = () => {
    dispatch({type: 'add', number: 2})
  }
  const onClickMulti2 = () => {
    dispatch({type: 'multi2'})
  }

  return (
    <div className="app">
      <h1>n: {state.n}</h1>
      <button onClick={onClick}>n + 1</button>
      <button onClick={onClickAdd2}>n + 2</button>
      <button onClick={onClickMulti2}>n * 2</button>
    </div>
  )
}
const rootElement = document.getElementById('root')
reactDOM.render(<App />, rootElement)
  • 不可以直接操作属性 state.n += 1; return stateuseState的规则
  • 对多个状态做整体操作
  • 将所有操作聚拢在函数const reducer = (state, action) => {}
    • 通过action的不同属性,来调用对应不同的分支操作
    • 通过action还可以传额外属性
  • 每次用户操作更新数据,App就重新render一遍

如何判断使用useReducer还是useState

  • 事不过三原则:有多个数据可以在一起放入同一个对象里,就适用useReducer,对这个对象做整体操作
  • 例如表单中的数据,见 useReducer表单代码示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// import...

const initFormData = {
  name: 'xxx',
  age: 18,
  nationality: '泰伦'
}

// 汇总操作
function reducer(state, action) {
  switch (action.type) {
    case "patch": // 更新 合并新旧属性
      return {...state, ...action.formData}
    case "reset": // 重置
      return initFormData
    default:
      throw new Error('type传错了')
  }
}

// 生成表单数据
const formDataGenerator = (text, value, cb) => ({ text, value, cb })

function App() {
  console.log('App执行了一遍')

  // 获取读写接口
  const [formData, dispatch] = useReducer(reducer, initFormData)

  // 表单操作
  const onReset = () => { dispatch({type: 'reset'}) }
  const onSubmit = () => {alert('提交了'); onReset()}

  // 提取patch公共方法
  const patch = (key, value) => {
    dispatch({type: 'patch', formData: {[key]: value} })
  }

  // 表单汇总数据
  const formContentList = [
    formDataGenerator( '姓名', formData.name, e => patch('name', e.target.value) ),
    formDataGenerator( '年龄', formData.age, e => patch('age', e.target.value) ),
    formDataGenerator( '名族', formData.nationality, e => patch('nationality', e.target.value) )
  ]

  return (
    <form onSubmit={onSubmit} onReset={onReset}>
      {
        formContentList.map(item => {
          return (
            <div key={item.text}>
              <label>
                {item.text}
                <input value={item.value} onChange={item.cb} />
              </label>
            </div>
          )
        })
      }
      <div>
        <button type="submit">提交</button>
        <button type="reset">重置</button>
      </div>
      <hr/>
      {JSON.stringify(formData)}
    </form>
  )
}

// render...

3. 用 useReducer 代替 redux

辅助函数和基本结构

辅助函数,假的AJAX获取数据,调用时传入参数 url,返回对应数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 辅助函数
// 假 ajax
// 两秒钟后,根据 path 返回一个对象,必定成功不会失败
function ajax(path) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (path === "/user") {
        resolve({
          id: 1,
          name: "Frank"
        });
      } else if (path === "/books") {
        resolve([
          {
            id: 1,
            name: "JavaScript 高级程序设计"
          },
          {
            id: 2,
            name: "JavaScript 精粹"
          }
        ]);
      } else if (path === "/movies") {
        resolve([
          {
            id: 1,
            name: "爱在黎明破晓前"
          },
          {
            id: 2,
            name: "恋恋笔记本"
          }
        ]);
      }
    }, 2000);
  });
}

基本结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React, { useReducer, useContext, useEffect, createContext } from "react";
import ReactDOM from "react-dom";

// 统一的数据储存
const store = {
  user: null,
  books: null,
  movies: null
}

// 统一的操作
const reducer = (state, action) => {}

// 统一的上下文 数据提供 组件
const Context = createContext(null)

function User() {
  const {state, dispatch} = useContext(Context) // 注意是获取对象使用花括号
  return (
    <div>
      <h1>个人信息</h1>
    </div>
  )
}

function Books() {
  const {state, dispatch} = useContext(Context)
  return (
    <div>
      <h2>书籍信息</h2>
    </div>
  )
}

function Movies() {
  const {state, dispatch} = useContext(Context)
  return (
    <div>
      <h2>电影信息</h2>
    </div>
  )
}

function App() {
  // 创建对数据的读写API
  const [state, dispatch] = useReducer(reducer, store)
  return (
    {/* 统一的上下文 数据提供 */} {/* 提供数据对象 {state, dispatch} ES6对象语法缩写 */}
    <Context.Provider value={{state, dispatch}}>
      <User />
      <hr />
      <Books />
      <Movies />
    </Context.Provider>
  )
}

// render...

复杂useReducer使用步骤

  1. 将数据几种在一个store对象中
  2. 将所有操作集中在reducer方法中
  3. 创建一个Context
  4. 创建对数据的读写API
  5. 将第四步的内容{state, dispatch}放到第三步的上下文数据提供组件中
  6. Context.ProviderContext提供给所有子组件
  • <Context.Provider value={{state, dispatch}}>{/* 里面是各个子组件 */}</Context.Provider>
  1. 各个组件用useContext获取读写APIconst {state, dispatch} = useContext(Context)(注意是获取对象使用花括号)

当前示例代码

  • 请求获取mock数据,ajax('/user').then(user => {dispatch({type: 'setUser', user})}(ES6对象属性简写)
  • 每次刷新组件时,都会再去请求一次AJAX,为了防止每次渲染就请求一次,可以使用useEffect只在进入页面时请求一次
    • useEffect(() => {}, []),第二个参数传一个空数组,那么只会在第一次进入页面时运行,之后永远不会再执行(之后有更好的方式)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// import...

// 统一的数据储存
const store = {
  user: null,
  books: null,
  movies: null
}

// 统一的操作
const reducer = (state, action) => {
  switch (action.type) {
    case "setUser":
      return { ...state, user: action.user };
    case "setBooks":
      return { ...state, books: action.books };
    case "setMovies":
      return { ...state, movies: action.movies };
    default:
      throw new Error();
  }
}

// 统一的上下文 数据提供 组件
const Context = createContext(null)

function User() {
  const {state, dispatch} = useContext(Context)
  // 模拟请求用户数据
  useEffect(() => {
    ajax('/user').then(user => {dispatch({type: 'setUser', user})})
  }, []) // 模拟 componentDidMount
  return (
    <div>
      <h1>个人信息</h1>
      <div>name: {state.user ? state.user.name : "加载中"}</div>
    </div>
  )
}

function Books() {
  const {state, dispatch} = useContext(Context)
  // 模拟请求书籍数据
  useEffect(() => {
    ajax('/books').then(books => {dispatch({type: 'setBooks', books})})
  }, [])
  return (
    <div>
      <h2>书籍信息</h2>
      <ol>
        {state.books
          ? state.books.map(book => <li key={book.id}>{book.name}</li>)
          : '加载中'
        }
      </ol>
    </div>
  )
}

function Movies() {
  const {state, dispatch} = useContext(Context)
  // 模拟请求电影数据
  useEffect(() => {
    ajax('/movies').then(movies => {dispatch({type: 'setMovies', movies})})
  }, [])
  return (
    <div>
      <h2>电影信息</h2>
      <ol>
        {state.movies
          ? state.movies.map(movie => <li key={movie.id}>{movie.name}</li>)
          : '加载中'
        }
      </ol>
    </div>
  )
}

function App() {
  // 创建对数据的读写API
  const [state, dispatch] = useReducer(reducer, store)
  return (
    <Context.Provider value={{state, dispatch}}>
    {/* 统一的上下文 数据提供 */} {/* 提供数据对象 {state, dispatch} ES6对象语法缩写 */}
      <User />
      <hr />
      <Books />
      <Movies />
    </Context.Provider>
  )
}

// render...

模块化处理

目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
src
┣ component
   ┣ User.jsx
   ┣ Books.jsx
   ┗ Movies.jsx
┣ reducers
   ┣ use_reducer.js
   ┣ books_reducer.jsx
   ┗ movies_reducer.jsx
┣ context.js
┣ ajax.js
┣ main.jsx
┗ ...

main.jsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { useReducer, useContext, useEffect, createContext } from "react";
import ReactDOM from "react-dom";
import User from './component/User'
import Books from './component/Books'
import Movies from './component/Movies'
import Context from './Context'
import ajax from './ajax'
import userReducer from './reducers/use_reducer'
import booksReducer from './reducers/books_reducer'
import moviesReducer from './reducers/movies_reducer'

// 统一的数据储存
const store = {
  user: null,
  books: null,
  movies: null
}

// 引入reducer操作表
const reducerHash = {
  ...userReducer,
  ...booksReducer,
  ...moviesReducer,
}

// 统一的reducer操作 查找reducer操作表
const reducer = (state, action) => {
  if(reducerHash[action.type]) {
    return reducerHash[action.type](state, action)
  } else {
    throw new Error('type错误')
  }
}

function App() {
  // 创建对数据的读写API
  const [state, dispatch] = useReducer(reducer, store)
  return (
    <Context.Provider value={{state, dispatch}}>
    {/* 统一的上下文 数据提供 */} {/* 提供数据对象 {state, dispatch} ES6对象语法缩写 */}
      <User />
      <hr />
      <Books />
      <Movies />
    </Context.Provider>
  )
}
  • 注意使用策略模式(表驱动)重构reducer代码。如下,之后可拆分为模块文件,分别导入,方便增加其他接口
1
2
3
4
5
const reducerHash = {
  'setUser': (state, action) => ({...state, user: action.user}),
  'setBooks': (state, action) => ({...state, books: action.books}),
  'setMovies': (state, action) => ({...state, movies: action.movies}),
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 改为引入模块
import userReducer from './reducers/use_reducer'
import booksReducer from './reducers/books_reducer'
import moviesReducer from './reducers/movies_reducer'
// 引入reducer操作表
const reducerHash = {
  ...userReducer,
  ...booksReducer,
  ...moviesReducer,
}

use_reducer

1
2
3
export default {
  'setUser': (state, action) => ({...state, user: action.user})
}

books_reducer

1
2
3
export default {
  'setBooks': (state, action) => ({...state, books: action.books})
}

movies_reducer

1
2
3
export default {
  'setMovies': (state, action) => ({...state, movies: action.movies})
}

User.jsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, {useContext, useEffect} from "react";
import Context from './Context'
import ajax from './ajax'

function User() {
  const {state, dispatch} = useContext(Context)
  // 模拟请求用户数据
  useEffect(() => {
    ajax('/user').then(user => {dispatch({type: 'setUser', user})})
  }, []) // 模拟 componentDidMount
  return (
    <div>
      <h1>个人信息</h1>
      <div>name: {state.user ? state.user.name : "加载中..."}</div>
    </div>
  )
}
export default User

Books.jsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {useContext, useEffect} from "react";
import Context from './Context'
import ajax from './ajax'

function Books() {
  const {state, dispatch} = useContext(Context)
  // 模拟请求书籍数据
  useEffect(() => {
    ajax('/books').then(books => {dispatch({type: 'setBooks', books})})
  }, [])
  return (
    <div>
      <h2>书籍信息</h2>
      <ol>
        {state.books
          ? state.books.map(book => <li key={book.id}>{book.name}</li>)
          : '加载中...'
        }
      </ol>
    </div>
  )
}
export default Books

Movies.jsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {useContext, useEffect} from "react";
import Context from './Context'
import ajax from './ajax'

function Movies() {
  const {state, dispatch} = useContext(Context)
  // 模拟请求电影数据
  useEffect(() => {
    ajax('/movies').then(movies => {dispatch({type: 'setMovies', movies})})
  }, [])
  return (
    <div>
      <h2>电影信息</h2>
      <ol>
        {state.movies
          ? state.movies.map(movie => <li key={movie.id}>{movie.name}</li>)
          : '加载中...'
        }
      </ol>
    </div>
  )
}
export default Movies

Context.js

1
2
3
// 统一的上下文 数据提供 组件
const Context = createContext(null)
export default Context

ajax.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 假 ajax
// 两秒钟后,根据 path 返回一个对象,必定成功不会失败
function ajax(path) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (path === "/user") {
        resolve({
          id: 1,
          name: "Frank"
        });
      } else if (path === "/books") {
        resolve([
          {
            id: 1,
            name: "JavaScript 高级程序设计"
          },
          {
            id: 2,
            name: "JavaScript 精粹"
          }
        ]);
      } else if (path === "/movies") {
        resolve([
          {
            id: 1,
            name: "爱在黎明破晓前"
          },
          {
            id: 2,
            name: "恋恋笔记本"
          }
        ]);
      }
    }, 2000);
  });
}
export default ajax
  • 小结:使用useReduceuseContext/createContext就可以代替Redux
  • 但更复杂的场景,可使用Redux-Saga
  • 在自定义hooks中可实现完全代替

4. useContext

上下文

  • 全局变量 是 全局的 上下文
  • 上下文 是 局部的 全局变量

使用方法,可分为三步

  • 使用const Ctx = createContext(initialValue) 创建上下文
    • 初始值initialValue可以为null
  • 使用<Context.Provider></Context.Provider>圈定作用域范围,在标签内部写子组件
    • 在此标签上添加需要传入的初始属性value,一般是一个包含读和写接口的对象
    • <Context.Provider value={{state, dispatch}}></Context.Provider>
    • 也可声明中间变量 const contextValue = { state, dispatch } 直接传这个对象
      • <Context.Provider value={contextValue}></Context.Provider>
  • 在作用域内,使用useContext(Ctx),来使用上下文
    • 获取读和写的接口const {state, dispatch} = useContext(Ctx)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const Ctx = createContext(null)

function App() {
  const [n, setN] = useState(0)
  return (
    <Ctx.Provider value={{n, setN}}>
      <div className="app">
        <Baba />
      </div>
    </Ctx.Provider>
  )
}

function Baba() {
  const {n, setN} = useContext(Ctx)
  return (
    <div>
      爸爸 得到的n: {n}
      <Child />
    </div>
  )
}
function Child() {
  const {n, setN} = useContext(Ctx)
  const onClick = () => {setN(i => i + 1)}
  return (
    <div>
      儿子 得到的n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  )
}
  • useContext将数据传递给圈定的作用域内所有子孙代组件
  • 可以配合useState或者useReducer来获取读写接口

使用useContext注意事项

区别于Vue,数据更新不是响应式的,而只是一个重新渲染的过程(从上而下逐级通知setN重新渲染)

  • 在一个模块中,将Ctx里的值改变(动态Context
  • 在另一个模块不会感知到这个变化
  • 在某个子组件中触发setN变更数据,通知根组件<App />变更,然后 自顶向下,逐级 通知使用到数据的组件去更新数据
  • 数据流向始终是单向的

5. useEffect & useLayoutEffect

useEffect

副作用(函数式概念)

  • 对环境的改变即为 副作用,例如修改 document.title
  • 但不一定非要把副作用放在useEffect
  • 实际上,称为 useAfterRender 更好,即每次render后运行

用途

  • 模拟组件生命周期的三个钩子函数
    • 作为componentDidMount使用,将空数组[]作为第二个参数
    • 作为componentDidUpdate使用,第二个参数指定依赖 [n] 放在数组中
    • 作为componentWillUnmount使用,通过return () => {}
      • 路由中常用,在此钩子中做清理工作,防止内存泄漏
      • 对回京做的任何变动,组件卸载时都需要清理
  • 第二个参数 和 返回的回调 决定了回调函数什么时候再次执行
  • 以上三种用途可同时存在

特点

  • 如果同时存在多个useEffect,会按照 出现次序 执行

代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function App() {
  const [n, setN] = useState(0)
  const onClick = () => {
    setN(i => i + 1)
  }

  // App第一次渲染 mount
  useEffect(() => {
    console.log('第一次渲染之后执行')
  }, []) // 表示 [] 里的状态变化后再次执行,没有变量就不会再次执行

  // App 任何数据更新后渲染 update
  useEffect(() => {
    console.log('任何一个state状态变化时,即每次渲染之后都执行')
  }) // 不写第二个参数表示 任何依赖变化时都执行

  // 具体到某个数据更新后渲染 update
  useEffect(() => {
    console.log(`当依赖n变化,渲染后执行,第${n}次也变化了`)
  }, [n]) // 依赖n变化时执行

  // 排除第一次初始化数据的更新 具体到某个数据更新后渲染 update
  useEffect(() => {
    if(n !== 0) {
      console.log(`当依赖n变化,渲染后执行,不算初始化的那次`)
    }
  }, [n])

  // App第一次渲染 实现 setInterval
  useEffect(() => {
    const id = setInterval(() => {
      console.log('hi')
    }, 1000)
    return () => {
      window.clearInterval(id) // 防止内存泄漏
    }
  }, [])

  return (
    <div>
      n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  )
}

useLayoutEffect

useEffect在浏览器渲染完成后执行,代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const BlinkyRender = () => {
  const [v, setV] = useState(0)
  useEffect(() => {
    document.querySelector('#x').innerText = 'value: 1000'
  }, [v])
  return (
    <div id="x" onClick={() => {
      setValue(0)
    }}>
      value: {v}
    </div>
  )
}
  • 页面渲染时有一个闪烁,value0迅速变为1000

useLayoutEffect称为 “布局副作用”, 在浏览器渲染前执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const BlinkyRender = () => {
  const [v, setV] = useState(0)
  useLayoutEffect(() => {
    document.querySelector('#x').innerText = 'value: 1000'
  }, [v])
  return (
    <div id="x" onClick={() => {
      setValue(0)
    }}>
      value: {v}
    </div>
  )
}
  • 页面没有出现闪烁问题
  • 有很多DOM操作,浏览器会等DOM操作完了再渲染
    • useEffect在浏览器渲染改变屏幕像素之后
    • useLayoutEffectDOM操作完了,紧接着执行回调(对 Layout有影响的操作),然后再去渲染(改变屏幕像素之前)

useLayoutEffect


useEffect V.S. useLayoutEffect

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const App = () => {
  const [n, setN] = useState(0)
  const time = useRef(null)
  const onClick = () => {
    setN(i => i + 1)
    time.current = performance.now() // beforeRender
  }

  useLayoutEffect(() => {
    // afterRender
    if(time.current) {
      console.log(performance.now() - time.current)
    }
  })

  useEffect(() => {
    // afterRender
    if(time.current) {
      console.log(performance.now() - time.current)
    }
  })

  return (
    <div className="app">
      <h1>n: {n}</h1>
      <button onClick={onClick}>Click</button>
    </div>
  )
}
  • 全局API performance.now()可以记录当前时间
  • 从第二次点击开始计算
  • useEffect总是比useLayoutEffect慢,是因为useEffect在改变外观之后执行,而useLayoutEffect在改变外观之前执行

特点

  • useLayoutEffect总是比useEffect先执行,代码示例
  • useLayoutEffect里的回调最好时改变 Layout,否则没有意义

经验

  • 虽然可以避免屏幕闪烁,但大部分时候不会去改变DOM,所以并不是总是使用useLayoutEffect更好
  • 为了用户体验,优先使用 useEffect (优先渲染)
    • useLayoutEffect操作的优先级,但会影响(延长)用户看到画面变化的时间

6. useMemo

使用useMemo前,先需要理解 React.memo

  • React默认总是有多余的render,即使渲染的数据没有发生改变,代码示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m}/>
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log('假设这里有大量代码')
  return <div>child: {props.data}</div>;
}
  • Child只依赖了{m},而当数据{n}改变了,重新渲染App,连带着也会重新渲染Child,即使{m}未发生改变
  • 如果props不变,就没有必要再次执行一个函数组件
  • 代码中的ChildReact.memo(Child)代替,封装一层

变更后的效果,见示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(i => i + 1);
  };
  const onClick2 = () => {
    setM(i => i + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
        <button onClick={onClick2}>update m {m}</button>
      </div>
      <Child2 data={m} />
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div>child: {props.data}</div>;
}

const Child2 = React.memo(Child);
  • React.memo(Child)使得一个组件只有在依赖(例如props)变化时,才会再次执行渲染

简化代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(i => i + 1);
  };
  const onClick2 = () => {
    setM(i => i + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
        <button onClick={onClick2}>update m {m}</button>
      </div>
      <Child1 data={m} />
    </div>
  );
}

const Child = React.memo( (props) => { // 封装一层 React.memo
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div>child: {props.data}</div>;
  }
);

React.memobug

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function App() {
  console.log('App执行了')
  const [n, setN] = React.useState(0);
  const [m] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClickChild = () => { // 每次App运行,生成新的函数
    console.log(m);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
      {/* Child2 居然又执行了 */}
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return (
    <div onClick={props.onClick}>child: {props.data}</div>
  );
}

const Child2 = React.memo(Child);
  • 在监听了函数之后,一秒破功,Child2 居然又执行了
  • 因为App每次重新运行时,都会在执行const onClickChild = () => {}处,生成新的函数
  • 新旧函数虽然功能一样,但是内存地址不一样

配合使用useMemo解决React.memo的问题

  • useMemo可以实现函数的缓存,比对函数内容,相同的话使用之前的缓存的函数
  • useMemo的返回值就是需要缓存的函数
  • 使用useMemo来返回出需要传入子组件的函数,并给定依赖作为第二个参数

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function App() {
  const [n, setN] = useState(0);
  const [m, setM] = useState(0);
  const onClick = () => {
    setN(i => i + 1);
  };
  const onClick2 = () => {
    setM(i => i + 1);
  };
  const onClickChild = useMemo(() => {
    const fn = div => {
      console.log("on click child, m: " + m);
      console.log(div);
    };
    return fn;
  }, [m]); // 这里把 [m] 改成 [n] 就会打印出旧的 m
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
        <button onClick={onClick2}>update m {m}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return (
    <div onClick={e => props.onClick(e.target)}>
      child: {props.data}
    </div>
  );
}

const Child2 = React.memo(Child);

useMemo特点

  • 第一个参数是() => value,见源码useMemo 的TS类型声明
  • 第二个参数是依赖 [m, n]
  • 只有当依赖变化时,才会计算出新的value
  • 如果依赖不变,那么就重用之前的value
  • 这与Vuecomputed相类似,用来缓存依赖一些不变化的数据

useMemo注意事项

  • 如果value传的是函数,那么就要写成 useMemo(() => (x) => {}),这是一个返回函数的函数(高阶函数/组件)

形式上复杂难用,于是就有了useCallback


useCallbackuseMemo的语法糖

用法

  • 使用 useCallback(x => consolog(x), [m])
  • 等价于 useMemo(() => x => console.log(x), [m])

7. useRef

useRef用途:

  • 如果需要一个值,在组件不断render时保持不变,可以使用useRef
    • 初始化:const count = useRef(0)
    • 读取值:count.current
  • 区别于setNn 每次渲染都会变更

需求:要知道组件渲染了几次,使用一个变量记录(全局变量会污染命名环境)

  • 使用useRef,每次渲染时更新useEffect(() => {count.current += 1}),得到一个新的count,而count.value在内存中的引用不变
  • 渲染 count对象地址不变,其中的current值被记录在useRef对应的一个fiberNode上,示例代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function App() {
  console.log('App 执行')
  const count = useRef(0)
  const [n, setN] = useState(0)
  const onClick = () => {
    setN(i => i + 9)
  }
  // DidUpdate
  useEffect(() => {
    count.current += 1
    console.log('count.current', count.current)
  })
  return (
    <div className="app">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
    </div>
  )
}

useRef


useRef读取值为什么需要.current

  • 为了保持新旧组件更新前后两次useRef是同一个引用对象(只有引用能做到,保持引用地址不变)

对比三个数据变化的钩子函数

  • useState/useReducern每次都变
  • useMemo/useCallback:在依赖[n]变更时,对应的函数fn改变,有条件地变
  • useRefn永远不变

useHooks_state

延伸

  • Vue3ref()
    • 初始化:const count = ref(0)
    • 读取值:count.value
  • 不同点:
    • count.value变化时,Vue3会自动render,帮你实现了UI更新
    • React中使用useRef不会自动更新UI代码示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function App() {
  console.log("App 执行");
  const count = useRef(0);
  const [n, setN] = useState(0);
  const onClick = () => {
    setN((i) => i + 9);
  };
  const onClick2 = () => { // 并不会触发UI更新
    count.current += 1;
    console.log("count.current onClick", count.current);
  };
  useEffect(() => {
    console.log("count.current update", count.current);
  });
  return (
    <div className="app">
      <div>
        <button onClick={onClick}>update n {n}</button>
        <br />
        <button onClick={onClick2}>update count.current {count.current}</button>
      </div>
    </div>
  );
}

useRef不能做到变化时自动render刷新UI,需要额外步骤

  • 因为不符合React的函数式理念:UI = render(data)
    • 每次data变化时,使用函数(例如setState)去渲染成新的虚拟DOM
    • 将新的虚拟DOM与页面上旧的虚拟DOMDOM Diff,得到一个patch补丁
    • 根据这个patch补丁去重渲染DOM
  • 如果想要变化时自动render,可以自定义去实现(使用setN),React不管
  • 监听ref,当ref.current变化时,再加一个依赖,调用setX即可,示例代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function App() {
  console.log("App 执行");
  const count = useRef(0);
  const [n, setN] = useState(0);
  const [_, set_] = useState(null);
  const onClick = () => {
    setN((i) => i + 9);
  };
  const onClick2 = () => {
    count.current += 1;
    set_(Math.random());
    console.log("count.current onClick", count.current);
  };
  useEffect(() => {
    console.log("count.current update", count.current);
    console.log("--------------------");
  });
  return (
    <div className="app">
      <div>
        <button onClick={onClick}>update n {n}</button>
        <br />
        <button onClick={onClick2}>update count.current {count.current}</button>
      </div>
    </div>
  );
}

8. useRef配合forwardRef用在函数组件中

基本用法:让函数组件 <Button2 /> 支持 ref,获取其虚拟DOM的引用

  • 代替原生使用id,和document.getElementById来获取元素

函数组件中会警告无法直接使用useRef 代码示例1props无法传递ref属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function App() {
  const buttonRef = useRef(null);
  return (
    <div className="app">
      <div>
        <Button2 ref={buttonRef}>按钮</Button2>
      </div>
    </div>
  );
}

const Button2 = (props) => {
  console.log("props", props);
  return <button className="red" {...props}></button>;
};
  • 报警告Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
  • useRef作为元素的引用是在类组件中使用的,即只有类组件可以直接接受ref元素引用
  • 提示函数组件去使用forwardRef
  • 组件内打印console.log("props", props);得到object {children: "按钮"},只把按钮传过去了,其他属性都忽略了

代码示例2:函数组件中使用React.forwardRef包裹一层组件,可声明第二个形参ref,实现函数组件ref的组件间传递

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button3 ref={buttonRef}>按钮</Button3>
    </div>
  );
}

/*
const Button2 = (props, ref) => {
  console.log("props: ", props);
  console.log("ref:", ref);
  return <button className="red" ref={ref} {...props} />;
};
const Button3 = forwardRef(Button2);
*/

const Button3 = React.forwardRef((props, ref) => {
  return <button className="red" ref={ref} {...props} />;
});
  • 函数组件需要接受外部传来的ref参数,必须用React.forwardRef包起来
    • <Button ref={buttonRef}>...</Button>
    • const Button = React.forwardRef((props, ref) => (<button ref={ref} {...props} />))

代码示例3:两次ref传递得到button的引用,实现可移动的按钮(高阶组件封装)(仍有bug,待修复)

  • 功能包装:const MovableButton = movable(Button2)
  • 通过ref引用到Button2里面的按钮:const buttonRef = useRef(null),需要做两次传递
    • 第一次 将 <MovableButton name="email" ref={buttonRef}> 传给 movable(Component) 中返回的 <Component ref={ref} {...props} />
      • movable(Button2) 中返回的 <Button2 ref={ref} {...props} />
    • 第二次 Button2 组件声明时传递 React.forwardRef((props, ref) => { return <button ref={ref} {...props} />; });
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function App() {
  // 赋上 可拖拽的 功能
  const MovableButton = movable(Button2);
  // 获取引用
  const buttonRef = useRef(null);
  // update
  useEffect(() => {
    console.log(buttonRef.current);
  });
  return (
    <div className="App">
      <MovableButton name="email" ref={buttonRef}>
        按钮
      </MovableButton>
    </div>
  );
}

// 函数组件 forwardRef 引用包装
const Button2 = React.forwardRef((props, ref) => {
  return <button ref={ref} {...props} />;
});

// 仅用于实验目的,不要在公司代码中使用
function movable(Component) {
  function Component2(props, ref) {
    console.log(props, ref);

    // 初始化
    const [position, setPosition] = useState([0, 0]);
    const lastPosition = useRef(null);

    // 方法
    const onMouseDown = (e) => {
      lastPosition.current = [e.clientX, e.clientY];
    };
    const onMouseMove = (e) => {
      // e.nativeEvent.stopImmediatePropagation(); //阻止冒泡
      if (lastPosition.current) {
        const x = e.clientX - lastPosition.current[0];
        const y = e.clientY - lastPosition.current[1];
        setPosition([position[0] + x, position[1] + y]);
        lastPosition.current = [e.clientX, e.clientY];
      }
    };
    const onMouseUp = () => {
      lastPosition.current = null;
    };

    return (
      <>
        {/*套一层 div */}
        <div
          className="movable"
          onMouseDown={onMouseDown}
          onMouseMove={onMouseMove}
          onMouseUp={onMouseUp}
          style={{
            left: position && position[0],
            top: position && position[1]
          }}
        >
          <Component {...props} ref={ref} />
        </div>
      </>
    );
  }
  return React.forwardRef(Component2);
}

useRef V.S. forwardRef

  • 由于大部分时候不需要,所以props不包含ref
  • 由于函数组件的参数 props不包含ref,所以需要React.forwardRef包裹一层,才可以传递 ref
  • React.forwardRef就是在函数组件中用来转发ref

9. useImperativeHandle

  • Imperative 重要的
  • 命名太长:其实意思就是setRef:用来变更ref
  • 作用是自定义ref的属性

不用useImperativeHandle代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function App() {
  const buttonRef = useRef(null);
  useEffect(() => {
    console.log(buttonRef.current); // 打印出 <button>按钮</button>
  });
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按钮</Button2>
      <button
        className="close"
        onClick={() => {
          console.log(buttonRef);
          buttonRef.current.remove();
        }}
      >
        x
      </button>
    </div>
  );
}
  • 如果希望得到的ref是一个对button的自定义封装对象,则需要使用useImperativeHandle

使用useImperativeHandle代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function App() {
  const buttonRef = useRef(null);
  useEffect(() => {
    console.log(buttonRef.current); // 打印出 {x: ƒ x(), realButton: Object}
  });
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按钮</Button2>
      <button
        className="close"
        onClick={() => {
          console.log(buttonRef);
          buttonRef.current.x();
        }}
      >
        x
      </button>
    </div>
  );
}

const Button2 = React.forwardRef((props, ref) => {
  const realButton = createRef(null);
  const setRef = useImperativeHandle;
  setRef(ref, () => {
    return {
      x: () => {
        realButton.current.remove();
      },
      realButton: realButton
    };
  });
  return <button ref={realButton} {...props} />;
});

10. 自定义 Hook

  • 使用自定义 Hook 来封装数据操作

简单代码示例

  • 直接获取处理过的数据 const { list /*, setList */ } = useList();
  • 处理数据的逻辑封装在自定义Hook中,只暴露读和写的接口
  • 初始化状态 const [list, setList] = useState(null)
  • DidMount时 请求数据
  • 返回出读和写的接口
  • 处理相关的逻辑都写在一个钩子函数中,运行时暴露出读和写的接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// import...
import useList from "./hooks/useList";
function App() {
  const { list /*, setList */ } = useList();
  return (
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
          {list.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ol>
      ) : (
        "加载中..."
      )}
    </div>
  );
}
  • useList.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { useState, useEffect } from "react";

const useList = () => {
  // 初始化 state
  const [list, setList] = useState(null);
  // mounted时 请求数据
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行

  // 暴露读和写的接口
  return {
    list: list,
    setList: setList
  };
};
export default useList;

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: "Frank" },
        { id: 2, name: "Jack" },
        { id: 3, name: "Alice" },
        { id: 4, name: "Bob" }
      ]);
    }, 2000);
  });
}

功能更全的代码示例

  • 不仅提供了读和写的借口,增删改查等其他功能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// import...
import useList from "./hooks/useList";
function App() {
  const { list, deleteIndex } = useList();
  return (
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
          {list.map((item, index) => (
            <li key={item.id}>
              {item.name}
              <button onClick={() => {deleteIndex(index);}}>
                x
              </button>
            </li>
          ))}
        </ol>
      ) : (
        "加载中..."
      )}
    </div>
  );
}
  • useList.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { useState, useEffect } from "react";

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
  return {
    list: list, // 查
    addItem: name => { // 增
      setList([...list, { id: Math.random(), name: name }]);
    },
    deleteIndex: index => { // 删
      setList(list.slice(0, index).concat(list.slice(index + 1)));
    }
  };
};
export default useList;

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: "1", name: "Frank" },
        { id: "2", name: "Jack" },
        { id: "3", name: "Alice" },
        { id: "4", name: "Bob" }
      ]);
    }, 2000);
  });
}

分析

  • 还可以在自定义Hook里使用Context
  • useState只说了不能在if里,没说不能在函数里运行
    • 只要这个函数在函数组件里运行即可
  • 实际业务中,使用良好的组件封装,更抽象的自定义Hook封装,来代替页面组件中直接使用useStateuesEffect等钩子
  • 使用函数组件,就没有必要使用Redux

11. Stale Closure过时的闭包

代码示例

  • 期待打印自增的count
  • 结果每次打印都是相同的count
  • 打印的count是过时的闭包,而不是后续操作产生的新的count
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function WatchCount() {
  const [count, setCount] = useState(0);
  useEffect(function() {
    setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
  }, []);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}
  • 把依赖[count]放入useEffect,更新DidUpdate时同时更新闭包
  • 同时卸载WillUnmount时(return () => {}) 清除上一次组件卸载时的 id,来干掉旧的闭包
  • 代码中的两处id调用时机不同
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function WatchCount() {
  const [count, setCount] = useState(0);
  useEffect(function() {
    const id = setInterval(function log() { // 每次更新时创建新的id
      console.log(`Count is: ${count}`);
    }, 2000);
    return function() {
      clearInterval(id); // 清除上一次组件卸载时的id,调用时机不同
    }
  }, [count]);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}
  • 避免保留下过时的闭包,每次更新前需要重新获取新的闭包
  • 或者在返回的函数中 读取最新的闭包,而不是在返回的函数外

题外话

  • JS中有许多需要避免的bug或者反模式
    • 隐式类型转换,用===
    • 过时的闭包
    • 空值判断,用??
    • 异步
    • 过长的条件分支,用策略模式(表驱动)

12. 总结

1
2
3
4
5
6
7
8
9
flowchart LR
    React_Builtin_Hooks --> 状态...useState
    React_Builtin_Hooks --> 副作用...useEffect --> useLayoutEffect
    React_Builtin_Hooks --> 上下文...useContext
    React_Builtin_Hooks --> Redux...useReducer
    React_Builtin_Hooks --> 记忆...useMemo...配合React.memo --> 回调...useCallback
    React_Builtin_Hooks --> 引用...useRef --> forwardRef
    引用...useRef --> useImperativeHandle
    React_Builtin_Hooks --> 自定义...Hook --> useDebugValue


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名