协程在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. 减小没必要要的变量

若是发现其余优势, 欢迎留言.

其余使用场景

照片查看器

若是还有见过其余使用场景, 欢迎留言.

参考连接

相关文章
相关标签/搜索