Preface
A library with very concise API design, has some exquisite small tricks and functional programming flavor
I. Structure
src/
│ applyMiddleware.js
│ bindActionCreators.js
│ combineReducers.js
│ compose.js
│ createStore.js
│ index.js
│
└─utils/
warning.js
index exposes all APIs:
export {
createStore, // Key
combineReducers, // reducer combination helper
bindActionCreators, // wrap dispatch
applyMiddleware, // middleware mechanism
compose // Bonus, function composition util
}
The two most core things are createStore and applyMiddleware, status equivalent to core and plugin
II. Design Philosophy
Core idea is the same as Flux:
(state, action) => state
Reflected in source code (createStore/dispatch()):
try {
isDispatching = true
// Recalculate state
// (state, action) => state Flux basic idea
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
Pass currentState and action into top-level reducer, calculated layer by layer through reducer tree to get new state
No dispatcher concept, each action comes, starts from top-level reducer and flows through entire reducer tree, each reducer only focuses on action it's interested in, creates a small piece of state, state tree corresponds to reducer tree, when reducer calculation process ends, get new state, discard previous state
P.S. For more Redux design philosophy (roles of action, store, reducer and how to understand), please see Redux
III. Tricks
minified Detection
function isCrushed() {}
// min detection, warn if using min in non-production environment
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
// warning(...)
}
Code obfuscation changes isCrushed's name, used as detection basis
Non-interfering throw
// Small detail, can trace call stack when enabling all exception breakpoints, doesn't affect if not enabled
// Can also be kept in production environment
try {
throw new Error('err')
} catch(e) {}
Compare with async throw trick used in velocity:
/!!! Trick, async throw, doesn't affect logic flow
setTimeout(function() {
throw error;
}, 1);
Both don't affect logic flow, non-interfering throw's advantage is not losing call stack and other context information, specifically:
This error was thrown as a convenience so that if you enable "break on all exceptions" in your console, it would pause the execution at this line.
master-dev queue
This trick doesn't have a very suitable name (master-dev queue was also casually named, but quite vivid), let's call it mutable queue:
// 2 queues, current can't be directly modified, need to sync from next, like master and dev relationship
// Used to ensure listener execution process is not interfered
// If subscribe() when listener queue is executing, newly registered listener takes effect next time
let currentListeners = []
let nextListeners = currentListeners
// Use nextListeners as backup, only modify next array each time
// Sync before flush listener queue
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
Writing and reading need some extra operations:
// Write
ensureCanMutateNextListeners();
updateNextListeners();
// Read
currentListeners = nextListeners;
Equivalent to opening a new dev branch when writing (if doesn't exist), merge dev to master and delete dev branch when reading
Very suitable for listener queue scenario:
// Write (subscribe/unsubscribe)
function subscribe(listener) {
// Not allowed to parachute in
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// Not allowed to jump off
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// Read (flush queue execute all listeners)
// Sync two listener arrays
// flush listener queue process not interfered by subscribe/unsubscribe
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
Can analogize to driving scenario:
nextListeners is waiting room, take all people from waiting room before driving, close waiting room
After car leaves, if someone wants to get on (subscribe()), open a new waiting room (slice())
People enter waiting room first, taken away next trip, not allowed to parachute in
Getting off is the same, if car hasn't stopped, first record who wants to get off through waiting room, won't take him next trip, not allowed to jump off
Very interesting trick, similar to git workflow
compose util
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
Used to implement function composition:
compose(f, g, h) === (...args) => f(g(h(...args)))
Core is reduce (i.e., reduceLeft), specific process as follows:
// Array reduce API
arr.reduce(callback(accumulator, currentValue, currentIndex, array)[, initialValue])
// Input -> Output
[f1, f2, f3] -> f1(f2(f3(...args)))
1. Do ((a, b) => (...args) => a(b(...args)))(f1, f2)
Get accumulator = (...args) => f1(f2(...args))
2. Do ((a, b) => (...args) => a(b(...args)))(accumulator, f3)
Get accumulator = (...args) => ((...args) => f1(f2(...args)))(f3(...args))
Get accumulator = (...args) => f1(f2(f3(...args)))
Notice two orders:
Parameter evaluation from inside out: f3-f2-f1 i.e., from right to left
Function call from outside in: f1-f2-f3 i.e., from left to right
applyMiddleware part uses this order, in parameter evaluation process bind next (from right to left), in function call process next() tail trigger (from left to right). So middleware looks quite strange:
// Middleware structure
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
Has its reasons
Fully Utilize Own Mechanism
Initially quite puzzled point:
function createStore(reducer, preloadedState, enhancer) {
// Calculate first state
dispatch({ type: ActionTypes.INIT })
}
Obviously could be more direct, like store.init(), why must go through dispatch itself? Actually has 2 purposes:
-
Special
typeused incombineReducerasreducerreturn value legality check, as a simpleactionuse case -
And marks that current
stateis initial, uncalculated byreducer
When checking reducer legality, directly threw this initial action in to execute twice, saved one action case, additionally saved initial environment identifier variable and extra store.init method
Fully utilized own dispatch mechanism, quite clever approach
IV. applyMiddleware
This part of source code is challenged most, looks quite confusing, somewhat difficult to understand
Look at middleware structure again:
// Middleware structure
// fn1 fn2 fn3
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
Why must use such ugly higher-order function?
function applyMiddleware(...middlewares) {
// Inject {getState, dispatch} to every middleware, strip fn1
chain = middlewares.map(middleware => middleware(middlewareAPI))
// fn = compose(...chain) is reduceLeft chained combination from left to right
// fn(store.dispatch) pass original dispatch in, as last next
// Parameter evaluation process inject next from right to left, strip fn2, get series of (action) => {} standard dispatch combination
// When calling tampered dispatch, pass action from left to right
// Action first flows through all middleware according to next chain order, last ring is original dispatch, enter reducer calculation process
dispatch = compose(...chain)(store.dispatch)
}
Focus on how fn2 is stripped:
// Parameter evaluation process inject next from right to left, strip fn2 dispatch = compose(...chain)(store.dispatch)
As comments:
-
fn = compose(...chain)is reduceLeft chained combination from left to right -
fn(store.dispatch)passes originaldispatchin, as lastnext(innermost parameter) -
Previous step parameter evaluation process inject next from right to left, strip fn2
Use reduceLeft parameter evaluation process bind next
Then look at call process:
-
When calling tampered
dispatch, passactionfrom left to right -
actionfirst flows through allmiddlewareaccording tonextchain order, last ring is originaldispatch, enterreducercalculation process
So each layer of higher-order function in middleware structure has specific purpose:
fn1 Accept middlewareAPI injection
fn2 Accept next bind
fn3 Implement dispatch API (receive action)
applyMiddleware will be refactored, clearer version see pull request#2146, core logic is like this, refactoring might consider whether to do break change, whether to support boundary cases, whether readable enough (many people focus on these few lines of code, related issue/pr at least dozens) etc., Redux maintenance team is quite cautious, this part's confusion was questioned many times before deciding to refactor
V. Source Code Analysis
Git address: https://github.com/ayqy/redux-3.7.0
P.S. Comments are detailed enough. Although latest is 3.7.2 now, won't have big differences, 4.0 might have a wave of long-planned changes
No comments yet. Be the first to share your thoughts.