本文介绍了Component和PureComponent的之间的区别、如何封装原生模块及原生View供React Native调用。
本文首发:http://yuweiguocn.github.io/
《春望》
国破山河在,城春草木深。
感时花溅泪,恨别鸟惊心。
烽火连三月,家书抵万金。
白头搔更短,浑欲不胜簪。
—唐,杜甫
Component vs PureComponent
在上一篇文章中我们简单介绍了Component的使用,PureComponent又是用来做什么的,和Component有什么区别?
在React中,只要我们调用了 this.setState
更新组件状态,组件就会被重新渲染。我们通常会重写 shouldComponentUpdate 方法返回 true 或 false 告诉系统当前组件是否需要重新渲染以此来提升性能。我们来改一下上一篇文章中的示例,初始赋值为0,点击按钮更新为10,然后重写shouldComponentUpdate方法判断是否需要重新渲染。
App.js
import React, {Component} from 'react';
import {StyleSheet, Text, Button, View} from 'react-native';
class CountText extends Component {
render() {
return (<Text>{this.props.count}</Text>);
}
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
shouldComponentUpdate(nextProps, nextState){
console.log("shouldComponentUpdate");
if(this.state.count != nextState.count){
return true;
}
return false;
}
pressButton = () => {
this.setState({count:10})
};
render() {
console.log("render");
return (
<View style={styles.container}>
<Button onPress={this.pressButton}
title="Click Me"
color="#841584"/>
<CountText style={styles.countText} count={this.state.count} />
</View>
);
}
}
...
tips:在终端上查看日志打印命令:
react-native log-android
第一次点击按钮,输出日志:
01-29 19:58:47.633 5029 5090 I ReactNativeJS: shouldComponentUpdate
01-29 19:58:47.634 5029 5090 I ReactNativeJS: render
第二次及第三次点击,输出日志:
01-29 19:58:52.036 5029 5090 I ReactNativeJS: shouldComponentUpdate
01-29 19:58:53.636 5029 5090 I ReactNativeJS: shouldComponentUpdate
可以看到只有第一次点击按钮执行了render方法进行了渲染,之后便不再进行重新渲染。
使用Component我们需要自己重写shouldComponentUpdate方法判断组件是否需要重新渲染以此来提升性能,PureComponent帮我们重写了shouldComponentUpdate方法,但是对props和state只是进行浅比较(shadow comparison),当props或者state本身是嵌套对象或数组等时,浅比较并不能得到预期的结果,这会导致实际的props和state发生了变化,但组件却没有更新的问题。
PureComponent对性能的提升是非常可观的,因为它减少了应用中的渲染次数,所以推荐使用 PureComponent。使用PureComponent我们只需要简单地将Component替换为PureComponent即可:
App.js
import React, {PureComponent} from 'react';
import {StyleSheet, Text, Button, View} from 'react-native';
class CountText extends PureComponent {
render() {
return (<Text>{this.props.count}</Text>);
}
}
export default class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
pressButton = () => {
this.setState({count:10})
};
render() {
console.log("render");
return (
<View style={styles.container}>
<Button onPress={this.pressButton}
title="Click Me"
color="#841584"/>
<CountText style={styles.countText} count={this.state.count} />
</View>
);
}
}
...
封装原生模块(Native Module)
通过封装原生模块使我们可以通过JS调用原生代码,例如调用Android中的Toast显示一个消息。本示例仅作为了解封装原生模块说明,React Native已经帮我们封装了ToastAndroid模块。我们先来看一下Java代码实现部分。创建一个类继承ReactContextBaseJavaModule类,实现getName方法返回module名称,添加一个public void的方法并添加@ReactMethod注解,可以重写getConstants方法定义一些JS端方便使用的常量。
public class RNToast extends ReactContextBaseJavaModule {
private static final String DURATION_SHORT_KEY = "SHORT";
private static final String DURATION_LONG_KEY = "LONG";
public RNToast(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "CustomToast"; //这个就是JS调用的module的名称
}
@Nullable
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
return constants;
}
@ReactMethod
public void show(String message, int duration) {
Toast.makeText(getReactApplicationContext(), message, duration).show();
}
}
创建一个类继承LazyReactPackage类实现抽象方法,其中getNativeModules用于注册原生模块,将我们新写的RNToast注册进去:
public class RNPackage extends LazyReactPackage {
@Override
public List<ModuleSpec> getNativeModules(final ReactApplicationContext reactContext) {
return Arrays.asList(
ModuleSpec.nativeModuleSpec(
RNToast.class,
new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new RNToast(reactContext);
}
}));
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return LazyReactPackage.getReactModuleInfoProviderViaReflection(this);
}
}
最后将我们自定义的ReactPackage在MainApplication注册一下:
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
...
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNPackage()
);
}
...
};
...
}
至此Java代码实现部分就完成了,接下来看一下JS代码实现部分。为了方便使用我们新建一个JS文件引入CustomToast模块:
CustomToast.js
'use strict';
/**
* This exposes the native CustomToast module as a JS module. This has a function 'show'
* which takes the following parameters:
*
* 1. String message: A string with the text to toast
* 2. int duration: The duration of the toast. May be CustomToast.SHORT or CustomToast.LONG
*/
import { NativeModules } from 'react-native';
export default NativeModules.CustomToast;
然后在App.js文件中引入CustomToast模块,在按钮点击方法中调用原生模块方法:
App.js
import React, {PureComponent} from 'react';
import {StyleSheet, Text, Button, View} from 'react-native';
import CustomToast from './CustomToast';
class CountText extends PureComponent {
render() {
return (<Text>{this.props.count}</Text>);
}
}
export default class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
pressButton = () => {
let result=this.state.count+1;
CustomToast.show("count is "+result,CustomToast.SHORT);
this.setState(preState=>{
return {count: preState.count+1}
})
};
render() {
console.log("render");
return (
<View style={styles.container}>
<Button onPress={this.pressButton}
title="Click Me"
color="#841584"/>
<CountText style={styles.countText} count={this.state.count} />
</View>
);
}
}
...
图 JS调用原生Toast效果
封装原生View
React Native已经帮我们封装了大部分常见组件,但我们也可能会遇到需要封装自定义View的组件情况。接下来介绍如何封装原生View,本示例仅作封装原生View的说明。例如封装一个原生ImageView。
新建一个类继承SimpleViewManager类并指定自定义的View:
public class RNImageView extends SimpleViewManager<ImageView> {
@Override
public String getName() {
return "CustomImageView";
}
@Override
protected ImageView createViewInstance(final ThemedReactContext reactContext) {
ImageView imageView = new ImageView(reactContext);
imageView.setImageResource(R.drawable.logo);
return imageView;
}
}
然后在我们自定义的ReactPackage中注册一下:
public class RNPackage extends LazyReactPackage {
...
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new RNImageView()
);
}
...
}
接下来看一下JS端需要处理的代码。同样新建一个类引入原生View,...View.propTypes
用于引入原生View的原有属性。
CustomImageView.js
import { requireNativeComponent, View } from 'react-native';
var iface = {
name: 'CustomImageView',
propTypes: {
...View.propTypes
}
};
module.exports = requireNativeComponent('CustomImageView', iface);
然后在App.js文件中引入CustomImageView组件并指定宽高:
import React, {PureComponent} from 'react';
import {StyleSheet, Text, Button, View} from 'react-native';
import CustomToast from './CustomToast';
import CustomImageView from './CustomImageView'
class CountText extends PureComponent {
render() {
return (<Text>{this.props.count}</Text>);
}
}
export default class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
...
render() {
console.log("render");
return (
<View style={styles.container}>
<Button onPress={this.pressButton}
title="Click Me"
color="#841584"/>
<CountText style={styles.countText} count={this.state.count} />
<CustomImageView style={{width: 200,height: 200}}/>
</View>
);
}
}
...
图 JS引用原生View效果
查看完整源码:https://github.com/yuweiguocn/RNTest
查看React Native学习笔记相关文章。