協程在RN中的實踐
本篇並不是 ScrollView 的新輪子, 而是對比兩種實現方式的差別, 來認識coroutine.
要實現的是一個對 RN 中 ScrollView 的封裝, 給它新增一個隱藏的 Header, 具有下拉重新整理功能.
假設你已經對 js 的 Iterators and generators有所瞭解.
什麼是 Coroutine
function* idMaker() {
let index = 0;
while(true)
yield index++;
}
let gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
複製程式碼
這是官網 generator 的栗子, yield 作為一個類似 return 的語法返回id, 下次呼叫 next()
時候, 繼續上次位置 -> 迴圈 -> 繼續返回新id.
The next() method also accepts a value which can be used to modify the internal state of the generator. A value passed to next() will be treated as the result of the last yield expression that paused the generator.
yield 還可以捕獲 next(x)
傳的引數, 所以可以根據傳的不同引數, yield 代理轉接不同的方法.
再舉個新的栗子.
function* logTest(x) {
console.log('hello, in logTest!');
while (true) {
console.log('received:', yield);
}
}
let gen = logTest();
gen.next(); // hello, in logTest!
gen.next(1); // received: 1
gen.next('b'); // received: b
gen.next({a: 1}); // received: {a: 1}
複製程式碼
這個方法中, 獲取了 next 的引數, 呼叫 gen.next(1)
直接輸出了結果.
如何自動執行 generator , 而不是手動呼叫 next()
呢? 使用 coroutine
:
function coroutine(f) {
var o = f(); // instantiate the coroutine
o.next(); // execute until the first yield
return function(x) {
o.next(x);
}
}
複製程式碼
這樣可以給 logTest
裝備上 coroutine
:
let coLogTest = coroutine(logTest); // hello, in logTest!
coLogTest('abc'); // received: abc
coLogTest(2); // received: 2
複製程式碼
再看個簡單栗子吧:
let loginState = false;
function* loginStateSwitcher() {
while (true) {
yield;
loginState = true;
console.log('Login!');
yield;
loginState = false;
console.log('Logout!');
}
}
let switcher = coroutine(loginStateSwitcher);
switcher(); // Login!
switcher(); // Logout!
switcher(); // Login!
複製程式碼
直接一個 switcher()
使用者登入登出便捷明瞭.
ScrollView 下拉重新整理的邏輯
可以大致看下沒有使用 coroutine 的處理方式:
- 放一個
RefreshHeader
到ScrollView
的頭上 - 繫結
onScrollBeginDrag
,onScroll
,onScrollEndDrag
方法 - 使用者開始拖拽 scrollview, 記錄
_dragFlag = true
和_offsetY
- 使用者拖拽過程中
- 判斷是否為使用者手動觸發的
onScroll
- 判斷此時是否正在重新整理
- 拖拽高度大於觸發高度, 設定
this.state,refreshStatus
為releaseToRefresh
- 拖拽高度小於出發高度, 設定
this.state,refreshStatus
為pullToRefresh
- 判斷是否為使用者手動觸發的
- 使用者釋放手指
- 設定標誌位
_dragFlag = false
和記錄_offsetY
- 如果沒在重新整理, 並且剛才的狀態為
releaseToRefresh
, 去重新整理, 設定_isRefreshing = true
並且this.state,refreshStatus
設定為refreshing
, 呼叫props.onRefresh()
方法, scrollView 滾動到保持重新整理狀態位置{ x: 0, y: -80 }
- props 裡的
onRefresh(onEndRefresh)
, 需要將結束重新整理的方法回撥給使用者 onRefreshEnd
方法裡將_isRefreshing
設為 false,this.state,refreshStatus
設為pullToRefresh
, scrollView 滾動到初始位置{ x: 0, y: 0}
- 設定標誌位
可以去看下程式碼, 幾乎所有拖拽釋放邏輯分散到 onScrollBeginDrag
, onScroll
, onScrollEndDrag
方法中了, 如果這幾個方法要共享狀態就需要申請幾個臨時變數, 比如 _offsetY
, _isRefreshing
, 和 _dragFlag
.
使用 coroutine 統籌管理
this.loop = coroutine(function* () {
let e = {};
while (e = yield) {
if (
e.type === RefreshActionType.drag
&& that.state.refreshStatus !== RefreshStatus.refreshing
) {
while (e = yield) {
if (e.type === RefreshActionType.scroll) {
if (e.offsetY <= -REFRESH_VIEW_HEIGHT) {
that.changeRefreshStateTo(RefreshStatus.releaseToRefresh);
} else {
that.changeRefreshStateTo(RefreshStatus.pullToRefresh);
}
} else if (e.type === RefreshActionType.release) {
if (e.offsetY <= -REFRESH_VIEW_HEIGHT) {
that.changeRefreshStateTo(RefreshStatus.refreshing);
that.scrollToRefreshing();
that.props.onRefresh(() => {
// in case the refreshing state not change
setTimeout(that.onRefreshEnd, 500);
});
} else {
that.scrollToNormal();
}
break;
}
}
}
}
});
複製程式碼
只需要在相應的事件時候呼叫 this.loop
即可.
onScroll = (event) => {
const { y } = event.nativeEvent.contentOffset;
this.loop({ type: RefreshActionType.scroll, offsetY: y });
}
onScrollBeginDrag = (event) => {
this.loop({ type: RefreshActionType.drag });
}
onScrollEndDrag = (event) => {
const { y } = event.nativeEvent.contentOffset;
this.loop({ type: RefreshActionType.release, offsetY: y });
}
複製程式碼
協程方法接受引數 {type: drag, offsetY: 0}
, 用來根據當時拖拽事件和位置處理相應邏輯.
可以看到協程方法裡有兩個 while (e = yield)
:
while (e = yield) {
if (
e.type === RefreshActionType.drag
&& that.state.refreshStatus !== RefreshStatus.refreshing) {
// ..
}
複製程式碼
第一個配合 if, 可以限制使用者只有當第一次拖拽開始時候來開啟下一步.
while (e = yield) {
if (e.type === RefreshActionType.scroll) {}
else if (e.type === RefreshActionType.release) {}
}
複製程式碼
第二個用來處理滑動過程中和釋放的事件, 這裡可以肯定使用者是進行了拖拽才有的事件, 於是就免去了 _dragFlag
臨時變數.
當事件為 RefreshActionType.scroll
, 再根據 offsetY
呼叫 changeRefreshStateTo()
設定當前重新整理的狀態為 releaseToRefresh
還是 pullToRefresh
.
當事件為 RefreshActionType.release
, 判斷 offsetY
, 如果超過觸發重新整理位置, 呼叫 changeRefreshStateTo()
設定當前重新整理狀態為 refreshing
, 將 scrollview 固定到重新整理狀態的位置(否則會自動滑上去), 並且呼叫 props.onRefresh()
; 如果不超過觸發重新整理位置, 則將 scrollView 滑動到初始位置(隱藏header). break 退出當前 while 迴圈, 繼續等待下次 drag 事件到來.
<Header />
會根據當前狀態展示不同文字, 提示使用者繼續下拉重新整理,釋放重新整理和重新整理中
, 根據重新整理狀態設定下尖頭,上箭頭還是 Loading.
PS.
setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
一直是下拉狀態的issue, 是由於setState不會立即觸發改變狀態導致的, 為解決這個問題, 我的處理方式是加一個半秒的延遲:
that.props.onRefresh(() => {
// in case the refreshing state not change
setTimeout(that.onRefreshEnd, 500);
});
複製程式碼
使用 coroutine 的優點
- 邏輯清晰
- 減少不必要的變數
如果發現其他優點, 歡迎留言.
其他使用場景
如果還有見過其他使用場景, 歡迎留言.