React Native - 减少渲染时间以优化使用 React 钩子时的性能



Background


发布React v16.8之后,现在我们有钩子可以在 React Native 中使用。
我正在做一些简单的测试,以查看
Hooked 功能组件和类组件之间的渲染时间和性能。这是我的示例:

@Components/按钮.js

import React, { memo } from 'react';
import { TouchableOpacity, Text } from 'react-native';
const Button = memo(({ title, onPress }) => {
console.log("Button render"); // check render times
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<Text>{title}</Text>
</TouchableOpacity>
);
});
export default Button;

@Contexts/用户.js

import React, { createContext, useState } from 'react';
import User from '@Models/User';
export const UserContext = createContext({});
export const UserContextProvider = ({ children }) => {
let [ user, setUser ] = useState(null);
const login = (loginUser) => {
if (loginUser instanceof User) { setUser(loginUser); }
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{value: user, login: login, logout: logout}}>
{children}
</UserContext.Provider>
);
};
export function withUserContext(Component) {
return function UserContextComponent(props) {
return (
<UserContext.Consumer>
{(contexts) => <Component {...props} {...contexts} />}
</UserContext.Consumer>
);
}
}


我们在下面有两个案例来构造屏幕组件:

@Screens/登录.js

案例 1:带钩

子的功能组件
import React, { memo, useContext, useState } from 'react';
import { View, Text } from 'react-native';
import Button from '@Components/Button';
import { UserContext } from '@Contexts/User';
const LoginScreen = memo(({ navigation }) => {
const appUser = useContext(UserContext);
const [foo, setFoo] = useState(false);
const userLogin = async () => {
let response = await fetch('blahblahblah');
if (response.is_success) {
appUser.login(user);
} else {
// fail on login, error handling
}
};
const toggleFoo = () => {
setFoo(!foo);
console.log("current foo", foo);
};
console.log("render Login Screen"); // check render times
return (
<View>
<Text>Login Screen</Text>
<Button onPress={userLogin} title="Login" />
<Button onPress={toggleFoo} title="Toggle Foo" />
</View>
);
});
export default LoginScreen;

情况 2:用 HOC 包装的组件

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import Button from '@Components/Button';
import { withUserContext } from '@Contexts/User';
import UserService from '@Services/User';
class LoginScreen extends Component {
state = { foo: false };
userLogin = async () => {
let response = await UserService.login();
if (response.is_success) {
login(user);      // function from UserContext
} else {
// fail on login, error handling
}
};
toggleFoo = () => {
const { foo } = this.state;
this.setState({ foo: !foo });
console.log("current foo", foo);
};
render() {
console.log("render Login Screen"); // check render times
return (
<View>
<Text>Login Screen</Text>
<Button onPress={userLogin} title="Login" />
<Button onPress={toggleDisable} title="Toggle" />
</View>
);
}
}

结果


这两种情况在开始时具有相同的渲染时间:

render Login Screen
Button render
Button render

但是当我按下"切换"按钮时,状态发生了变化,结果如下:

案例 1:带钩

子的功能组件
render Login Screen
Button render
Button render

情况 2:用 HOC 包装的组件

render Login Screen

问题


尽管按钮组件不是一大堆代码,但考虑到两种情况之间的重新渲染时间,Case 2应该具有比Case 1更好的性能。 但是,考虑到代码的可读性,我绝对更喜欢使用钩子而不是使用 HOC。(特别是功能:appUser.login()login()(

所以问题来了。有什么解决方案可以保留两种大小的好处,减少使用钩子时的重新渲染时间?谢谢。

即使在功能组件的情况下使用memo,这两个按钮也会重新呈现的原因是,函数引用在每次重新呈现时都会更改,因为它们是在功能组件中定义的。

如果您在类组件的渲染中使用arrow functions,也会发生类似的情况

对于类,函数引用不会随您定义它们的方式而更改,因为函数是在 render 方法之外定义的

要优化重新渲染,您应该使用useCallback钩子来记住函数引用

const LoginScreen = memo(({ navigation }) => {
const appUser = useContext(UserContext);
const [foo, setFoo] = useState(false);
const userLogin = useCallback(async () => {
let response = await fetch('blahblahblah');
if (response.is_success) {
appUser.login(user);
} else {
// fail on login, error handling
}
}, []); // Add dependency if need i.e when using value from closure
const toggleFoo = useCallback(() => {
setFoo(prevFoo => !prevFoo); // use functional state here
}, []);
console.log("render Login Screen"); // check render times
return (
<View>
<Text>Login Screen</Text>
<Button onPress={userLogin} title="Login" />
<Button onPress={toggleFoo} title="Toggle Foo" />
</View>
);
});
export default LoginScreen;

另请注意,由于上下文值更改,React.memo无法阻止重新渲染。另请注意,在将值传递给上下文提供程序时,您也应该使用useMemo

export const UserContextProvider = ({ children }) => {
let [ user, setUser ] = useState(null);
const login = useCallback((loginUser) => {
if (loginUser instanceof User) { setUser(loginUser); }
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const value = useMemo(() => ({
value: user,
login: login,
logout: logout,
}), [user, login, logout]); 
/*
Note that login and logout functions are implemented using `useCallback` and 
are created on initial render only and hence adding them as dependency here 
doesn't make a difference and will definitely not lead to new referecne for 
value. Only `user` value change will create a new object reference
*/
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
};

原因是在功能组件中,每当组件重新渲染时,新创建的userLogin=>Button组件都会重新渲染。

const userLogin = async () => {
const response = await fetch("blahblahblah")
if (response.is_success) {
appUser.login(user)
} else {
// fail on login, error handling
}
}

您可以使用useCallback来记住userLogin函数 + 用React.memo包装Button组件(就像您所做的那样(防止不必要的重新渲染:

const userLogin = useCallback(async () => {
const response = await fetch("blahblahblah")
if (response.is_success) {
appUser.login(user)
} else {
// fail on login, error handling
}
}, [])

它没有发生在类组件中的原因是当类组件被重新渲染时,只有render函数是触发器(当然还有其他一些生命周期函数,如 shoudlComponentUpdate,componentDidUpdate 触发器(。 ==>userLogin不更改 ==>Button组件不重新渲染。

这是一篇很棒的文章,可以看看useCallback+memo

注意:当您使用Context时,memo无法阻止组件(这是一个Consumer(在上下文提供程序的值更改时重新呈现。 例如: 如果在UserContext=>UserContext中调用setUser重新渲染 =>value={{value: user, login: login, logout: logout}}更改 =>LoginScreen重新渲染。您不能使用shouldComponentUpdate(类组合(或memo(功能组件(来防止重新渲染,因为它不是通过props更新的,而是通过上下文提供的值更新的

相关内容

  • 没有找到相关文章

最新更新