This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!
This article starts with the main process, the hooks state management library has a long way to go
preface
Previously, I rarely used Redux because of the technology stack. Because at first I thought it was very unwise to use redux indiscriminately under hooks. I’ve always thought that we should focus on the main things and not add extra complexity and mental burden to the project.
But most of the current projects are based on DVA, which… It gives me a headache.
If you are familiar with DVA, you should know that it mainly uses Redux and Redux-Saga to encapsulate the data layer (also seal the router and sever). I always told my friends that if dVA was even a framework. Isn’t every project we do a framework when it encapsulates the basic functionality?
If we don’t want to use it, then we must come up with a better alternative. So I decided to start with DVA in order to come up with a state management library that would be suitable for all aspects.
But redux doesn’t have anything on its own, so why not start with Saga
1. What is a redux-saga
Redux-saga is a library for managing application Side effects (Side effects, such as asynchronously fetching data, accessing the browser cache, etc.). The goal is to make Side effects easier to manage, more efficient to perform, easier to test, and easier to handle when a failure occurs.
Well, it’s a bit too official. Redux-saga is a redux middleware, and the most common thing we use in our work is to handle asynchronous actions. When you think about middleware that deals with asynchrony you probably know that there’s another one that has similar functionality. That’s story – thunk
1.1 story – thunk
Redux-thunk is simple, and we all know it. Redux’s so-called middleware is basically all about Dispatch. Because native Dispatches can only dispatch normal actions, we have no way to handle asynchrony. But what if you let dispatch dispatch a function?
Like this,
dispatch((dispatch,getState) = >{
setTimeout(() = >{
dispatch({type:'xxx'})},1000)})Copy the code
That’s ok. That’s the idea behind Redux-Thunk, so its source code is very simple.
However, Redux-Saga is different from it. Redux-saga has more powerful functions, so it is more tedious to use
1.2 The simple case of Saga
Take a look at a demo using Saga
Let’s say we implement a little example like this
1.3 Code implementation
In the component
Click the asy Add button disPatch({type:’async_add’})
The following code
import { useSelector, useDispatch } from 'react-redux'
const App = () = > {
const number = useSelector( (state) = >state.number )
const disPatch = useDispatch()
const add =() = >{
disPatch({type:'add'})}const asyncAdd=() = >{
disPatch({type:'async_add'})}return (
<div>
{ number }
<button onClick={ add} >add</button>
<button onClick={ asyncAdd} >asy add</button>
</div>
);
}
export default App;
Copy the code
In the store
The store is the same, but the redux-Saga has to run. You also need to pass a saga to its run method
import {
createStore,
applyMiddleware
} from 'redux'
import createSagaMiddleware from 'redux-saga'
import { rootSaga } from './sagas'
const reducer = (state = {
number: 1,
}, action) = > {
switch (action.type) {
case 'add':
return {
...state,
number: state.number + 1
}
default:
return state
}
}
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
export default store
Copy the code
literacy
In redux-Saga, there are simply three types of saga. namely
- Root Saga can be used as the main entry of a saga, that is, to enable all Watch saga
- Watch Saga is responsible for monitoring dispatch behavior, and worker Saga will handle it
- Worker Saga finally processes the corresponding logic
Let’s look at the Saga code
// Our worker Saga: Increments asynchronously
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'add'})}// Our watcher Saga: Spawn a new incrementAsync task in each async_add action
export function* watchIncrementAsync() {
yield takeEvery('async_add', incrementAsync)
}
// notice how we now only export the rootSaga
// single entry point to start all Sagas at once
export function* rootSaga() {
yield all([
watchIncrementAsync(),
])
}
Copy the code
1.4 Basic flow of work
2. Core implementation
2.1 Several core concepts
Before implementation, a few core concepts
effect api
Some of the Saga Effect apis (I prefer to call them Saga directives) are as follows. So these are all methods, and they also return an object with actionCreate
channel
The pipeline in Saga, let’s just briefly say what it does.
First saga uses a generator. Consider a scenario where we want to dispatch an action to get the interface data and then update the state in the store. Is that what we’re going to do
(Watch, worker +-+)
Use take to listen for this specific action, then fetch data, and then use put to trigger the reducer execution to update the store state
function* getData() {
while (true) {
yield take('async_add')
const data = yield fetchData()
yield put({
type: 'updata'}}})Copy the code
Xiaohei: What’s the role of channel? Is that beside the point?
Uh, no, no. Here we go. This saga is definitely what we brought over to implement. Because the generator executes differently than a normal function, it needs to be executed to make it an iterator and then write a co to handle it.
So let’s say we have this iterator, it, and we want it to run down to a yield. So we need it.next() to begin with
You get the return value of the first yield expression, which is take(‘async_add’). Take is used to match a specific action. At this point we don’t know if the action has been fired, so the iterator cannot be executed.
At the same time, we need to call channel.take to save the function that it can continue to execute – namely, the next function (subscribe).
When the component’s action of type async_add is dispatched, we call channel.put and execute the saved function. That is to make this iterator go down.
Mm ~, don’t know so say, good not understand, (really not understand it doesn’t matter, continue to look down, for a while go through the final code ok). A channel is basically a publish subscribe. Let’s write a simple channel
/** * is just a publish-and-subscribe *@returns {take,put}* /
const channel = () = > {
let takers = []
/** Control the execution of co */
const take = (taker) = > {
taker.cancel = () = > {
takers = takers.filter(item= >item ! == taker); } takers.push(taker) }/** When the page has action come */
const put = (action) = > {
takers.forEach(taker= > {
taker.cancel();
taker(action)
})
}
return {
take,
put
}
}
export default channel
Copy the code
3. The source code
createSagaMiddleware
Redux’s middleware format is basically dead
The only difference here is that sagaMiddleware has a run method, and for the run method to get getState and Dispatch from the store. We need to do something inside the sagaMiddleware function. Because this is the only place to get what we want.
We can also call channel.put(action) here because the next function is the dispatch used by our component.
As soon as the goods arrive, notify the subscriber to deal with the corresponding logic. That is, make the iterator go down quickly
import createChannel from './createChannel';
import runSaga from './runSaga';
const createSagaMiddleware=() = >{
/** Create pipe */
let channel = createChannel();
let boundRunSaga;
/**sagaMiddleware */
const sagaMiddleware=({getState,dispatch}) = >{
/** The purpose is to get getState, dispatch, etc */ in runSaga
boundRunSaga = runSaga.bind(null,{getState,dispatch,channel})
return (next) = >{
return (action) = >{ next(action) channel.put(action); }}}/** Run method of sagaMiddleware */
sagaMiddleware.run=(saga) = >boundRunSaga(saga)
return sagaMiddleware
}
export default createSagaMiddleware
Copy the code
runSaga
This runSaga is the final run method
So what’s the main thing that’s going on here? What I’ve written here is very simple. You can ignore immediately, I copied it from the source code…
You can understand that functions passed to immediately are called immediately and return
Just like that return immediately (cb) ===return cb()
The main idea here is to hand over the iterator to a CO, which is obviously what the proc does here
import {
immediately
} from './scheduler'
import proc from './proc'
/**
* run saga
* @param {*} param0
* @param {*} saga
*/
const runSaga = (env, saga) = > {
let it = typeof saga === 'function' ? saga() : saga
return immediately(() = > {
const task = proc(env, it)
return task
})
}
export default runSaga
Copy the code
proc
The logic of CO mainly uses recursion. Here, the corresponding processing is mainly done through the type of effect passed by the yiled expression
const proc = (env, it) = > {
const {
getState,
dispatch,
channel
} = env
const next = (value, isError) = > {
let result
result = it.next(value)
let {
value: effect,
done
} = result
if(! done) {switch (effect.type) {
case 'take':
channel.take(next)
break;
case 'put':
dispatch(effect.actionType);
next()
break;
default:
if(typeof effect.then==='function'){
effect.then(res= >{
if(res){
next(res)
}else{
next()
}
})
}
break; }}else{
return
}
}
next()
}
export default proc
Copy the code
effect
This one is a little bit easier, and it says it’s pretty much the same as actionCreate.
export const take = (actionType) = > {
return {
type: 'take',
actionType
};
}
export const put = (actionType) = > {
return {
type: 'put',
actionType
};
}
Copy the code
4. Try it out
Come on, the basics are all written down. Now it’s time for mules and horses to take a walk. Replace everything about Saga with your own
An interface requests demo
Let’s see what happens first
My mock data
Our saga
import {
take,
put
} from '.. /redux-saga/effect'
const fetchData = () = > fetch('http://localhost:5001/demo').then(res= > res.json())
function* demo() {
while (true) {
yield take('async_add')
const data = yield fetchData()
yield put({
type: 'updata'.payload:data.data
})
}
}
export default demo
Copy the code
Change some store things
import {
createStore,
applyMiddleware
} from 'redux'
import createSagaMiddleware from '.. /redux-saga'
// import { rootSaga } from './sagas'
import demo from './my_saga'
const reducer = (state = {
number: 1,
data: ' '
}, action) = > {
switch (action.type) {
case 'add':
return {
...state,
number: state.number + 1
}
case 'updata':
return {
...state,
data: action.payload
}
default:
return state
}
}
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(demo)
export default store
Copy the code
Component code tuning
import { useSelector, useDispatch } from 'react-redux'
const App = () = > {
const state = useSelector( (state) = >{return {number:state.number,data:state.data}} )
const disPatch = useDispatch()
const add =() = >{
disPatch({type:'add'})}const asyncAdd=() = >{
disPatch({type:'async_add'})}return (
<div>
{ state.number }
{ state.data }
<button onClick={ add} >add</button>
<button onClick={ asyncAdd} >asy add</button>
</div>
);
}
export default App;
Copy the code
5. Write to the end
My code here is somewhat different from the source code, mainly for the convenience of implementation. The emphasis is still on learning ideas.
Now only run through some simple API, there is a need to take to github: github.com/gong9/saga-…
Reference:
zhuanlan.zhihu.com/p/30098155
Github.com/redux-saga/…
redux-saga-in-chinese.js.org/