ReactNative进阶-性能优化、hooks

性能优化

  1. 图片加载速度优化
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打印的内容。