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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/713365

相关文章

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可

Vue项目中Element UI组件未注册的问题原因及解决方法

《Vue项目中ElementUI组件未注册的问题原因及解决方法》在Vue项目中使用ElementUI组件库时,开发者可能会遇到一些常见问题,例如组件未正确注册导致的警告或错误,本文将详细探讨这些问题... 目录引言一、问题背景1.1 错误信息分析1.2 问题原因二、解决方法2.1 全局引入 Element

详解如何在React中执行条件渲染

《详解如何在React中执行条件渲染》在现代Web开发中,React作为一种流行的JavaScript库,为开发者提供了一种高效构建用户界面的方式,条件渲染是React中的一个关键概念,本文将深入探讨... 目录引言什么是条件渲染?基础示例使用逻辑与运算符(&&)使用条件语句列表中的条件渲染总结引言在现代

详解Vue如何使用xlsx库导出Excel文件

《详解Vue如何使用xlsx库导出Excel文件》第三方库xlsx提供了强大的功能来处理Excel文件,它可以简化导出Excel文件这个过程,本文将为大家详细介绍一下它的具体使用,需要的小伙伴可以了解... 目录1. 安装依赖2. 创建vue组件3. 解释代码在Vue.js项目中导出Excel文件,使用第三

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

vue解决子组件样式覆盖问题scoped deep

《vue解决子组件样式覆盖问题scopeddeep》文章主要介绍了在Vue项目中处理全局样式和局部样式的方法,包括使用scoped属性和深度选择器(/deep/)来覆盖子组件的样式,作者建议所有组件... 目录前言scoped分析deep分析使用总结所有组件必须加scoped父组件覆盖子组件使用deep前言

VUE动态绑定class类的三种常用方式及适用场景详解

《VUE动态绑定class类的三种常用方式及适用场景详解》文章介绍了在实际开发中动态绑定class的三种常见情况及其解决方案,包括根据不同的返回值渲染不同的class样式、给模块添加基础样式以及根据设... 目录前言1.动态选择class样式(对象添加:情景一)2.动态添加一个class样式(字符串添加:情

React实现原生APP切换效果

《React实现原生APP切换效果》最近需要使用Hybrid的方式开发一个APP,交互和原生APP相似并且需要IM通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常... 目录背景需求概览技术栈实现步骤根据 react-router-dom 文档配置好路由添加过渡动画使用

使用Vue.js报错:ReferenceError: “Vue is not defined“ 的原因与解决方案

《使用Vue.js报错:ReferenceError:“Vueisnotdefined“的原因与解决方案》在前端开发中,ReferenceError:Vueisnotdefined是一个常见... 目录一、错误描述二、错误成因分析三、解决方案1. 检查 vue.js 的引入方式2. 验证 npm 安装3.

vue如何监听对象或者数组某个属性的变化详解

《vue如何监听对象或者数组某个属性的变化详解》这篇文章主要给大家介绍了关于vue如何监听对象或者数组某个属性的变化,在Vue.js中可以通过watch监听属性变化并动态修改其他属性的值,watch通... 目录前言用watch监听深度监听使用计算属性watch和计算属性的区别在vue 3中使用watchE