blog icon indicating copy to clipboard operation
blog copied to clipboard

redux-saga源码学习

Open rudyxu1102 opened this issue 4 years ago • 0 comments

前言

好奇redux-saga是如何实现副作用的流程的,下面一起来看看源码。

注意:本次阅读redux-saga源码版本是1.1.3

示例demo

main.js

import * as React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import Counter from './components/Counter'
import reducer from './reducers'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer, applyMiddleware(sagaMiddleware))

sagaMiddleware.run(rootSaga)

const action = type => store.dispatch({ type })

function render() {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => action('INCREMENT')}
      onDecrement={() => action('DECREMENT')}
      onIncrementIfOdd={() => action('INCREMENT_IF_ODD')}
      onIncrementAsync={() => action('INCREMENT_ASYNC')}
    />,
    document.getElementById('root'),
  )
}

render()
store.subscribe(render)

reducer.js

export default function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'INCREMENT_IF_ODD':
      return (state % 2 !== 0) ? state + 1 : state
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

saga.js

import { put, takeEvery, delay } from 'redux-saga/effects'

export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

export default function* rootSaga() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

源码调试

注册中间件

执行createSagaMiddleware方法。

在sagaMiddleware中通过next(action)命中reducer逻辑,然后再执行channel.put,执行action对应的回调函数

export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
  let boundRunSaga
  // ...
  function sagaMiddleware({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, {
      ...options,
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
    })

    return next => action => {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      // 执行reducer的逻辑   
      const result = next(action) // hit reducers
      // 执行回调
      channel.put(action)
      return result
    }
  }

  sagaMiddleware.run = (...args) => {
    //...
    return boundRunSaga(...args)
  }

  // ...

  return sagaMiddleware
}

channel是redux-saga存储回调和触发回调的对象,通过执行stdChannel获取,在stdChannel方法内部会执行multicastChannel

  • 执行take方法,将回调存储到currentTakers数组中,并将matcher赋值给回调函数的MATCH属性
  • 执行put方法,如果action与MATCH函数相匹配,则触发相应的回调函数,
export function multicastChannel() {
  let closed = false
  let currentTakers = []
  let nextTakers = currentTakers
  //...

  return {
    put(input) {
      //...
      const takers = (currentTakers = nextTakers)

      for (let i = 0, len = takers.length; i < len; i++) {
        const taker = takers[i]

        if (taker[MATCH](input)) {
          taker.cancel()
          taker(input)
        }
      }
    },
    take(cb, matcher = matchers.wildcard) {
      //...
      cb[MATCH] = matcher
      ensureCanMutateNextTakers()
      nextTakers.push(cb)

      cb.cancel = once(() => {
        ensureCanMutateNextTakers()
        remove(nextTakers, cb)
      })
    }
  }
}

那什么时候执行channel.take存储对应的回调函数呢?

执行中间件的run方法

sagaMiddleware.run方法中,会执行proc函数,proc函数会自动执行generator。

co模块原理相同,都在内部封装了next函数,自动执行generator。

// co模块的简单实现
function co (gen) {
    const g = gen();
   
    function next(val) {
        const stage = g.next(val);
        if (stage.done) {
            return stage.value;
        }
        if (!!stage.value.then) {
            stage.value.then(next);
        } else {
            stage.value(next);
        }
    };

    next()
};

proc函数的next方法,会调用digestEffect函数,去执行传入effect对象。

function next(arg, isErr) {
    let result

    // ...

    if (!result.done) {
        digestEffect(result.value, parentEffectId, next)
    } 
}

不同的effect对象会根据type值从effectRunnerMap中取出相应的执行函数。例如fork类型的effect会执行runForkEffect,take类型的effect回去执行runTakeEffect

function runEffect(effect, effectId, currCb) {
    //...
    const effectRunner = effectRunnerMap[effect.type]
    effectRunner(env, effect.payload, currCb, executingContext)
   
}

参考链接

rudyxu1102 avatar Jan 23 '21 10:01 rudyxu1102