Skip to content

Latest commit

 

History

History
358 lines (274 loc) · 8.63 KB

006.浅析 redux-saga 实现原理.md

File metadata and controls

358 lines (274 loc) · 8.63 KB

浅析 redux-saga 实现原理

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

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

前言

redux-saga 监听 action 的代码如下:

import { takeEvery } from 'redux-saga';

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

用 generator 究竟是怎么实现takeEvery的呢?我们先来看稍微简单一点的take的实现原理:

take 实现原理

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

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

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

要在$btn点击时候,能够读到 action 的值。

channel

这里我们需要引入一个概念——channel

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

channel 的简单实现如下:

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 事件改写如下:

$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 的对比