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 28import 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 28import 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(()=>{})
setN(i => i + 1)-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23import {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来操作
- 1.创建初始值
总的来说
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 52import {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 state同useState的规则对多个状态做整体操作
将所有操作聚拢在函数
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,返回对应数据
|
|
基本结构
|
|
复杂useReducer使用步骤 ⇧
- 将数据几种在一个
store对象中 - 将所有操作集中在
reducer方法中 - 创建一个
Context - 创建对数据的读写
API - 将第四步的内容
{state, dispatch}放到第三步的上下文数据提供组件中 - 用
Context.Provider将Context提供给所有子组件<Context.Provider value={{state, dispatch}}>{/* 里面是各个子组件 */}</Context.Provider>
- 各个组件用
useContext获取读写API:const {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...
模块化处理 ⇧
目录
|
|
main.jsx
|
|
注意使用策略模式(表驱动)重构
reducer代码。如下,之后可拆分为模块文件,分别导入,方便增加其他接口1 2 3 4 5const 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
|
|
books_reducer
|
|
movies_reducer
|
|
User.jsx
|
|
Books.jsx
|
|
Movies.jsx
|
|
Context.js
|
|
ajax.js
|
|
- 小结:使用
useReduce和useContext/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 32const 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,会按照 出现次序 执行
|
|
useLayoutEffect ⇧
useEffect在浏览器渲染完成后执行,代码示例
|
|
- 页面渲染时有一个闪烁,
value从0迅速变为1000
useLayoutEffect称为 “布局副作用”, 在浏览器渲染前执行
|
|
- 页面没有出现闪烁问题
- 有很多
DOM操作,浏览器会等DOM操作完了再渲染useEffect在浏览器渲染改变屏幕像素之后useLayoutEffect在DOM操作完了,紧接着执行回调(对Layout有影响的操作),然后再去渲染(改变屏幕像素之前)
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 29const 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 22function 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不变,就没有必要再次执行一个函数组件代码中的
Child用React.memo(Child)代替,封装一层
变更后的效果,见示例代码
|
|
React.memo(Child)使得一个组件只有在依赖(例如props)变化时,才会再次执行渲染
简化代码
|
|
React.memo的bug ⇧
|
|
- 在监听了函数之后,一秒破功,
Child2居然又执行了 - 因为
App每次重新运行时,都会在执行const onClickChild = () => {}处,生成新的函数 - 新旧函数虽然功能一样,但是内存地址不一样
配合使用useMemo解决React.memo的问题
useMemo可以实现函数的缓存,比对函数内容,相同的话使用之前的缓存的函数useMemo的返回值就是需要缓存的函数- 使用
useMemo来返回出需要传入子组件的函数,并给定依赖作为第二个参数
|
|
useMemo特点
- 第一个参数是
() => value,见源码useMemo的TS类型声明 - 第二个参数是依赖
[m, n] - 只有当依赖变化时,才会计算出新的
value - 如果依赖不变,那么就重用之前的
value - 这与
Vue的computed相类似,用来缓存依赖一些不变化的数据
useMemo注意事项
- 如果
value传的是函数,那么就要写成useMemo(() => (x) => {}),这是一个返回函数的函数(高阶函数/组件)
形式上复杂难用,于是就有了
useCallback
useCallback是useMemo的语法糖
用法
- 使用
useCallback(x => consolog(x), [m]) - 等价于
useMemo(() => x => console.log(x), [m])
7. useRef ⇧
useRef用途: ⇧
- 如果需要一个值,在组件不断
render时保持不变,可以使用useRef- 初始化:
const count = useRef(0) - 读取值:
count.current
- 初始化:
- 区别于
setN,n每次渲染都会变更
需求:要知道组件渲染了几次,使用一个变量记录(全局变量会污染命名环境)
- 使用
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 20function 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读取值为什么需要.current? ⇧
- 为了保持新旧组件更新前后两次
useRef是同一个引用对象(只有引用能做到,保持引用地址不变)
对比三个数据变化的钩子函数
useState/useReducer:n每次都变useMemo/useCallback:在依赖[n]变更时,对应的函数fn改变,有条件地变useRef:n永远不变
延伸
Vue3的ref()- 初始化:
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 24function 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与页面上旧的虚拟DOM做DOM 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 27function 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代码示例1:props无法传递ref属性
|
|
- 报警告
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的组件间传递
|
|
- 函数组件需要接受外部传来的
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 69function 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); }
- 第一次 将
useRefV.S.forwardRef
- 由于大部分时候不需要,所以
props不包含ref - 由于函数组件的参数
props不包含ref,所以需要React.forwardRef包裹一层,才可以传递ref React.forwardRef就是在函数组件中用来转发ref的
9. useImperativeHandle ⇧
Imperative重要的- 命名太长:其实意思就是
setRef:用来变更ref - 作用是自定义
ref的属性
不用
useImperativeHandle的 代码示例
|
|
- 如果希望得到的
ref是一个对button的自定义封装对象,则需要使用useImperativeHandle
使用
useImperativeHandle的 代码示例
|
|
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.js1 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 32import { 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.js1 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 33import { 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封装,来代替页面组件中直接使用useState、uesEffect等钩子 - 使用函数组件,就没有必要使用
Redux了
11. Stale Closure过时的闭包 ⇧
- 期待打印自增的
count - 结果每次打印都是相同的
count 打印的
count是过时的闭包,而不是后续操作产生的新的count1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16function 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 19function 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. 总结 ⇧
|
|
参考文章 ⇧
相关文章 ⇧
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名