├── CHANGELOG.md ├── LICENSE ├── 附02:React扩展阅读.md ├── 07 useContext高级用法.md ├── 17 React Hook 总结.md ├── 14 useLayoutEffect基础用法.md ├── 18 示例:React使用Echarts所用到的hooks.md ├── 19 useTransition基础用法.md ├── 15 useDebugValue基础用法.md ├── 05 useEffect高级用法.md ├── 11 useMemo基础用法.md ├── 02 useState基础用法.md ├── 00 前言.md ├── 13 useImperativeHandle基础用法.md ├── 04 useEffect基础用法.md ├── 01 React Hook 简介.md ├── README.md ├── 06 useContext基础用法.md ├── 08 useReducer基础用法.md ├── 03 useState高级用法.md ├── 09 useReducer高级用法.md ├── 附01:React基础知识.md ├── 16 自定义hook.md ├── 10 useCallback基础用法.md └── 12 useRef基础用法.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2020.05.16 4 | 在 “02 useState基础用法.md” 的 “useState函数源码” 中,增加一些基础的TypeScript泛型知识。 5 | 6 | ## 2020.05.15 7 | 在 “02 useState基础用法.md” 的 “useState使用示例” 中,修改不恰当的示例代码。 8 | 感谢网友 '鱼的记忆' 发现此处问题 9 | 10 | ## 2020.05.13 11 | 认真校对一遍文章,修改了N多个错误和语言描述不恰当的地方。 12 | 13 | ## 2020.05.12 14 | 终于写完,上传该系列文章。 15 | 16 | ## 2020.04.20 17 | 开始写该系列文章。 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /附02:React扩展阅读.md: -------------------------------------------------------------------------------- 1 | # React扩展阅读 2 | 3 | #### Youtube中Codevolution专栏下的React Hooks Tutorial系列视频 4 | 链接:[https://pan.baidu.com/s/1Lj_kN-FuO5bbZ2rqMVz6xw](https://pan.baidu.com/s/1Lj_kN-FuO5bbZ2rqMVz6xw) 5 | 提取码:70ni 6 | 7 | #### 自定义 Hook 大全 8 | 必不可少的 React Hooks集合。 9 | [https://github.com/zenghongtu/react-use-chinese](https://github.com/zenghongtu/react-use-chinese) 10 | 11 | #### React技术揭秘 12 | 卡颂写的 React 源码分析。 13 | [https://react.iamkasong.com/](https://react.iamkasong.com/) 14 | 15 | #### 阿里 ahooks 工具库 16 | 蚂蚁umi团队、淘系ice团队、阿里体育共同建设的 React Hooks 工具库 17 | [https://ahooks.js.org/zh-CN](https://ahooks.js.org/zh-CN) 18 | 19 | #### 网易云音乐前端团队 React Hooks 最佳实践 20 | 实际项目中总结出的 React Hooks 实用经验 21 | [https://mp.weixin.qq.com/s/HwlnvAh18saKwXC_nZwSHw](https://mp.weixin.qq.com/s/HwlnvAh18saKwXC_nZwSHw) 22 | 23 | #### React 300问 24 | React 300多道面试题和答案 25 | [https://github.com/semlinker/reactjs-interview-questions](https://github.com/semlinker/reactjs-interview-questions) 26 | 27 | #### 阿里推出的React框架:antd 28 | antd 4.0版本全部采用函数组件开发而成。 29 | [https://ant.design/docs/react/introduce-cn](https://ant.design/docs/react/introduce-cn) 30 | 31 | #### 京东推出的多端统一开发框架:taro 32 | Taro是一套遵循React语法规范的多端开发解决方案。 33 | [https://taro-docs.jd.com/taro/](https://taro-docs.jd.com/taro/) 34 | 35 | #### 学习React的几个好的微信公众号 36 | React中文社区、不知非攻、魔术师卡颂 37 | -------------------------------------------------------------------------------- /07 useContext高级用法.md: -------------------------------------------------------------------------------- 1 | # 07 useContext高级用法 2 | 3 | 所谓高级用法,只不过是一些深层知识点和实用技巧,你甚至可以把本章当做对前面知识点的一个巩固和学习。 4 | 5 | ## 同时传递多个共享数据值给1个子组件 6 | 7 | 实现以下组件需求: 8 | 1、有2个共享数据对象 UserContext、NewsContext; 9 | 2、父组件为AppComponent、子组件为ChildComponent; 10 | 3、父组件需要同时将UserContext、NewsContext的数据同时传递给子组件; 11 | 12 | 实现代码: 13 | 14 | import React,{ useContext } from 'react' 15 | 16 | const UserContext = React.createContext(); 17 | const NewsContext = React.createContext(); 18 | 19 | function AppComponent() { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | function ChildComponent(){ 30 | const user = useContext(UserContext); 31 | const news = useContext(NewsContext); 32 | return
33 | {user.name} - {news.title} 34 |
35 | } 36 | 37 | export default AppComponent; 38 | 39 | 代码分析: 40 | 1、父组件同时要实现传递2个共享数据对象value值,需要使用标签进行2次嵌套。 41 | 2、子组件使用了useContext,他可以自由随意使用父组件传递过来的共享数据value,并不需要多次嵌套获取。 42 | 43 | ## 同时将1个共享数据值传递给多个子组件 44 | 使用标签将多个子组件包裹起来,即可实现。 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 3个子组件都可使用useContext获取共享数据值。 53 | 54 | 55 | ## 为什么不使用Redux? 56 | 在Hook出现以前,React主要负责视图层的渲染,并不负责组件数据状态管理,所以才有了第三方Redux模块,专门来负责React的数据管理。 57 | 58 | 但是自从有了Hook后,使用React Hook 进行函数组件开发,实现数据状态管理变得切实可行。只要根据实际项目需求,使用useContext以及下一章节要学习的useReducer,一定程度上是可以满足常见需求的。 59 | 60 | 毕竟使用Redux会增大项目复杂度,此外还要花费学习Redux成本。 61 | 62 | 具体需求具体分析,不必过分追求Redux。 63 | 64 | 65 | --- 66 | 67 | 至此,关于useContext高级用法已经讲完,useContext降低了组件之间数据传递的复杂性,让我们编写代码更加心情愉悦,而不用去关心层层嵌套问题。 68 | 69 | 接下来学习第4个Hook函数useReducer。 70 | 71 | 欢迎进入下一章节:[useReducer基本用法](https://github.com/puxiao/react-hook-tutorial/blob/master/08%20useReducer%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 72 | -------------------------------------------------------------------------------- /17 React Hook 总结.md: -------------------------------------------------------------------------------- 1 | # 17 React Hook 总结 2 | 3 | 首先,对你能够坚持到现在,表示深深的赞扬,学习React Hook之路不容易。 4 | 5 | 我们快速的回顾一下之前学习过的各个hook。 6 | 7 | ## react hook 回顾 8 | 9 | ##### 定义变量 10 | [useState()](https://github.com/puxiao/react-hook-tutorial/blob/master/02%20useState%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):定义普通变量 11 | [useReducer()](https://github.com/puxiao/react-hook-tutorial/blob/master/08%20useReducer%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):定义有不同类型、参数的变量 12 | 13 | ##### 组件传值 14 | [useContext()](https://github.com/puxiao/react-hook-tutorial/blob/master/06%20useContext%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):定义和接收具有全局性质的属性传值对象,必须配合React.createContext()使用 15 | 16 | ##### 对象引用 17 | [useRef()](https://github.com/puxiao/react-hook-tutorial/blob/master/12%20useRef%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):获取渲染后的DOM元素对象,可调用该对象原生html的方法,可能需要配合React.forwardRef()使用 18 | [useImperativeHandle()](https://github.com/puxiao/react-hook-tutorial/blob/master/13%20useImperativeHandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):获取和调用渲染后的DOM元素对象拥有的自定义方法,必须配合React.forwardRef()使用 19 | 20 | ##### 生命周期 21 | [useEffect()](https://github.com/puxiao/react-hook-tutorial/blob/master/04%20useEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):挂载或渲染完成后、即将被卸载前,调度 22 | [useLayoutEffect()](https://github.com/puxiao/react-hook-tutorial/blob/master/14%20useLayoutEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):挂载或渲染完成后,同步调度 23 | 24 | ##### 性能优化 25 | [useCallback()](https://github.com/puxiao/react-hook-tutorial/blob/master/10%20useCallback%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):获取某处理函数的引用,必须配合React.memo()使用 26 | [useMemo()](https://github.com/puxiao/react-hook-tutorial/blob/master/11%20useMemo%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):获取某处理函数返回值的副本 27 | 28 | ##### 代码调试 29 | [useDebugValue()](https://github.com/puxiao/react-hook-tutorial/blob/master/15%20useDebugValue%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md):对react开发调试工具中的自定义hook,增加额外显示信息 30 | 31 | ##### 自定义hook 32 | [useCustomHook()](https://github.com/puxiao/react-hook-tutorial/blob/master/16%20%E8%87%AA%E5%AE%9A%E4%B9%89hook.md):将hook相关逻辑代码从组件中抽离,提高hook代码可复用性 33 | 34 | 35 | ## react hook 扩展阅读 36 | [附01:React基础知识](https://github.com/puxiao/react-hook-tutorial/blob/master/%E9%99%8401%EF%BC%9AReact%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md) 37 | [附02:React扩展阅读](https://github.com/puxiao/react-hook-tutorial/blob/master/%E9%99%8402%EF%BC%9AReact%E6%89%A9%E5%B1%95%E9%98%85%E8%AF%BB.md) 38 | 39 | 40 | ## 信息反馈 41 | 若有错误欢迎指正,本人微信同QQ (78657141),或通过邮件联系:yangpuxiao@gmail.com 42 | 43 | --- 44 | 45 | 至此,React Hook 你已学习完成。 46 | 47 | 真心为你鼓掌,加油,在实践中去提升自己的 React Hook 战斗值吧。 48 | -------------------------------------------------------------------------------- /14 useLayoutEffect基础用法.md: -------------------------------------------------------------------------------- 1 | # 14 useLayoutEffect基础用法 2 | 3 | ## useLayoutEffect概念解释 4 | 我们第九个要学习的Hook(钩子函数)是useLayoutEffect,他的作用是“勾住”挂载或重新渲染完成这2个组件生命周期函数。useLayoutEffect使用方法、所传参数和useEffect完全相同。 5 | 6 | 他们的不同点在于,你可以把useLayoutEffect等同于componentDidMount、componentDidUpdate,因为他们调用阶段是相同的。而useEffect是在componentDidMount、componentDidUpdate调用之后才会触发的。 7 | 8 | 也就是说,当组件所有DOM都渲染完成后,同步调用useLayoutEffect,然后再调用useEffect。 9 | 10 | useLayoutEffect永远要比useEffect先触发完成。 11 | 12 | 那通常在useLayoutEffect阶段我们可以做什么呢? 13 | 答:在触发useLayoutEffect阶段时,页面全部DOM已经渲染完成,此时可以获取当前页面所有信息,包括页面显示布局等,你可以根据需求修改调整页面。 14 | 15 | 请注意,useLayoutEffect对页面的某些修改调整可能会触发组件重新渲染。如果是对DOM进行一些样式调整是不会触发重新渲染的,这点和useEffect是相同的。 16 | 17 | 在react官方文档中,明确表示只有在useEffect不能满足你组件需求的情况下,才应该考虑使用useLayoutEffect。 官方推荐优先使用useEffect。 18 | 19 | 请注意:如果是服务器渲染,无论useEffect还是useLayoutEffect 都无法在JS代码加载完成之前执行,因此都会收到错误警告。 服务器渲染时若想使用useEffect,解决方案不在本章中讨论。 20 | 21 | 让我们回到useLayoutEffect基础学习中。 22 | 23 | 24 | ## useLayoutEffect是来解决什么问题的? 25 | 答:useLayoutEffect的作用是“当页面挂载或渲染完成时,再给你一次机会对页面进行修改”。 26 | 27 | 如果你选择使用useLayoutEffect,对页面进行了修改,更改样式不会引发重新渲染,但是修改变量则会触发再次渲染。 28 | 如果你不使用useLayoutEffect,那么之后就应该调用useEffect。 29 | 30 | 补充说明: 31 | 1、优先使用useEffect,useEffect无法满足需求时再考虑使用useLayoutEffect。 32 | 2、useLayoutEffect先触发,useEffect后触发。 33 | 3、useEffect和useLayoutEffect在服务器端渲染时,都不行,需要寻求别的解决方案。 34 | 35 | ## useLayoutEffect函数源码: 36 | 回到useLayoutEffect的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 37 | 38 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 39 | export function useLayoutEffect( 40 | create: () => (() => void) | void, 41 | deps: Array | void | null, 42 | ): void { 43 | const dispatcher = resolveDispatcher(); 44 | return dispatcher.useLayoutEffect(create, deps); 45 | } 46 | 47 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^ 你只需知道useLayoutEffect的用法和useEffect一模一样即可。 48 | 49 | 50 | ## useLayoutEffect基本用法 51 | 52 | useLayoutEffect的用法和useEffect的用法相同,所以不再阐述。 53 | 54 | 55 | ## useLayoutEffect使用示例: 56 | 57 | 请原谅,目前竟然找不到一个useLayoutEffect合适的例子,因为能够想到的应用场景其实都可以用useEffect来代替。 58 | 59 | 那只能贴出一段简单的代码,让你看确认一下,useLayoutEffect先于useEffect触发调用。 60 | 61 | 代码示例如下: 62 | 63 | import React,{useState,useEffect,useLayoutEffect} from 'react' 64 | 65 | function LayoutEffect() { 66 | const [count,setCount] = useState(0); 67 | 68 | useEffect(() => { 69 | console.log('useEffect...'); 70 | },[count]); 71 | 72 | useLayoutEffect(() => { 73 | console.log('useLayoutEffect...'); 74 | },[count]); 75 | 76 | return ( 77 |
78 | {count} 79 | 80 |
81 | ) 82 | } 83 | export default LayoutEffect 84 | 85 | 86 | 实际运行就会发现: 87 | 无论是首次挂载,还是重新渲染,console面板中,输出顺序都是 88 | useLayoutEffect... 89 | useEffect... 90 | 91 | 也就确认,先执行useLayoutEffect,后执行useEffect。 92 | 93 | --- 94 | 95 | 至此,关于useLayoutEffect基础用法已经讲完,没有高级用法,直接进入下一个Hook。 96 | 97 | 欢迎进入下一章节:[useDebugValue基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/15%20useDebugValue%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 98 | -------------------------------------------------------------------------------- /18 示例:React使用Echarts所用到的hooks.md: -------------------------------------------------------------------------------- 1 | # 18 示例:React使用Echarts所用到的hooks 2 | 3 | 本篇文章写于 2020年11月13日,距离前面文章已经过去半年,因此本文的讲述风格和示例代码,可能和前面的章节不同。 4 | 5 | 6 | 7 | ## Echarts简介 8 | 9 | Echarts 是百度公司推出的,基于原生 JS 的图表库,免费开源 ,可用于数据可视化项目。 10 | 11 | 官网地址:https://echarts.apache.org/zh/feature.html 12 | 13 | 14 | 15 | ## Echarts基础操作 16 | 17 | 1、**Echarts 是基于原生 JS 的库,而不是 React 组件**,需要将 “图表” 挂载到 DOM 18 | 19 | 2、echarts.init(xxx-dom) 是创建 “图表” 的入口函数,该函数将创建创建真正的图表实例,并填充到 xxx-dom 中 20 | 21 | 3、一个图表 对应一个 DOM,N 个图标需要 N 个 DOM 22 | 23 | 4、图表实例通过 setOption(option) 来设置(更新)数据 24 | 25 | 26 | 27 | ## 针对以上Echarts特性,对应的 hooks 28 | 29 | 1、使用 useRef 来勾住 jsx 中的某个 DOM 30 | 31 | 2、使用 useEffect( () => {}, [] ) 来勾住 React 第一次挂载,并通过 echarts.init(xxx-dom) 创建出真正的图表 32 | 33 | 3、使用 useState 来勾住 创建出的真正图表,以便以后做各种更新操作 34 | 35 | 4、使用 useEffect( () => {}, [xxx-echart,option] ) 来不断监听组件传递过来的数据变化,并更新图表数据 36 | 37 | 38 | 39 | > 补充说明:尽管 NPM 中已经有 echarts-for-react 这个包,已经将 Echarts 封装成可直接使用的 React 组件,但是我并不是建议使用,因为毕竟 Echarts 并不是特别难,没有必要使用别人封装好的。学习本文后,你自己也可以轻松封装自己的 Echarts 组件,灵活方便。 40 | 41 | 42 | 43 | ## 使用(封装)Echarts示例代码 44 | 45 | > 细节不过多说,此处只演示 2 个组件源码,使用 TypeScript 编写 46 | > 47 | > 1. 子组件为一个图表,图表是什么类型,由 配置数据 option 中 xAxis.type 的值决定 48 | > 2. 父组件负责调用子组件并传递图表配置数据 49 | 50 | **父组件:** 51 | 52 | ``` 53 | import React from 'react' 54 | import { EChartOption } from 'echarts' 55 | import Echart from '../../components/echart' 56 | 57 | import './index.scss' 58 | 59 | const option: EChartOption = { 60 | xAxis: { 61 | type: 'category', 62 | data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 63 | }, 64 | yAxis: { 65 | type: 'value' 66 | }, 67 | series: [{ 68 | data: [820, 932, 901, 934, 1290, 1330, 1320], 69 | type: 'line' 70 | }] 71 | } 72 | 73 | const IndexPage: React.FC = () => { 74 | return ( 75 | 76 | ) 77 | } 78 | 79 | export default IndexPage 80 | ``` 81 | 82 | 83 | 84 | **子组件:** 85 | 86 | ``` 87 | import React, { useState, useRef, useEffect } from 'react' 88 | import echarts, { EChartOption, ECharts } from 'echarts' 89 | 90 | import './index.scss' 91 | 92 | interface EchartProp { 93 | option: EChartOption 94 | } 95 | 96 | const Echart: React.FC = ({ option }) => { 97 | 98 | const chartRef = useRef(null) //用来勾住渲染后的 DOM 99 | const [echartsInstance, setEchartsInstance] = useState() //用来勾住生成后的 图表实例对象 100 | 101 | //仅第一次挂载时执行,将 DOM 传递给 echarts,通过 echarts.init() 得到真正的图表 JS 对象 102 | useEffect(() => { 103 | if (chartRef.current) { 104 | setEchartsInstance(echarts.init(chartRef.current)) 105 | } 106 | }, []) 107 | 108 | //监听依赖变化,并根据需要更新图表数据 109 | useEffect(() => { 110 | echartsInstance?.setOption(option) 111 | }, [echartsInstance, option]) 112 | 113 | return ( 114 |
115 | ) 116 | } 117 | 118 | export default Echart 119 | ``` 120 | 121 | 122 | 123 | 以上示例中,父组件功能相对简单,负责调用子组件,并将图表配置数据传递给子组件。 124 | 125 | 真正需要关注的就是 子组件,在子组件中分别用到了 useRef、useState、useEffect 这 3 个 hook,尤其是 useEffect 还被使用了 2 次。 126 | 127 | 128 | 129 | 本文其实出自我写的另外一篇学习笔记:[React-Typescript中使用Echarts.md](https://github.com/puxiao/notes/blob/master/React-Typescript%E4%B8%AD%E4%BD%BF%E7%94%A8Echarts.md) 130 | 131 | 加油,打工人! 132 | -------------------------------------------------------------------------------- /19 useTransition基础用法.md: -------------------------------------------------------------------------------- 1 | ## 19 useTransition基础用法 2 | 3 | ## useTransition概念介绍 4 | react提供了``useDeferredValue``发挥类似防抖节流的作用,而``useTransition``也是类似的作用,但是该hook是通过降低数据渲染的优先级来达到优先更新其他数据 5 | 6 | ## useTransition用来解决什么问题? 7 | - 首先给定一个场景,开发时经常会遇到需要联想输入,也就是输入的同时要返回联想搜索结果的列表。 8 | - 但是这个列表有时返回值非常的长,有时会导致用户输入值的更新缓慢,这里就产生了一个问题,当页面有大量UI更新的时候,怎么处理数据更新不会卡顿。 9 | - 可以手写防抖节流,防抖有一个弊端,当我们长时间的持续输入(时间间隔小于防抖设置的时间),页面就会长时间都不到响应。而startTransition 可以指定 UI 的渲染优先级,哪些需要实时更新,哪些需要延迟更新。即使用户长时间输入最迟 5s 也会更新一次。 10 | - 也可以用``useDeferredValue``,也可以用“可视窗口加载”的方案, 11 | 在这里介绍怎么用``useTransition``解决 12 | 13 | ## useTransition源码 14 | 回到useTransition的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 15 | 16 | ```typescript 17 | export function useTransition(): [ 18 | boolean, 19 | (callback: () => void, options?: StartTransitionOptions) => void, 20 | ] { 21 | const dispatcher = resolveDispatcher(); 22 | return dispatcher.useTransition(); 23 | } 24 | ``` 25 | 再根据引入文件,到``react-reconciler/src/ReactInternalTypes.js``找到Dispatch里最终调用的useTransition 26 | ```typescript 27 | useTransition(): [ 28 | boolean, 29 | (callback: () => void, options?: StartTransitionOptions) => void, 30 | ], 31 | ``` 32 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^ 33 | 34 | ## useTransition基本用法 35 | useTransition()函数可以不传参,传参可以传一个毫秒值用来修改最迟更新时间,startTransition回调里的赋值将会被降低优先级。isPending 指示过渡任务何时活跃以显示一个等待状态。 36 | 37 | > 代码形式 38 | ```typescript 39 | const [isPending, startTransition] = useTransition(); 40 | 41 | startTransition(() => { 42 | setCount(count + 1); 43 | }) 44 | ``` 45 | 传参写法 46 | ```typescript 47 | // 延迟两秒 48 | const [isPending, startTransition] = useTransition(2000); 49 | 50 | startTransition(() => { 51 | setCount(count + 1); 52 | }) 53 | ``` 54 | 55 | ## useTransition使用示例 56 | 举例:搜索引擎的关键词联想。一般来说,对于用户在输入框中输入都希望是实时更新的,如果此时联想词比较多同时也要实时更新的话,这就可能会导致用户的输入会卡顿。这样一来用户的体验会变差,这并不是我们想要的结果。 57 | 58 | 我们将这个场景的状态更新提取出来:一个是用户输入的更新;一个是联想词的更新,这个两个更新紧急程度显然前者大于后者。 59 | 60 | 这里更新效果可能还不够明显,可以打开浏览器控制台,点击``performance insights``项,在Measure page load右边有个下拉选项,在cpu那栏的右边下拉选择``4x slowdown``可以将浏览器运行速度调慢四倍,这样卡顿会明显些。 61 | 62 | 这里拆分为两个组件,父组件是useTransition的使用,子组件是列表渲染,代码如下: 63 | 64 | 父组件: 65 | ```tsx 66 | import { useState, useTransition } from 'react' 67 | import ProductList from './components/ProductList' 68 | 69 | // 列表数据的生成 70 | export function generateProducts() { 71 | const products: Array = [] 72 | for (let i = 0; i < 10000; i++) { 73 | products.push(`Product ${i + 1}`) 74 | } 75 | return products 76 | } 77 | 78 | // 列表数据 79 | const dummyProducts = generateProducts() 80 | 81 | // 用户输入时过滤搜索,达到一个联想输入的效果 82 | function filterProducts(filterTerm) { 83 | if (!filterTerm) { 84 | return dummyProducts 85 | } 86 | return dummyProducts.filter((product) => product.includes(filterTerm)) 87 | } 88 | 89 | function App() { 90 | const [isPending, startTransition] = useTransition() 91 | const [filterTerm, setFilterTerm] = useState('') 92 | 93 | const filteredProducts = filterProducts(filterTerm) 94 | 95 | function updateFilterHandler(event) { 96 | // 列表数据赋值的运行等级 97 | startTransition(() => { 98 | setFilterTerm(event.target.value) 99 | }) 100 | } 101 | 102 | return ( 103 |
104 | 105 | {isPending &&

更新列表。.

} 106 | 107 |
108 | ) 109 | } 110 | 111 | export default App 112 | ``` 113 | 114 | 子组件: 115 | ```tsx 116 | import { useDeferredValue } from "react"; 117 | 118 | function ProductList({ products }) { 119 | const deferredProducts = useDeferredValue(products); 120 | return ( 121 |
    122 | {deferredProducts.map((product, index) => ( 123 |
  • {product}
  • 124 | ))} 125 |
126 | ); 127 | } 128 | 129 | export default ProductList; 130 | ``` 131 | 132 | 通过这个案例,相信你对useMemo的机制和用法一定有所掌握。 133 | 134 | --- 135 | 136 | 至此,关于useTransition基础用法已经讲完。 -------------------------------------------------------------------------------- /15 useDebugValue基础用法.md: -------------------------------------------------------------------------------- 1 | # 15 useDebugValue基础用法 2 | 3 | ## useDebugValue概念解释 4 | 我们第十个要学习的Hook(钩子函数)是useDebugValue,他的作用是“勾住”React开发调试工具中的自定义hook标签,让useDebugValue勾住的自定义hook可以显示额外的信息。 5 | 6 | ##### “React开发调试工具”是什么? 7 | 答:谷歌浏览器中的一个扩展插件,名字叫“React Developer Tools”,方便我们在谷歌浏览器上进行react项目调试。 8 | 9 | 如何安装? 10 | 答:可在Chrome扩展程序商店搜索并安装。由于国内网络原因,如果你不会科学上网,那么可以通过国内的一些Chrome扩展程序商店网站,下载“React Developer Tools”离线的crx安装文件进行安装。具体办法可以自己百度。 11 | 12 | “React开发调试工具”的使用简单说明: 13 | 如果该扩展程序安装成功,那么会有以下几种情况: 14 | 1、对于本机开发调试的项目网页,该插件图标会变成橘黄色,且图标中间有一个小虫子,表示可以进行react源码式的调试,当代码出现错误时会精准定位出错的代码位置。 15 | 16 | 2、对于别人开发的项目网页,该插件图标会变成蓝色,表示该网页由react开发,当代码出现错误时不能精准定位出错的代码位置。 17 | 例如阿里云后台、腾讯云后台、百度翻译这些网页都是用react开发,访问这些网页你就会看到 调试工具图光标为蓝色。 这些大厂都用react,所以虽然学习过程中很痛苦,但是是值得的。 18 | 19 | 3、对于没有使用react的网页,该插件图标会变成灰色。 20 | 21 | 22 | 让我们回到useDebugValue基础学习中。 23 | 24 | 25 | ## useDebugValue是来解决什么问题的? 26 | 答:useDebugValue的目的是“在react开发者工具自定义hook标签中显示额外信息”,方便我们“一眼就能找到”对应的自定义hook。 27 | 28 | 补充说明: 29 | 1、react官网文档中明确表示,在普通项目开发中不推荐使用useDebugValue,默认的调试输出已经很清晰可用了。 30 | 2、除非你的自定义 hook 是作为共享库中的一部分才有价值。这样其他人更容易注意到你自定义的hook状态变化。 31 | 32 | ##### 自定义hook? 33 | 你可能注意到本章中提到了“自定义hook”,没错。像之前学习的useState、useContext等等都是react自带的hook,这些默认的hook是我们项目开发所需要用到的各种钩子函数。 34 | 35 | 但是实际开发中,我们需要借助这些基础的、默认的、自带的hook函数,通过组合以及添加业务逻辑代码,形成自己的hook函数。 36 | 37 | 具体如何自定义hook,稍后会单独有一章如何“自定义hook”中详细讲述。 38 | 39 | ## useDebugValue函数源码: 40 | 回到useDebugValue的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 41 | 42 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 43 | export function useDebugValue( 44 | value: T, 45 | formatterFn: ?(value: T) => mixed, 46 | ): void { 47 | if (__DEV__) { 48 | const dispatcher = resolveDispatcher(); 49 | return dispatcher.useDebugValue(value, formatterFn); 50 | } 51 | } 52 | 53 | 54 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^ 55 | 56 | 57 | ## useDebugValue基本用法 58 | 59 | useDebugValue(value,formatterFn)函数第1个参数为我们要额外显示的内容变量。第2个参数是可选的,是对第1个参数值的数据化格式函数。 60 | 61 | 请注意: 62 | 1、useDebugValue应该在自定义hook中使用,如果直接在组件内使用是无效的,不会报错也不会有任何额外信息展示。 63 | 1、一般调试不需要使用useDebugValue,除非你编写的hook是公共库中的一部分,实在是想凸显额外信息,引起别人注意。 64 | 2、如果使用useDebugValue,最好设置第2个参数,向react开发调试工具讲清楚如何格式化展示第1个参数。 65 | 66 | ##### 代码形式: 67 | 68 | useDebugValue(xxx, xxx => xxxxx) 69 | 70 | 71 | ##### 拆解说明: 72 | 73 | 1、xxx 为我们要重点关注的变量。 74 | 2、xxx => xxxxx 是 (xxx) => {return xxxxx} 的简写。表明如何格式化变量xxx。 75 | 76 | ## 如何在react调试工具中查看useDebugValue表现形式 77 | 78 | 前提条件: 79 | 1、在谷歌浏览器中成功安装了react开发调试工具 80 | 2、react项目中使用了自定义hook,且hook中使用了useDebugValue 81 | 82 | 那么你可以进行一下步骤: 83 | 1、打开react调试网页,例如http://localhost:3000/ 84 | 2、打开谷歌浏览器调试面板(快捷键为F12) 85 | 3、找到并点击“Components”一栏 86 | 4、在右侧窗口中,找到“hooks”,在“hooks”下就能看到自定义hook中useDebugValue自定义显示的信息。 87 | 88 | 具体还是以下面实际例子来说明。 89 | 90 | ## useDebugValue使用示例: 91 | 92 | 举例:useTime是我们自定义的一个hook函数,那么在这个自定义hook中,可以通过useDebugValue对变量time进行额外信息展示。 93 | 94 | 代码示例如下: 95 | 96 | //自定义hook:useTime 97 | function useTime(date){ 98 | const [time,setTime] = useState(date); 99 | useDebugValue(time,time => new Date(time));//请注意这一行代码 100 | return [time,setTime]; 101 | } 102 | 103 | //组件中使用useTime,伪代码片段 104 | const [time,setTime] = useTime(Date.now());//请注意此处使用的是自定义hook:useTime 105 | 106 | 代码分析: 107 | 1、我们在自定义hook中,使用了useDebugValue 108 | 2、useDebugValue第1个参数是time,向react开发调试工具表明要重点关注的变量是time。 109 | 3、第2个参数是对time的一个格式化函数。由于time实际为一个时间戳数字,通过time => new Date(time)将时间戳转化成具体的可读时间字符串,例如此时此刻:Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间) 110 | 111 | 112 | ##### 具体表现 113 | 在谷歌浏览器调试面板的“Component”右侧,你会看到: 114 | 115 | hooks 116 | time:Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间) 117 | State:1589178459090 118 | 119 | 假设不使用useDebuValue,默认看到的是: 120 | 121 | hooks 122 | time: 123 | State:1589178459090 124 | 125 | “Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间) ”就是useDebugValue额外展示出的信息。 126 | 127 | 你甚至还可以使用模板字符串,对格式化数据进行修改,比如将原本的第2个参数 time => new Date(time) 修改为:time => \`看这里 ${new Date(time)}\` 128 | 129 | 在谷歌浏览器调试面板的“Component”右侧,你会看到: 130 | 131 | hooks 132 | time:看这里 Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间) 133 | State:1589178459090 134 | 135 | 136 | ##### 再次强调,对于一般性的项目开发,是不需要使用useDebugValue来额外标记某些变量的,默认的调试输出足够我们使用了。 137 | 138 | 139 | --- 140 | 141 | 至此,关于useDebugValue基础用法已经讲完,没有高级用法,直接进入下一个Hook。 142 | 143 | 不!基于react 16.13版本的全部 hook,终于讲完了,没有下一个hook了。 144 | 145 | 能坚持到现在,真的不容易,默认自带的 react hook 学完后,还需要学习如何自定义hook... 146 | 扶我起来,再坚持一下。 147 | 148 | 欢迎进入下一章节:[自定义hook](https://github.com/puxiao/react-hook-tutorial/blob/master/16%20%E8%87%AA%E5%AE%9A%E4%B9%89hook.md) 149 | -------------------------------------------------------------------------------- /05 useEffect高级用法.md: -------------------------------------------------------------------------------- 1 | # 05 useEffect高级用法 2 | 3 | 所谓高级用法,只不过是一些深层知识点和实用技巧,你甚至可以把本章当做对前面知识点的一个巩固和学习。 4 | 5 | ## 让useEffect只在挂载后和卸载前执行一次 6 | 7 | 让我们实现 “04 useEffect基础用法” 中 举例2 提到的功能。 8 | 9 | 组件需求: 10 | 1、若某类组件中有变量a,默认值为0,当组件第一次被挂载后或组件重新渲染后,将网页标题显示为a的值。 11 | 2、当组件第一次被挂载后执行一个自动累加器 setInterval,每1秒 a 的值+1。为了防止内存泄露,我们在该组件即将被卸载前清除掉该累加器。 12 | 13 | 需求分析: 14 | 关于自动累加器的操作,只关联 “组件挂载后和组件卸载前” 这2个生命周期函数中,那useEffect还包含了每次组件重新渲染后,这该怎么办? 15 | 16 | 答:useEffect函数的第2个参数表示该依赖关系,**将useEffect的第2个参数,设置为空数组 []**,即表示告诉React,这个useEffect不依赖任何变量的更新所引发的组件重新渲染,以后此组件再更新也不需要调用此useEffect。 17 | 18 | 这样就可以实现只在第一次挂载后和卸载前调用此useEffect的目的。 19 | 20 | import React, { useState,useEffect} from 'react'; 21 | 22 | function Component() { 23 | const [a, setA] = useState(0);//定义变量a,并且默认值为0 24 | 25 | //定义第1个useEffect,专门用来处理自动累加器 26 | useEffect(() => { 27 | let timer = setInterval(() => {setA(a+1)},1000);// <-- 请注意这行代码,暗藏玄机 28 | return () => { 29 | clearInterval(timer); 30 | } 31 | }, []);//此处第2个参数为[],告知React以后该组件任何更新引发的重新渲染都与此useEffect无关 32 | 33 | //定义第2个useEffect,专门用来处理网页标题更新 34 | useEffect(() => { 35 | document.title = `${a} - ${Math.floor(Math.random()*100)}`; 36 | },[a]) 37 | return
{a}
38 | } 39 | 40 | export default Component; 41 | 42 | 以上代码实际运行正确吗? 43 | 答:不正确! 44 | 45 | ? 46 | 小朋友,脸上是否有很多问号??? 47 | 48 | 实际运行会发现,当组件挂载后,确实会执行一次 setA(a+1),a 的值修改为了 1,然后... a 的值一直为 1,并没有继续累加。 49 | 50 | 上述代码会收到react的一个错误警告提示:Either include it or remove the dependency array. You can also do a functional update 'setA(a => ...)' if you only need 'a' in the 'setA' call. 51 | 该错误警告意思是:如果你确认你传入的第2个参数是空数组,那么你可能会用到 setA(a => ...) 这种方式来更新a的值。 52 | 53 | 问题出在哪里? 54 | 55 | 让我们再看看那行有玄机的代码: 56 | 57 | let timer = setInterval(() => {setA(a+1)},1000); 58 | 59 | 再看看 react 给我们的错误警告提示:You can also do a functional update 'setA(a => ...)' if you only need 'a' in the 'setA' call. 你可能会用到 setA(a => ...) 这种方式来更新a的值。 60 | 61 | setA(a => ...) 这是在 “03 useState高级用法”中,解决数据异步 时讲的更新方式。 62 | 63 | 那我们就按照提示,将那行代码修改为: 64 | 65 | let timer = setInterval(() => {setA(a => a+1)},1000); 66 | 67 | 再次执行,错误提示警告没有了,组件也完全按照我们的预期来执行了。react自带的语法检查真的好智能。 68 | 69 | ##### 为什么会有这个问题? 70 | 关于刚才setInterval中累加 a 的值遇到的问题,React官方文档中也有类似示例,只不过他们用的变量是count,而我们这里用的变量是 a。 71 | 72 | 我们看看从React官方文档中引用的话: 73 | > 有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug,传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。 74 | 75 | 再次重复一遍:如果useEffect函数第2个参数为空数组,那么react会将该useEffect的第1个参数 effect 建立一个闭包,该闭包里的变量 a 被永远设定为当初的值,即 0。尽管setInterval正常工作,每次都“正常执行了”,可是 setA(a+1)中 a 的值一直没变化,一直都是当初的0,所以造成 0 + 1 一直都等于 1 的结果。 76 | 77 | 而如果修改成 setA(a => a+1) 的形式,那么就解决了 a 数据异步的问题,每次都是读取最新当前 a 的值。 78 | 79 | 这个点是使用 useEffect 很容易掉进去的一个坑,切记切记。 80 | 81 | 或者以后养成都用异步更新数据的习惯。 82 | 83 | 84 | ## 性能优化 85 | 86 | 通过上面的例子,我们其实已经实现了 前文中 举例2 和举例3 的效果。 87 | 88 | 咦~ 刚才讲的是举例2,没有将举例3啊... 因为举例3中提到的类组件中有多个变量数据,在函数组件中这个问题本身是靠useState来解决的,跟useEffect无关。 89 | 90 | 接下来讲一下useEffect函数第2个参数提高性能的正确用法。 91 | 92 | 举例:若一个组件中有一个自定义变量obj,obj有两个属性a、b,当a发生变化时,网页标题也跟着a发生变化。 93 | 补充说明: 94 | 1、我们为了让a、b都可以发生变化,将在组件中创建2个按钮,点击之后分别可以修改a、b的值; 95 | 2、为了更加清楚看到每次渲染,我们在网页标题中 a 的后面再增加一个随机数字; 96 | 97 | 我们首先看以下代码: 98 | 99 | import React, { useState,useEffect} from 'react'; 100 | function Component() { 101 | const [obj,setObj] = useState({a:0,b:0}); 102 | useEffect(() => { 103 | document.title = `${obj.a} - ${Math.floor(Math.random()*50)}`; 104 | }); //注意此时我们并未设置useEffect函数的第2个参数 105 | 106 | //如果下面代码看不懂,你需要重新去温习useState高级用法中的“数据类型为Objcet,修改方法” 107 | return
108 | {JSON.stringify(obj)} 109 | 110 | 111 |
112 | } 113 | export default Component; 114 | 115 | 由于我们在网页标题中添加了随机数,因此实际运行你会发现即使修改b的值,也会引发网页标题重新“变更一次”。 116 | 117 | 理由显而易见,修改b的值也会触发组件重新渲染,进而触发useEffect中的代码。 118 | 119 | 正确的做法应该是我们给useEffect添加上第2个参数:[obj.a],明确告诉React,只有当obj.a变更引发的重新渲染才执行本条useEffect。 120 | 121 | useEffect(() => { 122 | document.title = `${obj.a} - ${Math.floor(Math.random()*50)}`; 123 | },[obj.a]); //第2个参数为数组,该数组中可以包含多个变量 124 | 125 | 添加过[obj.a]之后,再次运行,无论obj.b或者其他数据变量引发的组件重新渲染,都不会执行该useEffect。 126 | 127 | 因此达到提高性能的目的。 128 | 129 | 130 | --- 131 | 132 | 至此,关于useEffect高级用法已经讲完,相信useState和useEffect的组合使用,已经能够让你写出一些简单的React Hook 组件。 133 | 134 | 接下来学习第3个Hook函数useContext。 135 | 136 | 欢迎进入下一章节:[useContext基本用法](https://github.com/puxiao/react-hook-tutorial/blob/master/06%20useContext%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 137 | -------------------------------------------------------------------------------- /11 useMemo基础用法.md: -------------------------------------------------------------------------------- 1 | # 11 useMemo基础用法 2 | 3 | ## useMemo概念解释 4 | 我们第六个要学习的Hook(钩子函数)是useMemo,他的作用是“勾住”组件中某些处理函数的返回值,创建这些返回值对应在react原型链上的索引。当组件重新渲染时,需要再次用到这些函数返回值,此时不再重新执行一遍运算,而是直接使用之前运算过的返回值。useMemo第2个参数是处理函数的变量依赖,只有当处理函数依赖的变量发生改变时才会重新计算并保存一次函数返回结果。 5 | 6 | 假设你已经对React.memo,useCallback的运行机制充分了解,那么对你而言useMemo的用法非常好理解。 7 | 8 | useCallback是将某个函数“放入到react底层原型链上,并返回该函数的索引”,而useMemo是将某个函数返回值“放入到react底层原型链上,并返回该返回值的索引”。一个是针对函数,一个是针对函数返回值。 9 | 10 | 网上有些人的文章里,会提到:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。 11 | 12 | 这句话似乎是没有问题,但是他隐藏或者说忽略了几个重要关键点: 13 | 1、不是所有fn(函数)都适用的,必须是该函数有返回值,即函数有 return xx 才可以。 14 | 2、虽然都是fn,但是函数体内代码内容却相差很大,useCallback中的fn主要用来处理各种操作事务的代码,例如修改某变量值或加载数据等。而useMemo中的fn主要用来处理各种计算事务的代码。 15 | 3、useCallback和useMemo都是为了提升组件性能,但是他们两个的适用场景却不相同,不是谁是谁的替代品或谁是谁的简化版。 16 | 17 | 再次强调一遍,useCallback中的函数是侧重“操作事务”,useMemo中的函数是侧重“计算结果”,永远不要在useMemo的函数中添加修改数据之类的代码。 18 | 19 | 让我们回到useMemo基础学习中。 20 | 21 | 22 | ## useMemo是来解决什么问题的? 23 | 答:useMemo的目的是“减少组件重新渲染时不必要的函数计算”。 24 | useMemo可以将某些函数的计算结果(返回值)挂载到react底层原型链上,并返回该函数返回值的索引。当组件重新渲染时,如果useMemo依赖的数据变量未发生变化,那么直接使用原型链上保存的该函数计算结果,跳过本次无意义的重新计算,达到提高组件性能的目的。 25 | 26 | 补充说明: 27 | 1、useMemo并不需要子组件必须使用React.memo。 28 | 2、“不必要的函数计算”中的函数计算必须是有一定复杂度的,例如需要1000个for循环才能计算出的某个值。如果计算量本身很简单,例如1+2,那完全没有必要使用useMemo,就直接每次重新计算一遍也无所谓。 29 | 30 | 31 | ## useMemo函数源码: 32 | 回到useMemo的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 33 | 34 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 35 | export function useMemo( 36 | create: () => T, 37 | deps: Array | void | null, 38 | ): T { 39 | const dispatcher = resolveDispatcher(); 40 | return dispatcher.useMemo(create, deps); 41 | } 42 | 43 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^ 44 | 45 | 46 | ## useMemo基本用法 47 | 48 | useMemo(create,deps)函数通常传入2个参数,第1个参数为我们定义的一个“包含复杂计算且有返回值的函数”,第2个参数为该处理函数中存在的依赖变量,请注意凡是处理函数中有的数据变量都需要放入deps中。如果处理函数没有任何依赖变量,可以传入一个空数组[]。 49 | 50 | 请注意: 51 | 1、useMemo只是理论上帮你进行组件计算性能优化,但是react并不能保证100%都是按照你的预期来执行的。比如说当你的网页处于离屏(休眠、挂起)等状态时,react底层原型链也许就会释放(删除)之前保存的函数返回值。等到下次网页重新被唤醒时,重新计算一次。 52 | 2、关于useMemo第2个参数,和useCallback一样,也许在未来版本中react会智能识别,不需要要我们再手工传入。 53 | 54 | 55 | ##### 代码形式: 56 | 57 | const xxxValue = useMemo(() => { 58 | let result = xxxxx; 59 | //经过复杂的计算后 60 | return result; 61 | }, [xx]); 62 | 63 | 64 | ##### 拆解说明: 65 | 66 | 1、使用useMemo()将计算函数包裹住,将计算函数中使用到的数据变量作为作为第2个参数。 67 | 2、计算函数体内,把计算结果以 return 形式返回出去。 68 | 3、xxxValue 为该函数返回值在react原型链上的引用。 69 | 70 | 71 | ## useMemo使用示例: 72 | 73 | 举例:若某React组件内部有2个number类型的变量num,random,有2个button,点击之后分别可以修改num,random的值。 74 | 与此同时,该组件中还要求显示出num范围内的所有质数个数总和。 75 | 76 | 补充说明:加入random纯粹是为了引发组件重新渲染,方便我们查看到useMemo是否启了作用。 77 | 78 | 需求分析: 79 | 1、显示出num范围内的所有质数个数总和,这个就是本组件中的“复杂的计算”。 80 | 2、只要num的值未发生变化,质数总数是固定的,那么我们应该避免每次重新渲染时都需要计算一遍。 81 | 3、useMemo函数,就是帮我们解决这个问题。 82 | 83 | 使用useMemo,代码示例如下: 84 | 85 | import React,{useState,useMemo} from 'react' 86 | 87 | function UseMemo() { 88 | const [num,setNum] = useState(2020); 89 | const [random,setRandom] = useState(0); 90 | 91 | //通过useMemo将函数内的计算结果(返回值)保存到react底层原型链上 92 | //totalPrimes为react底层原型链上该函数计算结果的引用 93 | const totalPrimes = useMemo(() => { 94 | console.log('begin....'); //这里添加一个console.log,方便验证在重新渲染时是否重新执行了一遍计算 95 | 96 | let total = 0; //声明质数总和对应的变量 97 | 98 | //以下为计算num范围内所有质数个数总和的计算代码,不需要认真阅读,只需要知道这是一段“比较复杂的计算代码”即可 99 | for(let i = 1; i<=num; i++){ 100 | let boo = true; 101 | for(let j = 2; j { 117 | setNum(num+1); 118 | } 119 | 120 | const clickHandler02 = () => { 121 | setRandom(Math.floor(Math.random()*100)); //修改random的值导致整个组件重新渲染 122 | } 123 | 124 | return ( 125 |
126 | {num} - {totalPrimes} - {random} 127 | 128 | 129 |
130 | ) 131 | } 132 | 133 | export default UseMemo; 134 | 135 | 实际运行就会发现: 136 | 1、点击修改random的值会引发组件重新渲染,但是{totalPrimes}对应的计算函数却不需要重新计算一遍。 137 | 2、点击修改num的值,{totalPrimes}对应的计算函数肯定会重新执行一遍,因为num是该计算函数的依赖。 138 | 139 | 通过这个案例,相信你对useMemo的机制和用法一定有所掌握。 140 | 141 | --- 142 | 143 | 至此,关于useMemo基础用法已经讲完,没有高级用法,直接进入下一个Hook。 144 | 145 | 欢迎进入下一章节:[useRef基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/12%20useRef%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 146 | -------------------------------------------------------------------------------- /02 useState基础用法.md: -------------------------------------------------------------------------------- 1 | # 02 useState基础用法 2 | 3 | ## useState概念解释 4 | 我们第一个要学习的Hook(钩子函数)是useState,他的作用是“勾住”函数组件中自定义的变量。 5 | 6 | “勾住”? 7 | 回顾一下 “React Hook 简介” 文中那句话:Hook本身单词意思是“钩子”,作用就是“勾住某些生命周期函数或某些数据状态,并进行某些关联触发调用”。 8 | 9 | “如何勾住”? 10 | 在React底层代码中,是通过自定义dispatcher,采用“发布订阅模式”实现的。 11 | 12 | 关于“钩子”、“勾住”、“如何勾住”的概念以后在学习其他Hook函数时不再做解释。 13 | 14 | ## useState是来解决类组件什么问题的? 15 | 16 | 答:useState能够解决类组件 **所有自定义变量只能存储在this.state** 的问题。 17 | 18 | 举例:若某组件需要有2个自定义变量name和age,那么在类组件中只能如下定义 19 | 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | name:'puxiao', 24 | age:34 25 | } 26 | } 27 | 28 | name和age只能作为this.state的一个属性。 29 | 30 | 没有对比就没有伤害,看一下使用useState后,函数组件是如何实现上述需求的 31 | 32 | const [name,setName] = useState('puxiao'); 33 | const [age,setAge] = useState(34); 34 | 35 | 1、函数组件本身是一个函数,不是类,因此没有构造函数constructor(props); 36 | 2、任何你想定义的变量都可以单独拆分出去,独立定义,互不影响; 37 | 38 | 两段代码对比之下,你就会发现使用Hook的useState后,会让我们定义的变量相对独立,清晰简单,便于管理。 39 | 40 | 接下来开始学习useState。 41 | 42 | ## useState函数源码: 43 | 44 | 首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 45 | 46 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 47 | export function useState( 48 | initialState: (() => S) | S, 49 | ): [S, Dispatch>] { 50 | const dispatcher = resolveDispatcher(); 51 | return dispatcher.useState(initialState); 52 | } 53 | 54 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。之所以贴出源码只是为了显得本文比较有深度。^_^ 55 | 56 | **更新于2020.11.10,这里强调一下:React 源码中使用的是 flow 语法,根本不是 TypeScript 语法,只不过 2 者实在是太像了,以至于让我之前一直误以为 React 源码中是 TS。不过你完全可以将 TS 的泛型知识去套用到 flow 中。在此特别说明一下,至于后续章节中就不再做提醒和修改了,你就当成 TS 语法去理解也行。** 57 | 58 | ##### 补充一些TypeScript常识: 59 | 1、react 本身采用TypeScript编写,还是补充点TS常识,方便对各个 hook 函数源码的理解。 60 | 2、对于useState以及以后要学习的其他hook函数源码,函数参数中会反复出现,这些大写字母,react约定他们对应的单词如下: 61 | state -> S -> 约定表示某种“数据” 62 | type -> T -> 约定表示某种“类型” 63 | props -> P -> 约定表示“属性传值对应的props” 64 | initial -> I -> 约定表示某个“初始值” 65 | 66 | 1、这种用包裹起来的类型声明,在TS中成为“泛型”。理论上是可以使用任意单词的,上面那些缩写只是react自己约定单词缩写。 67 | 2、对于一段TS代码,如果出现了,那么后面所有的都将表示“某种相同类型的数据”。对于TypeScript的泛型相关知识,请自己百度学习。 68 | 69 | 70 | ## useState基本用法 71 | 72 | useState(value)函数会返回一个数组,该数组包含2个元素:第1个元素为我们定义的变量,第2个元素为修改该变量对应的函数名称。 73 | 74 | ##### 代码形式: 75 | 76 | const [variable,setVariable] = useState(value); 77 | //.... 78 | setVariable(newValue);//修改variable的值 79 | 80 | 81 | ##### 拆解说明: 82 | 83 | 1、const [a,b] = [a,b] 这种形式为ES6的“解构赋值”; 84 | 2、'variable'为函数组件中自定义的变量名; 85 | 3、'setVariable'为修改'variable'对应的函数名; 86 | 4、'useState'为本次学习的Hook函数; 87 | 5、'value'为变量默认值 88 | 6、'setVariable(newValue)'为调用setVariable并将新的值newValue赋值给variable; 89 | 90 | #### 'variable'补充说明 91 | 1、variable为变量名,实际使用中可以修改成任意变量名,比如name、age、count等等; 92 | 2、但是,函数组件接收父级组件属性传值的变量名为props,因此建议你不要将变量名定为props,以免混淆; 93 | 3、我不听话,我就非要将变量名定义成props,那又会怎么样?答案是不会有什么问题,不仅不会报错而且还会正常执行。 94 | 95 | ##### 'setVariable'补充说明: 96 | 1、该名称采用 "set"+"变量名" 的驼峰命名形式,只是为了提高代码可读性。 97 | 2、一般React项目都约定使用此种命名方式,所以推荐你也如此使用。 98 | 3、当然你也可以使用任意你喜欢的命名风格,但是切记不能以数字开头。 99 | 100 | ##### 'value'补充说明: 101 | 1、必填项,不可缺省,若缺省则实际运行时会提示变量名未定义; 102 | 2、值的类型可以是字符串、数字、数组、对象; 103 | 3、值还可以为null,但不可以为undefined; 104 | 105 | ##### 'newValue'补充说明(非常重要): 106 | setVariable采用 “异步直接赋值” 的形式,并不会像类组件中的setState()那样做“异步对比累加赋值”。 107 | 108 | “异步”? 109 | 这里的“异步”和类组件中setState中的异步是同一个意思,都是为了优化React渲染性能而故意为之。 110 | 111 | "直接赋值"? 112 | 1、在Hook中,对于简单类型数据,比如number、string类型,可以直接通过setVariable(newValue)直接进行赋值。 113 | 2、但对于复杂类型数据,比如array、object类型,若想修改其中某一个属性值而不影响其他属性,则需要先复制出一份,修改某属性后再整体赋值。具体如何做,请看下一篇“useState高级用法”中“数据类型为Objcet/Array修改方法”内容。 114 | 115 | 如果新值和当前值完全一样,那么会引发React重新渲染吗?请看下一篇“useState高级用法”中“性能优化”内容。 116 | 117 | 停!上面的信息量有点多,让我们把思绪先回到最基础的用法上。 118 | 119 | ## useState使用示例: 120 | 121 | //函数组件内定义变量name 122 | const [name,setName] = useState('nodejs'); //name默认值为nodejs 123 | 124 | //在函数组件内,某些事件交互处理函数中修改name的值,例如某次鼠标点击的处理函数handleClick 125 | const handleClick = () => { 126 | setName('koa'); 127 | //请注意,setName('koa')是异步修改的,如果此时执行console.log(name) 输出的值依然是nodejs 128 | //请留意下一篇文章 “03 useState高级用法” 中 “解决数据异步” 相关部分 129 | } 130 | 131 | 132 | 上述代码中,我们进行了以下操作: 133 | 1、声明一个变量name、修改name的方法setName、并将name默认值设置为'nodejs'; 134 | 2、通过setName将name值修改为'koa'; 135 | 136 | 注意:在一个组件中,可以不限次数使用useState(),因此,我们可以声明多个变量,例如下面代码: 137 | 138 | const [name,setName] = useState('puxiao'); 139 | const [age,setAge] = useState(34); 140 | 141 | 在该代码片段中,我们分别定义了2个变量:name、age 以及他们对应的修改函数setName、setAge。 142 | 143 | 144 | ## 练习题 145 | 用useState实现一个计数器,默认为0,每次点击+1。 146 | 147 | ##### 完整示例: 148 | 149 | import React, { useState } from 'react'; 150 | 151 | function Component() { 152 | 153 | const [count, setCount] = useState(0); 154 | 155 | function clickHandler(){ 156 | setCount(count+1); 157 | } 158 | 159 | return
160 | {count} 161 |
162 | } 163 | 164 | export default Component; 165 | 166 | 请注意上述代码中,没有用到this,这就是函数组件中使用Hook的魅力之一,再也不用去关心烦人的this到底指向谁这个问题了。 167 | 168 | 实际代码中,本人更加倾向于使用箭头函数来定义方法,所以上述 function clickHandler() 会写成: 169 | 170 | const clickHandler = () => { 171 | setCount(count+1); 172 | } 173 | 174 | --- 175 | 176 | 至此,关于useState基础用法已经讲完。 177 | 178 | 欢迎进入下一章节:[useState高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/03%20useState%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 179 | -------------------------------------------------------------------------------- /00 前言.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | > 以下信息更新于 2020.11.10 4 | > 5 | > 最近学习了 JS 原型链、数据结构与算法,以及在思否编程课上,看了一些 卡颂(微信公众号:魔术师卡颂) 录制的《[自顶向下学 React 源码](https://ke.sifou.com/course/1650000023864436)》课程,对 React 又有了更深的认识。 6 | > 7 | > 此时再回顾我半年前写的这系列文章,有几点现需要补充说明一下: 8 | > 9 | > 1. 强烈建议你在学习 hook 之前,先学习了解一下:JS 原型链、数据与结构中的 “链” 和 “树”。 10 | 补充强调一点:在 react 源码中,并不是使用 TypeScript,而是使用和 TS 非常类似的 flow 语法,flow 是 facebook 推出的一种 JS 静态类型检查器。我之前一直误会以为 React 源码是用 TS 写的。 11 | > 2. 强烈推荐你先阅读我的另外一篇文章:[《自顶向下学习React源码》学习笔记#第一章:理念篇](https://github.com/puxiao/notes/blob/master/%E3%80%8A%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E5%AD%A6%E4%B9%A0React%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.md) ,不需要精读,只需要大体了解一下 React 设计理念,会更加容易让你去理解 React 的渲染逻辑,利于理解 hook 。 12 | > 3. 本系列文章中,每一个 hook 中所列出来的该 hook 源码虽然出自 React 官方源码,但实际并不是真的 hook 源码,而仅仅是对 hook 实现的简单引用。 13 | > 14 | > 以上信息更新于 2020.11.10 15 | 16 | 17 | 18 | 19 | ## 我是谁? 20 | 21 | 你好,欢迎你来阅读我写的关于React Hook相关的文章。 22 | 23 | 我是2020年4月才开始接触学习React的,起初摆在我面前的问题是该学习Vue还是React? 24 | 25 | 网上关于Vue和React,有以下2条论断: 26 | 1、Vue相当于扩展了html、而React相当于扩展了js。 27 | 2、如果你希望快速构建应用,那么应选择Vue、如果你希望构建复杂的应用,那么应选择React。 28 | 29 | 在做了一些了解后,我决定选择学习React。不是Vue不好,而是据我了解,国内一线大厂使用React的更多一些。 30 | 31 | 32 | ## 学习 React Hook 过程 33 | 34 | 当我决定开始学习React时,我先下载了一些React视频教程,对React、类组件开发有了基础的掌握,这个时候我接触到了 React Hook,当我稍微深入了解之后,发现 React Hook 函数组件开发才是 React 的最新主流趋势。 35 | 36 | > 备注:React Hook 是 React 2019年2月在16.8版本中才正式发布的。 37 | 38 | 当我满怀激动准备学习 React Hook 时才发现相关教程非常少。 39 | 40 | 最具权威的React官方文档 翻译腔 比较重,对于 Hook 的讲解看了2遍之后依然懵懵懂懂,不明所以。 思否、掘金、雀语上面相关的文章不仅少,而且也不系统全面。 41 | 42 | 此时我通过科学上网,在YouTube上找到了 Codevolution 专栏下的一套 “React Hooks Tutorial” 课程,开始了 React Hook 系统学习。 43 | 44 | 其中useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef、自定义Hook这些知识都来自这门课程。 45 | 46 | 后期学习的useImperativeHandle、useLayoutEffect、useDebugValue这些知识来自于 Bitovi 专栏下的 “React Hooks — The Weird Ones” 视频课程。 47 | 48 | ## 为什么要写? 49 | 50 | 在学习每一个Hook过程中,通常我是这样进行的: 51 | 1、看一遍视频教程 52 | 2、看一遍React官网文档 53 | 3、敲一遍示例代码 54 | 4、遇到理解不了的,去各大技术站点搜索一下 55 | 5、最后再以教给别人的口吻,写下对应Hook的教程文章 56 | 57 | 通过这种方式,我对 React Hook 有了系统的学习,我把我写的教程文章分享出去,如果你正在准备学习 React Hook,希望能够帮助到你。 58 | 59 | ## 文章目录 60 | 61 | [00 前言](https://github.com/puxiao/react-hook-tutorial/blob/master/00%20%E5%89%8D%E8%A8%80.md) 62 | [01 React Hook 简介](https://github.com/puxiao/react-hook-tutorial/blob/master/01%20React%20Hook%20%E7%AE%80%E4%BB%8B.md) 63 | [02 useState基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/02%20useState%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 64 | [03 useState高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/03%20useState%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 65 | [04 useEffect基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/04%20useEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 66 | [05 useEffect高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/05%20useEffect%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 67 | [06 useContext基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/06%20useContext%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 68 | [07 useContext高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/07%20useContext%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 69 | [08 useReducer基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/08%20useReducer%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 70 | [09 useReducer高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/09%20useReducer%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 71 | [10 useCallback基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/10%20useCallback%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 72 | [11 useMemo基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/11%20useMemo%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 73 | [12 useRef基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/12%20useRef%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 74 | [13 useImperativeHandle基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/13%20useImperativeHandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 75 | [14 useLayoutEffect基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/14%20useLayoutEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 76 | [15 useDebugValue基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/15%20useDebugValue%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 77 | [16 自定义hook](https://github.com/puxiao/react-hook-tutorial/blob/master/16%20%E8%87%AA%E5%AE%9A%E4%B9%89hook.md) 78 | [17 React Hook 总结](https://github.com/puxiao/react-hook-tutorial/blob/master/17%20React%20Hook%20%E6%80%BB%E7%BB%93.md) 79 | [18 示例:React使用Echarts所用到的hooks](https://github.com/puxiao/react-hook-tutorial/blob/master/18%20%E7%A4%BA%E4%BE%8B%EF%BC%9AReact%E4%BD%BF%E7%94%A8Echarts%E6%89%80%E7%94%A8%E5%88%B0%E7%9A%84hooks.md) 80 | [附01:React基础知识](https://github.com/puxiao/react-hook-tutorial/blob/master/%E9%99%8401%EF%BC%9AReact%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md) 81 | [附02:React扩展阅读](https://github.com/puxiao/react-hook-tutorial/blob/master/%E9%99%8402%EF%BC%9AReact%E6%89%A9%E5%B1%95%E9%98%85%E8%AF%BB.md) 82 | 83 | ## 重要说明 84 | 85 | 本系列 React Hook 教程里的观点、思维、解释、代码 均出自我个人学习 Hook 之后的感悟和总结,难免有不准确的地方。 86 | 87 | 甚至个别的地方掺杂了我个人的一些习惯用语和思维模式,对于 hook 的有些概念解释,我使用了自己的语言习惯,这会和React官网文档的解释略有不同,但是这些不同地方我认为是没有问题的。 88 | 89 | 恰恰是这些不同之处,有助你更加多角度理解 React Hook。 90 | 91 | 我写的这些教程只能作为你学习React Hook 众多参考资料中的其中一种。 92 | 93 | 94 | ## 文章版权 95 | 96 | 该系列文章没有任何版权,任何人都可以在不注明出处的情况下自由转载。 97 | 98 | 99 | ## 信息反馈 100 | 101 | 若有错误欢迎指正,本人微信同QQ (78657141),或通过邮件联系:yangpuxiao@gmail.com 102 | 103 | 104 | 本系列文章在Github中的地址为:[https://github.com/puxiao/react-hook-tutorial](https://github.com/puxiao/react-hook-tutorial) 105 | -------------------------------------------------------------------------------- /13 useImperativeHandle基础用法.md: -------------------------------------------------------------------------------- 1 | # 13 useImperativeHandle基础用法 2 | 3 | ## useImperativeHandle概念解释 4 | 我们第八个要学习的Hook(钩子函数)是useImperativeHandle,他的作用是“勾住”子组件中某些函数(方法)供父组件调用。 5 | 6 | 先回顾一下之前学到的。 7 | 第1个知识点: 8 | react属于单向数据流,父组件可以通过属性传值,将父组件内的函数(方法)传递给子组件,实现子组件调用父组件内函数的目的。 9 | 10 | 第2个知识点: 11 | 1、useRef 可以“勾住”某些本组件挂载完成或重新渲染完成后才拥有的某些对象。 12 | 2、React.forwardRef 可以“勾住”某些子组件挂载完成或重新渲染完成后才拥有的某些对象。 13 | 上面无论哪种情况,由于勾住的对象都是渲染后的原生html对象,父组件只能通过ref调用该原生html对象的函数(方法)。 14 | 15 | 如果父组件想调用子组件中自定义的方法,该怎么办? 16 | 答:使用useImperativeHandle()。 17 | 18 | 让我们回到useImperativeHandle基础学习中。 19 | 20 | 21 | ## useImperativeHandle是来解决什么问题的? 22 | 答:useImperativeHandle可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过useImperativeHandle添加到父组件中useRef定义的对象中。 23 | 24 | 补充说明: 25 | 1、useRef创建引用变量 26 | 2、React.forwardRef将引用变量传递给子组件 27 | 3、useImperativeHandle将子组件内定义的函数作为属性,添加到父组件中的ref对象上。 28 | 29 | 因此,如果想使用useImperativeHandle,那么还要结合useRef、React.forwardRef一起使用。 30 | 31 | 32 | ## useImperativeHandle函数源码: 33 | 回到useImperativeHandle的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 34 | 35 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 36 | export function useImperativeHandle( 37 | ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void, 38 | create: () => T, 39 | deps: Array | void | null, 40 | ): void { 41 | const dispatcher = resolveDispatcher(); 42 | return dispatcher.useImperativeHandle(ref, create, deps); 43 | } 44 | 45 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^ 46 | 47 | 48 | ## useImperativeHandle基本用法 49 | useImperativeHandle(ref,create,[deps])函数前2个参数为必填项,第3个参数为可选项。 50 | 第1个参数为父组件通过useRef定义的引用变量; 51 | 第2个参数为子组件要附加给ref的对象,该对象中的属性即子组件想要暴露给父组件的函数(方法); 52 | 第3个参数为可选参数,为函数的依赖变量。凡是函数中使用到的数据变量都需要放入deps中,如果处理函数没有任何依赖变量,可以忽略第3个参数。 53 | 54 | 请注意: 55 | 1、这里面说的“勾住子组件内自定义函数”本质上是子组件将内部自定义的函数添加到父组件的ref.current上面。 56 | 2、父组件若想调用子组件暴露给自己的函数,可以通过 res.current.xxx 来访问或执行。 57 | 58 | 59 | ##### 代码形式: 60 | 61 | const xxx = () => { 62 | //do smoting... 63 | } 64 | useImperativeHandle(ref,() => ({xxx})); 65 | 66 | 上述代码中,useImperativeHandle(ref,() => ({xxx})) 其实是 useImperativeHandle(ref,() => {return {xxx:xxx}})的简写。 67 | 68 | 特别注意:() => ({xxx}) 不可以再简写成 () => {xxx},如果这样写会直接react报错。 69 | 因为这两种写法意思完全不一样: 70 | 1、() => ({xxx}) 表示 返回一个object对象,该对象为{xxx} 71 | 2、() => {xxx} 表示 执行 xxx 语句代码 72 | 73 | 74 | ##### 拆解说明: 75 | 76 | 1、子组件内部先定义一个 xxx 函数 77 | 2、通过useImperativeHandle函数,将 xxx函数包装成一个对象,并将该对象添加到父组件内部定义的ref中。 78 | 3、若 xxx 函数中使用到了子组件内部定义的变量,则还需要将该变量作为 依赖变量 成为useImperativeHandle第3个参数,上面示例中则选择忽略了第3个参数。 79 | 4、若父组件需要调用子组件内的 xxx函数,则通过:res.current.xxx()。 80 | 5、请注意,该子组件在导出时必须被 React.forwardRef()包裹住才可以。 81 | 82 | 83 | ## useImperativeHandle使用示例: 84 | 85 | 举例,若某子组件的需求为: 86 | 1、有变量count,默认值为0 87 | 2、有一个函数 addCount,该函数体内部执行 count+1 88 | 3、有一个按钮,点击按钮执行 addCount 函数 89 | 90 | 父组件的需求为: 91 | 1、父组件内使用上述子组件 92 | 2、父组件内有一个按钮,点击执行上述子组件内定义的函数 addCount 93 | 94 | 子组件的代码为: 95 | 96 | import React,{useState,useImperativeHandle} from 'react' 97 | 98 | function ChildComponent(props,ref) { 99 | const [count,setCount] = useState(0); //子组件定义内部变量count 100 | //子组件定义内部函数 addCount 101 | const addCount = () => { 102 | setCount(count + 1); 103 | } 104 | //子组件通过useImperativeHandle函数,将addCount函数添加到父组件中的ref.current中 105 | useImperativeHandle(ref,() => ({addCount})); 106 | return ( 107 |
108 | {count} 109 | 110 |
111 | ) 112 | } 113 | 114 | //子组件导出时需要被React.forwardRef包裹,否则无法接收 ref这个参数 115 | export default React.forwardRef(ChildComponent); 116 | 117 | 118 | 父组件的代码为: 119 | 120 | import React,{useRef} from 'react' 121 | import ChildComponent from './childComponent' 122 | 123 | function Imperative() { 124 | const childRef = useRef(null); //父组件定义一个对子组件的引用 125 | 126 | const clickHandle = () => { 127 | childRef.current.addCount(); //父组件调用子组件内部 addCount函数 128 | } 129 | 130 | return ( 131 |
132 | {/* 父组件通过给子组件添加 ref 属性,将childRef传递给子组件, 133 | 子组件获得该引用即可将内部函数添加到childRef中 */} 134 | 135 | 136 |
137 | ) 138 | } 139 | 140 | export default Imperative; 141 | 142 | 143 | #### 思考一下真的有必要使用useImperativeHandle吗? 144 | 145 | 从实际运行的结果,无论点击子组件还是父组件内的按钮,都将执行 addCount函数,使 count+1。 146 | 147 | react为单向数据流,如果为了实现这个效果,我们完全可以把需求转化成另外一种说法,即: 148 | 1、父组件内定义一个变量count 和 addCount函数 149 | 2、父组件把 count 和 addCount 通过属性传值 传递给子组件 150 | 3、点击子组件内按钮时调用父组件内定义的 addCount函数,使 count +1。 151 | 152 | 你会发现即使把需求中的 父与子组件 描述对调一下,“最终实际效果”是一样的。 153 | 154 | 所以,到底使用哪种形式,需要根据组件实际需求来做定夺。 155 | 156 | 157 | #### 说一个有点绕的情况 158 | 子组件导出时: 159 | 1、假设某个子组件为了提高性能,导出时需要用React.memo包裹。 160 | 2、可是他也需要暴露自己内部函数给父组件,导出时也需要用React.forwardRef包裹。 161 | 162 | 子组件内部函数: 163 | 1、假设该组件内部函数为了性能,需要用到 useCallback包裹该函数。 164 | 2、同时为了让将该函数暴露给父级,也需要用 useImperativeHandle包裹。 165 | 166 | 呵,该怎么办,层层包裹吗? 虽然性能提升了,可是那样的代码可读性还有多少,怎么办? 167 | 答:不知道,反正本系列文章只是单独来讲解某个hook怎么使用,这种复杂包裹的情况,你自己看着办吧。 168 | 169 | 事实上这种情况出现的几率非常小,当我们开发react组件时,不应该为了使用某hook而使用。还是应该是依据单向数据流的原则来做设计方案。 170 | 就好像本章中示例代码,其实完全可以不用useImperativeHandle,而是继续使用最常见的父组件属性传值给子组件的方式。 171 | 172 | 173 | --- 174 | 175 | 至此,关于useImperativeHandle基础用法已经讲完,没有高级用法,直接进入下一个Hook。 176 | 177 | 欢迎进入下一章节:[useLayoutEffect基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/14%20useLayoutEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 178 | -------------------------------------------------------------------------------- /04 useEffect基础用法.md: -------------------------------------------------------------------------------- 1 | # 04 useEffect基础用法 2 | 3 | ## useEffect概念解释 4 | 我们第二个要学习的Hook(钩子函数)是useEffect,他的作用是“勾住”函数组件中某些生命周期函数。 5 | 6 | 都能勾住哪些生命周期函数? 7 | 答:componentDidMount(组件被挂载完成后)、componentDidUpdate(组件重新渲染完成后)、componentWillUnmount(组件即将被卸载前) 8 | 9 | 为什么是这3个生命周期函数? 10 | 答:因为修改数据我们可以使用前面学到的useState,数据变更会触发组件重新渲染,上面3个就是和组件渲染关联最紧密的生命周期函数。 11 | 12 | 那其他生命周期函数呢? 13 | 答:该问题的回答,引用[React官方中文文档FAQ](https://react.docschina.org/docs/hooks-faq.html#do-hooks-cover-all-use-cases-for-classes),如下 14 | > 我们给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,但我们计划尽早把它们加进来。 15 | 16 | 17 | ## useEffect是来解决类组件什么问题的? 18 | 答:useEffect是来解决类组件 **某些执行代码被分散在不同的生命周期函数中** 的问题。 19 | 20 | 举例1:若某类组件中有变量a,默认值为0,当组件第一次被挂载后或组件重新渲染后,将网页标题显示为a的值。 21 | 那么在类组件里,我们需要写的代码是: 22 | 23 | //为了更加清楚看到每次渲染,我们在网页标题中 a 的后面再增加一个随机数字 24 | componentDidMount(){ 25 | document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`; 26 | } 27 | componentDidUpdate(){ 28 | document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`; 29 | } 30 | 31 | 从上面这种代码里你会看到,为了保证第一次被挂载、组件重新渲染后都执行修改网页标题的行为,相同的代码我们需要分别在componentDidMount、componentDidUpdate中写2次。 32 | 33 | 举例2:假设需要给上面那个组件新增一个功能,当组件第一次被挂载后执行一个自动累加器 setInterval,每1秒 a 的值+1。为了防止内存泄露,我们在该组件即将被卸载前清除掉该累加器。 34 | 那么在类组件里,我们需要写的代码是: 35 | 36 | timer = null;//新增一个可内部访问的累加器变量(注:类组件定义属性时前面无法使用 var/let/const) 37 | componentDidMount(){ 38 | document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`; 39 | this.timer = setInterval(() => {this.setState({a:this.state.a+1})}, 1000);//添加累加器 40 | } 41 | componentDidUpdate(){ 42 | document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`; 43 | } 44 | componentWillUnmount(){ 45 | clearInterval(this.timer);//清除累加器 46 | } 47 | 48 | 从上面代码可以看到,增加累加器和清除累加器这2个相关的执行代码被分别定义在componentDidMount、componentWillUnmount这两个生命周期函数中。 49 | 50 | 举例3:假设给上面的组件再新增一个变量 b,当 b 的值发生变化后也会引发组件重新渲染,然后呢?有什么隐患吗? 51 | 答:b 的值改变引发组件重新渲染,然后肯定是会触发componentDidUpdate函数,这时会让修改网页标题的代码再次执行一次,尽管此时a的值并没有发生任何变化。 52 | 53 | 再来回顾一下上面的3个例子: 54 | 1、举例1中,相同的代码可能需要在不同生命周期函数中写2次; 55 | 2、举例2中,相关的代码可能需要在不同生命周期函数中定义; 56 | 3、举例3中,无论是哪个原因引发的组件重新渲染,都会触发生命周期函数的执行,造成一些不必要的代码执行; 57 | 58 | 以上就是 类组件“某些执行代码被分散在不同的生命周期函数中”引发的问题具体表现,而useEffect就是来解决这些问题的。 59 | 60 | 接下来开始学习useState。 61 | 62 | ## useEffect函数源码: 63 | 回到useEffect的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 64 | 65 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 66 | export function useEffect( 67 | create: () => (() => void) | void, 68 | deps: Array | void | null, 69 | ): void { 70 | const dispatcher = resolveDispatcher(); 71 | return dispatcher.useEffect(create, deps); 72 | } 73 | 74 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。之所以贴出源码只是为了让你以后也可以给面试官吹嘘你读过React源码。^_^ 75 | 76 | 77 | ## useEffect基本用法 78 | 79 | useEffect(effect,[deps])函数可以传入2个参数,第1个参数为我们定义的执行函数、第2个参数是依赖关系(可选参数)。若一个函数组件中定义了多个useEffect,那么他们实际执行顺序是按照在代码中定义的先后顺序来执行的。 80 | 81 | 具体说明如下: 82 | 第1个值effect是一个function,用来编写useEffect对应的执行代码。 83 | 还记得本文开头提到的useEffect能勾住哪3个生命周期函数吗? 84 | componentDidMount、componentDidUpdate、componentWillUnmount ,当上述3个生命周期函数执行后,就会触发useEffect函数,进而执行而第1个参数 effect 中的内容。 85 | 86 | 组件挂载后(componentDidMount)与组件重新渲染后(componentDidUpdate)对应的代码合并为一个函数这个容易理解,可是组件卸载前(componentWillUnmount)也能融入进来? 87 | 答:是的,通过在 effect 中 return 一个函数来实现的。 88 | 89 | 关于第2个参数 [deps] ,先知道这个是可选参数,是Hook用来向React表明useEffect依赖关系的即可。关于它的用法会在useEffect高级用法中有更多详细讲述。 90 | 91 | ##### 代码形式: 92 | 93 | useEffect(() => { 94 | //此处编写 组件挂载之后和组件重新渲染之后执行的代码 95 | ... 96 | 97 | return () => { 98 | //此处编写 组件即将被卸载前执行的代码 99 | ... 100 | } 101 | },[deps]) 102 | 103 | 104 | 之前说过useEffect第1个参数 effect 是个 function,只是这个 function 稍显复杂。 105 | 106 | ##### 拆解说明: 107 | 108 | 1、effect 函数主体内容中的代码,就是组件挂载之后和组件重新渲染之后你需要执行的代码; 109 | 2、effect 函数 return 出去的返回函数主体内容中的代码,就是组件即将被卸载前你需要执行的代码; 110 | 3、第2个参数 [deps],为可选参数,若有值则向React表明该useEffect是依赖哪些变量发生改变而触发的; 111 | 112 | #### 'effect'补充说明 113 | 1、若你不需要在组件卸载前执行任何代码,那么可以忽略不写 effect 中的 return相关代码; 114 | 115 | ##### '[deps]'补充说明: 116 | 1、若缺省,则组件挂载、组件重新渲染、组件即将被卸载前,每一次都会触发该useEffect; 117 | 3、若传值,则必须为数组,数组的内容是函数组件中通过useState自定义的变量或者是父组件传值过来的props中的变量,告诉React只有数组内的变量发生变化时才会触发useEffect; 118 | 4、若传值,但是传的是空数组 [],则表示该useEffect里的内容仅会在“挂载完成后和组件即将被卸载前”执行一次; 119 | 120 | 121 | ## useEffect使用示例: 122 | 123 | 还记得本文上面关于 类组件“某些执行代码被分散在不同的生命周期函数中”引发的问题时,所举的3个例子吗? 124 | 我们用Hook来依次分别实现举例1、举例2、举例3,通过3个功能的代码示例,让你明白useEffect的具体用法。 125 | 126 | 举例1:若某类组件中有变量a,默认值为0,当组件第一次被挂载后或组件重新渲染后,将网页标题显示为a的值。 127 | 补充说明: 128 | 1、为了让 a 的值可以发生变化,我们在组件中添加一个按钮,每次点击 a 的值 +1 129 | 2、为了更加清楚看到每次渲染,我们在网页标题中 a 的后面再增加一个随机数字 130 | 131 | import React, { useState,useEffect} from 'react'; 132 | 133 | function Component() { 134 | const [a, setA] = useState(0);//定义变量a,并且默认值为0 135 | useEffect(() => { 136 | //无论是第一次挂载还是以后每次组件更新,修改网页标题的执行代码只需要在这里写一次即可 137 | document.title = `${a} - ${Math.floor(Math.random()*100)}`; 138 | }) 139 | const clickAbtHandler = (eve) =>{ 140 | setA(a+1); 141 | } 142 | return
143 | {a} 144 | 145 |
146 | } 147 | 148 | export default Component; 149 | 150 | 从上述代码可以看出,“类组件中相同的代码可能需要在不同生命周期函数中写2次”这个问题已通过Hook useEffect已解决。 151 | 152 | 这里只是实现列 举例1 中的功能,是useEffect最基础的用法。举例2、举例3 中的功能实现我们放到 useEffect 高级用法 中来讲解。 153 | 154 | 155 | --- 156 | 157 | 至此,关于useEffect基础用法已经讲完。 158 | 159 | 欢迎进入下一章节:[useEffect高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/05%20useEffect%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 160 | -------------------------------------------------------------------------------- /01 React Hook 简介.md: -------------------------------------------------------------------------------- 1 | # 01 React Hook 简介 2 | 3 | 首先,欢迎你来学习React Hook,通过本教程你会了解到React Hook工作原理以及我们推荐使用Hook的理由。 4 | 5 | ## 学习前提 6 | 7 | 在学习本课程之前,需要你对以下知识点有基础的了解: 8 | 1、React基础原理; 9 | 2、函数组件(Functional components)和类组件(class components),属性传参(props),自定义内部数据(state),生命周期函数等; 10 | 3、使用谷歌浏览器且安装了“React Developer Tools”调试工具。 11 | 12 | 本系列文章适合有一定React开发基础的人,若是React新手,建议先从阅读React官方中文文档学起。 13 | 14 | 接下来正式开始本教程。 15 | 16 | 17 | ## 什么是Hooks? 18 | 19 | Hook是React 16.8版本中新增的一个新特性,丰富扩展了原有函数组件的功能,让函数组件拥有了像类组件一样的相似特性。 20 | 21 | 在之前版本中函数组件不能使用React生命周期函数,Hook本身单词意思是“钩子”,作用就是“勾住某些生命周期函数或某些数据状态,并进行某些关联触发调用”。 22 | 23 | 不同的Hook(钩子)有不同的作用,可以勾住不同的“点”,比如“勾住组件更新完成对应的生命周期函数”、“勾住某props值的变化”等。 24 | 25 | 正因为React有多个内置Hook,所以本小节的标题才是“什么是Hooks?”,没错,用到了Hook的复数单词Hooks。 26 | 27 | ##### 特别提醒:在React官网中使用的是Hook,而在有些教程中使用的是Hooks。在本教程中Hook和Hooks是同一个意思,不要纠结什么时候用单数什么时候是复数。 28 | 29 | ##### 请注意: 30 | 31 | 1、尽管函数组件拥有了类组件多大多数的相似特性,但有一点除外:函数组件中没有类组件中“自定义state”的特性,因此你无法在函数组件中使用“this.state.xx”这样的代码。 32 | 33 | 没有不代表功能的缺失,恰恰相反,因为当你充分了解Hooks之后,你会发现函数组件内部自定义数据状态功能远远超出类组件。 34 | 35 | 2、Hooks只能运行在函数组件中,不能运行在类组件中。 36 | 补充:准确来说,Hooks只能运行在函数组件的“内部顶层中”,不能运行在if/for等其他函数的代码体内,不允许被if/for等包裹住。 37 | 38 | 3、Hooks函数必须为纯函数,所谓纯函数就是函数内部不能修改可能影响执行结果的任意参数,确保每次执行的代码结果都是一样的。 39 | 40 | 41 | ## 为什么要用Hooks? 42 | 43 | 先说一下类组件的一些缺点: 44 | 45 | ##### 缺点一:复杂且不容易理解的“this” 46 | 例如事件绑定处理函数,都需要bind(this)才可以正确执行。 47 | 例如想获取某些自定义属性,都需要使用this.state.xx或this.props.xx。 48 | 49 | 这样造成代码不够精简,并且有些时候热更新不能正常运行。 50 | 51 | ##### 缺点二:组件数据状态逻辑不能重用、组件之间传值过程复杂 52 | 53 | “组件数据状态逻辑不能重用”,详细解释如下: 54 | “组件数据状态”是由:定义数据、默认赋值、获取数据、修改数据、数据逻辑几个环节构成。 由于类组件中的组件数据状态state必须写在该组件构造函数内部,无法将state抽离出组件,因此别的组件如果有类似state逻辑,也必须内部自己实现一次,所以才得出“组件数据状态逻辑不能重用”的结论。 55 | 56 | “组件之间传值过程复杂”,详细解释如下: 57 | React本身为单向数据流,即父组件可以传值给子组件,但子组件不允许直接修改父组件中的数据状态。 58 | 59 | 子组件为了达到修改父组件中的数据状态,通常采用“高阶组件(HOC)”或“父组件暴露修改函数给子组件(render props)”这2种方式。 这2种方式都会让组件变得复杂且降低可复用性。 60 | 61 | 62 | ##### 缺点三:复杂场景下代码难以组织在一起 63 | 64 | 复杂场景下,比如数据获取(data fetching)和事件订阅(event listeners),相关代码难以组织在一起。 65 | 66 | “相关代码难以组织在一起”,详细解释如下: 67 | 68 | 第1个“难以组织”的原因:数据获取和事件订阅被分散在不同生命周期函数中。 69 | 70 | 例如数据获取:组件第一次被挂载(componentDidMount)、组件每次更新完毕(componentDidUpdate) 71 | 例如事件监听:组件第一次被挂载(componentDidMount)、组件即将被卸载(componentWillUnmount) 72 | 73 | 74 | 第2个“难以组织”的原因:内部state数据只能是整体,无法被拆分更细致 75 | 76 | 类组件中所有内部数据都被储存在this.state中,例如某个组件定义有2个内部数据 name,age,那么永远都是this.state.name、this.state.age。 name和age永远都只是this.state中的一个属性,无法做到将name和age拆分成独立对象个体。 77 | 78 | 所有内部数据都储存在this.state中,当内部数据复杂时,势必增加维护this.state的难度和复杂性。 79 | 80 | “复杂场景下代码难以组织在一起”会造成另外一个延伸性问题:加大了代码自动测试难度。 81 | 82 | 83 | ##### Hooks是如何解决上述类组件的缺点? 84 | 85 | 如果你现在迫切想知道答案,我想对你说:恭喜你,欢迎进入Hooks的世界。 86 | 87 | 类组件缺点一:复杂且不容易理解的“this” 88 | Hooks解决方式:函数组件和普通JS函数非常相似,在普通JS函数中定义的变量、方法都可以不使用“this.”,而直接使用该变量或函数,因此你不再需要去关心“this”了。 89 | 90 | 类组件缺点二:组件数据状态逻辑不能重用 91 | Hooks解决方式: 92 | 通过自定义Hook,可以数据状态逻辑从组件中抽离出去,这样同一个Hook可以被多个组件使用,解决组件数据状态逻辑并不能重用的问题。 93 | 94 | 类组件缺点二:组件之间传值过程复杂、缺点三:复杂场景下代码难以组织在一起 95 | Hooks解决方式: 96 | 通过React内置的useState()函数,可以将不同数据分别从"this.state"中独立拆分出去。降低数据复杂度和可维护性,同时解决类组件缺点三中“内部state数据只能是整体,无法被拆分更细”的问题。 97 | 98 | 通过React内置的useEffect()函数,将componentDidMount、componentDidUpdate、componentWillUncount 3个生命周期函数通过Hook(钩子)关联成1个处理函数,解决事件订阅分散在多个生命周期函数的问题。 99 | 100 | ##### 最为关键的是,hook还能实现一些类组件根本不能实现的功能,比如全局共享数据,代替Redux。 101 | 102 | 如果阅读过上面文字,你依然一头雾水,不要着急,你现在只需要对Hooks有一个大体了解即可。 103 | 随着后面的深入学习,你将逐个掌握Hooks的关键用法。 104 | 105 | ##### 你只需记住一个结论:忘掉类组件,使用Hook进行函数组件开发,将是一个明智选择。 106 | 107 |
108 | 109 | > 以下内容更新于 2021.01.10 110 | 111 | 下面讲解一下 React 的生命周期函数,面对如此复杂的生命周期函数,是没有必要过于了解和研究的,目前来说,一般只需学习使用 useEffect 这个 hook 即可。 112 | 113 | useEffect 这个 hook 会在稍后讲解。 114 | 115 | ## React生命周期函数 116 | 117 | React 一次状态更新,一共分为 2 个阶段、4 个生命周期。 118 | 119 | **2 个阶段:** 120 | 121 | 1. render阶段:包含Diff算法,计算出状态变化 122 | 2. commit渲染阶段:ReactDom渲染器,将状态变化渲染在视图中 123 | 124 | **4个生命周期:** 125 | 126 | 1. Mount(第一次挂载) 127 | 2. Update(更新) 128 | 3. Unmount(卸载) 129 | 4. Error(子项发生错误) 130 | 131 | 132 | 133 | | 生命周期函数 | 所属阶段 | 所属生命周期 | 134 | | ------------------------- | ---------- | ------------------- | 135 | | constructor | Render阶段 | Mount | 136 | | componentWillReceiveProps | Render阶段 | Update | 137 | | getDerivedStateFromProps | Render阶段 | 并存于Moun、Update | 138 | | getDerivedStateFromError | Render阶段 | Error | 139 | | shouldComponentUpdate | Render阶段 | Update | 140 | | componentWillMount | Render阶段 | Mount | 141 | | componentWillUpdate | Render阶段 | Update | 142 | | render | Render阶段 | 并存于Mount、Update | 143 | | componentDidMount | Commit阶段 | Mount | 144 | | getSnapshotBeforeUpdate | Commit阶段 | Update | 145 | | componentDidUpdate | Commit阶段 | Update | 146 | | componentWillUnmount | Commit阶段 | Unmount | 147 | | componentDidCatch | Commit阶段 | Error | 148 | 149 | **注意事情:** 150 | 151 | componentWillReceiveProps、componentWillMount、componentWillUpdate 这 3 个生命周期函数正在逐步被 React 官方放弃使用,不推荐继续使用这 3 个生命周期函数。 152 | 153 | 与之对应的是 getDerivedStateFromProps、getDerivedStateFromError 这 2 个这是被推荐使用的。 154 | 155 | 关于各个生命周期函数详细介绍,可以参考 React 官方文档: 156 | https://zh-hans.reactjs.org/docs/react-component.html#commonly-used-lifecycle-methods 157 | 158 | 159 | **补充说明:** 160 | 161 | **目前并不是所有的生命周期函数都对应有 hook 函数。** 162 | 163 | 再次重复一遍,这些生命周期函数你只需大致了解,初学者只需学会 useEffect 这个 hook 即可。 164 | 165 | 166 | 167 | > 以上内容更新于 2021.01.10 168 | 169 | 170 | ## 本节小结 171 | 172 | 1、Hook是React 16.8及以上版本才拥有的特性。 173 | 2、Hook只是React“增加”的概念和一些API,对原有React体系并没有任何破坏。 174 | 3、Hook有很多优势,比如不需要使用“this”、数据状态细致拆分、数据状态逻辑抽离出组件、代码组织更加自由灵活等。 175 | 4、Hook只能用于函数组件,不能用于类组件中。 176 | 5、Hook虽好,但React依然保留对类组件的支持,如果你就是不喜欢Hook,更偏向于继续使用类组件,那么也是可以的,只是你需要继续面对类组件的一些缺点。 177 | 178 | --- 179 | 180 | 至此,你对Hook有了一个初步概念,接下来开始学习第1个Hook函数 useState。 181 | 182 | 欢迎进入下一章节:[useState基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/02%20useState%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | > 以下信息更新于 2020.11.10 4 | > 5 | > 最近学习了 JS 原型链、数据结构与算法,以及在思否编程课上,看了一些 卡颂(微信公众号:魔术师卡颂) 录制的《[自顶向下学 React 源码](https://ke.segmentfault.com/course/1650000023864436)》课程,对 React 又有了更深的认识。 6 | > 7 | > 此时再回顾我半年前写的这系列文章,有几点现需要补充说明一下: 8 | > 9 | > 1. 强烈建议你在学习 hook 之前,先学习了解一下:JS 原型链、数据与结构中的 “链” 和 “树”。 10 | > 补充强调一点:在 react 源码中,并不是使用 TypeScript,而是使用和 TS 非常类似的 flow 语法,flow 是 facebook 推出的一种 JS 静态类型检查器。我之前一直误会以为 React 源码是用 TS 写的。 11 | > 2. 强烈推荐你先阅读我的另外一篇文章:[《自顶向下学习React源码》学习笔记#第一章:理念篇](https://github.com/puxiao/notes/blob/master/%E3%80%8A%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E5%AD%A6%E4%B9%A0React%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.md) ,不需要精读,只需要大体了解一下 React 设计理念,会更加容易让你去理解 React 的渲染逻辑,利于理解 hook 。 12 | > 3. 本系列文章中,每一个 hook 中所列出来的该 hook 源码虽然出自 React 官方源码,但实际并不是真的 hook 源码,而仅仅是对 hook 实现的简单引用。 13 | > 14 | > 以上信息更新于 2020.11.10 15 | 16 | 17 | 18 | 19 | ## 我是谁? 20 | 21 | 你好,欢迎你来阅读我写的关于React Hook相关的文章。 22 | 23 | 我是2020年4月才开始接触学习React的,起初摆在我面前的问题是该学习Vue还是React? 24 | 25 | 网上关于Vue和React,有以下2条论断: 26 | 1、Vue相当于扩展了html、而React相当于扩展了js。 27 | 2、如果你希望快速构建应用,那么应选择Vue、如果你希望构建复杂的应用,那么应选择React。 28 | 29 | 在做了一些了解后,我决定选择学习React。不是Vue不好,而是据我了解,国内一线大厂使用React的更多一些。 30 | 31 | 32 |
33 |

34 | wechat.jpg 35 |

36 | 37 | 38 | ## 学习 React Hook 过程 39 | 40 | 当我决定开始学习React时,我先下载了一些React视频教程,对React、类组件开发有了基础的掌握,这个时候我接触到了 React Hook,当我稍微深入了解之后,发现 React Hook 函数组件开发才是 React 的最新主流趋势。 41 | 42 | > 备注:React Hook 是 React 2019年2月在16.8版本中才正式发布的。 43 | 44 | 当我满怀激动准备学习 React Hook 时才发现相关教程非常少。 45 | 46 | 最具权威的React官方文档 翻译腔 比较重,对于 Hook 的讲解看了2遍之后依然懵懵懂懂,不明所以。 思否、掘金、雀语上面相关的文章不仅少,而且也不系统全面。 47 | 48 | 此时我通过科学上网,在YouTube上找到了 Codevolution 专栏下的一套 “React Hooks Tutorial” 课程,开始了 React Hook 系统学习。 49 | 50 | 其中useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef、自定义Hook这些知识都来自这门课程。 51 | 52 | 后期学习的useImperativeHandle、useLayoutEffect、useDebugValue这些知识来自于 Bitovi 专栏下的 “React Hooks — The Weird Ones” 视频课程。 53 | 54 | ## 为什么要写? 55 | 56 | 在学习每一个Hook过程中,通常我是这样进行的: 57 | 1、看一遍视频教程 58 | 2、看一遍React官网文档 59 | 3、敲一遍示例代码 60 | 4、遇到理解不了的,去各大技术站点搜索一下 61 | 5、最后再以教给别人的口吻,写下对应Hook的教程文章 62 | 63 | 通过这种方式,我对 React Hook 有了系统的学习,我把我写的教程文章分享出去,如果你正在准备学习 React Hook,希望能够帮助到你。 64 | 65 | ## 文章目录 66 | 67 | [00 前言](https://github.com/puxiao/react-hook-tutorial/blob/master/00%20%E5%89%8D%E8%A8%80.md) 68 | [01 React Hook 简介](https://github.com/puxiao/react-hook-tutorial/blob/master/01%20React%20Hook%20%E7%AE%80%E4%BB%8B.md) 69 | [02 useState基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/02%20useState%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 70 | [03 useState高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/03%20useState%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 71 | [04 useEffect基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/04%20useEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 72 | [05 useEffect高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/05%20useEffect%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 73 | [06 useContext基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/06%20useContext%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 74 | [07 useContext高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/07%20useContext%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 75 | [08 useReducer基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/08%20useReducer%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 76 | [09 useReducer高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/09%20useReducer%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 77 | [10 useCallback基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/10%20useCallback%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 78 | [11 useMemo基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/11%20useMemo%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 79 | [12 useRef基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/12%20useRef%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 80 | [13 useImperativeHandle基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/13%20useImperativeHandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 81 | [14 useLayoutEffect基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/14%20useLayoutEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 82 | [15 useDebugValue基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/15%20useDebugValue%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 83 | [16 自定义hook](https://github.com/puxiao/react-hook-tutorial/blob/master/16%20%E8%87%AA%E5%AE%9A%E4%B9%89hook.md) 84 | [17 React Hook 总结](https://github.com/puxiao/react-hook-tutorial/blob/master/17%20React%20Hook%20%E6%80%BB%E7%BB%93.md) 85 | [18 示例:React使用Echarts所用到的hooks](https://github.com/puxiao/react-hook-tutorial/blob/master/18%20%E7%A4%BA%E4%BE%8B%EF%BC%9AReact%E4%BD%BF%E7%94%A8Echarts%E6%89%80%E7%94%A8%E5%88%B0%E7%9A%84hooks.md) 86 | [19 useTransition基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/19%20useTransition%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 87 | [附01:React基础知识](https://github.com/puxiao/react-hook-tutorial/blob/master/%E9%99%8401%EF%BC%9AReact%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md) 88 | [附02:React扩展阅读](https://github.com/puxiao/react-hook-tutorial/blob/master/%E9%99%8402%EF%BC%9AReact%E6%89%A9%E5%B1%95%E9%98%85%E8%AF%BB.md) 89 | 90 | ## 重要说明 91 | 92 | 本系列 React Hook 教程里的观点、思维、解释、代码 均出自我个人学习 Hook 之后的感悟和总结,难免有不准确的地方。 93 | 94 | 甚至个别的地方掺杂了我个人的一些习惯用语和思维模式,对于 hook 的有些概念解释,我使用了自己的语言习惯,这会和React官网文档的解释略有不同,但是这些不同地方我认为是没有问题的。 95 | 96 | 恰恰是这些不同之处,有助你更加多角度理解 React Hook。 97 | 98 | 我写的这些教程只能作为你学习React Hook 众多参考资料中的其中一种。 99 | 100 | 101 | 102 |
103 | 104 | > 以下内容更新于 2022.12.08 105 | 106 | **特别感谢:** 107 | 108 | * 帮我指出一些错误、提交 PR 的朋友们 109 | * 第 19 小节是由 @[Bill70058](https://github.com/Bill70058) 独立编写的 110 | 111 | 112 | 113 |
114 | 115 | 116 | ## 信息反馈 117 | 118 | 若有错误欢迎指正,本人微信同QQ (78657141),或通过邮件联系:yangpuxiao@gmail.com 119 | 120 | 121 | 本系列文章在Github中的地址为:[https://github.com/puxiao/react-hook-tutorial](https://github.com/puxiao/react-hook-tutorial) 122 | -------------------------------------------------------------------------------- /06 useContext基础用法.md: -------------------------------------------------------------------------------- 1 | # 06 useContext基础用法 2 | 3 | ## useContext概念解释 4 | 我们第三个要学习的Hook(钩子函数)是useContext,他的作用是“勾住”获取由React.createContext()创建、添加设置的共享数据value值。useContext可以替代标签,简化获取共享数据的代码。 5 | 6 | 我们知道,原本不同级别的组件之间传递属性值,必须逐层传递,即使中间层的组件不需要这些数据。 7 | 注意:这里说的组件指React所有组件,包含类组件和函数组件。 8 | 9 | 数据层层传递增加了组件的复杂性,降低了可复用性。为了解决这个问题,我们可以使用以下方式: 10 | 1、在组件顶层或单独的模块中,由React.createContext()创建一个共享数据对象; 11 | 2、在父组件中添加共享数据对象的引用,通过且只能通过的形式将数据传递给子组件。请注意传值必须使用value={obj}这种形式,若值本身为字符串则可以改为 value='xxx'; 12 | 3、若下一层的子组件用不到共享数据对象中的数据,则可以不做任何属性标签传递; 13 | 4、若某一层的子组件需要用到共享数据对象的数据,则可通过获取到数据; 14 | 5、在类组件中除了标签,还有另外一种获取共享数据方式:static xxx = XxxContext; 但是这种形式在函数组件中无法使用。 15 | 16 | 简而言之用来添加共享数据、用来获取共享数据。 17 | 备注:provider单词本意为供应者、consumer单词本意为消费者,刚好对应他们相对于共享数据的关系。 18 | 19 | 上面简单描述了React.createContext()的用法,由于本系列文章主要讲Hook的使用方法,React本身的知识点并不是重点讲解对象。若你对React.createContext()、的用法还不太明白,请通过其他途径自行学习。 20 | 21 | 让我们回到useContext学习中。 22 | 23 | 24 | ## useContext是来解决什么问题的? 25 | 答:useContext是的替代品,可以大量简化获取共享数据值的代码。 26 | 27 | 补充说明: 28 | 1、函数组件和类组件,对于使用方式没有任何差别。 29 | 2、你可以在函数组件中不使用useContext,继续使用,这都没问题。只不过使用useContext后,可以让获取共享数据相关代码简单一些。 30 | 31 | 32 | ## useContext函数源码: 33 | 回到useContext的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 34 | ```ts 35 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 36 | export function useContext( 37 | Context: ReactContext, 38 | unstable_observedBits: number | boolean | void,): T { 39 | const dispatcher = resolveDispatcher(); 40 | if (__DEV__) { 41 | if (unstable_observedBits !== undefined) { 42 | console.error( 43 | 'useContext() second argument is reserved for future ' + 44 | 'use in React. Passing it is not supported. ' + 45 | 'You passed: %s.%s', 46 | unstable_observedBits, 47 | typeof unstable_observedBits === 'number' && Array.isArray(arguments[2]) 48 | ? '\n\nDid you call array.map(useContext)? ' + 49 | 'Calling Hooks inside a loop is not supported. ' + 50 | 'Learn more at https://fb.me/rules-of-hooks' 51 | : '', 52 | ); 53 | } 54 | 55 | // TODO: add a more generic warning for invalid values. 56 | if ((Context: any)._context !== undefined) { 57 | const realContext = (Context: any)._context; 58 | // Don't deduplicate because this legitimately causes bugs 59 | // and nobody should be using this in existing code. 60 | if (realContext.Consumer === Context) { 61 | console.error( 62 | 'Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' + 63 | 'removed in a future major release. Did you mean to call useContext(Context) instead?', 64 | ); 65 | } else if (realContext.Provider === Context) { 66 | console.error( 67 | 'Calling useContext(Context.Provider) is not supported. ' + 68 | 'Did you mean to call useContext(Context) instead?', 69 | ); 70 | } 71 | } 72 | } 73 | return dispatcher.useContext(Context, unstable_observedBits); 74 | } 75 | ``` 76 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。之所以贴出源码只是想让你看懂以后告诉我,反正我是没看懂。^_^ 77 | 78 | 79 | ## useContext基本用法 80 | 81 | useContext(context)函数可以传入1个参数,该参数为共享数据对象的实例,useContext函数会返回该共享对象实例的value值。 82 | 83 | 84 | ##### 代码形式: 85 | ```jsx 86 | import GlobalContext from './global-context'; //引入共享数据对象 87 | 88 | function Component(){ 89 | const global = useContext(GlobalContext); //在函数组件中声明一个变量来代表该共享数据对象的value值 90 | 91 | //若想获取共享数据对象中的属性xxx的值,直接使用global.xxx即可 92 | return
93 | {global.xxx} 94 |
95 | } 96 | ``` 97 | 98 | ##### 拆解说明: 99 | 100 | 1、子组件(函数组件)需要先引入共享数据对象GlobalContext; 101 | 2、内部定义一个常量global,用来接收useContext函数返回GlobalContext的value值; 102 | 3、函数组件在return时,可以不使用标签,而是直接使用global.xx来获取共享数据; 103 | 4、请注意,这里执行的依然是单向数据流,只可以获取global.xx,不可以直接更改global.xx; 104 | 105 | #### '引入GlobalContext'补充说明 106 | 示例中是通过import方式引入的,如果直接把GlobalContext定义在该组件内部,那不是就不用import了吗? 107 | 答:是的,你可以这么做。只不过定义在外部单独的模块中,各个组件都可以引用。 108 | 109 | #### 'global'补充说明 110 | 为了代码语义化,上述代码中使用到了global这个单词,但是请注意,该单词和原生JS中global(全局变量)无任何关联。实际项目中你可以使用任意具有语义的相关单词。比如定义用户共享数据你可以定义为UserContext、新闻共享数据你可以定义为NewsContext等。 111 | 112 | 113 | ## useContext使用示例: 114 | 115 | 举例:若某React组件一共由3层组件嵌套而成,从外到里分别是AppComponent、MiddleComponent、ChildComponent。AppComponent需要传递数据给ChildComponent。 116 | 117 | 若使用useContext来实现,代码示例如下: 118 | ```jsx 119 | //global-context.js 120 | import React from 'react'; 121 | const GlobalContext = React.createContext(); //请注意,这里还可以给React.createContext()传入一个默认值 122 | //例如:const GlobalContext = React.createContext({name:'Yang',age:18}) 123 | //假如中没有设置value的值,就会使用上面定义的默认值 124 | export default GlobalContext; 125 | 126 | ... 127 | 128 | //component.js 129 | import React, { useContext } from 'react'; 130 | import GlobalContext from './global-context'; 131 | 132 | function AppComponent() { 133 | //标签中向下传递数据,必须使用value这个属性,且数据必须是键值对类型的object 134 | //如果不添加value,那么子组件获取到的共享数据value值是React.createContext(defaultValues)中的默认值defaultValues 135 | return
136 | 137 | 138 | 139 |
140 | } 141 | 142 | function MiddleComponent(){ 143 | //MiddleComponent 不需要做任何 “属性数据传递接力”,因此降低该组件数据传递复杂性,提高组件可复用性 144 | return
145 | 146 |
147 | } 148 | 149 | function ChildComponent(){ 150 | const global = useContext(GlobalContext); //获取共享数据对象的value值 151 | //忘掉标签,直接用global获取需要的值 152 | return
153 | {global.name} - {global.age} 154 |
155 | } 156 | 157 | export default AppComponent; 158 | ``` 159 | 假如ChildComponent不使用useContext,而是使用标签,那么代码相应修改为: 160 | ```jsx 161 | function ChildComponent(){ 162 | return 163 | { 164 | global => { 165 | return
{global.name} - {global.age}
166 | } 167 | } 168 |
169 | } 170 | ``` 171 | 使用useContext可以大大降低获取数据代码复杂性。 172 | 173 | 请注意:useContext只是简化了获取共享数据value的代码,但是对于的使用没有做任何改变,如果组件需要设置2个XxxContext,那么依然需要进行嵌套。 174 | 175 | 176 | 上述代码中AppComponent只向下传递出去1个共享数据对象value值,那如果需要同时传递多个共享数据对象的value值,那该如何实现? 177 | 关于这个问题,会在 useContext高级用法中讲解。 178 | 179 | 180 | --- 181 | 182 | 至此,关于useContext基础用法已经讲完。 183 | 184 | 欢迎进入下一章节:[useContext高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/07%20useContext%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 185 | -------------------------------------------------------------------------------- /08 useReducer基础用法.md: -------------------------------------------------------------------------------- 1 | # 08 useReducer基础用法 2 | 3 | ## useReducer概念解释 4 | 我们第四个要学习的Hook(钩子函数)是useReducer,他的作用是“勾住”某些自定义数据对应的dispatch所引发的数据更改事件。useReducer可以替代useState,实现更为复杂逻辑的数据修改。 5 | 6 | 在React 16.8版本以前,通常需要使用第三方Redux来管理React的公共数据,但自从 React Hook 概念出现以后,可以使用 useContext + useReducer 轻松实现 Redux 相似功能。这一部分会在 “useReducer高级用法” 中做详细讲解。 7 | 8 | 让我们回到useReducer基础学习中。 9 | 10 | 11 | ## useReducer是来解决什么问题的? 12 | 答:useReducer是useState的升级版(实际上应该是原始版),可以实现复杂逻辑修改,而不是像useState那样只是直接赋值修改。 13 | 14 | 补充说明: 15 | 1、在React源码中,实际上useState就是由useReducer实现的,所以useReducer准确来说是useState的原始版。 16 | 2、无论哪一个Hook函数,本质上都是通过事件驱动来实现视图层更新的。 17 | 18 | 19 | ## useReducer函数源码: 20 | 回到useReducer的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 21 | 22 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 23 | export function useReducer( 24 | reducer: (S, A) => S, 25 | initialArg: I, 26 | init?: I => S, 27 | ): [S, Dispatch] { 28 | const dispatcher = resolveDispatcher(); 29 | return dispatcher.useReducer(reducer, initialArg, init); 30 | } 31 | 32 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。之所以贴出源码只是想让你重点看一下useReducer函数的第3个参数。一般我们只传2个参数,如果有一天你看到有人为了某些不常用的目的传了3个参数,你应该理解,第3个参数其实只是第1和第2个参数的某种转化。事实上你可以完全忽略这个问题,每次值传2个参数即可。^_^ 33 | 34 | 35 | ## useReducer基本用法 36 | 37 | useReducer(reducer,initialValue)函数通常传入2个参数,第1个参数为我们定义的一个“由dispatch引发的数据修改处理函数”,第2个参数为自定义数据的默认值,useReducer函数会返回自定义变量的引用和该自定义变量对应的“dispatch”。 38 | 39 | 请注意,当你看到了dispatch,肯定想到了原生JS中的EventEmitter,事实上React Hook帮我们做了底层的事件驱动处理,我们拿到的dispatch以及“事件处理函数”reducer,都时被React Hook 封装过后的,并不是真正的抛出和事件处理函数。 40 | 41 | 但是为了更容易让你理解,本文依然会在讲解useReducer时使用到“事件抛出、事件处理函数”等文字。 42 | 43 | 如果你了解事件驱动,使用过EventEmitter,或者你使用过Redux,那么你会很容易理解useReducer的用法。 44 | 45 | 46 | ##### 代码形式: 47 | 48 | import React, { useReducer } from 'react'; //引入useReducer 49 | 50 | //定义好“事件处理函数” reducer 51 | function reducer(state, action) { 52 | switch (action) { 53 | case 'xx': 54 | return xxxx; 55 | case 'xx': 56 | return xxxx; 57 | default: 58 | return xxxx; 59 | } 60 | } 61 | 62 | function Component(){ 63 | //声明一个变量xxx,以及对应修改xxx的dispatch 64 | //将事件处理函数reducer和默认值initialValue作为参数传递给useReducer 65 | const [xxx, dispatch] = useReducer(reducer, initialValue); 66 | 67 | //若想获取xxx的值,直接使用xxx即可 68 | 69 | //若想修改xxx的值,通过dispatch来修改 70 | dispatch('xx'); 71 | } 72 | 73 | //请注意,上述代码中的action只是最基础的字符串形式,事实上action可以是多属性的object,这样可以自定义更多属性和更多参数值 74 | //例如 action 可以是 {type:'xx',param:xxx} 75 | 76 | 77 | ##### 拆解说明: 78 | 79 | 1、具体讲解已在上面示例代码中做了多项注释,此处不再重复; 80 | 81 | 82 | #### 'reducer'补充说明 83 | 1、reducer英文单词本身意思是“减速器、还原剂”,但是本文中一直把reducer称呼为“事件处理函数”,但事实上reducer确实扮演一个事件处理函数。 84 | 2、千万不要把useReducer中的reducer 和 原生JS中的Array.prototype.reduce()弄混淆,他们两个只是刚好都使用了这个reduce单词而已,两者本身没有任何内在关联。 85 | 86 | #### 'xxx'补充说明 87 | 假设我们定义的变量名为xxx,那么只能通过dispatch来修改xxx,不要尝试通过 xxx = newValue 这种形式直接修改变量的值,React 不允许这样做。 88 | 89 | #### 'dispatch'补充说明 90 | 再次强调,dispacth并不是真正的Event.dispatch,但是你完全可以把它当成Event.dispatch来理解,只不过useReducer中的dispacth(xxx)函数抛出内容不是event,而是一个包含修改信息的对象,该对象不仅可以是字符串,还可以是复杂对象。 91 | 92 | #### 'initialValue'补充说明 93 | initialValue是我们自定义变量的默认值,该值可以是简单类型(number、string),也可以是复杂类型(object、array)。 94 | 推荐建议:即使该值是简单类型,也建议单独定义出来而不是直接将值写在useReducer函数中,因为单独定义可以让我们更加清晰读懂数据结构,尤其是initialValue为复杂类型时。 95 | 96 | 97 | ## useReducer使用示例1: 98 | 99 | 举例:若某React组件内部有一个变量count,默认值为0,有3个button,点击之后分别可以修改count的值。3个按钮具体的功能为:第1个button点击之后count+1,第2个button点击之后count -1,第3个button点击之后 count x 2 (翻倍)。 100 | 101 | 若使用useState来实现,那肯定没问题,每个button点击之后分别运算得到对应的新值,将该值直接通过setCount赋予给count。 102 | 103 | 若使用useReducer来实现相同功能,代码示例如下: 104 | 105 | import React, { useReducer } from 'react'; 106 | 107 | function reducer(state,action){ 108 | switch(action){ 109 | case 'add': 110 | return state + 1; 111 | case 'sub': 112 | return state - 1; 113 | case 'mul': 114 | return state * 2; 115 | default: 116 | console.log('what?'); 117 | return state; 118 | } 119 | } 120 | 121 | function CountComponent() { 122 | const [count, dispatch] = useReducer(reducer,0); 123 | 124 | return
125 | {count} 126 | 127 | 128 | 129 |
; 130 | } 131 | 132 | export default CountComponent; 133 | 134 | 代码分析: 135 | 3个按钮点击之后,不再具体去直接修改count的值,而是采用 dispatche('xxx')的形式 “抛出修改count的事件”,事件处理函数reducer“捕获到修改count的事件后”,根据该事件携带的命令类型来进一步判断,并真正执行对count的修改。 136 | 137 | 请注意上面这句话中加引号的语句,本文只是以事件驱动的语言来描述整个过程,目的希望你能更加容易理解。 138 | 139 | 3个按钮只是负责通知reducer“我希望做什么事情”,具体怎么做完全由reducer来执行。这样实现了修改数据具体执行逻辑与按钮点击处理函数的抽离。 140 | 141 | 如果不使用useReducer,而是使用之前学习过的useState,那么对count的每一种修改逻辑代码,都必须分散写在每个按钮的点击事件处理函数中。 142 | 143 | 若只是修改count的功能,那么useReducer的优势还未全部体现出来,我们接着看另外一个示例。 144 | 145 | 146 | ## useReducer使用示例2 147 | 148 | 举例:在示例1中对count 执行的修改,数值变动都是固定的,即 +1、-1、x 2。假设我们希望按钮点击之后,能够自主控制增加多少、减多少、或乘以几,这个效果该怎么实现呢? 149 | 150 | 很简单,我们将dispatch('xxx')中的xxx由字符串改为obj,obj可以携带更多属性作为参数传给reducer。 比如之前对 "加"的命令 dispatch('add'),修改为 dispatch({type:'add',param:2})。 reducer可以通过action.type来区分是哪种命令、通过action.param来获取对应的参数。 151 | 152 | 为了简化代码,我们将在点击按钮后,随机产生一个数字,并将该数字作为 param 的值,传递给reducer。 153 | 154 | 修改后的代码为: 155 | 156 | import React, { useReducer } from 'react'; 157 | 158 | function reducer(state,action){ 159 | //根据action.type来判断该执行哪种修改 160 | switch(action.type){ 161 | case 'add': 162 | //count 最终加多少,取决于 action.param 的值 163 | return state + action.param; 164 | case 'sub': 165 | return state - action.param; 166 | case 'mul': 167 | return state * action.param; 168 | default: 169 | console.log('what?'); 170 | return state; 171 | } 172 | } 173 | 174 | function getRandom(){ 175 | return Math.floor(Math.random()*10); 176 | } 177 | 178 | function CountComponent() { 179 | const [count, dispatch] = useReducer(reducer,0); 180 | 181 | return
182 | {count} 183 | 184 | 185 | 186 |
; 187 | } 188 | 189 | export default CountComponent; 190 | 191 | 同样的道理,我们可以把示例中的count由简单类型改为复杂类型,来储存更多的变量。 但是,建议不要把 useReducer 对应的变量设计的过于复杂。 192 | 193 | 使用useReducer,可以让我们使用比较复杂的逻辑和参数对内部变量进行修改。 194 | 195 | 不过你是否发现,示例1和示例2中所有的变量都是在同一个组件内定义和修改的,现实项目中肯定牵扯到不同模块组件之间共享并修改某个变量,那又该怎么办呢? 196 | 在下一章节 useReducer高级用法 中,我们会详细讲述如何用 useReducer + useContext 来实现全局不同层级组件共享并修改某变量。 197 | 198 | 199 | --- 200 | 201 | 至此,关于useReducer基础用法已经讲完。 202 | 203 | 欢迎进入下一章节:[useReducer高级用法](https://github.com/puxiao/react-hook-tutorial/blob/master/09%20useReducer%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95.md) 204 | -------------------------------------------------------------------------------- /03 useState高级用法.md: -------------------------------------------------------------------------------- 1 | # 03 useState高级用法 2 | 3 | 所谓高级用法,只不过是一些深层知识点和实用技巧,你甚至可以把本章当做对前面知识点的一个巩固和学习。 4 | 5 | ## 恢复默认值 6 | 7 | 组件需求:实现一个计数器,有3个按钮,点击后分别实现:恢复默认值、点击+1、点击-1 8 | 9 | 实现代码: 10 | 11 | import React, { useState } from 'react'; 12 | 13 | function Component() { 14 | const initCount = 0; 15 | const [count, setCount] = useState(initCount); 16 | 17 | return
18 | {count} 19 | 20 | 21 | 22 |
23 | } 24 | 25 | export default Component; 26 | 27 | 代码分析: 28 | 1、通过额外定义一个变量initCount=0,作为count的默认值; 29 | 2、任何时候想恢复默认值,直接将initCount赋值给count; 30 | 31 | ## 解决数据异步 32 | 33 | 还是基于上面那个示例,假设现在新增1个按钮,点击该按钮后执行以下代码: 34 | 35 | for(let i=0; i<3; i++){ 36 | setCount(count+1); 37 | } 38 | 39 | 通过for循环,执行了3次setCount(count+1),那么你觉得count会 +3 吗? 40 | 答案是:肯定不会 41 | 42 | 无论for循环执行几次,最终实际结果都将是仅仅执行一次 +1。 43 | 44 | 为什么? 45 | 类组件中setState赋值过程是异步的,同样在Hook中 setXxx 赋值也是异步的,比如上述代码中的setCount。 46 | 47 | 虽然执行了3次setCount(count+1),可是每一次修改后的count并不是立即生效的。当第2次和第3次执行时获取到count的值和第1次获取到的count值是一样的,所以最终其实相当于仅执行了1次。 48 | 49 | ##### 解决办法: 50 | 51 | 你肯定第一时间想到的是这样解决方式: 52 | 53 | let num = count; 54 | for(let i=0; i<3; i++){ 55 | num +=1; 56 | } 57 | setCount(num); 58 | 59 | 这样做肯定没问题,只不过有更简便、性能更高的方式。 60 | 61 | 和类组件中解决异步的办法类似,就是不直接赋值,而是采用“箭头函数返回值的形式”赋值。 62 | 63 | 把代码修改为: 64 | 65 | for(let i=0; i<3; i++){ 66 | setCount(prevData => {return prevData+1}); 67 | //可以简化为 setCount(prevData => prevData+1); 68 | } 69 | 70 | 代码分析: 71 | 1、prevData为我们定义的一个形参,指当前count应该的值; 72 | 2、{return prevData+1} 中,将 prevData+1,并将运算结果return出去。当然也非常推荐使用更加简化的写法:setCount(prevData => prevData+1); 73 | 3、最终将prevData赋值给count; 74 | 75 | 补充说明:你可以将prevData修改成任意你喜欢的变量名称,比如prev,只需要确保和后面return里的一致即可。 76 | 77 | 78 | ## 数据类型为Objcet的修改方法 79 | 80 | 之前的示例中,每个useState对应的值都是简单的string或number,如果对应的值是object,又该如何处理呢? 81 | 82 | 例如: 83 | 84 | const [person, setPerson] = useState({name:'puxiao',age:34}); 85 | 86 | 若想将age的值修改为18,该怎么写? 87 | 88 | 如果你有类组件编程经验,你肯定第一时间想是这样的: 89 | 90 | setPerson({age:18}); 91 | 92 | 在类组件中,setState是执行的是“异步对比累加赋值”,何为“对比”? 就是先对比之前数据属性中是否有age,如果有则修改age值,同时不会影响到其他属性的值。我猜测react是使用ES6中新增加的Object.assign()这个函数来实现这一步的。 93 | 94 | 但是,用useState定义的修改函数 setXxxx,例如setPerson中,执行的是 “异步直接赋值”。 95 | 96 | 请看实际执行的结果: 97 | 98 | console.log(person);//{name:'puxiao',age:34} 99 | setPerson({age:18}); 100 | console.log(person);//{age:18} 101 | 102 | 没错,虽然只是希望修改age的值,但是由于是“直接赋值”,导致{age:18}替换了整个{name:'puxiao',age:34} 103 | 104 | 105 | ##### 正确的做法: 106 | 107 | 我们需要先将person拷贝一份,修改之后再进行赋值。 108 | 109 | let newData = {...person}; 110 | newData.age = 18; 111 | setPerson(newData); 112 | 113 | 以上代码还有一种简写形式: 114 | 115 | setPerson({...person,age:18}); //这种简写是解构赋值带来的,并不是React提供的 116 | 117 | 代码分析: 118 | 1、先通过...person,将原有person做一次解构,得到一份复制品(浅拷贝); 119 | 2、修改age的值; 120 | 3、将修改过后的新数据,通过setPerson赋值给person; 121 | 122 | 完整示例: 123 | 124 | import React, { useState } from 'react'; 125 | 126 | function Component() { 127 | 128 | const [person, setPerson] = useState({name:'puxiao',age:34}); 129 | 130 | const nameChangeHandler = (eve) => { 131 | setPerson({...person,name:eve.target.value}); 132 | } 133 | 134 | const ageChangeHandler = (eve) => { 135 | setPerson({...person,age:eve.target.value}); 136 | } 137 | 138 | return
139 | 140 | 141 | {JSON.stringify(person)} 142 |
143 | } 144 | export default Component; 145 | 146 | 147 | ## 数据类型为Array的修改方法 148 | 149 | 和数据类型为Object相似,都是需要通过先拷贝一次,修改后再整体赋值。 150 | 151 | 这里举一个简单的小例子,以下代码实现了一个类似学习计划列表的功能组件。 152 | 153 | import React, { useState } from 'react'; 154 | 155 | function Component() { 156 | 157 | const [str, setStr] = useState(''); 158 | const [arr, setArr] = useState(['react', 'Koa']); 159 | 160 | const inputChangeHandler = (eve) => { 161 | setStr(eve.target.value); 162 | } 163 | 164 | const addHeadHandler = (eve) => { 165 | setArr([str,...arr]);//添加至头 166 | setStr(''); 167 | } 168 | 169 | const addEndHandler = (eve) => { 170 | setArr([...arr, str]);//添加至尾 171 | setStr(''); 172 | } 173 | 174 | const delHeadHandler = (eve) => { 175 | let new_arr = [...arr]; 176 | new_arr.shift();//从头删除1项目 177 | setArr(new_arr); 178 | } 179 | 180 | const delEndHandler = (eve) => { 181 | let new_arr = [...arr]; 182 | new_arr.pop();//从尾删除1项目 183 | setArr(new_arr); 184 | } 185 | 186 | const delByIndex = (eve) => { 187 | let index = eve.target.attributes.index.value; 188 | let new_arr = [...arr]; 189 | new_arr.splice(index,1);//删除当前项 190 | setArr(new_arr); 191 | } 192 | 193 | return
194 | 195 | 196 | 197 | 198 | 199 |
    200 | {arr.map( 201 | (item, index) => { 202 | return
  • 学习{index} - {item} 203 | 删除 204 |
  • 205 | } 206 | )} 207 |
208 |
209 | } 210 | 211 | export default Component; 212 | 213 | 214 | ## 性能优化 215 | 216 | 通过 setXxx 设置新值,但是如果新值和当前值完全一样,那么会引发React重新渲染吗? 217 | 218 | 通过React官方文档可以知道,当使用 setXxx 赋值时,Hook会使用Object.is()来对比当前值和新值,结果为true则不渲染,结果为false就会重新渲染。 219 | 220 | let str='a'; 221 | Object.is(str,'a'); //true 222 | 223 | let str='18'; 224 | Object.is(str,18); //str为String类型,18为Number类型,因此结果为false 225 | 226 | let obj={name:'a'}; 227 | Object.is(obj,{name:'a'}); //false 228 | //虽然obj和{name:'a'}字面上相同,但是obj==={name:'a'}为false,并且在Object.is()运算下认为两者不是同一个对象 229 | //事实上他们确实不是同一个对象,他们各自占用了一份内存 230 | 231 | let obj={name:'a'}; 232 | let a=obj; 233 | let b=obj; 234 | Object.is(a,b); //因为a和b都指向obj,因此结果为true 235 | 236 | 237 | 由上面测试可以看出: 238 | 1、对于简单类型的值,例如String、Number 新旧值一样的情况下是不会引起重新渲染的; 239 | 2、对于复杂类型的值,即使新旧值 “看上去是一样的” 也会引起重新渲染。除非新旧值指向同一个对象,或者可以说成新旧值分别是同一个对象的引用; 240 | 241 | 采用复杂类型的值不是不可以用,很多场景下都需要用到,但是请记得上面的测试结果。 242 | 243 | 为了可能存在的性能问题,如果可以,最好避免使用复杂类型的值。 244 | 245 | 246 | ## 自定义Hook 247 | 248 | 所谓自定义Hook,就是将Hook函数从函数组件中抽离,抽离之后多个函数组件可以共用该自定义Hook,共享该Hook的逻辑。 249 | 250 | 因为目前仅学习了useState,再多学习几个Hook函数后,会单独拿出一个篇章来讲解如何自定义Hook。 251 | 252 | 253 | --- 254 | 255 | 至此,关于useState高级用法已经讲完,相信你已经掌握了useState的使用方法。 256 | 257 | useState是本系列文章讲解的第一个Hook函数,同时也是使用频率最高的Hook,甚至可以说useState是函数组件开发的基石,因此本章稍显啰嗦,但目的就是希望你能理解透彻。 258 | 259 | 在后面讲解其他Hook函数时,将会尽量使用简洁、高冷的文章风格。 260 | 261 | 接下来学习第2个Hook函数useEffect。 262 | 263 | 欢迎进入下一章节:[useEffect基本用法](https://github.com/puxiao/react-hook-tutorial/blob/master/04%20useEffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 264 | -------------------------------------------------------------------------------- /09 useReducer高级用法.md: -------------------------------------------------------------------------------- 1 | # 09 useReducer高级用法 2 | 3 | 所谓高级用法,只不过是一些深层知识点和实用技巧,你甚至可以把本章当做对前面知识点的一个巩固和学习。 4 | 5 |
6 | 7 | ## 使用useReducer来管理复杂类型的数据 8 | 9 | 举例,若某组件内通过ajax请求数据,获取最新一条站内短信文字,需要组件显示整个ajax过程及结果: 10 | 1、当ajax开始请求时,界面显示“loading...”; 11 | 2、当ajax请求发生错误时,界面显示“wrong!”; 12 | 3、当ajax请求成功获取数据时,界面显示获取到的数据内容; 13 | 14 | 如果我们使用useState来实现上述功能,伪代码如下: 15 | 16 | function Component() { 17 | const [loading,setLoading] = useState(true); //是否ajax请求中,默认为true 18 | const [result,setResult] = useState(''); //请求数据内容,默认为'' 19 | const [error,setError] = useState(false); //请求是否发生错误,默认为false 20 | 21 | { 22 | //ajax请求成功 23 | setLoading(false); 24 | setResult('You have a good news!');//请注意,这是一行伪代码,只是为了演示,并不是真正ajax获取的结果 25 | setError(false); 26 | 27 | //ajax请求错误 28 | setLoading(false); 29 | setError(true); 30 | } 31 | 32 | return
33 | {loading ? 'loading...' : result} 34 | {error ? 'wrong!' : null} 35 |
36 | } 37 | 38 | 如果我们使用useReducer来实现,则可将上述3个变量都放在我们定义的变量state中,伪代码如下: 39 | 40 | const initralData = {loading: true,result: '',error: false}; 41 | 42 | const reducer = (state, action) => { 43 | switch (action.type) { 44 | case 'succes': 45 | return {loading:false,result:action.res,error:false} 46 | case 'error': 47 | return {loading:false,error:true} 48 | } 49 | } 50 | 51 | function Component() { 52 | const [state, dispatch] = useReducer(reducer, initralData); 53 | 54 | { 55 | //ajax请求成功 56 | dispatch({type:'succes',res:'You have a good news!'}); 57 | 58 | //ajax请求错误 59 | dispatch({type:'error'}); 60 | } 61 | 62 | return
63 | {state.loading ? 'loading...' : state.result} 64 | {state.error ? 'wrong!' : null} 65 |
66 | } 67 | 68 | 你可能会有疑问? 69 | 1、为什么看上去使用useReducer后代码变得更多? 70 | 答:因为使用useReducer,我们将修改数据拆分为2个部分,即“抛出修改事件和事件修改处理函数”。虽然代码增多了,但是逻辑更加清晰。 71 | 72 | 2、为什么不使用useState,同时把它对应的变量也做成一个obj,就像useReducer的initralData那种? 73 | 答:单纯从1次ajax请求很难看出使用useState或useReducer的差异,但是试想一下多次且ajax返回值在结构类型上容易发生变更,那么使用useReducer这种更加利于代码阅读、功能扩展。 74 | 75 | 76 | 77 |
78 | 79 | 80 | ## 使用useContext和useReducer实现操作全局共享数据 81 | 82 | 试想一下,如果想实现以下组件需求: 83 | 1、父组件中定义某变量xx; 84 | 2、任何层级下的子组件都可以轻松获取变量xx、并且可以“修改”变量xx; 85 | 86 | 注意这里的修改是加引号的,因为事实上你永远无法以直接赋值的方式进行修改,永远都需要调用父级组件提供的方法来修改。 87 | 88 | #### 需求分析 89 | 90 | 激动的心,颤抖的手,忘掉Redux,拥抱 React Hook! 91 | 92 | 首先这个功能是类组件无法做到的,也是React 16.8版本以前根本不能实现的,今天,当你使用Hook可轻松实现类似 Redux 共享数据管理功能。 93 | 94 | #### 实现原理 95 | 96 | 用 useContext 实现“获取全局数据” 97 | 用 useReducer 实现“修改全局数据” 98 | 99 | #### 实现思路 100 | 101 | 1、用React.createContext()定义一个全局数据对象; 102 | 2、在父组件中用 useReducer 定义全局变量xx和负责抛出修改事件的dispatch; 103 | 3、在父组件之外,定义负责具体修改全局变量的处理函数reducer,根据修改xx事件类型和参数,执行修改xx的值; 104 | 4、在父组件中用 标签把 全局共享数据和负责抛出修改xx的dispatch 暴露给子组件; 105 | 5、在子组件中用 useContext 获取全局变量; 106 | 6、在子组件中用 xxContext.dispatch 去抛出修改xx的事件,携带修改事件类型和参数; 107 | 108 | 109 | #### 补充说明 110 | 111 | 上面一直提到了 “抛出事件” “事件处理函数” "dispatch" 都是字面上的,不是真正意义上的事件驱动。 这些都只是 React 暴露给我们的函数或形参。 真正的事件驱动是由 React Hook 底层为我们完成的。 112 | 113 | 以上观点仅为个人理解,不能保证100%正确。 114 | 115 | 116 | #### 伪代码演示 117 | 118 | 假设React组件需求为: 119 | 1、有全局数据变量count; 120 | 2、不同层级的子组件均可获取并修改全局变量count; 121 | 122 | 共享对象 代码如下: 123 | 124 | import React from 'react'; 125 | const CountContext = React.createContext(); 126 | export default CountContext; 127 | 128 | 129 | 父组件 代码如下: 130 | 131 | import React, { useReducer } from 'react'; 132 | import CountContext from './CountContext'; 133 | import ComponentA from './ComponentA'; 134 | import ComponentB from './ComponentB'; 135 | import ComponentC from './ComponentC'; 136 | 137 | const initialCount = 0; //定义count的默认值 138 | 139 | //修改count事件处理函数,根据修改参数进行处理 140 | function reducer(state, action) { 141 | //注意这里先判断事件类型,然后结合携带的参数param 来最终修改count 142 | switch (action.type) { 143 | case 'add': 144 | return state + action.param; 145 | case 'sub': 146 | return state - action.param; 147 | case 'mul': 148 | return state * action.param; 149 | case 'reset': 150 | return initialCount; 151 | default: 152 | console.log('what?'); 153 | return state; 154 | } 155 | } 156 | 157 | function ParentComponent() { 158 | //定义全局变量count,以及负责抛出修改事件的dispatch 159 | const [count, dispatch] = useReducer(reducer, initialCount); 160 | 161 | //请注意:value={{count,dispatch} 是整个代码的核心,把将count、dispatch暴露给所有子组件 162 | return 163 |
164 | ParentComponent - count={count} 165 | 166 | 167 | 168 |
169 |
170 | } 171 | 172 | export default ParentComponent; 173 | 174 | 175 | 子组件A 代码如下: 176 | 177 | import React,{ useState, useContext } from 'react'; 178 | import CountContext from './CountContext'; 179 | 180 | function CopmpoentA() { 181 | const [param,setParam] = useState(1); 182 | //引入全局共享对象,获取全局变量count,以及修改count对应的dispatch 183 | const countContext = useContext(CountContext); 184 | 185 | const inputChangeHandler = (eve) => { 186 | setParam(eve.target.value); 187 | } 188 | 189 | const doHandler = () => { 190 | //若想修改全局count,先获取count对应的修改抛出事件对象dispatch,然后通过dispatch将修改内容抛出 191 | //抛出的修改内容为:{type:'add',param:xxx},即告诉count的修改事件处理函数,本次修改的类型为add,参数是param 192 | //这里的add和param完全是根据自己实际需求自己定义的 193 | countContext.dispatch({type:'add',param:Number(param)}); 194 | } 195 | 196 | const resetHandler = () => { 197 | countContext.dispatch({type:'reset'}); 198 | } 199 | 200 | return
201 | ComponentA - count={countContext.count} 202 | 203 | 204 | 205 |
206 | } 207 | 208 | export default CopmpoentA; 209 | 210 | 211 | 总结: 212 | 1、3个子组件他们主要区别是组件内 doHandler 函数,对count进行不同形式的修改; 213 | 2、3个子组件分别可以实现对全局变量 count 的获取与修改; 214 | 3、当任何一个子组件对count进行了修改,都会立即反映在其他子组件中,实现子组件之间的数据共享。 215 | 216 | 至此,实现了比较简单的,类似 Redux 全局数据管理效果。 217 | 218 | 219 | 220 |
221 | 222 | 223 | ## 为什么不使用Redux? 224 | 225 | 这个问题以前提出过,现在可以明确回答:因为我自己使用 useReducer + useContext 自己可以轻松实现,干嘛还要用Redux。 226 | 再见 Redux。 227 | 228 | 229 | 230 |
231 | 232 |
233 | 234 | > 以下内容更新于 2021.05.18 235 | 236 | ## 忘掉 Redux,忘掉 useReducer+useContext,拥抱 Recoil 吧! 237 | 238 | 强烈推荐使用 React 开发人员针对 Hooks 函数组件推出的新一代状态管理库:Recoil 239 | 240 | Recoil 官方网站:https://recoiljs.org/ 241 | 242 | 我写的 Recoil 教程:https://github.com/puxiao/recoil-tutorial 243 | 244 | 245 | 246 | > 以上内容更新于 2021.05.18 247 | 248 |
249 | 250 | 251 | 252 |
253 | 254 | ## 什么时候用useState?什么时候用useReducer? 255 | 256 | 本人的建议是:组件自己内部的简单逻辑变量用useState、多个组件之间共享的复杂逻辑变量用useReducer。 257 | 258 | 259 | --- 260 | 261 | 至此,关于useReducer高级用法已经讲完,useReducer可以让我们实现复杂逻辑的数据修改,结合useContext更能做到全局数据共享和修改。 262 | 263 | 目前已经学习过的4个Hook函数useState、useEffect、useContext、useReducer,他们都是用来实现组件某些具体业务功能的,而接下来要学习的Hook函数则是用来提高组件整体性能的,例如第5个Hook函数useCallback和第6个Hook函数useMemo。 264 | 265 | 欢迎进入下一章节:[useCallback基本用法](https://github.com/puxiao/react-hook-tutorial/blob/master/10%20useCallback%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 266 | -------------------------------------------------------------------------------- /附01:React基础知识.md: -------------------------------------------------------------------------------- 1 | # React基础知识 2 | 3 | 说明:以下这些基础知识适用于类组件和函数组件,并不是函数组件独有的。 4 | 5 | ## 安装react并初始化 6 | ##### 1、安装:npm install -g create-react-app 7 | 8 | ##### 2、创建hello-react目录并初始化:npx create-react-app hello-react 9 | 10 | 注意: 11 | 1. 目录名不允许有大写字母 12 | 2. 初始化过程比较慢,甚至可能需要5-10分钟 13 | 3. 如果报错:npm ERR! Unexpected end of JSON input while parsing near '...n\r\nwsFcBAEBCAAQBQJd', 解决方法:npm root -g 找到本机npm全局安装目录,cd 进入该目录,执行清除缓存:npm cache clean --force,然后再次初始化。 14 | 15 | ##### 3、启动项目:cd hello-react、npm start 16 | 17 | 默认将启动:http://localhost:3000 18 | 19 | 20 | ## 自定义组件基础知识 21 | 22 | 1、自定义组件必须以大写字母开头、默认网页原生标签还以小写开头。请注意这里表述的"默认网页原生标签"本质上并不是真实的原生网页标签,他们是react默认定义好的、内置的自定义组件标签,只不过这些标签刚好和原生标签的作用,功能,名称一模一样而已。 23 | 24 | 2、自定义组件如果不希望设定最外层的标签,那么可以使用react(16+版本)提供的占位符Fragment来充当最外层标签; 25 | 26 | import React,{Component,Fragment} from 'react'; 27 | 类组件:render(){return xxxxxxx} 28 | 函数组件:return xxxxxxx 29 | 30 | 在最新的react版本中,也可以直接使用<>来代替Fragment。其中<>唯一可以拥有的属性为key。即< key='xxx'> 31 | 32 | 3、使用数组map循环更新li,一定要给li添加对应的key值,否则虽然正常运行,但是会报错误警告。不建议直接使用index作为key值。 33 | 34 | 4、在最新的react版本中,为了提高更新性能,推荐采用异步的方式更新数据。具体使用方式为:setXxx((prevData) => {return xxx})。其中参数prevData指之前的变量值,return的对象指修改之后的数据值。 35 | 36 | 可以将上面代码简写为:setXxx(prevData => xxx) 37 | 若没有用到prevData参数,还可以省略,即 setXxx(() => xxx); 38 | 39 | 异步的目的是为了优化更新性能,react短期内发现多条数据变量发生修改,那么他会将所有修改合并成一次修改再最终执行。 40 | 41 | 5、在JSX中写注释,格式为:{/* xxxxx */}或{//xxxx},注意如果使用单行注释,最外的大括号必须单独占一行。注释尽在开发源代码中显示,在导出的网页中不会有该注释。 42 | 43 | 6、给标签添加样式时,推荐使用className,不推荐使用class。如果使用class虽然运行没问题,但是会报错误警告,因为样式class这个关键词和js中声明类的class冲突。类似的还有标签中for关键词,推荐改为htmlFor。 44 | 45 | 7、通常情况下,react是针对组件开发,并且只负责对html中某一个div进行渲染,那么意味着该html其他标签不受影响,这样引申出来一个结果:一个html既可以使用react,也可以使用vue,两者可以并存。 46 | 47 | 8、为了方便调试代码,可以在谷歌浏览器中安装React Developer Tools插件。安装后可在谷歌浏览器调试模式下,查看component标签下的内容。 若访问本机react调试网页则该插件图标为红色、若访问导出版本的React网页则该插线显示为蓝色、若访问的网页没使用react框架则为灰色。 48 | 49 | 9、给组件设定属性,只有属性名没有属性值,那么默认react会将该属性值设置为true。在ES6中如果只有一个属性对象没有属性值,通常理解为该属性名和属性值是相同的。 为了避免混淆,不建议不给属性不设置属性值。 50 | 51 | 10、ReactDOM.createPortal()用来将元素渲染到任意DOM元素中(包括顶级组件之外的其他DOM中)。 52 | 53 | 54 | 55 | ## "纯函数" 概念解释 56 | 57 | JS中定义的所有函数都可以增加参数,所谓"纯函数"是指函数内部并未修改过该参数的函数。 58 | 59 | 例如以下函数:function myFun(a){let c=a },该函数内部从未更改过参数a,那么这个函数就是纯函数。 60 | 61 | 反例,非纯函数 例如:function myFun(a){a=a+2; let c=a},该函数内部修改过参数a,那么这个函数就不再是纯函数了。 62 | 63 | 纯函数的特殊意义是什么? 64 | 因为纯函数内部从不会直接修改参数,那么无论运行多少次,执行结果永远是一致的。 65 | 66 | 若仅仅有一个函数,那么也无所谓,但是如果有多个函数都是都需要调用执行同一个变量(参数),为了确保多个函数执行结果是符合预期的,那么就要求每个函数都不能在自己内部修改该变量(参数)。 67 | 68 | 这就是为什么react不允许直接修改某变量的原因。 69 | 70 | 71 | ## "受控组件" 概念解释 72 | 73 | 像input、select、textarea、form等将自身value与某变量进行绑定的组件,称之为受控组件。 74 | 75 | "受控"即这些组件的可以值受到某变量的控制。 76 | 77 | 与之对应的是"非受控组件",即该组件对应的值并不能被某变量控制。 78 | 79 | 例如"",该组件的值为用户选中本地的文件信息,该值并不能直接通过某变量来进行value值的设定,因此该组件属于"非受控组件"。 80 | 81 | 82 | ## "声明式开发" 概念解释 83 | 84 | "声明式开发":基于数据定义和数据改变,视图层自动更新。 85 | "命令式开发":基于具体执行命令更改视图,例如DOM操作修改。 86 | 87 | 注意:声明式开发并不是不进行DOM操作,而是把DOM操作频率降到最低。 88 | 89 | 90 | ## "单项数据流" 概念解释 91 | 92 | react框架的原则中规定,子组件只可以使用父组件传递过来的xxx属性对应的值或方法,不可以改变。 93 | 94 | 数据只能单向发生传递(父传向子,不允许子直接修改父),若子组件想修改父组件中的数据,只能通过父组件暴露给子组件的函数(方法)来间接修改。 95 | 96 | react框架具体实现方式是设置父组件传递给子组件的"数据值或方法"仅仅为可读,但不可修改。 97 | 98 | 为什么要做这样的限制? 99 | 因为一个父组件可以有多个子组件,如果每个子组件都可修改父组件中的数据(子组件之间彼此共用父组件的数据),一个子组件的数据修改会造成其他子组件数据更改,最终会让整个组件数据变得非常复杂。 100 | 101 | 为了简化数据操作复杂程度,因此采用单向数据流策略,保证父组件数据的唯一最终可修改权归父组件所有。 102 | 103 | 104 | ## "视图层渲染框架" 概念解释 105 | 106 | react框架自身定位是"视图层渲染框架",单向数据流概念很好,但是实际项目中页面会很复杂。 107 | 108 | 例如顶级组件Root中分别使用了组件A(由子组件A0、A1、A2构成)、组件B(由子组件A0、A1、A2构成)、组件C(由子组件C0、C1、C2构成),若此时组件A的子组件A2想和组件C的子组件C1进行数据交互,那么按照单向数据流的规范,数据操作流程为 A2 -> A -> Root -> C - C1,可以看出操作流程非常复杂。 109 | 110 | 所以实际开发中,React框架也许会结合其他"数据层框架"(例如Redux、Flux等),但是请注意,只从有了hook以后,可以通过useReducer+useContext来实现类似Redux的功能。 111 | 112 | 113 | ## "函数式编程" 概念解释 114 | 115 | react自定义组件的各种交互都在内部定义不同的函数(js语法规定:类class中定义的函数不需要在前面写 function关键词),因此成为函数式编程。不像原生JS和html交互那样,更多侧重html标签、DOM操作来实现视图和交互。 116 | 117 | 函数式编程的几点好处: 118 | 1、可以把复杂功能的处理函数拆分成多个细小的函数。 119 | 2、由于都是通过函数来进行视图层渲染和数据交互,更加方便编写"前端自动化测试"代码。 120 | 121 | 122 | ## "虚拟DOM" 概念解释 123 | 虚拟DOM(Virtual Dom)就是一个JS对象(数组对象),用来描述真实DOM。相对通过html标签创建的真实DOM,虚拟DOM是保存在客户端内存里的一份JS表述DOM的数组对象。 124 | 125 | 用最简单的一个div标签来示意两者的差异,数据格式如下: 126 | 127 | //真实DOM数据格式(网页标签) 128 |
hell react
129 | 130 | //虚拟DOM数据格式(JS数组对象) 131 | //虚拟DOM数组对象格式为:标签名+属性集合+值 132 | ['div',{id:'mydiv'},'hell react'] 133 | 134 | //在JSX的创建模板代码中,通常代码格式为 135 | render(){return
hello react} 136 | 137 | //还可以使用react提供的,更加底层的方法来实现 138 | render(){return React.createElement('div',{id:'mydiv'},'hello react')} 139 | 140 | 141 | 虚拟DOM更新性能快的原因并不是因为在内存中(理论上任何软件都是运行在内存中),而是因为虚拟DOM储存的数据格式为JS对象,用JS来操作(生成/查询/对比/更新)JS对象很容易。用JS操作(生成/查询/对比/更新)真实DOM则需要调用Web Action层的API,性能相对就慢。 142 | 143 | react运行(更新)步骤,大致为: 144 | 1、定义组件数据变量 145 | 2、定义组件模板JSX 146 | 3、数据与模板结合,生成一份虚拟DOM 147 | 4、将虚拟DOM转化为真实DOM 148 | 5、将得到的真实DOM挂载到html中(通过真实DOM操作),用来显示 149 | 6、监听变量发生改变,若有改变重新执行第3步(数据与模板结合,生成另外一份新的虚拟DOM) 150 | 7、在内存中对比前后两份虚拟DOM,找出差异部分(diff算法) 151 | 8、将差异部分转化为真实的DOM 152 | 8、将差异化的真实DOM,通过真实DOM操作进行更新 153 | 154 | 当变量发生更改时,虚拟DOM减少了真实DOM的创建和对比次数(通过虚拟DOM而非真实DOM),从而提高了性能。 155 | 156 | 157 | ## "Diff算法" 概念解释 158 | 159 | 当变量发生改变时,需要重新生成新的虚拟DOM,并且对旧的虚拟DOM进行差异化比对。 160 | Diff算法就是这个差异化比对的算法。 161 | 162 | Diff算法为了提高性能,优化算法,通常原则为: 163 | 164 | 165 | ##### 同层(同级)虚拟DOM比对 166 | 167 | 先从两个虚拟DOM(JS对象)同层(即顶层)开始比对,如果发现同层就不一致,那么就直接放弃下一层(级别)的对比,采用最新的虚拟DOM。 168 | 169 | 疑问点:假如两心虚拟DOM顶层不一致,但下一级别以及后面的更多级别都一致,如果仅仅因为顶层不一致而就该放弃下一级别,重新操作真实DOM从头渲染,岂不是性能浪费? 170 | 171 | 答:同层(同级)虚拟DOM比对,"比对"算法相对简单,比对速度快。如果采用多层(多级)比对,"比对"算法会相对复杂,比对速度慢。 同层虚拟DOM比对就是利用了比对速度快的优势来抵消"操作真实DOM操作性能上的浪费"。 172 | 173 | 174 | ##### 列表元素使用key值进行比对 175 | 176 | 这里的key值是值"稳定的key值(是有规律的字符串,非数字)",若key值为索引数字index,那么顺序发生改变时,索引数字也会发生变化,无法判断之前的和现在的是否是同一个对象。 177 | 178 | 如果key值是稳定的,那么在比对的时候,比较容易比对出是否发生变化,以及具体的变化是什么。 179 | 180 | Diff算法还有非常多的其他性能优化算法,以上列出的"同层比对、key值比对"仅仅为算法举例。 181 | 182 | 183 | ## "高阶组件" 概念解释 184 | 185 | 高阶组件是一种组件设计方式(设计模式),就是将一个组件作为参数传递给一个函数,该函数接收参数(组件)后进行处理和装饰,并返回出一个新的组件。 186 | 187 | 简单来说就是,普通组件是根据参数(props)生成一个UI(JSX语法支持的标签)。而高阶组件是根据参数(组件)生成一个新的组件。 188 | 189 | 190 | ## "生命周期函数" 概念解释 191 | 192 | 生命周期函数指在某一时刻组件会自动调用执行的函数。 193 | 194 | 这里的"某一时刻"可以是指组件初始化、挂载到虚拟DOM、数据更改引发的更新(重新渲染)、从虚拟DOM卸载这4个阶段。 195 | 196 | #### 生命周期4个阶段和该阶段内的生命周期函数: 197 | 198 | ##### 初始化(Initialization) 199 | constructor()是JS中原生类的构造函数,理论上他不专属于组件的初始化,但是如果把它归类成组件组初始化也是可以接受的。 200 | 201 | ##### 挂载(Mounting) 202 | componentWillMount(即将被挂载)、render(挂载)、componentDidMount(挂载完成) 203 | 204 | ##### 更新(Updation): 205 | props发生变化后对应的更新过程:componentWillReceiveProps(父组件发生数据更改,父组件的render重新被执行,子组件预测到可能会发生替换新数据)、shouldComponentUpdate(询问是否应该更新?返回true则更新、返回flash则不更新)、componentWillUpate(准备要开始更新)、render(更新)、componentDidUpdate(更新完成) 206 | 207 | 变量数据发生变化后对应的更新过程:shouldComponentUpdate(询问是否应该更新?返回true则更新、返回flash则不更新)、conponentWillUpdate(准备要开始更新)、、render(更新)、componentDidUpdate(更新完成) 208 | 209 | props和states发生变化后的更新过程,唯一差异是props多了一个 componentWillReceiveProps生命周期函数。 210 | 211 | componentWillReceiveProps触发的条件是: 212 | 1、一个组件要从父组件接收参数,并且已存在父组件中(子组件第一次被创建时是不会执行componentWillReceiveProps的) 213 | 2、只要父组件的render函数重新被执行(父组件发生数据更改,子组件预测到可能会发生替换新数据),componentWillReceiveProps就会被触发 214 | 215 | ##### 捕获子组件错误: 216 | componentDidCatch(捕获到子组件错误时被触发) 217 | 218 | ##### 卸载(Unmounting): 219 | componentWillUnmount(即将被卸载) 220 | 221 | 备注:类组件继承自Component组件,Component组件内置了除render()以外的所有生命周期函数。因此自定义组件render()这个生命周期函数必须存在,其他的生命周期函数都可以忽略不写。 而使用了hook的函数组件,简化了生命周期函数调用的复杂程度。 222 | 223 | ##### 生命周期函数的几个应用场景: 224 | 225 | 对于类组件(由class创建的)和函数组件(由function创建的),他们对于生命周期的调用方法不同。 226 | 227 | 1、只需要第一次获取数据的Ajax请求 228 | 如果类组件有ajax请求(只需请求一次),那么最好把ajax请求写在componentDidMount中(只执行一次)。因为"初始化、挂载、卸载"在一个组件的整个生命周期中只会发生一次,而"更新"可以在生命周期中多次执行。 229 | 如果是函数组件,则可以写在useEffect()中,并且将第2个参数设置为空数组,这样useEffect只会执行一次。 230 | 231 | 2、防止子组件不必要的重新渲染 232 | 如果是类组件,父组件发生变量改变,那么会调用render(),会重新渲染所有子组件。但是如果变量改变的某个值与某子组件并不相关,如果此时也重新渲染该子组件会造成性能上的浪费。为了解决这个情况,可以在子组件中的shouldComponentUpdate生命周期函数中,做以下操作: 233 | 234 | shouldComponentUpdate(nextProps,nextStates){ 235 | //判断xxx值是否相同,如果相同则不进行重新渲染 236 | return (nextProps.xxx !== this.props.xxx); //注意是 !== 而不是 != 237 | } 238 | 239 | 还可以让组件继承由React.Component改为React.PureComponent,react会自动帮我们在shouldComponentUpdate生命周期函数中做浅对比。 240 | 241 | 如果是函数组件,则在子组件导出时,使用React.memo()进行包裹,同时结合useCallback来阻止无谓的渲染,实现提高性能。 242 | 243 | 244 | # React中设置样式的几种形式 245 | 246 | ##### 第一种:引用外部css样式 247 | 248 | 伪代码示例: 249 | import from './xxx.css'; 250 | return
251 | 252 | 注意:在jsx语法中,使用驼峰命名。例如原生html中的classname需要改成className、background-color要改成backgroundColor。 253 | 254 | 255 | ##### 第二种:内部样式 256 | 257 | 伪代码示例: 258 | return
259 | 260 | 注意:内联样式值为一个对象,对象属性之间用","分割而不是原生html中的";"。 261 | 因为是一个对象,因此下面代码也是可行的: 262 | const mystyle = {backgroundColor:'green',width:'100px'}; 263 | return
264 | 265 | 266 | # Hook用法 267 | 268 | Hook是react16.8以上版本才出现的新特性,可以在函数组件中使用组件生命周期函数,且颗粒度更加细致。 269 | 270 | 可以把Hook逻辑从组件中抽离出来,多个组件可以共享该hook逻辑。 271 | 272 | **请注意hook本质上是为了解决组件之间共享逻辑,并不是单纯为了解决组件之间共享数据。** 273 | 274 | hook表现出来特别像一个普通的JS函数(仅仅是表现出来但绝不是真的普通JS函数)。 275 | 276 | -------------------------------------------------------------------------------- /16 自定义hook.md: -------------------------------------------------------------------------------- 1 | # 16 自定义hook 2 | 3 | ## 自定义hook概念解释 4 | 像useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef、useImperativeHandle、useLayoutEffect、useDebugValue这10个hook是react默认自带的hook,而所谓自定义hook就是由我们自己编写的hook。 5 | 6 | 所谓自定义hook就是把原来写在函数组件内的hook相关代码抽离出来,单独定义成一个函数,而这个抽离出来的hook函数就称之为“自定义hook钩子函数”,简称“自定义hook”。 7 | 8 | 9 | ## 自定义hook是来解决什么问题的? 10 | 答:自定义hook是将原来在组件中编写的相关hook代码抽离出组件,让hook相关代码独立存在,达到优化代码结构、相关hook代码可以重复使用的目的。 11 | 12 | 补充说明: 13 | 1、如果你在别人的项目代码中,发现除了react默认自带的那10个hook以外,出现了 useXxx() 这样的看着像hook的函数,可以肯定那些就是自定义的hook。 14 | 2、随着react新版本发布,可能会出现更多新的、默认自带的hook。 15 | 16 | 17 | ## 自定义hook基本用法 18 | 19 | 首先我们知道hook只能用在函数组件中,而函数组件本身是一个稍微特殊的函数,尽管稍微特殊但毕竟他也遵循一般函数的使用规律。 所谓“把原来写在函数组件内的hook相关代码抽离出来,单独定义成一个函数” 本质上就是把函数内部定义的变量或方法拿出来,放到函数外面单独定义成一个函数。 20 | 21 | 这个抽离出来新定义的函数,遵循JS默认的函数用法,即函数参数可以任意设定,返回值也可以是任意内容。 22 | 23 | 请注意:react规定所有的自定义hook函数命名时必须使用 useXxx 这种形式。 24 | 25 | 举一个最简单的例子:假设我们有一个组件,组件内部有一个count的变量,我们的代码之前是这样的: 26 | 27 | import React,{useState} from 'react' 28 | function CurrentComponent() { 29 | const [count,setCount] = useState(0);//请注意这行代码,就是我们要即将抽离出去的hook 30 | return ( 31 |
32 | {count} 33 | 34 |
35 | ) 36 | } 37 | export default CurrentComponent 38 | 39 | 在上面这个组件中,通过 const [count,setCount] = useState(0) 定义了组件内的变量count和修改count的方法。那我们现在将这行相关的hook抽离出函数组件。我们计划把抽离出来的、和count相关的函数,命名为useCount,修改后的代码如下: 40 | 41 | //useCount.js 42 | import {useState} from 'react'; 43 | function useCount(initialValue){ 44 | 45 | //依然使用 useState 创建countcount和setCount 46 | //并且将参数initialValue的值赋予给count作为默认值 47 | //将创建好的count和setCount作为函数返回值 return 出去 48 | 49 | const [count,setCount] = useState(initialValue); 50 | return [count,setCount]; 51 | } 52 | export default useCount; 53 | 54 | //CurrentComponent.js 55 | import React from 'react' 56 | import useCount from './useCount';//引入useCount 57 | function CurrentComponent() { 58 | const [count,setCount] = useCount(0);//请注意这里使用的是useCount,而不是useState 59 | return ( 60 |
61 | {count} 62 | 63 |
64 | ) 65 | } 66 | export default CurrentComponent 67 | 68 | 69 | ##### 代码分析: 70 | 71 | 1、我们将原本组件内定义的count相关代码抽离出组件,单独定义一个 useCount 函数。 72 | 2、组件需要使用 useCount,只需先引入useCount,然后把 useCount 当成普通函数使用就好了。 73 | 3、useCount就是我们自定义的hook。 74 | 75 | 注意:一般自定义hook顶部是不需要引入React的,只需要引入使用到的 hook 函数即可。 76 | 例如上面 useCount 顶部,我们写的是 import {useState} from 'react' 而不是 import React,{useState} from 'react'; 77 | 78 | 上面举例中的useCount非常简单,内部并没有过多逻辑,在实际开发中自定义hook内部肯定要有比较复杂的逻辑。 79 | 80 | 由于是单独定义的,所以自定义hook可以同时被多个组件引入和使用,达到代码复用的目的。 81 | 82 | 划重点,在实际项目中,通常自定义hook返回值有3种表现形式: 83 | 1、不带返回值的函数 84 | 2、带普通返回值的函数 85 | 3、带特殊结构返回值的函数 86 | 87 | 以上3种不同返回值各有各的适用场景,下面就以实际示例来逐一说明。 88 | 89 | 90 | ## 不带返回值的自定义hook使用示例: 91 | 92 | 举例:若父组件内有多个子组件,每个子组件内部都有不同的业务代码,但是所有子组件有一个相同的功能,就是当自身内部变量value发生变化时,将网页标题改为变量value的值。 93 | 94 | 首先我们知道修改网页标题是在组件内部的useEffect()函数中修改,那结合上面的使用场景,我们可以将useEffect()单独抽离出来,作为一个自定义hook,命名为 useDocumentTitle,让所有子组件都复用这个useDocumentTitle。 95 | 96 | useDocumentTitle 代码如下: 97 | 98 | import {useEffect} from 'react' 99 | function useDocumentTitle(value) { 100 | useEffect(() => { 101 | document.title = value; 102 | },[value]); 103 | } 104 | export default useDocumentTitle; 105 | 106 | 107 | 假设我们其中一个子组件的功能为: 108 | 1、有1个number类型的变量count 109 | 2、有1个按钮,点击按钮后将count修改为一个随机数字 110 | 3、当组件重新渲染完成后,将网页标题修改为count的值 111 | 112 | 子组件代码为: 113 | 114 | import React,{useState} from 'react' 115 | import useDocumentTitle from './useDocumentTitle'; 116 | function ChildComponent() { 117 | const [count,setCount] = useState(0); 118 | useDocumentTitle(count);//把内部变量count传给useDocumentTitle,既作为网页标题内容,同时也作为useEffect的变量依赖 119 | return ( 120 |
121 | 122 |
123 | ) 124 | } 125 | export default ChildComponent 126 | 127 | 128 | 其他子组件也使用useDocumentTitle,这样我们便将原本每个子组件都需要编写的useEffect改为统一的useDocumentTitle,实现了代码复用。 129 | 130 | 应用场景小总结: 131 | 在这个示例中,useDocumentTitle函数并没有任何返回值,子组件使用useDocumentTitle时就好像原本那段useEffect代码本身就定义在那里似的。 132 | 133 | 134 | ## 带普通返回值的自定义hook使用示例: 135 | 在本章最开始讲解“自定义hook基本用法”时,所举的useCount例子非常简单,这次我们将对useCount进行功能上的扩展。 136 | 137 | 原本useCount只是定义了count和setCount,这次所谓的功能扩展,就是将setCount改为其他几种修改count的函数。 138 | 例如: 139 | 1、添加 add() 140 | 2、减去 sub() 141 | 3、相乘 mul() 142 | 4、恢复初始值 reset() 143 | 144 | 修改后的useCount代码为: 145 | 146 | import {useState} from 'react' 147 | function useCount(initialValue){ 148 | const [count,setCount] = useState(initialValue); 149 | const add = param => {setCount(prev => prev + param);} 150 | const sub = param => {setCount(prev => prev - param);} 151 | const mul = param => {setCount(prev => prev * param);} 152 | const reset = () => {setCount(() => initialValue);} 153 | return [count,add,sub,mul,reset]; //将count和定义的4个方法作为返回值 return 出去 154 | } 155 | export default useCount; 156 | 157 | 请注意:为了避免4个修改函数中得到的是旧的count,所以我们采用的是 setCount(prev => xxxxx) 这种修改方式,而不是直接使用 setCount(count xxx)。 158 | 159 | CurrentComponent组件想使用useCount,代码为: 160 | 161 | import React from 'react' 162 | import useCount from './useCount' 163 | 164 | function CurrentComponent() { 165 | const [count,add,sub,mul,reset] = useCount(0); //使用useCount,并解构useCount的返回值 166 | return ( 167 |
168 | {count} 169 | 170 | 171 | 172 | 173 |
174 | ) 175 | } 176 | export default CurrentComponent 177 | 178 | 179 | 180 | 对于上面这个效果,你是否觉得眼熟? 没错,在讲解useReducer时,就使用useReducer实现了类似的一个效果。 只不过这次是为了讲解自定义hook,而那次是讲解如何使用useReducer替代useState实现复杂的业务。 181 | 182 | 应用场景小总结: 183 | 1、我们可以在自定义hook中编写相关业务逻辑函数(方法),并通过返回值的形式 return 出去,供其他组件调用。 184 | 185 | 186 | ## 带特殊结构返回值的自定义hook使用示例: 187 | 上一个代码示例讲解了“带普通返回值”的自定义hook,那这次要讲的“带特殊结构返回值”的自定义hook究竟差别在哪里? 188 | 189 | “带特殊结构返回值”中的特殊是指? 190 | 答:我们把组件需要用到的多项属性设置,合并为一个对象 并 return 出去,供组件使用。 191 | 192 | 还是以示例来讲解会更容易理解,假设我们有一个登录组件,功能为: 193 | 1、有一个用户名输入框 194 | 2、有一个密码输入框 195 | 3、有一个提交按钮 196 | 197 | 补充说明,为了简化代码,我们并不做真正的登录验证,点击提交按钮后: 198 | 1、仅仅是alert一下用户名和密码,即表示登录 199 | 2、同时清除用户名和密码输入框里的内容 200 | 201 | 需求分析: 202 | 1、每个输入框都是一个,都需要绑定一个变量,都需要设置onChange事件 203 | 2、每一个输入框都需要清空内容 204 | 205 | 我们将定义一个自定义hook,命名为useInput,useInput来实现这2个输入框共有的业务逻辑。 206 | 207 | useInput的代码为: 208 | 209 | import {useState} from 'react' 210 | function useInput(initialValue) { 211 | const [value,setValue] = useState(initialValue); //定义输入框对应的值value 212 | //定义reset函数,用来重置输入框 213 | const reset = () => { 214 | setValue(initialValue); 215 | } 216 | //定义一个 bind 对象,该对象有 value 和 onChange 2个属性 217 | const bind = { 218 | value, 219 | onChange: eve => { 220 | setValue(eve.target.value) 221 | } 222 | } 223 | return [value,reset,bind];//将输入框的值、重置输入框函数、定义的bind对象作为返回值 return 出去 224 | } 225 | export default useInput 226 | 227 | 请注意:在useInput中,返回值 value、reset 我们很容易理解,但是 bind 是来做什么的? 228 | 答:这个 bind 就是我们前面提到的“带特殊结构返回值”,bind对象本身结构由2个属性value和onChange组成。 229 | 至于 bind 怎么用,很快揭晓。 230 | 231 | 232 | 登录组件LoginForm的代码为: 233 | 234 | import React from 'react' 235 | import useInput from './useInput'; 236 | function LoginForm() { 237 | const [usename,resetUsename,bindUsename] = useInput(''); //定义用户名输入框相关的变量 238 | const [password,resetPassword,bindPassword] = useInput(''); //定义密码输入框相关的变量 239 | 240 | const submitHandle = (eve) => { 241 | eve.preventDefault(); //阻止form真正提交 242 | alert(`usename:${usename}\rpassword:${password}`); //通过alert,弹出用户名和密码的值 243 | resetUsename(); //重置用户名输入框 244 | resetPassword(); //重置密码输入框 245 | } 246 | 247 | //请特别留意用户名和密码输入框中的 {...bindUsename}和{...bindPassword} 248 | return ( 249 |
250 | 251 | 252 | 253 | 254 | 255 |
256 | ) 257 | } 258 | export default LoginForm; 259 | 260 | 对于获取输入框的值、以及调用输入框对应的reset()函数,相信你很容易理解。 261 | 262 | 下面对 {...bindUsename} 和 {...bindPassword} 做进一步说明: 263 | 1、首先我们知道 {...obj} 这种在原生JS中,相当于把obj对象进行解构,然后得到一个浅拷贝的新对象。 264 | 2、但是在上面的代码中并不是这个意思,千万不要被迷惑。 在JSX中的某组件,如果要添加某属性,格式为 xxx={xxx}。 265 | 266 | 例如常见的给一个输入框绑定某变量,同时添加onChange事件,一般写法为: 267 | 268 | 269 | 270 | 而我们本次代码中,采用的是: 271 | 272 | 273 | 274 | 275 | 这里面的 {...bindUsename} {...bindPassword} 其实相当于把 bindUsename 和 bindPassword 进行了解构,就好像直接写在这里似的。 276 | 277 | 如果中有非常多相同的属性,那么把这些相同属性提炼到 useInput 的 bind 中,这样可以简化组件里的代码。 278 | 279 | 应用场景小总结: 280 | 1、在自定义hook中,将组件需要的多项属性合并成一个对象,供组件属性解构使用,会简化组件代码,提高代码复用率。 281 | 282 | 相信通过上面3个示例,对自定义hook的返回值不同形式的演示,举一反三,会帮助你灵活的编写自定义hook。 283 | 284 | --- 285 | 286 | 至此,关于自定义hook已经讲完。 287 | 288 | 我们对之前所有学过的hook进行一次小总结。 289 | 290 | 欢迎进入下一章节:[React Hook 总结](https://github.com/puxiao/react-hook-tutorial/blob/master/17%20React%20Hook%20%E6%80%BB%E7%BB%93.md) 291 | -------------------------------------------------------------------------------- /10 useCallback基础用法.md: -------------------------------------------------------------------------------- 1 | # 10 useCallback基础用法 2 | 3 | ## useCallback概念解释 4 | 我们第五个要学习的Hook(钩子函数)是useCallback,他的作用是“勾住”组件属性中某些处理函数,创建这些函数对应在react原型链上的变量引用。useCallback第2个参数是处理函数中的依赖变量,只有当依赖变量发生改变时才会重新修改并创建新的一份处理函数。 5 | 6 | ##### react原型链? 7 | 我对react原型链也不太懂,你可以简单得把 react原型链 理解成 “react定义的一块内存”。我们使用某些 hook 定义的“变量或函数”都存放在这块内存里。这块内存里保存的变量或函数,并不会因为组件重新渲染而消失。 8 | 1、当我们需要使用时可以“对象的引用名”从该内存里获取,例如useContext 9 | 2、当希望更改某些变量时,可以通过特定的函数来修改该内存中变量的值,例如useState中的setXxxx() 10 | 3、当某些函数依赖变量发生改变时,react可以重新生成、并修改该内存中对应的函数,例如useReducer、useCallback 11 | 12 | > 此处更新与2020年10月13日 13 | > 今天学习了一下 JS 原型链:每一个对象或者说由 function 创建的对象,他们都有一个属性 `__proto__`,该属性值为创建该对象的构造函数的原型对象,又称 隐式原型,而这一层的隐式原型也有 `__proto__` 属性,即 `__proto__.__proto__` 属性值为 Object.prototype,还可以继续再往下深入 `__proto__.__proto__.__proto__`为了避免死循环,最终到此,即 `Object.prototype.__proto__` 为 null。作为构造函数对象,有属性 prototype,属性值为该函数的显示原型对象。constructor 则表示原型对象的构造函数本身。 14 | > 15 | > ``` 16 | > const arr = [1, 2, 3] 17 | > console.log(arr.__proto__ === Array.prototype) // true 18 | > console.log(arr.__proto__.__proto__ === Object.prototype) // true 19 | > console.log(Object.prototype.__proto__ === null) // true 20 | > 21 | > function MyFun() { this.name = 'puxiao' } 22 | > const myFun = new MyFun() 23 | > console.log(myFun.__proto__ === MyFun.prototype) // true 24 | > console.log(MyFun.__proto__ === Function.prototype) // true 25 | > console.log(myFun.__proto__.__proto__ === Object.prototype) // true 26 | > console.log(myFun.__proto__.__proto__.__proto__) // null 27 | > console.log(Object.prototype.__proto__) // null 28 | > ``` 29 | 30 | > 要想更加容易理解上面代码,就需要明白,所谓 object 是指 { },而 Object 其实是 JS 内置的对象函数。同理 所谓 array 是指 [],而 Array 其实是 JS 内置的 数组函数 31 | 32 | > 正是 JS 中 `__prototype__(隐式原型)` `prototype(显式原型)` `constructor(原型对象所在的构造函数本身)` 这 3个概念,最终组合成了 庞大的 JS 功能。我们平时定义的任何 类、对象、函数 都出在这种 链条 中,以及对 这个链条中某个环节属性功能的扩展,这种组织形式就叫 JS 原型链。 33 | 34 | > JS 原型还有一个原则就是可以无限得去扩展自身属性,当前级别的原型扩展属性之后,下层级别的对象自动就拥有该属性。 35 | > 36 | > 那么我们可以大概推理出来,React就是巧妙利用了这种 JS 原型链的原则,将底层模块需要用到的处理函数提升到更高(或者说更加原始)的级别中。这样即使底层中发生了变化,但是他依然拥有高层中定义好的函数引用。 37 | 38 | 请重点留意“修改”这个词,因为“修改”牵扯到react最为隐秘却极其重要的一层概念。 39 | “修改”有3种情况: 40 | 1、用完全不一样的新值去替换之前的旧值 ——> 这会触发react重新渲染 ——> 例如{age:34}去替换{age:18} 41 | 2、用和旧值看似一模一样的新值去替换之前的旧值 ——> 这依然会触发react重新渲染,因为react底层对新旧值做对比时使用的是 Object.is判断,字面上看似一模一样没有用,react依然会认为这是2个对象,依然会触发react重新渲染 ——> 例如{age:18}去替换{age:18} 42 | 3、用旧值的引用去替换旧值 ——> 这次就不会触发重新渲染 ——> 例如let obj={age:18}; let obj2=obj,用obj2去替换obj 43 | 44 | 为了提高react性能,就需要用旧值的引用去替换旧值,从而阻止本次无谓的渲染。 45 | 46 | 问题的关键就在于“如何获取旧值的引用”? 47 | 答:对于函数来说可以使用useCallback。 48 | 49 | 在本章或以后的章节中,我依然会使用 react原型链 这个词,你都按照我刚才说的“react定义的一块内存”概念去理解就好了。 50 | 51 | 懵圈了没?我已经尽量总结得让你容易理解了,如果你似懂非懂没有关系,本文后面会通过示例代码会让你明白如何使用。 52 | 53 | 让我们先忘掉useCallback,先来学习一下以下2个知识点。 54 | 55 | ##### 第1个知识点:React.memo() 的用法 56 | 首先我们知道,默认情况下如果父组件重新渲染,那么该父组件下的所有子组件都会随着父级的重新渲染而重新渲染。 57 | 1、无论子组件是类组件或是函数组件。 58 | 2、无论子组件在本次渲染过程中,子组件是否有任何相关的数据变化。 59 | 60 | 举例,假设某父组件中有3个子组件:子组件A、子组件B、子组件C。若因为子组件A发生了某些操作,引发父组件重新渲染,这时即使子组件B和子组件C没有任何需要更改的地方,但是默认他们两个也会重新被渲染一次。 61 | 62 | 为了减少这个不必要的重新渲染,如果是类组件,可以在组件shouldComponentUpdate(准备要开始更新前)生命周期函数中,通过比较props和state中前后两次的值,如果完全相等则跳过本次渲染,改为直接使用上一次渲染结果,以此提高性能提升。 63 | 64 | 伪代码如下: 65 | 66 | shouldComponentUpdate(nextProps,nextStates){ 67 | //判断xxx值是否相同,如果相同则不进行重新渲染 68 | return (nextProps.xxx !== this.props.xxx); //注意是 !== 而不是 != 69 | } 70 | 71 | 为了简化我们这一步操作,可以将类组件由默认继承自React.Component改为React.PureComponent。React.PureComponent默认会帮我们完成上面的浅层对比,以跳过本次重新渲染。 72 | 73 | 请注意:React.PureComponent会对props上所有可枚举属性做一遍浅层对比。而不像 shouldComponentUpdate中可以有针对性的只对某属性做对比。 74 | 75 | 上面讲的都是类组件,与之对应的是React.memo(),这个是针对函数组件的,作用和React.PureComponent完全相同。 76 | 77 | React.memo()的使用方法很简单,就是把要导出的函数组件包裹在React.memo中即可。 78 | 79 | 伪代码如下: 80 | 81 | import React from 'react' 82 | function Xxxx() { 83 | return
xx
; 84 | } 85 | export default React.memo(Xxxx); //使用React.memo包裹住要导出的函数组件 86 | 87 | 请记住以下2点: 88 | 1、React.memo()只会帮我们做浅层对比,例如props.name='puxiao'或props.list=[1,2,3],如果是props中包含复杂的数据结构,例如props.obj.list=[{age:34}],那么有可能达不到你的预期,因为不会做到深层次对比。 89 | 2、使用React.memo仅仅是让该函数组件具备了可以跳过本次渲染的基础,若组件在使用的时候属性值中有某些处理函数,那么还需要配合useCallback才可以做到跳过本次重新渲染。 90 | 91 | 呵,话题又回到useCallback上面了。 92 | 93 | 94 | ##### 第2个知识点:=== 等比运算 95 | 96 | 在原生JS中,你认为 97 | 1、{}==={} 为true还是false? 98 | 2、{a:2}==={a:2} 为true还是false? 99 | 100 | 这是一道很简单却很容易迷惑人的题目,若你对原生JS中 === 等比运算不够深入了解,你很容易会认为结果是true。 101 | 102 | 如果你轻松回答出来:以上均为false,那么恭喜你是个明白人。 103 | 如果你疑惑了一下或者你的答案是true,那么你可以自己去JS里测试一下看结果是什么。 104 | 105 | 答案是2者均是false。 106 | 107 | 以{}==={}为例,虽然从字面上 === 左右两侧完全相同的,但是实际上在JS中 左右两侧分别为独立的{}对象,各自占有各自的内存空间,因此他们对比的结果是false。 108 | 109 | 相反,看下面的代码: 110 | 111 | let obj = {}; 112 | let obj2 = obj; 113 | obj2.name='react'; 114 | console.log(obj===obj2); //true 115 | 116 | 上面输出结果为true,为何obj===obj2为true? 因为 obj和obj2都是对同一个对象的引用,所以对比结果为true,因为他们最终指向同一个对象。 117 | 118 | 还记得本文开头对于useCallback概念解释中的那段文字吗?useCallback的作用是“勾住”组件属性中某些处理函数,创建这些函数对应在react原型链上的变量引用。 119 | 120 | 呵,话题又回到useCallback上面了。 121 | 122 | 划重点:记住“useCallback”和“原型链上处理函数的引用”这两个关键词,基本上你就对useCallback的原理理解一大半了。 123 | 124 | 让我们回到useCallback基础学习中。 125 | 126 | 127 | ## useCallback是来解决什么问题的? 128 | 答:useCallback是通过获取函数在react原型链上的引用,当即将重新渲染时,用旧值的引用去替换旧值,配合React.memo,达到“阻止组件不必要的重新渲染”。 129 | 130 | 详细解释: 131 | useCallback可以将组件的某些处理函数挂载到react底层原型链上,并返回该处理函数的引用,当组件每次即将要重新渲染时,确保props中该处理函数为同一函数(因为是同一对象引用,所以===运算结果一定为true),跳过本次无意义的重新渲染,达到提高组件性能的目的。当然前提是该组件在导出时使用了React.memo()。 132 | 133 | 补充说明: 134 | 假设某组件使用到了myfun这个处理函数,回忆一下前面提到的JS中===运算规则,考虑一下。 135 | 136 | 默认不使用useCallback,其实组件执行了以下伪代码: 137 | 138 | let obj = {}; //上一次渲染时创建的props 139 | obj.myfun={xxx}; //props中的myfun属性值,实为独立创建的{xxx} 140 | 141 | let obj2 = {}; //本次渲染时创建的props 142 | obj2.myfun={xxx}; //props中的myfun属性值,实为独立创建的{xxx} 143 | 144 | if(obj.myfun === obj2.myfun){ 145 | //跳过本次重新渲染,改为使用上一次渲染结果即可 146 | } 147 | 148 | 由于obj.myfun 和 obj2.myfun 为分别独立创建的函数{xxx},所以对比结果为false,也就意味着无法跳过本次重新渲染,尽管函数{xxx}字面相同。 149 | 150 | 151 | 相反,如果使用useCallback,其实组件执行了以下伪代码: 152 | 153 | let myfun = {xxx}; //独立定义处理函数myfun 154 | 155 | let obj = {}; //上一次渲染时创建的props 156 | obj.myfun = myfun; //props中的myfun属性值,实为myfun的引用 157 | 158 | let obj2 = {}; //本次渲染时创建的props 159 | obj2.myfun = myfun; //props中的myfun属性值,实为myfun的引用 160 | 161 | if(obj.myfun === obj2.myfun){ 162 | //跳过本次重新渲染,改为使用上一次渲染结果即可 163 | } 164 | 165 | 此时 obj.myfun 和 obj2.myfun 均为myfun的引用,因此该对比结果为true,也就意味着可以顺利跳过本次渲染,达到提高组件性能的目的。 166 | 167 | 以上是代码仅仅是为了示意默认子组件为什么会被迫重新渲染,以及useCallback作用机理。 168 | 169 | 只有理解了这个机理,才会明白何时使用useCallback。 切记不要滥用useCallback。 170 | 171 | 多说一句:你是否觉得React Hook 很绕? 对,这就是Hook学习起来难度大的一些原因,但当你充分理解React的编程哲学思想后,用起来会如鱼得水。加油! 172 | 173 | 174 | ## useCallback函数源码: 175 | 回到useCallback的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 176 | 177 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 178 | export function useCallback( 179 | callback: T, 180 | deps: Array | void | null, 181 | ): T { 182 | const dispatcher = resolveDispatcher(); 183 | return dispatcher.useCallback(callback, deps); 184 | } 185 | 186 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^ 187 | 不过请注意第2个参数,deps为该函数依赖的数据变量,值为Array 或 void 或 null。 意味着如果该函数没有依赖的情况下,可以传入空数组[]或void或null。个人建议是传入空数组。 188 | 189 | 补充一点TypeScript知识(因为我最近刚学了TypeScript): 190 | 像 (callback:T):T 这种类型定义称为“泛型”,里面 T 的含义为“一模一样的同类型”。 191 | 举例: 192 | 1、若T为function,即参数callback类型为function,那么函数返回值也为function。 193 | 2、若T为object,即参数callback类型为object,那么函数返回值也为object。 194 | 195 | 196 | ## useCallback基本用法 197 | 198 | useCallback(callback,deps)函数通常传入2个参数,第1个参数为我们定义的一个“处理函数”,通常为一个箭头函数。第2个参数为该处理函数中存在的依赖变量,请注意凡是处理函数中有的数据变量都需要放入deps中。如果处理函数没有任何依赖变量,可以传入一个空数组[]。 199 | 200 | 特别强调一下:useCallback中的第2个依赖变量数组和useEffect中第2个依赖变量数组,作用完全不相同。 201 | useEffect中第2个依赖变量数组是真正起作用的,是具有关键性质的。而useCallback中第2个依赖变量数组目前作用来说仅仅是起到一个辅助作用。 202 | 203 | 仅仅是辅助?辅助什么了?甚至你还可能会有一个疑问,既然处理函数中所有的依赖变量都需要做为第2个参数的内容,为啥React不智能一些,让我们不传第2个参数,省略掉这一步? 204 | 205 | 在React官方文档中,针对第2个参数有以下这段话: 206 | 207 | > 注意:依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。 208 | 209 | 自己体会吧,你品,你细品。 210 | 211 | 212 | ##### 代码形式: 213 | 214 | import Button from './button'; //引入我们自定义的一个组件 227 | 228 | 229 | ##### 拆解说明: 230 | 231 | 1、具体讲解已在上面示例代码中做了多项注释,此处不再重复; 232 | 233 | 234 | #### 'age'补充说明 235 | 1、上述代码示例中,age为该组件通过useState创建的内部变量,事实上也可以是父组件通过属性传值的props.xx中的变量。 236 | 2、只要依赖变量不发生变化,那么重新渲染时就可以一直使用之前创建的那个函数,达到阻止本次渲染,提升性能的目的。但是如果依赖变量发生变化,那么下次重新渲染时根据变量重新创建一份处理函数并替换React底层原型链上原有的处理函数。 237 | 238 | #### 'clickHandler'补充说明 239 | 再次强调,clickHandler实际上是真正的处理函数在React底层原型链上的引用。 240 | 241 | #### ''补充说明 242 | 为我们自定义的一个组件,在上述代码中相当于“子组件”。 243 | 244 | 上面的示例伪代码仅仅是为了演示useCallback的使用方法,实际组件运用中,如果父组件中只有1个子组件,那其实完全没有必要使用useCallback。只有父组件同时有多个子组件时,才有必要去做性能优化,防止某一个子组件引发的重新渲染也导致其他子组件跟着重新渲染。 245 | 246 | ## useCallback使用示例: 247 | 248 | 若我们有一个自定组件,代码如下: 249 | 250 | import React from 'react' 251 | function Button({label,clickHandler}) { 252 | //为了方便我们查看该子组件是否被重新渲染,这里增加一行console.log代码 253 | console.log(`rendering ... ${label}`); 254 | return ; 255 | } 256 | export default React.memo(Button); //使用React.memo()包裹住要导出的组件 257 | 258 | 259 | 现在,我们要实现一个组件,功能如下: 260 | 1、组件内部有2个变量age,salary 261 | 2、有2个自定义组件Button,点击之后分别可以修改age,salary值 262 | 263 | 若我们不使用useCallback,代码示例如下: 264 | 265 | import React,{useState,useCallback,useEffect} from 'react'; 266 | import Button from './button'; 267 | 268 | function Mybutton() { 269 | const [age,setAge] = useState(34); 270 | const [salary,setSalary] = useState(7000); 271 | 272 | useEffect(() => { 273 | document.title = `Hooks - ${Math.floor(Math.random()*100)}`; 274 | }); 275 | 276 | const clickHandler01 = () => { 277 | setAge(age+1); 278 | }; 279 | 280 | const clickHandler02 = () => { 281 | setSalary(salary+1); 282 | }; 283 | 284 | return ( 285 |
286 | {age} - {salary} 287 | 288 | 289 |
290 | ) 291 | } 292 | 293 | 实际运行中你会发现,无论点击哪个按钮,都会收到: 294 | rendering ... Bt01 295 | rendering ... Bt02 296 | 297 | 你只是点击操作了其中一个按钮,另外一个按钮也要跟着重新渲染一次,试想一下如果该组件中有100个子组件都要跟着重新渲染,那真的是性能浪费。 298 | 299 | 我们再看一下如果使用useCallback,代码示例如下: 300 | 301 | import React,{useState,useCallback,useEffect} from 'react'; 302 | import Button from './button'; 303 | 304 | function Mybutton() { 305 | const [age,setAge] = useState(34); 306 | const [salary,setSalary] = useState(7000); 307 | 308 | useEffect(() => { 309 | document.title = `Hooks - ${Math.floor(Math.random()*100)}`; 310 | }); 311 | 312 | //使用useCallback()包裹住原来的处理函数 313 | const clickHandler01 = useCallback(() => { 314 | setAge(age+1); 315 | },[age]); 316 | 317 | //使用useCallback()包裹住原来的处理函数 318 | const clickHandler02 = useCallback(() => { 319 | setSalary(salary+1); 320 | },[salary]); 321 | 322 | return ( 323 |
324 | {age} - {salary} 325 | 326 | 327 |
328 | ) 329 | } 330 | 331 | 修改后的代码,实际运行就会发现,当点击某个按钮时,仅仅是当前按钮重新做了一次渲染,另外一个按钮则没有重新渲染,而是直接使用上一次渲染结果。 332 | 333 | 使用useCallback减少子组件没有必要的渲染目的达成。 334 | 335 | useCallback用法很简单,就是包裹住原本的处理函数。关键点在于你要理解useCallback背后的机理,才能知道在什么情况下可以使用useCallback。否则很容易滥用 useCallback,反而造成性能的浪费。 336 | 337 | 338 | ## 思考题 339 | 340 | 假设上面示例代码中,做以下修改:每个按钮上新增一个属性:random={Math.floor(Math.random()*100)} 341 | 342 | 343 | 344 | 修改为 345 | 346 | 347 | 348 | 那么请问,此时我们针对性能优化而使用的useCallback还有意义吗? 349 | 350 | 答:没有任何意义,虽然我们使用useCallback保证了每次clickHandler是相同的,可是 random 的值每次却是随机不一样的,尽管子组件并没有使用到 random 这个值,但是它的加入造成了 props 每次都不一样(其实是 props.random 不一样),结果就是子组件每一次都会被重新渲染。所以此时useCallback已经失去了存在的意义。 351 | 352 | --- 353 | 354 | 至此,关于useCallback基础用法已经讲完,没有高级用法,直接进入下一个Hook。 355 | 356 | 欢迎进入下一章节:[useMemo基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/11%20useMemo%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 357 | -------------------------------------------------------------------------------- /12 useRef基础用法.md: -------------------------------------------------------------------------------- 1 | # 12 useRef基础用法 2 | 3 | ## useRef概念解释 4 | 5 | 我们第七个要学习的Hook(钩子函数)是useRef,他的作用是“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象,并返回该对象的引用。该引用在组件整个生命周期中都固定不变,该引用并不会随着组件重新渲染而失效。 6 | 7 | 上面这段话,就算你认真读几遍,估计也是一头雾水,到底说的是啥? 8 | 我也实在想不出其他更加通俗的语言来描述useRef,不过经过下面的详细分解描述,相信能帮到你来理解useRef。 9 | 10 | ##### “某些组件挂载完成或重新渲染完成后才拥有的某些对象”: 11 | 12 | 这句话中的“某些对象”主要分为3种:JSX组件转换后对应的真实DOM对象、在useEffect中创建的变量、子组件内自定义的函数(方法)。 13 | 14 | **第1:JSX组件转换后对应的真实DOM对象**: 15 | 举例:假设在JSX中,有一个输入框,这个标签最终将编译转换成真正的html标签中的。 16 | 你应该知道以下几点: 17 | 1、JSX中小写开头的组件看似和原生html标签相似,但是并不是真的原生标签,依然是react内置组件。 18 | 2、什么时候转换? 虚拟DOM转化为真实DOM 19 | 3、什么时候可访问?组件挂载完成或重新渲染完成后 20 | 21 | 对于上面举例中的那个转换后的 真实DOM,只有组件挂载完成或重新渲染完成后才可以访问,它就就属于“某些组件挂载完成或重新渲染完成后才拥有的某些对象”。 22 | 23 | 特别强调:useRef只适合“勾住”小写开头的类似原生标签的组件。如果是自定义的react组件(自定义的组件必须大写字母开头),那么是无法使用useRef的。 24 | 25 | 思考:如何获取这个 真实DOM呢? 26 | 答:用useRef。 27 | 28 | 29 | **第2:在useEffect中创建的变量**: 30 | 举例,请看以下代码: 31 | 32 | useEffect(() => { 33 | let timer = setInterval(() => { 34 | setCount(prevData => prevData +1); 35 | }, 1000); 36 | return () => { 37 | clearInterval(timer); 38 | } 39 | },[]); 40 | 41 | 上述代码中,请注意这个timer是在useEffect中才定义的。 42 | 43 | 思考:useEffect 以外的地方,该如何获取这个 timer 的引用? 44 | 答:用useRef 45 | 46 | 47 | **第3:子组件内自定义的函数(方法)** 48 | 由于需要结合useImperativeHandle才可以实现,而useImperativeHandle目前还未学习,所以本章中不讨论这个怎么实现。 49 | 本章只讲前2中应用场景。 50 | 51 | 52 | 53 | ##### “并返回该对象的引用”: 54 | 55 | 上面的前2种情况,都提到用useRef来获取对象的引用。具体如何获取,稍后在useRef用法中会有演示。 56 | 57 | 58 | ##### “该引用在组件整个生命周期中都固定不变”: 59 | 60 | 假设通过useRef获得了该对象的引用,那么当react组件重新渲染后,如何保证该引用不丢失? 61 | 答:react在底层帮我们做了这个工作,我们只需要相信之前的引用可以继续找到目标对象即可。 62 | 63 | 请注意:React.createRef()也有useRef相似效果,但是React.createRef无法全部适用上面提到的3种情况。 64 | 65 | 让我们回到useRef基础学习中。 66 | 67 | 68 | ## useRef是来解决什么问题的? 69 | 70 | 答:useRef可以“获取某些组件挂载完成或重新渲染完成后才拥有的某些对象”的引用,且保证该引用在组件整个生命周期内固定不变,都能准确找到我们要找的对象。 71 | 具体已经在useRef中做了详细阐述,这里不再重复。 72 | 73 | 补充说明: 74 | 1、useRef是针对函数组件的,如果是类组件则使用React.createRef()。 75 | 2、React.createRef()也可以在函数组件中使用。 76 | 只不过React.createRef创建的引用不能保证每次重新渲染后引用固定不变。如果你只是使用React.createRef“勾住”JSX组件转换后对应的真实DOM对象是没问题的,但是如果想“勾住”在useEffect中创建的变量,那是做不到的。 77 | 78 | 2者都想可以“勾住”,只能使用useRef。 79 | 80 | 81 | ## 注意注意 82 | 83 | 在后面useImperativeHandle的学习中,你会知道useRef还可以 “勾住并调用” 子组件内定义的函数(方法)。 84 | 85 | 86 | 87 |
88 | 89 | > 以下内容更新于 2022.04.06 90 | 91 | ## 特别注意:修改 .current 的值并不会触发组件重新渲染 92 | 93 | 在本文开头介绍 useRef 时用了这句话 “useRef 是“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象,并返回该对象的引用。” 94 | 95 | 也就是说 **先有了 组件渲染,之后才更新了 useRef 中 .current 的值。** 96 | 97 | > 也就是说 useRef 变量的 current 的值实际上是 组件渲染 后的一个副产品。 98 | 99 | **这句话暗含了另外一层含义:主动更新 useRef 变量的 .current 的值并不会触发组件重新渲染。** 100 | 101 | 例如下面这个示例: 102 | 103 | ``` 104 | import { useRef } from "react"; 105 | 106 | export default function MyButton() { 107 | const countRef = useRef(0) 108 | 109 | const handleClick = () => { 110 | countRef.current = countRef.current + 1 111 | }; 112 | 113 | return ; 114 | } 115 | ``` 116 | 117 | 实际运行就会发现,在点击事件中我们修改了 countRef.current 的值,尽管该值确实发生了变化,可是并不会触发组件的重新渲染。 118 | 119 | > 使用 useState() 产生的变量值发生变化后,是会触发组件重新渲染的。 120 | 121 | 122 | 123 |
124 | 125 | > 以上内容更新于 2022.04.06 126 | 127 | 128 | 129 |
130 | 131 | 132 | ## useRef函数源码: 133 | 134 | 回到useRef的学习中,首先看一下React源码中的[ReactHooks.js](https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js)。 135 | 136 | //备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难 137 | export function useRef(initialValue: T): {|current: T|} { 138 | const dispatcher = resolveDispatcher(); 139 | return dispatcher.useRef(initialValue); 140 | } 141 | 142 | 上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^ 143 | 144 | 145 | ## useRef基本用法 146 | 147 | useRef(initialValue)函数只有1个可选参数,该参数为默认“勾住”的对象。绝大多数实际的情况是,默认“勾住”的对象在JSX未编译前(组件挂载或重新渲染后)根本不存在,所以更多时候都会传一个 null 作为默认值。如果不传任何参数,那么react默认将参数设置为undefined。 148 | 149 | 就目前本人所理解的,日常使用过程中useRef(null)和useRef() 实际上是没有什么区别的。 150 | 151 | --- 152 | 153 | 以下更新于 2020.12.10 154 | 155 | 156 | 157 | **补充一下 React + TypeScript 知识点:** 158 | 159 | useRef(null) 和 useRef() 在 React + TypeScript 项目中还是有差别的。 160 | 161 | 假设我们要勾住一个 DOM元素,那么: 162 | 163 | ``` 164 | const canvasRef1 = useRef(null) 165 | const canvasRef2 = useRef() 166 | ``` 167 | 168 | 上面代码中: 169 | 170 | 1. canvasRef1.current 的类型为:HTMLCanvasElement | null 171 | 2. canvasRef2.current 的类型为:HTMLCanvasElement | null | undefined 172 | 173 | 174 | 175 | 以上更新于 2020.12.10 176 | 177 | --- 178 | 179 | 180 | 181 | 182 | 第2遍强调:本文提到的组件,默认都是指小写开头的类似原生标签的组件,不可以是自定义组件。 183 | 184 | 接下来具体说说useRef关联对象的2种用法: 185 | 1、针对 JSX组件,通过属性 ref={xxxRef} 进行关联。 186 | 2、针对 useEffect中的变量,通过 xxxRef.current 进行关联。 187 | 188 | 189 | ##### 代码形式: 190 | 191 | //先定义一个xxRef引用变量,用于“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象 192 | const xxRef = useRef(null); 193 | 194 | //针对 JSX组件,通过属性 ref={xxxRef} 进行关联 195 | 196 | 197 | //针对 useEffect中的变量,通过 xxxRef.current 进行关联 198 | useEffect(() => { 199 | xxRef.current = xxxxxx; 200 | },[]); 201 | 202 | 203 | ##### 拆解说明: 204 | 205 | 1、具体讲解已在上面示例代码中做了多项注释,此处不再重复; 206 | 207 | #### 'ref'补充说明 208 | 209 | 1、组件的 ref 为特殊属性名,他并不存在组件属性传值的 props 中。 210 | 2、如果给一个组件设定了 ref 属性名,但是对应的值却不是由 useRef 创建的,那么实际运行中会收到react的报错,无法正常渲染。 211 | 212 | #### ''补充说明 213 | 214 | 1、useRef只能针对react中小写开头的类似原生标签的组件,所以这里用的是 而不是 。 215 | 216 | #### 'xxxRef.current'补充说明 217 | 218 | 1、当需要使用“勾住”的对象时,也是通过xxRef.current来获取该对象的。 219 | 220 | 221 | ## useRef使用示例1: 222 | 223 | 若我们有一个组件,该组件只有一个输入框,我们希望当该组件挂载到网页后,自动获得输入焦点。 224 | 225 | 需求分析: 226 | 1、我们可以很轻松使用创建出这个输入框。 227 | 2、需要使用useRef “勾住”这个输入框,当它被挂载到网页后,通过操作原生html的方法,将焦点赋予该输入框上。 228 | 229 | 完整代码如下: 230 | 231 | import React,{useEffect,useRef} from 'react' 232 | 233 | function Component() { 234 | //先定义一个inputRef引用变量,用于“勾住”挂载网页后的输入框 235 | const inputRef = useRef(null); 236 | 237 | useEffect(() => { 238 | //inputRef.current就是挂载到网页后的那个输入框,一个真实DOM,因此可以调用html中的方法focus() 239 | inputRef.current.focus(); 240 | },[]); 241 | 242 | return
243 | {/* 通过 ref 属性将 inputRef与该输入框进行“挂钩” */} 244 | 245 |
246 | } 247 | export default Component 248 | 249 | 注意: 250 | 1、在给组件设置 ref 属性时,只需传入 inputRef,千万不要传入 inputRef.current。 251 | 2、在“勾住”渲染后的真实DOM输入框后,能且只能调用原生html中该标签拥有的方法。 252 | 253 | 254 | 255 | 256 | ## useRef使用示例2: 257 | 258 | 若我们有一个组件,该组件的功能需求如下: 259 | 1、组件中有一个变量count,当该组件挂载到网页后,count每秒自动 +1。 260 | 2、组件中有一个按钮,点击按钮可以停止count自动+1。 261 | 262 | 需求分析: 263 | 1、声明内部变量count用 useState 264 | 2、可以在useEffect 通过setInterval创建一个计时器timer,实现count每秒自动 +1 265 | 3、当组件卸载前,需要通过 clearInterval 将timer清除 266 | 4、按钮点击处理函数中,也要通过 clearInterval 将timer清除 267 | 268 | 假设我们不使用useRef,那该如何实现? 269 | 270 | 为了确保timer可以被useEffect以外地方也能访问,我们通常做法是将timer声明提升到useEffect以外。 271 | 代码如下: 272 | 273 | import React,{useState,useEffect} from 'react' 274 | 275 | function Component() { 276 | const [count,setCount] = useState(0); 277 | const [timer,setTimer] = useState(null); //单独声明定义timer,目的是为了让组件内所有地方都可以访问到timer 278 | 279 | useEffect(() => { 280 | //需要用setTimer()包裹住 setInterval() 281 | setTimer(setInterval(() => { 282 | setCount((prevData) => {return prevData +1}); 283 | }, 1000)); 284 | return () => { 285 | //清除掉timer 286 | clearInterval(timer); 287 | } 288 | },[]); 289 | 290 | const clickHandler = () => { 291 | //清除掉timer 292 | clearInterval(timer); 293 | }; 294 | 295 | return ( 296 |
297 | {count} 298 | 299 |
300 | ) 301 | } 302 | 303 | export default Component 304 | 305 | 306 | 如果使用useRef,该如何实现? 307 | 代码如下: 308 | 309 | import React,{useState,useEffect,useRef} from 'react' 310 | 311 | function Component() { 312 | const [count,setCount] = useState(0); 313 | const timerRef = useRef(null);//先定义一个timerRef引用变量,用于“勾住”useEffect中通过setIntervale创建的计时器 314 | 315 | useEffect(() => { 316 | //将timerRef.current与setIntervale创建的计时器进行“挂钩” 317 | timerRef.current = setInterval(() => { 318 | setCount((prevData) => { return prevData +1}); 319 | }, 1000); 320 | return () => { 321 | //通过timerRef.current,清除掉计时器 322 | clearInterval(timerRef.current); 323 | } 324 | },[]); 325 | 326 | const clickHandler = () => { 327 | //通过timerRef.current,清除掉计时器 328 | clearInterval(timerRef.current); 329 | }; 330 | 331 | return ( 332 |
333 | {count} 334 | 335 |
336 | ) 337 | } 338 | 339 | export default Component 340 | 341 | 342 | 343 | **两种实现方式对比:** 344 | 345 | 1、两种实现方式最主要的差异地方在于 如何创建组件内对计时器的引用。 346 | 2、两种创建引用的方式,分别是:用useState创建的timer、用useRef创建的timerRef 347 | 3、在使用setInterval时,相对来说timerRef.current更加好用简单,结构清晰,不需要像 setTimer那样需要再多1层包裹。 348 | 4、timer更像是一种react对计时器的映射,而timerRef直接就是真实DOM中计时器的引用,timerRef能够调用更多的原生html中的JS方法和属性。 349 | 350 | 351 | **结论:** 352 | 1、如果需要对渲染后的DOM节点进行操作,必须使用useRef。 353 | 2、如果需要对渲染后才会存在的变量对象进行某些操作,建议使用useRef。 354 | 355 | 第3遍强调:useRef只适合“勾住”小写开头的类似原生标签的组件。如果是自定义的react组件(自定义的组件必须大写字母开头),那么是无法使用useRef的。 356 | 357 | 358 | 359 |
360 | 361 | > 以下内容更新于 2022.05.20 362 | 363 | ## useRef使用示例3:父组件调用子组件中的函数 364 | 365 | **首先特别强调:除非情况非常特殊,否则一般情况下都不要采用 父组件调用子组件的函数 这种策略。** 366 | 367 | 368 | 369 |
370 | 371 | **使用 useRef 实现父组件调用子组件中的函数 实现思路:** 372 | 373 | 1. 父组件中通过 useRef 定义一个钩子变量,例如 childFunRef 374 | 375 | 2. 父组件通过参数配置,将 childFunRef 传递给子组件 376 | 377 | 3. 子组件在自己的 useEffect() 中定义一个函数,例如 doSomting() 378 | 379 | > 划重点:一定要在 useEffect() 中定义 doSomting(),不能直接在子组件内部定义。 380 | > 381 | > 因为如果 doSomting() 定义在子组件内部,那么就会造成每一次组件刷新都会重新生成一份 doSomthing() 382 | 383 | 4. 然后将 doSomting() 赋值到 childFunRef.current 中 384 | 385 | 5. 这样,当父组件想调用子组件中的 doSomting() 时,可执行 childFunRef.current.doSomting() 386 | 387 | 388 | 389 |
390 | 391 | 具体示例代码: 392 | 393 | **ParentComponent** 394 | 395 | ``` 396 | import { useRef } from "react"; 397 | import ChildComponent from "./child"; 398 | 399 | const ParentComponent = () => { 400 | const childFunRef = useRef(); 401 | const handleOnClick = () => { 402 | if (childFunRef.current) { 403 | childFunRef.current.doSomething(); 404 | } 405 | }; 406 | return ( 407 |
408 | 409 | 410 |
411 | ); 412 | }; 413 | 414 | export default ParentComponent; 415 | ``` 416 | 417 | 418 | 419 |
420 | 421 | **ChildComponent** 422 | 423 | ``` 424 | import { useEffect, useState } from "react"; 425 | 426 | const ChildComponent = ({ funRef }) => { 427 | const [num, setNum] = useState(0); 428 | useEffect(() => { 429 | const doSomething = () => { 430 | setNum(Math.floor(Math.random() * 100)); 431 | }; 432 | funRef.current = { doSomething }; //在子组件中修改父组件中定义的childFunRef的值 433 | }, [funRef]); 434 | return
{num}
; 435 | }; 436 | 437 | export default ChildComponent; 438 | ``` 439 | 440 | 441 | 442 |
443 | 444 | **特别说明:** 445 | 446 | 1. 下一章要讲解的 useImperativeHandle 也是用来实现 父组件调用子组件内定义的函数的。 447 | 448 | 2. 再次强调,如非必要,真的不要使用 父组件调用子组件内函数 这种策略。 449 | 450 | > 最近我遇到了一个需求,子组件是一个第三方写好的轮播图,父组件需要调用这个轮播图的 next() 的函数来切换下一张,所以才使用了这种策略。 451 | 452 | 453 | 454 | > 以上内容更新于 2022.05.20 455 | 456 | 457 | 458 |
459 | 460 | --- 461 | 462 | 463 | 464 | > 以下内容更新于2020.11.18 465 | 466 | #### 在 TypeScript 中使用 useRef 创建计时器注意事项: 467 | 468 | 在上面代码示例中,请注意这一行代码: 469 | 470 | ``` 471 | timerRef.current = setInterval(() => { 472 | setCount((prevData) => { return prevData +1}); 473 | }, 1000); 474 | ``` 475 | 476 | 如果是在 TS 语法下,上面的代码会报错误: 477 | 478 | ``` 479 | 不能将类型“Timeout”分配给类型“number”。 480 | ``` 481 | 482 | **Timeout ???** 483 | 484 | 造成这个错误提示的原因是: 485 | 486 | 1. TypeScript 是运行在 Nodejs 环境下的,TS 编译之后的代码是运行在浏览器环境下的。 487 | 2. Nodejs 和浏览器中的 window 他们各自实现了一套自己的 setInterval 488 | 3. 原来代码 timerRef.current = setInterval( ... ) 中 setInterval 会被 TS 认为是 Nodejs 定义的 setInterval,而 Nodejs 中 setInterval 返回的类型就是 NodeJS.Timeout。 489 | 4. 所以,我们需要将上述代码修改为:timerRef.current = window.setInterval( ... ),明确我们调用的是 window.setInterval,而不是 Nodejs 的 setInterval。 490 | 491 | 492 | 493 | **附一个 TS 代码示例:** 494 | 495 | ``` 496 | import React, { useRef, useEffect } from 'react' 497 | 498 | const MyTemp = () => { 499 | const timer = useRef() 500 | 501 | useEffect(() => { 502 | timer.current = window.setInterval(() => { 503 | console.log(0) 504 | }, 1000) 505 | 506 | return () => { 507 | clearInterval(timer.current) 508 | } 509 | }, []) 510 | return ( 511 |
512 | ) 513 | } 514 | 515 | export default MyTemp 516 | ``` 517 | 518 | > 以上内容更新于2020.11.18 519 | 520 | ____ 521 | 522 | 523 | 524 | > 以下内容更新于2020.12.03 525 | 526 | #### 在 TypeScript 中给 useRef.current 赋值的注意事项 527 | 528 | 在 jsx 文件中,以下代码是不会有问题的。 529 | 530 | ``` 531 | const myRef = useRef(null) 532 | ... 533 | myRef.current = xxxx 534 | ``` 535 | 536 | 但是,在我们使用 TypeScript 之后,按照习惯改成以下代码: 537 | 538 | ``` 539 | const myRef = useRef(null) 540 | ... 541 | myRef.current = xxx 542 | ``` 543 | 544 | 此时,会收到 TypeScript 的报错:**无法分配到 "current" ,因为 myRef.current 是只读属性。** 545 | 546 | 547 | 548 | **报错原因:** 549 | 550 | React 的作者并没有规定使用 useRef(null) 之后 myRef.current 就不可以再修改了。 551 | 552 | 但是 TypeScript 的作者认为,若使用 useRef(null) 之后,myRef 就应该交由 React 来托管,外界不应该有权利去修改 myRef.current,因此此时会把 myRef.current 当做只读属性。 553 | 554 | 555 | 556 | **解决方式:** 557 | 558 | 解决方式1:不给 useRef 设置 null 这个默认值 559 | 560 | ``` 561 | const myRef = useRef() 562 | ``` 563 | 564 | 解决方式2:就是将原本的类型定义,修改成以下: 565 | 566 | ``` 567 | const myRef = useRef(null) 568 | 569 | //或者是 570 | 571 | const myRef = useRef() 572 | ``` 573 | 574 | myRef.current 的数据类型,除了 Xxx 之外,再加上 null 或 undefined ,这样 TypeScript 就认为 myRef.current 可能中途会发生修改,因此不会再将其设置为只读属性,此时再去执行 `myRef.current = xxx` 不再会报错。 575 | 576 | 577 | 578 | **验证一下:** 579 | 580 | 我们再去看看上面 2020.11.18 更新的 TypeScript 代码示例中: 581 | 582 | 由于将来需要执行 timer.current = window.setInterval ( ... ),也就是说需要给 timer.current 赋值。 583 | 584 | 所以在定义时就使用以下方式,以确保 timer.current 不会被 TS 认为是只读属性: 585 | 586 | ``` 587 | const timer = useRef() 588 | ``` 589 | 590 | 591 | 592 | > 以上内容更新于2020.12.03 593 | 594 | --- 595 | 596 | 597 | 598 | ## 那如何“勾住”自定义组件中的“小写开头的类似原生标签的组件”? 599 | 600 | 答:使用React.forwardRef()。 601 | 602 | ##### 你是否思考过这个问题:自定义组件到底是什么? 603 | 604 | 首先看一下“小写开头的类似原生标签的组件”,例如,我们很容易理解他是react内置的类似原生DOM的组件,最终都将直接转换成对应的真实DOM。 605 | 606 | 那自定义组件又该如何理解,如何定义呢? 607 | 608 | 假设我们有一个自定义组件,那么有以下几点是可以肯定的: 609 | 1、内部return出去的,可以是小写开头的类似原生标签的组件,也可以是其他自定义组件。 610 | 2、无论嵌套多少次,最底层组件return出去的,一定是小写开头的类似原生标签的组件。 611 | 3、内部一定创建了变量、处理函数等等。 612 | 4、挂载或渲染后的实际网页中,并不会存在这个标签,存在的依然是各种原生html标签。 613 | 614 | 为了简化更加容易理解,暂时姑且先把“小写开头的类似原生标签的组件”直接当做“原生html标签”。 615 | 那么“自定义组件”和“原生html标签”究竟区别在哪里呢? 616 | 617 | 先不回答这个问题,再说另外一个问题:交互式html页面都有哪些构成? 618 | 答:有各种html标签 + JS对象(JS中定义的变量和函数) 619 | 620 | 那我们使用react开发页面,“原生html标签”有了,那还缺什么? 当然是 JS对象(JS中定义的变量和函数)。 621 | 622 | 再回顾一下问题:“自定义组件”和“原生html标签”究竟区别在哪里呢? 623 | 答:“自定义组件”除了拥有“原生html标签”,还拥有JS对象(JS中定义的变量和函数)。 624 | 625 | 再回顾一下开始的疑问:自定义组件到底是什么? 626 | 答:其实根本不存在自定义组件,所谓自定义组件,只不过是react给我们的各种语法糖,react并没有创造另外一门语言,react整体就是原生JS的语法糖。 627 | 628 | JSX语法 + Hook 组合起来,形成一个强大的语法糖,让你编写html标签和JS更加简便而已。 629 | 语法糖的对象就是:原生html标签 + JS对象(JS中定义的变量和函数)。 630 | 631 | 这就解释了为啥自定义组件 return 出去的内容,最外层必须有一个原生html标签。 说白了,无论你怎么定义,折腾这个自定义组件,本质上都要保证这个自定义组件最终都能转换成一段原生html代码。 632 | 633 | 上面这一大段话都是“简单到不能再简单的道理”,但是你只有理解透这一层,理解自定义组件、理解react究竟是什么之后,你会对于学习React各种API和Hook才会更加容易理解和接受。 634 | 635 | 让我们回到 那如何“勾住”自定义组件中的“小写开头的类似原生标签的组件”? 这个问题上来。 636 | 637 | ##### React.forwardRef() 的具体用法 638 | 639 | React.forwardRef()包裹住要输出的组件,且将第2个参数设置为 ref 即可,示例代码: 640 | 641 | 642 | import React from 'react' 643 | 644 | const ChildComponent = React.forwardRef((props,ref) => { 645 | //子组件通过将第2个参数ref 添加到内部真正的“小写开头的类似原生标签的组件”中 646 | return 647 | }); 648 | 649 | /* 上面的子组件直接在父组件内定义了,如果子组件是单独的.js文件,则可以通过 650 | export default React.forwardRef(ChildComponent) 这种形式 */ 651 | 652 | function Forward() { 653 | const ref = React.useRef();//父组件定义一个ref 654 | const clickHandle = () =>{ 655 | console.log(ref.current);//父组件获得渲染后子组件中对应的DOM节点引用 656 | } 657 | return ( 658 |
659 | {/* 父组件通过给子组件添加属性 ref={ref} 将ref作为参数传递给子组件 */} 660 | 661 | 662 |
663 | ) 664 | } 665 | export default Forward; 666 | 667 | --- 668 | 669 | 至此,关于useRef()基础用法、React.forwardRef()已经讲完。 这2个函数的掌握,会对下一个要讲的Hook:useImperativeHandle 非常有用。 670 | 671 | 欢迎进入下一章节:[useImperativeHandle基础用法](https://github.com/puxiao/react-hook-tutorial/blob/master/13%20useImperativeHandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md) 672 | --------------------------------------------------------------------------------