my-redux

从 0 到 1 实现一个 redux

MIT License

Stars
13

redux

https://github.com/Haixiang6123/my-redux

https://www.npmjs.com/package/redux

Redux Redux Vue Redux Hello World

TMD reduxreact-reduxredux-toolkit redux Redux Redux Vuex React

Redux React

Redux React

Redux "A Predictable State Container for JS Apps" Vuex "Vuex is a state management pattern + library for Vue.js applications"

"react"

Redux for JS Apps Vuex for Vue.js applications

Redux Redux

  • store
  • getState dispatch dispatch(action)
  • dispatch subscribe

Redux Redux Redux

Redux

createStore

Object

function createStore(reducer, preloadedState, enhancer) {
  let currentState = preloadedState // 
  let currentReducer = reducer // 
  let isDispatching = false //  dispatch

  //  state
  function getState() {
    if (isDispatching) {
      throw new Error(' dispatching  state ')
    }
    return currentState
  }

  //  action 
  function dispatch(action) {
    if (isDispatching) {
      throw new Error(' dispatching dispatch ')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    return action
  }

  return {
    getState,
    dispatch
  }
}

currentState``getState dispatch reducer currentState

isDispatching dispatch

preloadedState currentState undefuned undefined state createStore dispatch action action reducer case reducer state reducer switch-case default state

//  toString(36)  36 
const randomString = () => Math.random().toString(36).substring(7).split('').join('.')

const actionTypes = {
  INIT: `@@redux/INIT${randomString()}`, // 
}

function createStore(reduce, preloadedState, enhancer) {
  ...

  //  state
  function getState() {
    ...
  }

  //  action 
  function dispatch(action) {
    ...
  }

  // 
  dispatch({type: actionTypes.INIT})

  return {
    getState,
    dispatch
  }
}

Redux ~

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return state + action.payload
    case 'decrement':
      return state - action.payload
    default:
      return state
  }
}

const store = createStore(reducer, 1) // 1 dispatch @@redux/INIT  state

store.dispatch({ type: 'increment', payload: 2 }) // 1 + 2

console.log(store.getState()) // 3

isPlainObject kindOf

Redux action action

//  action 
function dispatch(action: A) {
  if (!isPlainObject(action)) { // 
    throw new Error(` Object ${kindOf(action)} `) //  XXX 
  }
  ...
}

isPlainObject kindOf npm is-plain-object kind-of JS

npm

isPlainObject typeof obj === 'object' typeof null object JS type value JS type === 0 Object null 0x00 null type 0 typeof null === null

