前言

react hook是继16.6的Suspense、lazy、memo后的又一巨大的令人兴奋的特性。然后有各种文章说了hook的优缺点,其中缺点包括:没有直接替代getSnapshotBeforeUpdate、componentDidUpdate生命周期的hook、不能像class组件那样写this、函数太大。这只是表面的现象,只要稍微思考一下,hook其实是无所不能的,我甚至相信未来挑不出hook的毛病来。今天手把手带大家过一遍如何实现class组件特性。

基本用法可见官网,阅读本文需要先了解useStateuseEffectuseRefuseLayoutEffect的使用方法。本文核心hook——useRef,本文也算是一篇useRef的应用文章。当你知道核心是基于useRef的时候,或许已经想到实现办法了,很好,我们心有灵犀 「握个手」

useRef

useRef传入一个参数initValue,并创建一个对象{ current: initValue }给函数组件使用,在整个生命周期中该对象保持不变。所以,听到它名字叫做useRef的时候,很自然就想到它就是用来做元素的ref的:

const divRef = useRef();
return  <div ref={divRef}>;
复制代码

最基本的使用方法,接着想进行dom操作,那就这样玩:

if (divRef.current) {divRef.current.addEventListener(...);
}
复制代码

函数组件的执行,整个函数体所有的必然躲不掉重新执行,那么如果希望有一个不重新走一遍的变量,我们通常会把它放函数组件外面去:

let isMount = false;
function C(){useEffect(() => { isMount= true; return () => { isMount= false; } }, []);return <div />
}
复制代码

这就是一个判断组件有没有挂载到页面的实现方法,如果我们用useRef,显然优雅很多了,而且是不是有点this的感觉

function C(){const mount = useRef({}).current;useEffect(() => { mount.isMount= true; return () => { mount.isMount= false; } }, []);return <div />
}
复制代码

ok,现在假的this要原形毕露了:

export default () => {const _this = useRef({state: { a: 1, b: 0 },}).current;return (<div>a: {_this.state.a} / b : {_this.state.b}</div>)
}
复制代码

state更新相关的逻辑实现

useState就相当于hook版本的setStateconst [state, setState] = useState(initState);,state利用了函数组件重新执行,从闭包读取函数记忆的结果。调用hook的setState,则会更新state的值然后重新执行一遍整个函数组件。此处再次感叹一下,hook真的没什么黑魔法,少一点套路多一点真诚。

比如有一个这样子的组件:

function T(){const [count, setCount] = useState(0);return <span onClick={() => setCount(count + 1)}>{count}</span>
}
复制代码

第一次执行函数组件,最后渲染就相当于:

function T(){const count = 0return <span>{count}</span>
}
复制代码

点击一下,count+1,也就是相当于执行了一个这样子的函数组件:

function T(){const count = 1return <span>{count}</span>
}
复制代码

所以,真没有什么黑魔法,就是读前一个值然后+1展示而已。好了,回到正题,函数组件的更新就是useState,那强制更新呢?如何实现一个forceUpdate?其实也很简单,dispatcher(useState返回值第二个元素)传入一个函数,类似于class组件的setState传入一个函数一样,可以拿到当前的state值:

const useForceUpdate = () => {const forceUpdate = useState(0)[1];return () => forceUpdate(x => x + 1);
}export default () => {const forceUpdate = useForceUpdate(); // 先定义好,后面想用就用// ...return (<div />)
}
复制代码

我们已经知道了如何模拟this和state初始化了,那我们可以实现一个类似class组件的setState了:给ref里面的属性赋值,再forceUpdate。

本文只是希望全部收拢在useRef,然后修改状态的方法纯粹一点,当然可以用useState对着一个个state值进行修改

