React Native 進階(三)--定時器、直接操作(setNativeProps)、調試
定時器
- setTimeout, clearTimeout
- setInterval, clearInterval
- setImmediate, clearImmediate
- requestAnimationFrame, cancelAnimationFrame
requestAnimationFrame(fn)和setTimeout(fn, 0)不同,前者會在每幀刷新之后執行一次,而后者則會盡可能快的執行。
setImmediate則會在當前JavaScript執行塊結束的時候執行,就在將要發送批量響應數據到原生之前。注意如果你在setImmediate的回調函數中又執行了setImmediate,它會緊接著立刻執行,而不會在調用之前等待原生代碼。
Promise的實現就使用了setImmediate來執行異步調用。
InteractionManager
原生應用感覺如此流暢的一個重要原因就是在互動和動畫的過程中避免繁重的操作。在React Native里,你可以用InteractionManager將一些耗時較長的工作安排到所有互動或動畫完成之后再進行,這樣可以保證JavaScript動畫的流暢運行。
應用可以通過以下代碼來安排一個任務,使其在交互結束之后執行:
InteractionManager.runAfterInteractions(() => {
// ...需要長時間同步執行的任務...
});
我們來把它和之前的幾個任務安排方法對比一下:
- requestAnimationFrame(): 用來執行在一段時間內控制視圖動畫的代碼
- setImmediate/setTimeout/setInterval(): 在稍后執行代碼。注意這有可能會延遲當前正在進行的動畫。
- runAfterInteractions(): 在稍后執行代碼,不會延遲當前進行的動畫。
觸摸處理系統會把一個或多個進行中的觸摸操作認定為'交互',并且會將runAfterInteractions()的回調函數延遲執行,直到所有的觸摸操作都結束或取消了。
InteractionManager還允許應用注冊動畫,在動畫開始時創建一個交互“句柄”,然后在結束的時候清除它。
var handle = InteractionManager.createInteractionHandle();
// 執行動畫... (`runAfterInteractions`中的任務現在開始排隊等候)
// 在動畫完成之后
InteractionManager.clearInteractionHandle(handle);
// 在所有句柄都清除之后,現在開始依序執行隊列中的任務
runAfterInteractions接受一個普通的回調函數,或是一個PromiseTask對象,該對象需要帶有名為gen的方法,并返回一個Promise。如果提供的參數是一個PromiseTask, 那么即便它是異步的它也會阻塞任務隊列,直到它(以及它所有的依賴任務,哪怕這些依賴任務也是異步的)執行完畢后,才會執行下一個任務。
默認情況下,排隊的任務會在一次setImmediate方法中依序批量執行。如果你調用了setDeadLine方法并設定了一個正整數值,則任務只會在設定的時間到達后開始執行。在此之前,任務會通過setTimeout來掛起并阻塞其他任務執行,這樣可以給諸如觸摸交互一類的事件留出時間,使應用可以更快地響應用戶。
方法
static runAfterInteractions(callback: Function)
安排一個函數在所有的交互和動畫完成之后運行。返回一個可取消的promise。
static createInteractionHandle()
通知管理器有某個動畫或者交互開始了。
static clearInteractionHandle(handle: Handle)
通知管理器有某個動畫或者交互已經結束了。
static setDeadline(deadline: number)
如果設定了一個正整數值,則會使用setTimeout來掛起所有尚未執行的任務。在eventLoopRunningTime到達設定時間后,才開始使用一個setImmediate方法來批量執行所有任務。
屬性
Events: CallExpression
addListener: CallExpression
TimerMixin
很多React Native應用發生致命錯誤(閃退)是與計時器有關。具體來說,是在某個組件被卸載(unmount)之后,計時器卻仍然被激活。為了解決這個問題,引入了TimerMixin。如果你在組件中引入TimerMixin,就可以把你原本的setTimeout(fn, 500)改為this.setTimeout(fn, 500)(只需要在前面加上this.),然后當你的組件卸載時,所有的計時器事件也會被正確的清除。
這個庫并沒有跟著React Native一起發布。你需要在項目文件夾下輸入npm i react-timer-mixin --save來單獨安裝它。
import TimerMixin from 'react-timer-mixin';
var Component = React.createClass({
mixins: [TimerMixin],
componentDidMount: function() {
this.setTimeout(
() => { console.log('I do not leak!'); },
500
);
}
});
強烈建議您使用react-timer-mixin提供的this.setTimeout(...)來代替setTimeout(...)。這可以規避許多難以排查的BUG。
如果你的項目是用ES6代碼編寫,因為ES6中沒有內置Mixin,你可以使用react-mixin來代替TimerMixin或者在unmount組件時記住清除(clearTimeout/clearInterval)所有用到的定時器,那么也可以實現和TimerMixin同樣的效果。例如:
import React,{
Component
} from 'react';
export default class Hello extends Component {
componentDidMount() {
this.timer = setTimeout(
() => { console.log('把一個定時器的引用掛在this上'); },
500
);
}
componentWillUnmount() {
// 如果存在this.timer,則使用clearTimeout清空。
// 如果你使用多個timer,那么用多個變量,或者用個數組來保存引用,然后逐個clear
this.timer && clearTimeout(this.timer);
}
};
直接操作(setNativeProps)
有時候我們需要直接改動組件并觸發局部的刷新,但不使用state或是props。譬如在瀏覽器中使用React庫,有時候會需要直接修改一個DOM節點,而在手機App中操作View時也會碰到同樣的情況。在React Native中,setNativeProps就是等價于直接操作DOM節點的方法。
什么時候使用setNativeProps呢?在(不得不)頻繁刷新而又遇到了性能瓶頸的時候。
直接操作組件并不是應該經常使用的工具。一般來說只是用來創建連續的動畫,同時避免渲染組件結構和同步太多視圖變化所帶來的大量開銷。setNativeProps是一個“簡單粗暴”的方法,它直接在底層(DOM、UIView等)而不是React組件中記錄state,這樣會使代碼邏輯難以理清。所以在使用這個方法之前,請盡量先嘗試用setState和shouldComponentUpdate方法來解決問題。
setNativeProps與TouchableOpacity
TouchableOpacity這個組件就在內部使用了setNativeProps方法來更新其子組件的透明度:
setOpacityTo(value) {
// Redacted: animation related code
this.refs[CHILD_REF].setNativeProps({
opacity: value
});
},
由此我們可以寫出下面這樣的代碼:子組件可以響應點擊事件,更改自己的透明度。而子組件自身并不需要處理這件事情,也不需要在實現中做任何修改。
<TouchableOpacity onPress={this._handlePress}>
<View style={styles.button}>
<Text>Press me!</Text>
</View>
</TouchableOpacity>
如果不使用setNativeProps這個方法來實現這一需求,那么一種可能的辦法是把透明值保存到state中,然后在onPress事件觸發時更新這個值:
constructor(props) {
super(props);
this.state = { myButtonOpacity: 1, };
}
render() {
return (
<TouchableOpacity onPress={() => this.setState({myButtonOpacity: 0.5})}
onPressOut={() => this.setState({myButtonOpacity: 1})}>
<View style={[styles.button, {opacity: this.state.myButtonOpacity}]}>
<Text>Press me!</Text>
</View>
</TouchableOpacity>
)
}
比起之前的例子,這一做法會消耗大量的計算 —— 每一次透明值變更的時候都要重新渲染組件結構,即便視圖的其他屬性和子組件并沒有變化。一般來說這一開銷也不足為慮,但當執行連續的動畫以及響應用戶手勢的時候,只有正確地優化組件才能提高動畫的流暢度。setNativeProps方法實際是對RCTUIManager.updateView的封裝 —— 而這正是重渲染所觸發的函數調用。
復合組件與setNativeProps
復合組件并不是單純的由一個原生視圖構成,所以你不能對其直接使用setNativeProps。比如下面這個例子:
class MyButton extends React.Component {
render() {
return (
<View>
<Text>{this.props.label}</Text>
</View>
)
}
}
class App extends React.Component {
render() {
return (
<TouchableOpacity>
<MyButton label="Press me!" />
</TouchableOpacity>
)
}
}
運行這個例子會馬上看到一行報錯: <font color="#D02591">Touchable child must either be native or forward setNativeProps to a native component.</font> 這是因為MyButton并非直接由原生視圖構成,而我們只能給原生視圖設置透明值。你可以嘗試這樣去理解:如果你通過React.createClass方法自定義了一個組件,直接給它設置樣式prop是不會生效的,你得把樣式props層層向下傳遞給子組件,直到子組件是一個能夠直接定義樣式的原生組件。同理,我們也需要把setNativeProps傳遞給由原生組件封裝的子組件。
將setNativeProps傳遞給子組件
具體要做的就是在我們的自定義組件中再封裝一個setNativeProps方法,其內容為對合適的子組件調用真正的setNativeProps方法,并傳遞要設置的參數。
class MyButton extends React.Component {
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps);
}
render() {
return (
<View ref={component => this._root = component} {...this.props}>
<Text>{this.props.label}</Text>
</View>
)
}
}
現在你可以在TouchableOpacity中嵌入MyButton了!有一點需要特別說明:這里我們使用了ref回調語法,而不是傳統的字符串型ref引用。
你可能還會注意到我們在向下傳遞props時使用了{...this.props}語法(對象的擴展運算符,將多個對象合并到某個對象)。這是因為TouchableOpacity本身其實也是個復合組件, 它除了要求在子組件上執行setNativeProps 以外,還要求子組件對觸摸事件進行處理。因此,它會傳遞多個props,其中包含了onmoveshouldsetresponder 函數,這個函數需要回調給TouchableOpacity組件,以完成觸摸事件的處理。與之相對的是TouchableHighlight組件,它本身是由原生視圖構成,因而只需要我們實現setNativeProps。
setNativeProps清除TextInput的值
另一種很常見的setNativeProps的用法是清除TextInput的值。當緩沖區延遲低和用戶輸入很快的時候可以通過控制TextInput的屬性來減少輸入的字符。一些開發人員更喜歡完全跳過這個屬性,而直接使用setNativeProps直接操作TextInput值。
下面的代碼演示了點擊按鈕是清除TextInput的值。
class App extends React.Component {
constructor(props) {
super(props);
this.clearText = this.clearText.bind(this);
}
clearText() {
this._textInput.setNativeProps({text: ''});
}
render() {
return (
<View style={styles.container}>
<TextInput ref={component => this._textInput = component}
style={styles.textInput} />
<TouchableOpacity onPress={this.clearText}>
<Text>Clear text</Text>
</TouchableOpacity>
</View>
);
}
}
避免和render方法的沖突
如果要更新一個由render方法來維護的屬性,則可能會碰到一些出人意料的bug。因為每一次組件重新渲染都可能引起屬性變化,這樣一來,之前通過setNativeProps所設定的值就被完全忽略和覆蓋掉了。
setNativeProps與shouldComponentUpdate
通過巧妙運用 shouldComponentUpdate方法,可以避免重新渲染那些實際沒有變化的子組件所帶來的額外開銷,此時使用setState的性能已經可以與setNativeProps相媲美了。
調試
訪問App內的開發菜單
你可以通過搖晃設備或是選擇iOS模擬器的"Hardware"菜單中的"Shake Gesture"選項來打開開發菜單。另外,如果是在iOS模擬器中運行,還可以按下Command? + D 快捷鍵,Android模擬器對應的則是Command? + M(windows上可能是F1或者F2)。
[圖片上傳失敗...(image-800c38-1530674048884)]
注意:在成品(release/producation builds)中開發者菜單會被關閉。
刷新JavaScript
傳統的原生應用開發中,每一次修改都需要重新編譯,但在React Native中你只需要刷新一下JavaScript代碼,就能立刻看到變化。具體的操作就是在開發菜單中點擊"Reload"選項。也可以在iOS模擬器中按下Command? + R ,Android模擬器上對應的則是按兩下R。(注意,某些React Native版本可能在windows中reload無效,請等待官方修復)
如果在iOS模擬器中按下Command? + R沒啥感覺,則注意檢查Hardware菜單中,Keyboard選項下的"Connect Hardware Keyboard"是否被選中。
自動刷新
選擇開發菜單中的"Enable Live Reload"可以開啟自動刷新,這樣可以節省你開發中的時間。
更神奇的是,你還可以保持應用的當前運行狀態,修改后的JavaScript文件會自動注入進來(就好比行駛中的汽車不用停下就能更換新的輪胎)。要實現這一特性只需開啟開發菜單中的Hot Reloading選項。
某些情況下hot reload并不能順利實施。如果碰到任何界面刷新上的問題,請嘗試手動完全刷新。
但有些時候你必須要重新編譯應用才能使修改生效:
增加了新的資源(比如給iOS的Images.xcassets或是Andorid的res/drawable文件夾添加了圖片)
更改了任何的原生代碼(objective-c/swift/java)
應用內的錯誤與警告提示(紅屏和黃屏)
紅屏或黃屏提示都只會在開發版本中顯示,正式的離線包中是不會顯示的。
紅屏錯誤
應用內的報錯會以全屏紅色顯示在應用中(調試模式下),我們稱為紅屏(red box)報錯。你可以使用console.error()來手動觸發紅屏錯誤。
黃屏警告
應用內的警告會以全屏黃色顯示在應用中(調試模式下),我們稱為黃屏(yellow box)報錯。點擊警告可以查看詳情或是忽略掉。 和紅屏報警類似,你可以使用console.warn()來手動觸發黃屏警告。 在默認情況下,開發模式中啟用了黃屏警告。可以通過以下代碼關閉:
console.disableYellowBox = true;
console.warn('YellowBox is disabled.');
你也可以通過代碼屏蔽指定的警告,像下面這樣設置一個數組:
console.ignoredYellowBox = ['Warning: ...'];
數組中的字符串就是要屏蔽的警告的開頭的內容。(例如上面的代碼會屏蔽掉所有以Warning開頭的警告內容)
紅屏和黃屏在發布版(release/production)中都是自動禁用的。
訪問控制臺日志
在運行RN應用時,可以在終端中運行如下命令來查看控制臺的日志:
react-native log-ios
react-native log-android
此外,你也可以在iOS模擬器的菜單中選擇Debug → Open System Log...來查看。如果是Android應用,無論是運行在模擬器或是真機上,都可以通過在終端命令行里運行adb logcat *:S ReactNative:V ReactNativeJS:V命令來查看。
Chrome開發者工具
在開發者菜單中選擇"Debug JS Remotely"選項,即可以開始在Chrome中調試JavaScript代碼。點擊這個選項的同時會自動打開調試頁面 http://localhost:8081/debugger-ui.
在Chrome的菜單中選擇Tools → Developer Tools可以打開開發者工具,也可以通過鍵盤快捷鍵來打開(Mac上是Command? + Option? + I,Windows上是Ctrl + Shift + I或是F12)。打開有異常時暫停(Pause On Caught Exceptions)選項,能夠獲得更好的開發體驗。
Chrome中并不能直接看到App的用戶界面,而只能提供console的輸出,以及在sources項中斷點調試js腳本。
目前無法正常使用React開發插件(就是某些教程或截圖上提到的Chrome開發工具上多出來的React選項),但這并不影響代碼的調試。如果你需要像調試web頁面那樣查看RN應用的jsx結構,暫時只能使用Nuclide的"React Native Inspector"這一功能來代替。
使用Chrome開發者工具來在設備上調試
對于iOS真機來說,需要打開 RCTWebSocketExecutor.m文件,然后將其中的"localhost"改為你的電腦的IP地址,最后啟用開發者菜單中的"Debug JS Remotely"選項。
對于Android 5.0+設備(包括模擬器)來說,將設備通過USB連接到電腦上后,可以使用adb命令行工具來設定從設備到電腦的端口轉發:
adb reverse tcp:8081 tcp:8081
如果設備Android版本在5.0以下,則可以在開發者菜單中選擇"Dev Settings - Debug server host for device",然后在其中填入電腦的”IP地址:端口“。
如果在Chrome調試時遇到一些問題,那有可能是某些Chrome的插件引起的。試著禁用所有的插件,然后逐個啟用,以確定是否某個插件影響到了調試。
使用自定義的JavaScript調試器來調試
如果想用其他的JavaScript調試器來代替Chrome,可以設置一個名為REACT_DEBUGGER的環境變量,其值為啟動自定義調試器的命令。調試的流程依然是從開發者菜單中的"Debug JS Remotely"選項開始。
被指定的調試器需要知道項目所在的目錄(可以一次傳遞多個目錄參數,以空格隔開)。例如,如果你設定了REACT_DEBUGGER="node /某個路徑/launchDebugger.js --port 2345 --type ReactNative",那么啟動調試器的命令就應該是node /某個路徑/launchDebugger.js --port 2345 --type ReactNative /某個路徑/你的RN項目目錄。
以這種方式執行的調試器最好是一個短進程(short-lived processes),同時最好也不要有超過200k的文字輸出。
在Android上使用Stetho來調試
- 在android/app/build.gradle文件中添加:
compile 'com.facebook.stetho:stetho:1.3.1'
compile 'com.facebook.stetho:stetho-okhttp3:1.3.1'
- 在android/app/src/main/java/com/{yourAppName}/MainApplication.java文件中添加:
import com.facebook.react.modules.network.ReactCookieJarContainer;
import com.facebook.stetho.Stetho;
import okhttp3.OkHttpClient;
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import java.util.concurrent.TimeUnit;
- 在android/app/src/main/java/com/{yourAppName}/MainApplication.java文件中添加:
public void onCreate() {
super.onCreate();
Stetho.initializeWithDefaults(this);
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.cookieJar(new ReactCookieJarContainer())
.addNetworkInterceptor(new StethoInterceptor())
.build();
OkHttpClientProvider.replaceOkHttpClient(client);
}
運行react-native run-android
打開一個新的Chrome選項卡,在地址欄中輸入chrome://inspect并回車。在頁面中選擇'Inspect device' (標有"Powered by Stetho"字樣)。
調試原生代碼
在和原生代碼打交道時(比如編寫原生模塊),可以直接從Android Studio或是Xcode中啟動應用,并利用這些IDE的內置功能來調試(比如設置斷點)。這一方面和開發原生應用并無二致。
性能監測
你可以在開發者菜單中選擇"Pref Monitor"選項以開啟一個懸浮層,其中會顯示應用的當前幀數。
React Native學習筆記--進階(一)--嵌入到Android原生應用中、組件的生命周期、顏色、圖片、觸摸事件
React Native學習筆記--進階(二)--動畫
React Native學習筆記--進階(三)--定時器、直接操作(setNativeProps)、調試
React Native學習筆記--進階(四)--導航器
React Native學習筆記--進階(五)--性能、升級、特定平臺代碼