ReactNative进阶-性能优化、hooks
性能优化
- 图片加载速度优化
1. 网络图片
<Image source={{uri: 'https://......'}}/>
2. 本地图片
本地图片加载又可分为两种,一种是图片资源和JS代码放在一块。通过require来加载
<Image source={require('../images/image.png')}/>
第二种,也就是我们采取的优化加载图片性能的方式
Android下把图片放到 Android/app/main/res/drawable 下,新项目是没有该文件夹。
建议通过Android Studio 打开项目,创建 drawable资源文件夹
IOS 下把图片放到 imageAssets 中
// image.png
<Image source={{uri: 'image' }}/>
okay,接下来看一下一次性加载264张本地图片时两种方式所耗的时间。
从下图中可以看到,从 1秒开始,图片开始加载 ( 靠上的轨道是本地图片加载的第二种方式,轨道靠下的是第一种,通过 require 来加载图片的方式)
图片和JS代码放一块时,从下图可以看到,从开始到全部加载完成,耗时超过 3秒 。而优化后的加载方式,实际上耗时 300毫秒左右就将 264张图片全部加载完成。
结果
加载264张本地图片
第一种: 耗时 0.3秒左右
第二种:耗时 3 秒左右
2. 匿名函数优化
通常,很多点击事件所需要绑定的函数,我们都是直接以匿名函数形式作为参数传给组件。如下示例
<TouchableOpacity onPress={
() => { console.log(" 匿名函数体 ")}
</TouchableOpacity>
那实际上,这样的写法在APP很小的时候完全没有问题。但是如果APP变得功能多了,项目复杂了,这样的点击事件越来越多。如果还继续采用这种匿名写法。组件的每次刷新,匿名函数都会被重新声明,定义,传递,绑定。这是一个不小的性能开销。
优化方式就是将匿名函数拿出来,声明为常量,然后再进行绑定
const tapHandler = () => { console.log(" 匿名函数体 ")};
<TouchableOpacity onPress={ tapHandler }>
</TouchableOpacity>
3. 重复渲染
因为避免重复渲染用到的工具涉及了hooks,因此直接在hooks中进行讲解。
Hooks 和 重复渲染
有时,为了追求开发速度。我们不会过多的思考 state 的使用。如果我们需要更多的 state 变量,我们就会声明更多的state。
关于state,有一点需要注意的是,每当state发生变化。整个 组件 (function / class)的生命周期就会执行一遍。这里只讲函数组件。而函数组件中用来取代 class组件的 mounted 和 unmount 生命周期的hook函数就是 useEffect 也就是说,每当state 发生变化。 函数中每一个 useEffect 都会执行一遍。
function Page(props){
const [state, setstate] = useState(false)
useEffect(() => { TAG1
//获取网络数据
useEffect(() => { TAG2
//执行计算
当 Page 组件挂载时,TAG1, 2 两个副作用都会执行一遍。每当 state 发生变化时, TAG1,2 都会再次执行。
如果我们只想 副作用 在挂载期间执行一次,之后再也不执行,那么我们可以直接给 useEffect 传递第二个参数,一个空数组。
useEffect(() => { TAG1
//获取网络数据
},[])
数组中的元素则是副作用是否可以在下轮state变化时执行的标识。如下
function Page(props){
const [state, setstate] = useState(false);
const [state1, setstate1] = useState(true);
useEffect(() => { TAG1
//获取网络数据
useEffect(() => { TAG2
//初始化
},[])
useEffect(() => { TAG3
//执行计算
},[state1])
组件挂载期间:
TAG1 , TAG2, TAG3 副作用执行一遍
当state 发生改变 :
TAG1 执行
TAG2 因为第二个依赖参数是空数组,所以只会在挂载期间执行一遍。后续再也不会再执行
TAG3 因为依赖的参数是 state1 , 所以,只改变了state, state1没有变化。 TAG3不会执行。
当state1 发生变化:
TAG1 , TAG3 执行
TAG2不执行
至此,我们已经知道如何通过useEffect的第二个参数来控制 ”生命周期“函数的重复执行问题了。下面有一个疑问,state的变化,组件会发生渲染吗?看下面代码
/**
* 子组件 Child
function Child(props){
useEffect(() => {
return (
<Text>child widget</Text>
</View>
* 父组件 Parent
function Parent(props) {
const [name,setName] = useState("cathlina");
useEffect(() => {
return (
<Text>{name}</Text>
<Text>yep</Text>
<Child /> // 这个地方我们使用了子组件
</View>
首页,当APP执行了, TAG1 和 TAG2 哪个副作用会先执行呢?我想大多数人都没去思考过这个。有趣的是,TAG1会先执行。也不难理解,首先是 子组件挂载后,然后再挂载父组件。
回到正题, 当 Parent 组件中的 name 发生变化时,Child 组件会发生什么变化吗?
在上面这个例子中,不,Child不会发生变化。
那么上面那些组件哪些变化了,哪些没有重新渲染呢?
当name发生变化时,整个Parent组件以及子组件 Child 中,只有父组件的
<Text>{name}</Text> 发生了重新渲染。其他的都没有重新渲染。那是不是意味着只要父组件没有通过 props 给子组件传递值,子组件就不会再重新渲染吗?
不是的,父view如果发生渲染,它的子view都会重新渲染。看下面例子
const [w,setW] = useState(10);
useEffect(() => {
console.log("parent run")
setTimeout(() => {
setW(100);
},3000)
return (
<View style={{width: w}}>
<Text>{name}</Text>
<Text>yep</Text>
<Child />
</View>
这里,Child 组件的父view 用到了 state变量,我们在父view的宽度上用到了 state 。 并且生命周期里设了一个定时器,3秒后,state变量 w 发生改变。 最外层的 View 发生了变化。如上所说,父view的所有子view都会重新渲染。也就是说两个 Text 和 Child 都会重新渲染。
至此,我们已经知道了,state的变化,不一定会使得组件重新渲染。只有当view用到了state,那么state的变化就能够使得组价重新渲染。
接下来,我们看看另外两个用以避免重复渲染的hook, useMemo 和 useCallback
这两者非常相似,唯一的区别就是 useMemo 返回的是一个值 , 而 useCallback 返回的是一个函数。事实上,如果我们将 useMemo 返回的值换成 匿名函数,那么 useMemo的作用就和useCallback 一模一样了。
//Memo
const value = useMemo(() => v , []);
//Callback
const callback = useCallback(() => {}, []);
上面 callback 等于 useCallback的第一个参数、这个匿名函数 () =>{}
value等于useMemo的第一个参数匿名函数返回的值 v
我想大家都注意到了第二个参数 ,这个数组 [] , 这个数组也和 上面 useEffect的第二个参数一样。用来判断state变化时,是否要作出反应。
下面看个简单但很经典的示例,还是 Child 和 Parent
function Child(props){
useEffect(() => {
return (
<Text>child widget</Text>
<Text>{props.name}</Text>
</View>
function Parent(props) {
const [name,setName] = useState("cathlina");
const [count,setCount] = useState(0);
useEffect(() => {})
return (
<Text>{name}</Text>
<Child name={name}/>
</View>
注意到 Child 获得了父组件的 state变量 name 。 当我们改变 count 变量时,表面上看Child用到的state 、name 并没有发生变化。 但是实际上, 当count变化时, Child 的生命周期,也就是副作用 TAG1 还是执行了。
如何避免 ?
这时就可以用到上面刚刚说过的 useMemo , 下面看优化过的Child组件的代码
function Child(props){
useEffect(() => {
const memomizedName = useMemo(() => {
return props.name
},[props.name])
return (
<Text>child widget</Text>
<Text>{memomizedName}</Text>
</View>
首先, useMemo 第一个参数返回的值正式我们需要的通过props传过来的name 。 第二个参数,我们将 props.name 放进数组作为依赖参数。那么,只有当 props.name 发生变化时,Child组件才会判定 副作用TAG1应该执行了。
这时我们再改变父组件的 count 变量, Child 的副作用 TAG1 不再执行。没错,通过useMemo,我们使得子组件避免了重复渲染的问题。
最后,再看一个 useCallback 的使用场景。
function Parent(props) {
const [phone,setPhone] = useState("");
const [pwd,setPwd] = useState("");
const phoneHanlder = (value:string) => {
setPhone(value);
const pwdHandler = (value:string) => {
setPwd(value);
useEffect(() => {
console.log("Parent Render")
return (
<View style={styles.body}>
<TextInput value={phone} placeholder="phone" onChangeText={phoneHanlder}/>
<TextInput value={pwd} placeholder="password" onChangeText={pwdHandler}/>
</View>
账号密码输入框,这是一个很经典的场景。很多地方都会用到多文本输入情况。上面是非常常见,不考虑render性能问题的写法。
可以看到,只要有一个输入框的值发生变化,副作用就会执行一遍。那么这时有个问题了。当我们在第一个输入框输入一个数字时,整个Parent 组件run了几遍。生命周期副作用执行了多少次 ?
第二个问题很简单,因为state 只发生了一次变化,因此副作用只执行了一次。 但是Parent这个组件,换句话说, Parent 这个函数执行了几次?
2次 , 为什么是两次 。我们先看下面的例子。
function Parent(props) {
const [phone,setPhone] = useState("");
const [pwd,setPwd] = useState("");
const phoneHanlder = (value:string) => {
setPhone(value);
const pwdHandler = (value:string) => {
setPwd(value);
const x = 12;
let y = 'a';
console.log(x,y); //TAG1
useEffect(() => {
console.log("Parent Render"); //TAG2
y = 'b';
console.log(x,y); //TAG3
setTimeout(() => {setPhone("f")},3000);
......返回组件同上一个Parent
这里声明了一个变量 y , 一个常量 x 。一共3个console.log 。当我们运行APP时,首页函数执行, TAG1 处执行, 打印 x 和 y 的值, 12 'a' 。 接着副作用执行。 打印, "Parent Render" 。然后变量 y 的值改为 'b' , 紧接着 TAG3 处执行,再次打印 x,y的值。控制台输出如下。
12 "a"
Parent Render
12 "b"
接着,定时器 3秒后改变 state的值 。 这时函数 Parent 再次执行。上述又会打印一遍。
12 "a"
Parent Render
12 "b"
12 "a"
是不是很惊讶,为什么多出了一个 TAG1的控制台输出 ?
我们来捋一下,一共两个state, 两个TextInput , 但是各自对应自己的state。 看起来互不影响。
那么这多出的一次 函数执行是哪来的呢?
通过性能追踪器,其实可以看到,当数据发生变化后,最后React组件树会做一个总的 核对, 正是这个核对使得 上面的函数又执行了一遍。
那这又和上面我们讲的有什么关联呢?这也看不出当某个输入框变化时是否会影响到另一个呀。
第一个,变量 y 的值在改变后,在下一轮中又重新声明定义了。这一点说明了每一次变化,函数中的变量都会重新声明定义。
我们来看一下,如果我们先只留下第一个输入框,并改变第一个输入框的值,也就是 state变量 account ,看下会发生什么
可以看到,TextInput组件的update周期执行了,我们知道,TextInput 是可以接收点击事件的,所以黑圈标出来的可以 TouchableWithoutFeedback 也发生了更新。TextInput 可以接收点击,那么自然该组件最外层的view应该就是 TouchableWithoutFeedback 。理论上来讲,作为最外层的TouchableWithoutFeedback不应该更新,因为 TextInput的直接父view并没有用到state变量,父view没有更新。那么TextInput发生变化的应该只有其内部的展示Text的组件。但是其内部是什么我们也不知道,我们也不做更深入的探究。
接下里将第二个 TextInput 也放出来。那么Parent组件中就存在两个state变量,两个TextInput组件。我们再次更新第一个TextInput用到的 state变量。再次通过追踪器,可以看到
很明显的,第二个TextInput 不仅update周期函数执行了,它的可以接收点击事件的组件也更新了。这说明TouchableWithoutFeedback用到的回调函数必然发生变化了。
从组件更新完后的 React Tree 核对这里可以看到,只有一个 TextInput 的值发生了变化。
结合这两点,我们可以下结论了:其中一个输入框用到的state的变化使得另一个输入框重新对回调函数进行了更新绑定。
当两个输入框在一起时,且不采取任何措施的情况下。一个输入框的state改变会影响另一个输入框。
那如果我们将回调函数用 useCallback 包裹起来,是不是就可以了呢?
function Parent(props) {
const [phone,setPhone] = useState("");
const [pwd,setPwd] = useState("");
const phoneHanlder = useCallback( (value:string) => {
setPhone(value);
},[]);
const pwdHandler = useCallback( (value:string) => {
setPwd(value);
},[]);
useEffect(() => {
setTimeout(() => {
setPhone('f')
},3000)
return (
<View style={styles.body}>
<TextInput value={phone} placeholder={"phone"} onChangeText={phoneHanlder}/>
<TextInput value={pwd} placeholder={"password"} onChangeText={pwdHandler}/>
</View>
可以看到,第一个,第二个输入框的回调函数都用useCallback 包裹起来了,且第二个参数是空数组。也就是说不会因为任何参数的变化而发生变化了。那是不是就可以了呢?
我们再次刷新APP,并查看跟踪器
很遗憾,并没有用。
为什么会这样 ?我们来看一下我们是怎么定义的。
const pwdHandler = useCallback( (value:string) => {
setPwd(value);
},[]);
useEffect(() => {
setTimeout(() => {
setPhone('f')
},3000)
我们并没有在有依赖参数的副作用中定义这个回调。也就是说,这个 pwdHandler 实际上和之前的 y 变量一样,函数每次执行时都会重新定义声明。这也就导致了第二个输入框必定会再次受到影响,会重新绑定回调函数。
那么我们在副作用 useEffect 中使用 useCallback ?
不行,因为这两都是hook,而hook不能嵌套用,且只能在函数体中使用。
那么如何解决呢?
这里,我们要使用 React.memo 和 useCallback 配合
将 TextInput 再抽出来封装成子组件
const Input = memo(function({value,callback,ph}){
console.log("Input Run")
return <TextInput value={value} placeholder={ph} onChangeText={callback}/>
然后保持 函数用 useCallback包裹不变
父组件现在如下, 用封装的Input来代替之前的 TextInput
function Parent(props) {
const [phone,setPhone] = useState("");
const [pwd,setPwd] = useState("");
const phoneHanlder = useCallback( (value:string) => {
setPhone(value);
},[]);
const pwdHandler = useCallback( (value:string) => {
setPwd(value);
},[]);
return (
<View style={styles.body}>
<Input value={phone} ph="phone" callback={phoneHanlder}/>
<Input value={pwd} ph="password" callback={pwdHandler}/>
</View>
至此,解决了一个输入框的state变量变化,使得另一个输入重复绑定回调函数的问题。
那如果不用React.memo , 我们在Input 里使用 useMemo 再将传入的 callback 包裹一层呢?
okay,我们试一下
function Input({value,callback,ph}){
console.log("Input Run")
const call = useMemo(() => {
console.log('Input Render');
return callback
},[callback]);
return <TextInput value={value} placeholder={ph} onChangeText={call}/>
如上所述,我们将回调函数再用 useMemo包裹了一层,传递来的callback 如果不发生变化的话,那么 call 就不会改变。call 不改变,那么 TextInput 组件就不会发生变化。
我们查看控制台输出
Input Run
Input Render
Input Run
Input Render
上面4输出是组件挂载时,两个TextInput打印的内容。