協程在RN中的實踐

首發於個人博客 轉載需留出處 FaiChou 2018-05-29javascript

協程在RN中的實踐

Demo地址 vs 不使用Coroutine的控件地址java

本篇並非 ScrollView 的新輪子, 而是對比兩種實現方式的差異, 來認識coroutine.react

要實現的是一個對 RN 中 ScrollView 的封裝, 給它添加一個隱藏的 Header, 具備下拉刷新功能.ios

假設你已經對 js 的 Iterators and generators有所瞭解.git

什麼是 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.github

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.express

yield 還能夠捕獲 next(x) 傳的參數, 因此能夠根據傳的不一樣參數, yield 代理轉接不一樣的方法.react-native

再舉個新的栗子.bash

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) 直接輸出告終果.app

如何自動執行 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 的處理方式:

  1. 放一個 RefreshHeaderScrollView 的頭上
  2. 綁定 onScrollBeginDrag, onScroll, onScrollEndDrag 方法
  3. 用戶開始拖拽 scrollview, 記錄 _dragFlag = true_offsetY
  4. 用戶拖拽過程當中
    • 判斷是否爲用戶手動觸發的 onScroll
    • 判斷此時是否正在刷新
    • 拖拽高度大於觸發高度, 設置 this.state,refreshStatusreleaseToRefresh
    • 拖拽高度小於出發高度, 設置 this.state,refreshStatuspullToRefresh
  5. 用戶釋放手指
    • 設置標誌位 _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 的優勢

  1. 邏輯清晰
  2. 減小沒必要要的變量

若是發現其餘優勢, 歡迎留言.

其餘使用場景

照片查看器

若是還有見過其餘使用場景, 歡迎留言.

參考連接

相關文章
相關標籤/搜索