export default () => {const forceUpdate = useForceUpdate();const _this = useRef({state: { a: 1, b: 0 },setState(f) {console.log(this.state)this.state = {...this.state,...(typeof f === 'function' ? f(this.state) : f) // 两种方法都考虑一下};forceUpdate();},forceUpdate,}).current;return (<div>a: {_this.state.a} / b : {_this.state.b}<button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a传state</button><button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b传函数</button></div>);
}
复制代码

到此,我们已经实现了class组件的thissetStateforceUpdate

didmount、didupdate、willunmount的实现

其实我上一篇文章已经实现过,这里再糅合到ref里面重新实现一遍。还是一样的方法,基于两个useEffect实现三个生命周期:

export default () => {const forceUpdate = useForceUpdate();const isMounted = useRef(); // 挂载标记const _this = useRef({state: { a: 1, b: 0 },setState(f) {console.log(this.state)this.state = {...this.state,...(typeof f === 'function' ? f(this.state) : f) // 两种方法都考虑一下};forceUpdate();},forceUpdate,componentDidMount() {console.log('didmount')},componentDidUpdate() {console.warn('didupdate');},componentWillUnMount() {console.log('unmount')},}).current;useEffect(() => {_this.componentDidMount();return _this.componentWillUnMount;}, [_this]);useEffect(() => {if (!isMounted.current) {isMounted.current = true;} else {_this.componentDidUpdate();}})return (<div>a: {_this.state.a} / b : {_this.state.b}<button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a传state</button><button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b传函数</button></div>)
}
复制代码

记录上一次状态

有人可能也注意到了,上面的componentDidUpdate是没有传入上一次props和state的。是的,getDerivedStateFromProps也要上一个state的。所以我们还需要一个ref存上一个状态:

export default (props) => {const forceUpdate = useForceUpdate();const isMounted = useRef();const magic = useRef({ prevProps: props, prevState: {}, snapshot: null }).current;magic.currentProps = props; // 先把当前父组件传入的props记录一下const _this = useRef({state: { a: 1, b: 0 },setState(f) {console.log(this.state)this.state = {...this.state,...(typeof f === 'function' ? f(this.state) : f)};forceUpdate();},componentDidMount() {console.log('didmount')},getDerivedStateFromProps(newProps, currentState) {// 先放这里,反正等下要实现的},componentDidUpdate(prevProps, prevState, snapshot) {console.warn('didupdate');console.table([{ k: '上一个props', v: JSON.stringify(prevProps) },{ k: 'this.props', v: JSON.stringify(magic.currentProps) },{ k: '上一个state', v: JSON.stringify(prevState) },{ k: 'this.state', v: JSON.stringify(_this.state) },])},componentWillUnMount() {console.log('unmount')}}).current;useEffect(() => {_this.componentDidMount();// 后面都是赋值操作,防止同一个引用对象,实际上应该深拷贝的。这里为了方便,但至少要浅拷magic.prevProps = { ...props };  // 记录当前的,作为上一个props给下一次用magic.prevState = { ..._this.state }; // 同理return _this.componentWillUnMount;}, [_this, magic]);useEffect(() => {if (!isMounted.current) {isMounted.current = true;} else {//  这里就拿到了上一个props、state了,snapshot也先留个空位给他吧_this.componentDidUpdate(magic.prevProps, magic.prevState, magic.snapshot || null);// 拿完就继续重复操作,给下一次用magic.prevProps = { ...props };magic.prevState = { ..._this.state };}})return (<div>props: {props.p}/a: {_this.state.a} / b : {_this.state.b}<button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a传state</button><button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b传函数</button></div>);
}
复制代码

这下,可以去控制台做一些操作state和改变props的操作了,并看下打印的结果

getDerivedStateFromProps

这个函数的原意就是希望props可以作为初始化state或者在渲染之前修改state,那么根据它的意图,很容易就可以实现这个生命周期,我这里getDerivedStateFromProps还可以用假this哦。其实这个生命周期应该是最容易实现和想出来的了:

// 基于前面的组件直接加上这段代码const newState = _this.getDerivedStateFromProps(props, magic.prevState);if (newState) {_this.state = { ..._this.state, ...newState }; // 这里不要再更新组件了,直接改state就收了}
复制代码

getSnapshotBeforeUpdate

到了一个hook不能直接替代的生命周期了。这里再看一下useLayoutEffect和useEffect执行的时机对比:

注意到,下一个useLayoutEffect执行之前,先执行上一个useLayoutEffect的clean up函数,而且都是同步,可以做到近似模拟willupdate或者getSnapshotBeforeUpdate了

// 再增加一段代码useLayoutEffect(() => {return () => {// 上一个props、state也传进来,然后magic.snapshot 前面已经传入了componentDidUpdatemagic.snapshot = _this.getSnapshotBeforeUpdate(magic.prevProps, magic.prevState);}})
复制代码

componentDidCatch

另一个不能用hook直接替代的生命周期,说到错误,这个生命周期也是捕捉函数render执行的时候的错误。那些编译不过的,非函数渲染时候报的错,它无法捕获的哦。基于这个前提,我们还是基于try-catch大法实现一波:

// 对最后的return 修改,这里还可以个性化一下fallback ui呢try {return (<div>props: {props.p}/a: {_this.state.a} / b : {_this.state.b}<button onClick={() => { _this.setState({ a: _this.state.a + 1 }) }}>a传state</button><button onClick={() => { _this.setState(({ b }) => ({ b: b + 2 })) }}>b传函数</button></div>)} catch (e) {_this.componentDidCatch(e)return <div>some err accured</div>;}
复制代码

手把手带你用react hook撸一遍class组件的特性相关推荐

  1. dispatch作用 react_「React系列」手把手带你撸后台系统(Redux与路由鉴权)

    [React系列]手把手带你撸后台系统(Redux与路由鉴权) 来源:https://juejin.im/post/5d9b5ddee51d45781b63b8f7 上一篇我们介绍了系统架构,这一篇将 ...

  2. 手把手带你撸一个校园APP(五):新闻中心模块

    这个项目是很早之前在学校做的,如今再回首.很多代码很是粗糙,逻辑也不尽完善.还望各位看官海涵. 前言 通过上一篇文章的功能设计,我们可以发现新闻通知公告等是APP的最主要功能点.主要是聚合展示学校官网 ...

  3. 手把手带你撸一个校园APP(六):失物招领二手交易模块

    代码经过简单的整理,已经放到Github上了.https://github.com/zhengweichao/Hevttc 回首来看,代码质量一般,里面也有各种逻辑问题,还望各位看官海涵.接下来有时间 ...

  4. React Hook + Typescript,实现高颜值在线记账本

    React 已经是 JavaScript 生态系统中最受欢迎的前端框架之一.尽管人们已经对它赞不绝口,但 React 团队仍然在努力让它变得更好. 在 2018 ReactConf 大会上,React ...

  5. 【笔记-node】《Egg.js框架入门与实战》、《用 React+React Hook+Egg 造轮子 全栈开发旅游电商应用》

    20210226-20210227:<Egg.js框架入门与实战> 课程地址:https://www.imooc.com/learn/1185 第一章 课程导学 01-01 课程介绍 一. ...

  6. 手把手带你写一个JavaScript类型判断小工具

    业务写了很多,依然不是前端大神,我相信这是很多'入坑'前端开发同学的迷茫之处,个人觉得前端职业发展是有路径可寻的,前期写业务是一个积累过程,后期提炼总结,比如编程思想,父子类的原型继承,还是对象之间的 ...

  7. 手把手带你飞Python爬虫+数据清洗新手教程(一)

    本文共有2394字,读完大约需要10分钟. 目录 简介 思考 撸起袖子开始干 1 获取网页源代码 2 在网页源代码里找出所需信息的位置 3 数据清洗 4 完整代码 5 优化后的代码 简介 本文使用An ...

  8. 手把手带你学会Odoo OWL组件开发(5):浅析OWL原理

    [本系列内容直达:] 手把手带你学习Odoo OWL组件开发(1):认识 OWL 手把手带你学会Odoo OWL组件开发(2):OWL的使用 手把手带你学会Odoo OWL组件开发(3):核心内容指南 ...

  9. 手把手带你学会Odoo OWL组件开发(4):OWL组件

    [本系列内容直达:] 手把手带你学习Odoo OWL组件开发(1):认识 OWL 手把手带你学会Odoo OWL组件开发(2):OWL的使用 手把手带你学会Odoo OWL组件开发(3):核心内容指南 ...

  10. 手把手带你学会Odoo OWL组件开发(2):OWL的使用

    [本系列内容直达:] [手把手带你学习Odoo OWL组件开发(1):认识 OWL] [手把手带你学会Odoo OWL组件开发(2):OWL的使用] [手把手带你学会Odoo OWL组件开发(3):核 ...

最新文章

  1. opencv2函数学习之threshold:实现图像阈值化
  2. python中的装饰器有哪些-python中的装饰器详解
  3. 卫星导航系统脆弱性评估与对策
  4. initWithCoder: 与initWithFrame:
  5. Ubuntu平台 Qt 5.x 安装方法
  6. Python3.6学习笔记(四)
  7. 通过投影增强数据模型
  8. 前端学习(554):node实现登录和注册第二部分代码
  9. 现实地形导入UE4全流程
  10. 钟南山:疫情1周或10天左右达到高峰,不会大规模增加了!
  11. 7-10 A-B (20 分)
  12. protobuf使用错误总结
  13. MySQL-快速入门(9)视图
  14. 219.存在重复元素II
  15. 如何下载windows自带的锁屏壁纸
  16. 数学建模与计算机专业的关系,数学建模与计算机的重要性.doc
  17. HTML5中如何实现rpx布局
  18. Dango 之 Xadmin
  19. 找出列表中最大或最小的元素-python3
  20. 获取设备 AirPods、Apple TV、Apple Watch、HomePod、iPad、iPad Air、iPad Pro、iPad mini、iPhone、iPod touch的型号

热门文章

  1. python中求2-1000的完数_C++求2→1000之间的完数。
  2. 变形金刚图纸_变形金刚救援
  3. 衍射受限透镜成像_成像专题 | 基于孔径阵列的数字全息重建 (AIP APL)
  4. redis 验证订单_php+redis消息队列实现抢购功能
  5. python三种基本结构类型_Python入门_浅谈数据结构的4种基本类型
  6. 解决pip install 库 下载速度慢的问题
  7. android u盘加载_如何获取Android系统挂载U盘的路径
  8. 推导pca的降维损失_这应该是最全的PCA原理总结了(上)
  9. 成对抗网络代码全解析, 详细代码解析(TensorFlow, numpy, matplotlib, scipy)
  10. Linux监控之系统性能