const isPlainObject = (obj: any) => {
  // 
  if (typeof obj !== 'object' || obj === null) return false

  //  constructor 
  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

export default isPlainObject

kindOf typeof Array, Date, Error

const isDate = (value: any) => { //  Date
  if (value instanceof Date) return true
  return (
    typeof value.toDateString === 'function' &&
    typeof value.getDate === 'function' &&
    typeof value.setDate === 'function'
  )
}

const isError = (value: any) => { //  Error
  if (value instanceof Error) return true
  return (
    typeof value.message === 'string' &&
    value.constructor &&
    typeof value.constructor.stackTraceLimit === 'number'
  )
}

const getCtorName = (value: any): string | null => { // 
  return typeof value.constructor === 'function' ? value.constructor.name : null
}

const kindOf = (value: any): string => {
  if (value === void 0) return 'undefined'
  if (value === null) return 'null'

  const type = typeof value
  switch (type) { // 
    case 'boolean':
    case 'string':
    case 'number':
    case 'symbol':
    case 'function':
      return type
  }

  if (Array.isArray(value)) return 'array' //
  if (isDate(value)) return 'date' //  Date
  if (isError(value)) return 'error' //  Error

  const ctorName = getCtorName(value)
  switch (ctorName) { // 
    case 'Symbol':
    case 'Promise':
    case 'WeakMap':
    case 'WeakSet':
    case 'Map':
    case 'Set':
      return ctorName
  }

  return type
}

Redux

replaceReducer

replaceReducer Code Spliting 2 JS reducer reducer combineReducers

const newRootReducer = combineReducers({
  existingSlice: existingSliceReducer,
  newSlice: newSliceReducer
})

store.replaceReducer(newRootReducer)

API

reducer

const actionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`
}

function createStore(reducer, preloadedState, enhancer) {
  ...
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer

    dispatch({type: actionTypes.REPLACE} as A) // 

    return store
  }
  ...
}

dispatch @@redux/REPALCE action

subscribe

Redux Easy ~ dispatch

function createStore(reducer, preloadedState, enhancer) {
  let currentState = preloadedState
  let currentReducer = reducer
  let currentListeners = [] // 
  let nextListeners = currentListeners // 
  let isDispatching = false

  //  state
  function getState() {
    if (isDispatching) {
      throw new Error(' dispatching  state ')
    }
    return currentState
  }

  //  action 
  function dispatch(action: A) {
    if (!isPlainObject(action)) {
      throw new Error(` Object ${kindOf(action)} `)
    }

    if (isDispatching) {
      throw new Error(' dispatching dispatch ')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    listeners.forEach(listener => listener()) // 

    return action
  }

  //  nextListeners  listeners 
  //  dispatching  bug
  function ensureCanMutateNextListeners() {
    if (nextListeners !== currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 
  function subscribe(listener: () => void) {
    if (isDispatching) {
      throw new Error(' dispatching subscribe ')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener) // 

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(' dispatching unsubscribe ')
      }

      isSubscribed = false

      ensureCanMutateNextListeners()

      // 
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }

  // 
  dispatch({type: actionTypes.INIT})

  return {
    getState,
    dispatch,
    subscribe,
  }
}

currentListeners nextListeners Bug isDispatching

subscribe unsubscribe side-effect side-effect useEffect

subscribe unsubscribe listener listener listener unsubscribe unsubscribe listener``listener

const store = createStore(reducer, 1)

const listener = () => console.log('hello')

const unsubscirbe = store.subscribe(listener)

// 1 + 2
store.dispatch({ type: 'increment', payload: 2 }) //  "hello"

unsubscribe()

// 3 + 2
store.dispatch({ type: 'increment', payload: 2 }) //  "hello"

observable

observable tc39 subscribe Observer Observer next

store store observable observable

const $$observable = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')()

export default $$observable


function createStore<S, A extends Action>(reducer preloadedState, enhancer) {
  ...
  //  observable/reactive 
  function observable() {
    const outerSubscribe = subscribe

    return {
      subscribe(observer: unknown) {
        function observeState() {
          const observerAsObserver = observer
          if (observerAsObserver.next) {
            observerAsObserver.next(getState())
          }
        }

        observeState() //  state
        const unsubscribe = outerSubscribe(observeState)
        return {unsubscribe}
      },
      [$$observable]() {
        return this
      }
    }
  }
  ...
}
const store = createStore(reducer, 1)

const next = (state) => state + 2 // 

const observable = store.observable()

observable.subscribe({next}) //  next 1 + 2

store.dispatch({type: 'increment', payload: 2}) // 1 + 2 + 3

next redux-observable

applyMiddlewares

createStore enhancer createStore createStore createStoreenhance createStore reducer preloadedState store

function createStore<S, A extends Action>(reducer, preloadedState, enhancer) {
  if (enhancer) {
    return enhancer(createStore)(reducer, preloadedState)
  }
  ...
}

enhancer applyMiddlewares dispatch dispatch store store enhancer

applyMiddlewares dispatch applyMiddlewares enhancer

dispatch console.log dispatch

let originalDispatch = store.dispatch
store.dispatch = (action) => {  
    let result = originalDispatch(action)  
    console.log('next state', store.getState())  
    return result
}

** dispatch action action result **

Logger 2

const logger1 = (store) => {
    let originalDispatch = store.dispatch
    
    store.dispatch = (action) => {
        console.log('logger1 before')
        let result = originalDispatch(action) //  dispatch
        console.log('logger 1 after')
        return result
    }
}

const logger2 = (store) => {
    let originalDispatch = store.dispatch
    
    store.dispatch = (action) => {
        console.log('logger2 before')
        let result = originalDispatch(action) // logger 1 
        console.log('logger2 after')
        return result
    }
}

logger1(store)
logger2(store)

// logger2 before -> logger1 before -> dispatch -> logger1 after -> logger2 after
store.dispatch(...)

** logger1 logger 2 store.dispatch dispatch store.dispatch dispatch**

logger1 logger2 dispatch

logger2 before -> logger1 before -> dispatch -> logger1 after -> logger2 after

applyMiddlewares

function applyMiddleware(store, middlewares) {
    middlewares = middlewares.slice()   //  
    middlewares.reverse() // 

    // dispatch   
    middlewares.forEach(middleware => store.dispatch = middleware(store))
}

dispatch reverse

forEach store.dispatch side-effect dispatch store.dispatch

dispatch dispatch action action dispatch dispatch

const dispatch1 = (dispatch) => {...}
const dispatch2 = (dispatch1) => {...}
const dispatch3 = (dispatch2) => {...}
...

store

const logger1 => (store) => (next) => (action) => {
    console.log('logger1 before')
    let result = next(action)
    console.log('logger 1 after')
    return result
}

const logger2 = (store) => (next) => (action) => {
    console.log('logger2 before')
    let result = next(action)
    console.log('logger2 after')
    return result
}

function applyMiddleware(store, middlewares) {
    //  dispatch
    let dispatch = (action) => {
      throw new Error(' middlewares dispatch')
    }

    middlewares = middlewares.slice() //  
    middlewares.reverse() // 

    const middlewareAPI = {
      getState: store.getState,
      //  dispatch dispatch 
      //  dispatch  dispatch Bug
      // 
      dispatch: (...args) => dispatch(args)
    }

    //  dispatch 
    const xxx = middlewares.map(middleware => middleware(middlewareAPI))
    ...
}

reduce compose

function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    return (arg) => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((prev, curt) => (...args: any) => prev(curt(...args)))
}

compose(logger1, logger2)

logger1(
  logger1 before
  logger2(
    logger2 before
    dispatch ->  dispatch
    logger2 after
  )
  logger2 after
)

Redux reducer reverse

applyMiddlewares enhancer

function applyMiddlewares(...middlewares: Middleware[]) {
  return (createStore) => (reducer: Reducer, preloadState) => {
    const store = createStore(reducer, preloadState)

    let dispatch = (action) => {
      throw new Error(' middlewares dispatch')
    }

    const middlewareAPI: MiddlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(args)
    }

    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {...store, dispatch}
  }
}

Redux

combineReducers

reducer reducer

const nameReducer = () => '111'
const ageReducer = () => 222

const reducer = combineReducers({
  name: nameReducer,
  age: ageReducer
})

const store = createStore(reducer, {
  name: 'Jack',
  age: 18
})

store.dispatch({type: 'xxx'}) // state => {name: '111', age: 222}
function combineReducers(reducers: ReducerMapObject) {
  return function combination(state, action: AnyAction) {
    let hasChanged = false
    let nextState = {}
    Object.entries(finalReducers).forEach(([key, reducer]) => {
      const previousStateForKey = state[key] // 
      const nextStateForKey = reducer(previousStateForKey, action) // 

      if (typeof nextStateForKey === 'undefined') {
        throw new Error(' undefined ')
      }

      nextState[key] = nextStateForKey // 
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 
    })

    // reducer  key  state  key 
    hasChanged = hasChanged || Object.keys(finalReducers).length === Object.keys(state).length

    return hasChanged ? nextState : null
  }
}

reducerMapObject reducer state key stateRedux reducer reducer reducer undefined

const randomString = () => Math.random().toString(36).substring(7).split('').join('.')

const actionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

function assertReducerShape(reducers: ReducerMapObject) {
  Object.values(reducers).forEach(reducer => {
    const initialState = reducer(undefined, {type: actionTypes.INIT})
    if (typeof initialState === 'undefined') {
      throw new Error(' dispatch  undefined')
    }

    const randomState = reducer(undefined, {type: actionTypes.PROBE_UNKNOWN_ACTION})
    if (typeof randomState === 'undefined') {
      throw new Error(' dispatch  undefined')
    }
  })
}

dispatch @@redux/INIT @@redux/PROBE_UNKNOWN_ACTION reducer case undefuned state action

function getUnexpectedStateShapeWarningMessage(
  inputState: object,
  reducers: ReducerMapObject,
  action: Action,
  unexpectedKeyCache: {[key: string]: true}
) {
  if (Object.keys(reducers).length === 0) {
    return ' reducer  combine '
  }

  if (!isPlainObject(action)) {
    return ' action  Object '
  }

  if (action.type === actionTypes.REPLACE) return //  replaceReducer reducer 

  //  reducerMapObject  key
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )
  unexpectedKeys.forEach(unexpectedKey => unexpectedKeyCache[unexpectedKey] = true)

  if (unexpectedKeys.length > 0) {
    return ` Key  state ${unexpectedKeys.join(', ')}`
  }
}

unexpectedKeyCache Map state true Map

combineReducers

function combineReducers(reducers: ReducerMapObject) {
  // 
  let finalReducers: ReducerMapObject = {}
  Object.entries(reducers).forEach(([key, reducer]) => {
    if (typeof reducer === 'function') {
      finalReducers[key] = reducer
    }
  }, {})

  let shapeAssertionError: Error
  try {
    //  reducer  undefined
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  //  key
  let unexpectedKeyCache: {[key: string]: true} = {}

  return function combination(state, action: AnyAction) {
    if (shapeAssertionError) throw shapeAssertionError

    const warningMessage = getUnexpectedStateShapeWarningMessage(
      state,
      finalReducers,
      action,
      unexpectedKeyCache
    )

    if (warningMessage) {
      console.log(warningMessage)
    }

    let hasChanged = false
    let nextState = {}
    Object.entries(finalReducers).forEach(([key, reducer]) => {
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)

      if (typeof nextStateForKey === 'undefined') {
        throw new Error(' undefined ')
      }

      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    })

    // reducer  key  state  key 
    hasChanged = hasChanged || Object.keys(finalReducers).length === Object.keys(state).length

    return hasChanged ? nextState : null
  }
}

combineActionCreators

action creator () => dispatch(actionCreator(xxx))

const store = createStore(reducer, 1)

const combinedCreators = combineActionCreators({
  add: (offset: number) => ({type: 'increment', payload: offset}), //  actionCreator
  minus: (offset: number) => ({type: 'decrement', payload: offset}), //  actionCreator
}, store.dispatch)

combinedCreators.add(100)
combinedCreators.minus(2)

combinedCreators .add(100) .add(100) dispatch

//  actionCreator
function bindActionCreator(actionCreator, dispatch) {
  return function (this: any, ...args: any[]) {
    return dispatch(actionCreator.apply(this, args))
  }
}

//  actionCreator
const combineActionCreators = (actionCreators, dispatch) => {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  const boundActionCreators: ActionCreatorsMapObject = {}

  Object.entries(actionCreators).forEach(([key, actionCreator]) => {
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  })

  return boundActionCreators
}

actionCreator dispatch action

combineActionCreators dispatch dispatch action

** actionCreator actionCreator dispatch**

redux API

undefined TS TS TS TS

  • getState
  • dispatch(action)
  • subscribe(listener) dispatch
  • replaceReducer reducer reducer
  • observable tc39 RxJS
  • enhancer createStore createStore enhancer applyMiddlewares applyMiddlewares
  • applyMiddlewares compose dispatch dispatch
  • compose dispatch dispatch Array.reduce mid1(mid2(mid3()))
  • combineReducers reducer reducer dispatch map reducer Slice
  • combineActionCreators actionCreators () => dispatch(actionCreator())

Redux