The author is Joey, Ant Financial · Data Experience Technology team

Redux-saga has been used throughout the project to handle the flow of asynchronous actions. I am curious about how effect is implemented. Took time to study his implementation. This article will not describe the basic API and benefits of Redux-Saga, but just how it works. Feel free to leave a comment in the comments section.

preface

The code for redux-Saga to listen to action is as follows:

import { takeEvery } from 'redux-saga';

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

How exactly does generator implement takeEvery? Let’s start with the slightly simpler take implementation:

Implementation principle of Take

We will try to write a demo that uses the saga approach to listen on action with generator.

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

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

To read the value of the action when $BTN is clicked.

channel

Here we need to introduce a concept – channel.

A channel is an abstraction of the event source. It registers a take method, executes a take once when a PUT is triggered, and then destroys it.

A simple implementation of a channel is as follows:

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();
Copy the code

We use the channel to connect the GENERATOR to the DOM event and rewrite the DOM event as follows:

$btn.addEventListener('click', () => {
  const action =`action data${i++}`;
  chan.put(action);
}, false);
Copy the code

When put is triggered, the Taker executes if there is already a registered taker in the channel.

We need to register the actual method to run by calling the channel’s take method before the PUT triggers.

Let’s move on to the implementation in mainSaga.

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

This take is an effect type from Saga.

Let’s look at the implementation of effectTake ().

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

Copy the code

Unexpectedly, only an object with a type was returned.

In fact, all effects returned by Redux-saga are pure objects with types.

So when exactly does a channel’s take method trigger? You also need to look at the code that calls mainSaga for the reason.

The characteristic of a generator is that at a certain stage of execution, control can be given to external code, which, after receiving the returned results, decides what to do.

task

Here we introduce a new concept task.

Task is the execution environment for generator methods, and all saga generator methods run in task.

A simple implementation of Task is as follows:

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);Copy the code

When yield Take () is run and the result returned by take() is given to the outer task, control of the code has been transferred from the Gennerator method to the Task.

The value of result.value is the result returned by take() {type: ‘take’}.

Let’s look at the implementation of runTakeEffect:

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

At this point, we finally see where the channel’s take method is called.

The complete code is as follows:

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);
Copy the code

The overall process is to register a Taker in the channel through mainSaga. Once dom click occurs, the Put of the channel will be triggered, and the PUT will consume the registered Taker, thus completing the monitoring process of a click event.

Viewing online Demo

TakeEvery implementation principle

In the previous section, we implemented a single event listener just like Saga, but there is still a problem. We can only listen for one click. How can we listen for every click? Redux-saga provides a helper method called takeEvery. We tried to implement takeEvery in our simple version of saga.

function* takeEvery(worker) {
  yield fork(function* () {
    while(true) { const action = yield take(); worker(action); }}); }function* mainSaga() {
  yield takeEvery(action => {
    $result.innerHTML = action;
  });
}
Copy the code

A new effect method fork is used here.

fork

Fork starts a new task without blocking the execution of the original task. The code is modified as follows:

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; // Check whether effect is an iteratorif (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();
}
Copy the code

We started a new Task takeEvery by adding a new effectfork.

TakeEvery automatically puts a new taker into a channel when a put occurs.

We can only implement one taker in a channel at a time. The effect of while(true) is that every time a PUT triggers a taker, it automatically triggers the next method of the task passed in the runTakeEffect and puts another taker into the channel. So that you can listen for an endless stream of events.

The online demo

The nature of the effect

From the implementation above, we find that all yield effects are pure objects that send a signal to the execution container task on the outside of the generator telling the task what to do.

With this in mind, if we wanted to add an effect to cancel the task, we could easily implement it.

First we define a cancel method that sends a signal of cancel.

function cancel() {
  return {
    type: 'cancel'
  };
}
Copy the code

Then modify the task’s code so that it can actually perform cancel’s logic.

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

summary

This article introduces the principle of Redux-Saga by simply implementing several effect methods. To truly achieve all the functions of Redux-Saga, it only needs to add some details. As shown below:

Redux-saga is recommended for those interested in using generator. I recommend an article that uses generator to implement DOM event listening to continue exploring iterators in JS and comparing them to Observables

Interested students can follow the column or send your resume to ‘chaofeng. LCF ####alibaba-inc.com’. Replace (‘####’, ‘@’)

Original address: github.com/ProtoTeam/b…