浅析redux-saga实现原理

做者简介 joey 蚂蚁金服·数据体验技术团队html

项目中一直使用redux-saga来处理异步action的流程。对于effect的实现原理感到很好奇。抽空去研究了一下他的实现。本文不会描述redux-saga的基础API和优势,单纯聊实现原理,欢迎你们在评论区留言讨论。git

前言

redux-saga监听action的代码以下:github

import { takeEvery } from 'redux-saga';

function* mainSaga() {
  yield takeEvery('action_name', function* (action) {
    console.log(action);
  });
}
复制代码

用generator到底是怎么实现takeEvery的呢?咱们先来看稍微简单一点的take的实现原理:redux

take实现原理

咱们尝试写一个demo,用saga的方式实现用generator监听action。bash

$btn.addEventListener('click', () => {
  const action =`action data${i++}`;
  // trigger action
}, false);

function* mainSaga() {
  const action = yield take();
  console.log(action);
}
复制代码

要在$btn点击时候,可以读到action的值。dom

channel

这里咱们须要引入一个概念——channel异步

channel是对事件源的抽象,做用是先注册一个take方法,当put触发时,执行一次take方法,而后销毁他。学习

channel的简单实现以下:ui

function channel() {
  let taker;

  function take(cb) {
    taker = cb;
  }

  function put(input) {
    if (taker) {
      const tempTaker = taker;
      taker = null;
      tempTaker(input);
    }
  }

  return {
    put,
    take,
  };
}

const chan = channel();
复制代码

咱们利用channel作generator和dom事件的链接,将dom事件改写以下:spa

$btn.addEventListener('click', () => {
  const action =`action data${i++}`;
  chan.put(action);
}, false);
复制代码

当put触发时,若是channel里已经有注册了的taker,taker就会执行。

咱们须要在put触发以前,先调用channel的take方法,注册实际要运行的方法。

咱们继续看mainSaga里的实现。

function* mainSaga() {
  const action = yield take();
  console.log(action);
}
复制代码

这个take是saga里的一种effect类型。

先看effecttake()的实现。

function take() {
  return {
    type: 'take'
  };
}

复制代码

出乎意料,仅仅返回了一个带类型的object。

其实redux-saga里全部effect返回的值,都是一个带类型的纯object对象。

那到底是何时触发channel的take方法的呢?还须要从调用mainSaga的代码上找缘由。

generator的特色是执行到某一步时,能够把控制权交给外部代码,由外部代码拿到返回结果后,决定该怎么作。

task

这里咱们又要引入一个新的概念task

task是generator方法的执行环境,全部saga的generator方法都跑在task里。

task的简易实现以下:

function task(iterator) {
  const iter = iterator();
  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;
      if (effect.type === 'take) { runTakeEffect(result.value, next); } } } next(); } task(mainSaga); 复制代码

yield take()运行时,将take()返回的结果交给外层的task,此时代码的控制权就已经从gennerator方法中转到了task里了。

result.value的值就是take()返回的结果{ type: 'take' }

再看runTakeEffect的实现:

function runTakeEffect(effect, cb) {
  chan.take(input => {
    cb(input);
  });
}
复制代码

到这里,咱们终于看到调用channel的take方法的地方了。

完整代码以下:

function channel() {
  let taker;

  function take(cb) {
    taker = cb;
  }

  function put(input) {
    if (taker) {
      const tempTaker = taker;
      taker = null;
      tempTaker(input);
    }
  }

  return {
    put,
    take,
  };
}

const chan = channel();

function take() {
  return {
    type: 'take'
  };
}

function* mainSaga() {
  const action = yield take();
  console.log(action);
}

function runTakeEffect(effect, cb) {
  chan.take(input => {
    cb(input);
  });
}

function task(iterator) {
  const iter = iterator();
  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;
      if (effect.type === 'take') {
        runTakeEffect(result.value, next);
      }
    }
  }
  next();
}

task(mainSaga);

let i = 0;
$btn.addEventListener('click', () => {
  const action =`action data${i++}`;
  chan.put(action);
}, false);
复制代码

总体流程就是,先经过mainSaga往channel里注册了一个taker,一旦dom点击发生,就触发channel的put,put会消耗掉已经注册的taker,这样就完成了一次点击事件的监听过程。

查看在线demo

takeEvery实现原理

在上一节中,咱们已经模仿saga实现了一次事件监听,可是仍是有问题,咱们只能监听一次点击,怎么能作到监听每次点击事件呢?redux-saga提供了一个helper方法——takeEvery。咱们尝试在咱们的简易版saga中实现一下takeEvery

function* takeEvery(worker) {
  yield fork(function* () {
    while(true) {
      const action = yield take();
      worker(action);
    }
  });
}

function* mainSaga() {
  yield takeEvery(action => {
    $result.innerHTML = action;
  });
}
复制代码

这里用到了一个新的effect方法fork

fork

fork的做用是启动一个新的task,不阻塞原task执行。代码修改以下:

function fork(cb) {
  return {
    type: 'fork',
    fn: cb,
  };
}

function runForkEffect(effect, cb) {
  task(effect.fn || effect);
  cb();
}

function task(iterator) {
  const iter = typeof iterator === 'function' ? iterator() : iterator;
  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;

      // 判断effect是不是iterator
      if (typeof effect[Symbol.iterator] === 'function') {
        runForkEffect(effect, next);
      } else if (effect.type) {
        switch (effect.type) {
        case 'take':
          runTakeEffect(effect, next);
          break;
        case 'fork':
          runForkEffect(effect, next);
          break;
        default:
        }
      }
    }
  }
  next();
}
复制代码

咱们经过添加了一种新的effectfork,启动了一个新的task takeEvery。

takeEvery的做用就是当channel的put发生后,自动往channel里放进一个新的taker。

咱们实现的channel里同时只能有一个taker,while(true)的做用就是每当一个put触发消耗掉了taker后,就自动触发runTakeEffect中传入的task的next方法,再次往channel里放进一个taker,从而作到源源不断地监听事件。

在线demo

effect的本质

经过上文的实现,咱们发现全部的yield后返回的effect,都是一个纯object,用来给generator外层的执行容器task发送一个信号,告诉task该作什么。

基于这种思路,若是咱们要新增一个effect,来cancel task,也能够很容易实现。

首先咱们先定义一个cancel方法,用来发送cancel的信号。

function cancel() {
  return {
    type: 'cancel'
  };
}
复制代码

而后修改task的代码,让他能真正执行cancel的逻辑。

function task(iterator) {
  const iter = typeof iterator === 'function' ? iterator() : iterator;
  ...

  function runCancelEffect() {
    // do some cancel logic
  }

  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;

      if (typeof effect[Symbol.iterator] === 'function') {
        runForkEffect(effect, next);
      } else if (effect.type) {
        switch (effect.type) {
        case 'cancel':
          runCancelEffect();
        case 'take':
          runTakeEffect(result.value, next);
          break;
        case 'fork':
          runForkEffect(result.value, next);
          break;
        default:
        }
      }
    }
  }
  next();
}
复制代码

小结

本文经过简单实现了几个effect方法来地介绍了redux-saga的原理,要真正作到redux-saga的全部功能,只须要再添加一些细节就能够了。大概以下图所示:

对generator使用有兴趣的同窗推荐学习一下redux-saga源码。在此推荐一篇使用generator实现dom事件监听的文章 继续探索JS中的Iterator,兼谈与Observable的对比

感兴趣的同窗能够关注专栏或者发送简历至'chaofeng.lcf####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~

原文地址:github.com/ProtoTeam/b…