浅析 redux-saga 实现原理

发布于 2024-10-05 14:13:06 字数 7683 浏览 36 评论 0

项目中一直使用 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 类型。

先看 effect take() 的实现。

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();
}

我们通过添加了一种新的 effect fork ,启动了一个新的 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 的对比

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

山有枢

暂无简介

文章
评论
28 人气
更多

推荐作者

qq_K6tQnV

文章 0 评论 0

Shum1n

文章 0 评论 0

表情可笑

文章 0 评论 0

qq_mmilXo

文章 0 评论 0

tmzg0000

文章 0 评论 0

离旧人

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文