从 0 到 1 实现一个 redux
MIT License
Redux Redux Vue Redux Hello World
TMD reduxreact-reduxredux-toolkit redux Redux Redux Vuex 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)
subscribe
Redux Redux Redux
Redux
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
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
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
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 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
createStore
enhancer
createStore
createStore
createStore
enhance 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
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
}
}
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 reducerobservable
tc39 RxJSenhancer
createStore
createStore
enhancer applyMiddlewares
applyMiddlewares
applyMiddlewares
compose
dispatch dispatchcompose
dispatch dispatch Array.reduce
mid1(mid2(mid3()))
combineReducers
reducer reducer dispatch map reducer Slice
combineActionCreators
actionCreators () => dispatch(actionCreator())
Redux