React Native Hook浅析——重头戏useEffect

2024-02-16 03:30

本文主要是介绍React Native Hook浅析——重头戏useEffect,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前情提要《React Native Hook浅析——state处理》
请先忘记所有class组件相关的知识,忘记生命周期回调函数,忘记this,忘记this.state,忘记一层层向下传递的props,然后,开干。

rendering(渲染)

函数组件渲染是由state、props改变引发的,结合上一小节,我们可以知道当state或者props变化时,会调用React的render方法发起渲染,对于函数式组件,我们可以理解成重新执行函数组件的内容(箭头函数里的代码)。
基于此,组件每一次渲染都会有自己的props和state以及事件处理函数,也就是说,对于函数组件,每次渲染时,使用props、state的地方都相当于替换为常量:

const NumberView = (props: Props) => {const [num, setNum] = useState(0)return (<>  //    步骤        | ”看到“的num常量  | ”看到“的props.step常量  // 1、初始化       |     0          |         1// 2、点击Add Step |     0          |         2// 3、点击Add      |     2          |         2<Text>Num : {num}</Text><TouchableOpacity style={styles.button} onPress={() => { setNum(num + props.step) }}><Text style={{ fontSize: 16, color: 'white' }} >Add {props.step}</Text></TouchableOpacity></>);
}export default (props: Props) => {const [step, setStep] = useState(1)return (<SafeAreaView style={styles.root}>//    步骤        | ”看到“的step常量// 1、初始化       |     1// 2、点击Add Step |     2// 3、点击Add      |     2<NumberView step={step} /><TouchableOpacity style={styles.button} onPress={()=>setStep(step+1)}><Text style={{ fontSize: 16, color: 'white' }} >Add Step</Text></TouchableOpacity></SafeAreaView>);
};

在这里插入图片描述
对于事件处理函数,其也会“记住”当次渲染时的state或是props:

// setNum三次+1之后,触发handleClick,再连续触发两次,此后num值为5
const [num, setNum] = useState(0)// 触发时handleClick“看到”的num常量为3
function handleClick(){setTimeout(()=>{// 3秒后,显示仍为3,因为setTimeout执行的函数“看到”的是触发handleClick时的处理函数“常量”,以及为3的num常量console.log(num);},3000)
}

effect(副作用)

定义

class中有生命周期处理函数,如常用的componentDidMount,componentDidUpdate和componentWillUnmount等,componentDidMount与componentDidUpdate会在渲染时同步的执行,componentWillUnmount会在组件卸载时调用,而在心智模型不一样的函数式组件中,思想应转变为React会根据我们当前的props和state同步到DOM,我们关心的是针对props和state等数据流变化时(初始化也是一种变化,相当于props赋值),我们的页面怎样展示,这种数据变化后,页面怎样展示,也即是在React组件中执行过数据获取、订阅或者手动修改过DOM,就是所谓的effect。

useEffect

React使用useEffect处理effect:

useEffect(() => {setTimeout(() => {// 快速点击5次后,输出依次是1、2、3、4、5// 因为每次setTimeout执行的函数“看到”的是,每次点击时当次渲染的事件处理函数“常量”,以及当次的num常量console.log(num);}, 3000)})
const [num, setNum] = useState(0)

useEffect实现基本原理与useState类似(事实上所有hook都类似,利用闭包+单链表),同时useEffect加上了依赖判断与effect清理的功能:

function useEffect(effect: EffectCallback, deps?: DependencyList)useEffect(() => {// 每次执行effect的操作return xxx // effect清理(下文)
},
// 依赖(下文)
[deps])

从上一篇文章中,我们可以看出借助Promise微任务,使用useEffect调度的effect不会阻塞浏览器更新屏幕,传给useEffect的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。

effect清理

某些effect在下次渲染或页面卸载时,需要执行一些清理动作,比如订阅动作、资源清理等,useEffect函数可以使用return返回一个资源清理函数,其会在下次渲染或页面卸载时执行。

useEffect(() => {Observable.subscribe(props.id, handleFun);return () => {Observable.unsubscribe(props.id, handleFun);};
});

上例中,若props.id从1被修改为2,则其effect清理过程如下:
1、React渲染了{id:2}的UI
2、屏幕绘制{id:2}的UI
3、React执行了{id:1}的return里的清理函数清除effect
4、React执行了{id:2}的effect
可以看到,上一次的effect会在重新渲染后被清除,effect跟第一节里的state、props、事件处理函数一样,都能“看到”属于各自渲染时的effect里的清理函数“常量”。

依赖

useEffect第二个参数可以指定这个effect需要“订阅”的依赖,只有指定依赖变更时才会执行effect:

  • 不传第二个参数时,每次渲染都会执行
  • 传[]时,只会在首次渲染时执行
  • deps不为空时,只有deps变更时,才会执行effect,如[param1, param2]
    只有当两次渲染的deps里的任意一个值不相同(通过Object.is对比)时,才会执行effect。

指定依赖项

一般情况下,使用useEffect都不建议使用如下形式:

useEffect(() => {doSomeThing();return clear;
});

这样会导致每次渲染doSomeThing与上一次渲染的clear都会触发,另一种情况则是漏了或者指定错了依赖项:

const [num, setNum] = useState(0)
console.log('------页面render-----', num);useEffect(() => {const id = setInterval(() => {console.log('------设置-----', num);setNum(num + 1);}, 1000);return () => {console.log('------清除-----', num);clearInterval(id);}
}, [])
return <Text>Num : {num}</Text>

打印结果为:
------页面render----- 0 // 渲染{num:0}的UI
------设置----- 0 // 执行{num:0}的effect,设置为{num:1}
------页面render----- 1 // 渲染{num:1}的UI,由于依赖项没有变更([]),不会执行{num:1}的effect,也不会清除{num:0}的effect
------设置----- 0 // 还是{num:0}的effect的定时器
------页面render----- 1 // 为什么会又执行了一次渲染呢?请细看上一章hook简单原理中index!==0的逻辑
------设置----- 0 // 还是{num:0}的effect的定时器
------设置----- 0 // 还是{num:0}的effect的定时器

包含所有依赖

第一种也是最建议的方式,就是是在依赖中包含所有effect中用到的组件内的值,这样同时可以使后续维护时更清晰的明了此effect的依赖项。
上例中useEffect添加依赖[num],打印结果为:
------页面render----- 0
------设置----- 0
------页面render----- 1
------清除----- 0
------设置----- 1
------页面render----- 2
------清除----- 1
------设置----- 2

有时可能useEffect中的函数过长,或者使用了其他函数(下文细述),我们也不确定漏了哪些依赖,那么使用这条Lint规则就可以自动检测useEffect等hooks中,是否遗漏了哪些依赖项。

函数式更新

上一章中我们知道了对于useState来说,React会保证setState在组件的声明周期内保持不变,也就是说,在useEffect中,这些useState的setState或者useReducer的dispatch等可以不需指定依赖,同时,在上一章hook简单原理中,也可以知道setState这类方法提供了函数式更新的选项:

if (typeof newValue === "function") {// 函数式更新newValue = newValue(state[currentIndex]);
}

函数式更新会讲上一次渲染的“常量”提供出来,因此上例还可以改为:

useEffect(() => {const id = setInterval(() => {console.log('------设置-----', num);// 函数式更新setNum(oldNum=>oldNum + 1);}, 1000);return () => {console.log('------清除-----', num);clearInterval(id);}
// 无需依赖num
}, [])

打印与上面的是一样的。但是现在有个问题,定时器每次渲染都会重新启动,上一次的定时器会被清除,频繁这样操作总感觉不对劲,会造成CPU的无谓浪费。

使用useReducer解除依赖

假设上例中我们的num变更还依赖于props里的属性(或者其他state):

useEffect(() => {const id = setInterval(() => {console.log('------设置-----', num);// 函数式更新setNum(oldNum=>oldNum + props.step);}, 1000);return () => {console.log('------清除-----', num);clearInterval(id);}
// X,需要依赖props.step
}, [])

此时lint规则就会报错:需要依赖props.step。加上props.step的依赖固然可以解决问题,但实际业务中,可能会依赖多个其他看似无关的props或state,全加上去的话会显得不够“优雅”,同时,在本例中,props.step变更还会导致重新订阅定时器,这看起来很不符合常理。
对于这种更新依赖于另一个状态的值时,可以使用useReducer去替换,利用“作弊”般的手法,我们可以直接在reducer里面访问最新的props、state,同时可以帮助我们移除不必需的依赖,避免不必要的effect调用:

export default ({step = 1}) => {// 类似于上面的函数式更新const reducer = (state, action)=>{if(action.type = 'tick') {return state + step} else {return state}}const [num, dispatch] = useReducer(reducer,0)useEffect(() => {const id = setInterval(() => {console.log('------设置-----', num);// effect记住的是这个actiondispatch({type:"tick"})}, 1000);return () => {console.log('------清除-----', num);clearInterval(id);}// 这个dispatch的依赖可以去掉}, [dispatch])console.log('------页面render-----', num);return (<><Text>Num : {num}</Text></>);
};

函数与Effect

函数依赖

函数式组件中,函数也是一种依赖

// 等价于const getData = (url = "") => ...
function getData(url = "") {return "Fake Data:" + url
}useEffect(() => {getData("url1")
}, []) // lint报错,这里依赖于getDatauseEffect(() => {getData("url2")
}, []) // lint报错,这里依赖于getData

如果把getData放到useEffect依赖里,那每次渲染都会触发effect。解决方法有两种:

  1. 对于不用依赖于props或state的getData函数,我们可以将其放到函数组件外面,渲染时不在范围内,也就不存在依赖了
  2. 对于使用到了props或state的getData函数,我们可以使用useCallback,将依赖函数转为数据流,提供给effect使用:
// 只会在初次渲染时“初始化”出getData依赖供使用,再次渲染时不会变化,因此不会每次渲染都会触发effect
const getData = useCallback((url) => {return "Fake Data:" + url
}, [])// 有了正确的依赖
useEffect(() => {getData("url1")
}, [getData])// 有了正确的依赖
useEffect(() => {getData("url2")
}, [getData])

函数传递

使用useCallback也可以让我们将依赖传给子组件使用,使子组件可以回调父组件的方法:

...
return <SubComponent getData={getData}/>;
...// SubComponent
const SubComponent = ({getData})=>{let [data, setData] = useState(null);useEffect(() => {getData("subUrl").then(setData);}, [getData]); 
}

但是对于这种跨组件的操作,更建议使用useReducer的dispatch,如果是多层传递,还可以结合使用上一章的useContext,实现轻量级redux实现更解耦的调用。
useCallBack与useMemo(useCallback(fn, deps) 相当于 useMemo(() => fn, deps),用于优化部分高消耗依赖项的初始化)详细用法下一章再整。

竞态

实际开发中,同一请求返回时间不确定的情况时有发生:

function axios(url) {// 模拟网络请求return new Promise(resolve => {setTimeout(() => {resolve("fake data : " + url)// 模拟每次请求的响应时间不一定}, Math.floor(Math.random()*10) * 1000)})
}export default () => {const [data, setData] = useState("none")const [url, setUrl] = useState("none")useEffect(() => {const getData = async () => {const result = await axios(url);setData(result)}getData()}, [url])return (<><Text>{data}</Text><TextInput onChangeText={(text: string) => {setUrl(text)}}></TextInput></>);
};

上例中快速输入多位text,data的最终显示大概率会与text不一致。输入为12时,有可能{text:12}的响应比{text:1}的先回来,那么显示的data就会是1而不是后发起的12。
解决此类竞态问题的关键是如何“取消”掉上一次的操作,如果getData支持异步取消的话,那就是最好的,可以直接在操作前取消旧的异步操作,否则可以使用简单的bool解决:

useEffect(() => {let cancel = falseconst getData = async () => {const result = await axios(url);if (!cancel) {// 在实际作用于设置的时候,检测是否被取消setData(result)}}getData()return () => {cancel = true}
}, [url])

useLayoutEffect

在class组件生命周期的思维模型中,副作用的行为和渲染输出是不同的,UI渲染是被props和state驱动的,并且能确保步调一致,但副作用并不是这样,useEffect的思维模型中,副作用变成了React数据流的一部分。
如果确实需要与class组件生命周期执行时机相同,可以使用useLayoutEffect替换useEffect,其在所有的DOM变更之后同步调用effect,在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。

参考资料

Hook简介
useEffect 完整指南
How to fetch data with React Hooks
React Hooks原理探究

这篇关于React Native Hook浅析——重头戏useEffect的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:https://blog.csdn.net/lk007007sclk1/article/details/124825792
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/713365

相关文章

浅析Java如何保护敏感数据

《浅析Java如何保护敏感数据》在当今数字化时代,数据安全成为了软件开发中至关重要的课题,本文将深入探讨Java安全领域,聚焦于敏感数据保护的策略与实践,感兴趣的小伙伴可以了解下... 目录一、Java 安全的重要性二、敏感数据加密技术(一)对称加密(二)非对称加密三、敏感数据的访问控制(一)基于角色的访问

CSS3 布局样式及其应用举例

《CSS3布局样式及其应用举例》CSS3的布局特性为前端开发者提供了无限可能,无论是Flexbox的一维布局还是Grid的二维布局,它们都能够帮助开发者以更清晰、简洁的方式实现复杂的网页布局,本文给... 目录深入探讨 css3 布局样式及其应用引言一、CSS布局的历史与发展1.1 早期布局的局限性1.2

使用animation.css库快速实现CSS3旋转动画效果

《使用animation.css库快速实现CSS3旋转动画效果》随着Web技术的不断发展,动画效果已经成为了网页设计中不可或缺的一部分,本文将深入探讨animation.css的工作原理,如何使用以及... 目录1. css3动画技术简介2. animation.css库介绍2.1 animation.cs

CSS引入方式和选择符的讲解和运用小结

《CSS引入方式和选择符的讲解和运用小结》CSS即层叠样式表,是一种用于描述网页文档(如HTML或XML)外观和格式的样式表语言,它主要用于将网页内容的呈现(外观)和结构(内容)分离,从而实现... 目录一、前言二、css 是什么三、CSS 引入方式1、行内样式2、内部样式表3、链入外部样式表四、CSS 选

使用雪花算法产生id导致前端精度缺失问题解决方案

《使用雪花算法产生id导致前端精度缺失问题解决方案》雪花算法由Twitter提出,设计目的是生成唯一的、递增的ID,下面:本文主要介绍使用雪花算法产生id导致前端精度缺失问题的解决方案,文中通过代... 目录一、问题根源二、解决方案1. 全局配置Jackson序列化规则2. 实体类必须使用Long封装类3.

Nginx部署React项目时重定向循环问题的解决方案

《Nginx部署React项目时重定向循环问题的解决方案》Nginx在处理React项目请求时出现重定向循环,通常是由于`try_files`配置错误或`root`路径配置不当导致的,本文给大家详细介... 目录问题原因1. try_files 配置错误2. root 路径错误解决方法1. 检查 try_f

在React聊天应用中实现图片上传功能

《在React聊天应用中实现图片上传功能》在现代聊天应用中,除了文字和表情,图片分享也是一个重要的功能,本文将详细介绍如何在基于React的聊天应用中实现图片上传和预览功能,感兴趣的小伙伴跟着小编一起... 目录技术栈实现步骤1. 消息组件改造2. 图片预览组件3. 聊天输入组件改造功能特点使用说明注意事项

一文详解如何在Vue3中封装API请求

《一文详解如何在Vue3中封装API请求》在现代前端开发中,API请求是不可避免的一部分,尤其是与后端交互时,下面我们来看看如何在Vue3项目中封装API请求,让你在实现功能时更加高效吧... 目录为什么要封装API请求1. vue 3项目结构2. 安装axIOS3. 创建API封装模块4. 封装API请求

全解析CSS Grid 的 auto-fill 和 auto-fit 内容自适应

《全解析CSSGrid的auto-fill和auto-fit内容自适应》:本文主要介绍了全解析CSSGrid的auto-fill和auto-fit内容自适应的相关资料,详细内容请阅读本文,希望能对你有所帮助... css  Grid 的 auto-fill 和 auto-fit/* 父元素 */.gri

浅析如何使用xstream实现javaBean与xml互转

《浅析如何使用xstream实现javaBean与xml互转》XStream是一个用于将Java对象与XML之间进行转换的库,它非常简单易用,下面将详细介绍如何使用XStream实现JavaBean与... 目录1. 引入依赖2. 定义 JavaBean3. JavaBean 转 XML4. XML 转 J