The original address
preface
The author has been doing some background projects recently, using Ant Design Pro, which uses Redux-Saga to process asynchronous data flow. This paper will make a simple interpretation of the principle of Redux-Saga, and will implement a simple version of Redux-Saga.
Automatic flow control of Generator functions
In Redux-saga, saga refers to some long-running operation, represented by a generator function. The power of generator functions lies in their ability to manually pause, resume execution, and interact with data outside the function itself, as shown in the following example:
function *gen() {
const a = yield 'hello';
console.log(a);
}
cont g = gen();
g.next(); // { value: 'hello', done: false }
setTimeout((a)= > g.next('hi'), 1000) // when a => 'hi' print 'hi' after 1 second
Copy the code
It can be seen that when the genrator function performs the next operation is completely dependent on the external scheduling timing, and its internal execution state is also determined by the external input, which makes it convenient for the generator function to perform asynchronous flow control. For example, we first read the contents of a file as a query parameter, then request a query interface and print out the returned contents:
function getParams(file) {
return new Promise(resolve= > {
fs.readFile(file, (err, data) => {
resolve(data)
})
})
}
function getContent(params) {
// Request returns promise
return request(params)
}
function *gen() {
const params = yield getParams('config.json');
const content = yield getContent(params);
console.log(content);
}
Copy the code
We can manually control the execution of the gen function:
const g = gen();
g.next().value.then(params= > {
g.next(params).value.then(content= >{ g.next(content); })})Copy the code
The above works for our purpose, but it is too tedious. We want the generator function to execute automatically. We can write a simple automatic execution function as follows:
function genRun(gen) {
const g = gen();
next();
function next(err, pre) {
let temp;
(err === null) && (temp = g.next(pre)); (err ! = =null) && (temp = g.throw(pre));
if(! temp.done) { nextWithYieldType(temp.value, next); }}}function nextWithYieldType(value, next) {
if(isPromise(value)) {
value
.then(success= > next(null, success))
.catch(error= > next(error))
}
}
genRun(gen);
Copy the code
The generator function is then automatically executed. In fact, we can see that the internal state of the generator is completely determined by the nextWithYieldType, and we can perform different processing logic depending on the yield type.
Effect
In fact, Sagamiddleware. run(saga) can be similar to genRun(saga), and saga is composed of one effect after another, so what is effect? An effect is a Plain Object JavaScript Object that contains instructions to be executed by Saga Middleware. Redux-saga provides many Effect creators, such as Call, PUT, take, etc.
function saga* () {
const result = yield call(genPromise);
console.log(result);
}
Copy the code
Call (genPromise) generates an effect that might look something like this:
{
isEffect: true.type: 'CALL'.fn: genPromise
}
Copy the code
In fact, effect only indicates intent, and the actual behavior is done by a nextWithYieldType like the one above, for example:
function nextWithYieldType(value, next) {
...
if(isCallEffect(value)) {
value.fn(). then(success => next(null, success)).catch(error => next(error))
}
}
Copy the code
The result is printed when the promise returned by the genPromise function is resolved.
Producers and consumers
Look at the following example
function *saga() {
yield take('TEST');
console.log('test... ');
}
sagaMiddleware.run(test);
Copy the code
Saga will block at take(‘TEST’) and will not continue until dispatch({type: ‘TEST’}) is executed (note that the dispatch method is wrapped by sagaMiddleware). This gives us the impression that Take isa producer waiting for disaptch’s consumption. In fact, take is just an Effect generator. The specific processing logic is still completed in nextWithYieldType, which is similar to:
function nextWithYieldType(value, next) {...// take('TEST') generates effect simply {isEffect: true, type: 'take ', name: 'TEST'}
if(isTakeEffect(value)) {
channel.take({pattern: value.name, cb: params= > next(null, params)})
}
}
Copy the code
A channel is a task generator that has two methods: take to generate a task and put to consume a task:
function channel() {
/* task = { pattern, cb } */
let _task = null;
function take(task) {
_task = task;
}
function put(pattern, args) {
if(! _task)return;
if(pattern == _task.pattern) _task.cb.call(null, args);
}
return {
take,
put
}
}
Copy the code
It is obvious that the task is consumed during dispatch, which is done in sagaMiddleware like this:
const sagaMiddleware = store= > {
return next= > action => {
next(action);
const { type, ...payload } = action;
channel.put(type, payload);
}
}
Copy the code
What we need to do is to constantly improve the nextWithYieldType function. After completing the corresponding logic of PUT, fork and takeEvery, a Redux-saga with basic functions will be born. I will not repeat the implementation of these functions. Finally, you can check out tiny-Redux-Saga here, which is a simple version of Redux-Saga that I implemented to help you.
The full text.