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
更新的,而是通过上下文提供的值更新的