本文主要是介绍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。解决方法有两种:
- 对于不用依赖于props或state的getData函数,我们可以将其放到函数组件外面,渲染时不在范围内,也就不存在依赖了
- 对于使用到了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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!