앞에 쓰는 말
API 설계가 매우 정간한 라이브러리로, 몇 가지 정교한 작은 테크닉과 함수형의 맛이 있음
一.구조
src/
│ applyMiddleware.js
│ bindActionCreators.js
│ combineReducers.js
│ compose.js
│ createStore.js
│ index.js
│
└─utils/
warning.js
index 는 모든 API 를 공개:
export {
createStore, // 중요
combineReducers, // reducer 조합 helper
bindActionCreators, // dispatch 를 wrap
applyMiddleware, // 미들웨어 메커니즘
compose // 덤, 함수 조합 util
}
가장 코어인 2 개는 createStore 와 applyMiddleware 로, 지위는 core 와 plugin 에 해당
二.설계 이념
코어 사고는 Flux 와 동일:
(state, action) => state
소스코드 (createStore/dispatch()) 에서의 구현:
try {
isDispatching = true
// state 를 재계산
// (state, action) => state 의 Flux 기본 사고
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
currentState 와 action 을 탑레벨 reducer 에 전달, reducer 트리를逐層계산하여 새로운 state 를 획득
dispatcher 의 개념은 없으며, 각 action 이 올 때마다, 탑레벨 reducer 에서 시작하여 전체 reducer 트리를 흐르고, 각 reducer 는 자신이 관심 있는 action 만에 관심 가지며,一小塊의 state 를제조하고, state 트리는 reducer 트리에 대응하며, reducer 계산 프로세스가 종료되면, 새로운 state 가 얻어지고, 이전 state 를 폐기
P.S.Redux 의 더 많은 설계 이념 (action, store, reducer 의 작용 및 어떻게 이해하는가) 에 대해서는, Redux 참조
三.테크닉
minified 감지
function isCrushed() {}
// min 감지, 비생산 환경에서 min 을 사용한다면, 경고
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
// warning(...)
}
코드 혼동은 isCrushed 의 name 을 변경하며, 감지 기준으로 사용
간섭 없음 throw
// 소상세, 모든 예외에서 브레이크포인트를 유효하게 할 때 콜스택을 추적 가능, 무효해도 영향 없음
// 생산 환경에서도 보유 가능
try {
throw new Error('err')
} catch(e) {}
velocity 에서 사용되는 비동기 throw 테크닉과 비교:
/!!! 테크닉, 비동기 throw, 로직 플로우에 영향 없음
setTimeout(function() {
throw error;
}, 1);
둘 다 로직 플로우에 영향 없지만, 간섭 없음 throw 의장점은 콜스택 등의 컨텍스트 정보를 잃지 않는 것으로, 상세는 이하:
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
이 테크닉에는 적절한 이름이 없음 (master-dev queue 도適当에 붙인 것이지만, 비교적形象的), 일단가변 큐라고 부름:
// 2 개의 큐, current 는 직접 수정 불가, next 에서 동기, master 와 dev 의 관계와 같음
// listener 실행 프로세스가 간섭되지 않는 것을 보증
// subscribe() 시 listener 큐가 실행 중인 경우, 신규 등록의 listener 는 다음부터 유효
let currentListeners = []
let nextListeners = currentListeners
// nextListeners 를 백업으로,毎回 next 배열만 수정
// flush listener queue 전에 동기
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
쓰기와 읽기에 몇 가지 추가 조작이 필요:
// 쓰기
ensureCanMutateNextListeners();
updateNextListeners();
// 읽기
currentListeners = nextListeners;
쓰기 시에 새로 dev 브랜치를 열고 (없는 경우), 읽기 시에 dev 를 master 에 merge 하고 dev 브랜치를 삭제하는 것과 동등
listener 큐シーン에서 매우 적절하게 사용:
// 쓰기 (구독/구독 해제)
function subscribe(listener) {
// 空降을 허가하지 않음
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// 跳차를 허가하지 않음
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// 읽기 (flush queue 모든 listener 를 실행)
// 2 개의 listener 배열을 동기
// flush listener queue 프로세스가 subscribe/unsubscribe 에 간섭되지 않음
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
차를 운전하는 상황에 비유할 수 있음:
nextListeners 는 대기실, 발차 전에 대기실의 전원을 데려가고, 대기실을 폐쇄
차가 발차한 후에 승차하고 싶은 사람 (subscribe()) 이 있는 경우, 새로 대기실을 엶 (slice())
사람은 먼저 대기실에 들어가고, 다음 편으로 데려가지며, 空降을 허가하지 않음
하차 시에도 동일, 차가 정차하지 않은 경우, 먼저 대기실을 통해 누가 하차하는지를 기록하고, 다음 편에서는 데려가지 않음, 跳차를 허가하지 않음
매우 재미있는테크닉으로, git 워크플로우와 신사함
compose util
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
함수 조합을 실현하기 위해 사용:
compose(f, g, h) === (...args) => f(g(h(...args)))
코어는 reduce (즉 reduceLeft), 구체적인 프로세스는 이하:
// Array reduce API
arr.reduce(callback(accumulator, currentValue, currentIndex, array)[, initialValue])
// 입력 -> 출력
[f1, f2, f3] -> f1(f2(f3(...args)))
1.((a, b) => (...args) => a(b(...args)))(f1, f2) 를 실행
accumulator = (...args) => f1(f2(...args)) 를 획득
2.((a, b) => (...args) => a(b(...args)))(accumulator, f3) 를 실행
accumulator = (...args) => ((...args) => f1(f2(...args)))(f3(...args)) 를 획득
accumulator = (...args) => f1(f2(f3(...args))) 를 획득
2 개의순서에 주의:
파라미터 구치는 내로부터 외: f3-f2-f1 즉 오른쪽으로부터 왼쪽
함수 호출은 외로부터 내: f1-f2-f3 즉 왼쪽으로부터 오른쪽
applyMiddleware 부분에서 이 순서가 사용되며, 파라미터 구치 프로세스 bind next (오른쪽으로부터 왼쪽), 함수 호출 프로세스 next() 꼬리 발동 (왼쪽으로부터 오른쪽). 따라서미들웨어는 비교적 기괴하게 보임:
// 미들웨어 구조
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
이유가 있음
자신의 메커니즘을 충분히 이용
당초 비교적 의문에思った 점:
function createStore(reducer, preloadedState, enhancer) {
// 최초의 state 를 계산
dispatch({ type: ActionTypes.INIT })
}
명확히 직접 할 수 있음, 예를 들어 store.init(), 왜 자신이 dispatch 를 통할 필요가 있는가?실제로는 2 개의 작용:
-
특수
type가combineReducer중에서reducer되돌림 값의 합법성 검사에 사용되며, 간단한action용례로서 -
이 때의
state가 초기로,reducer계산을 거치지 않은 것을 나타냄
reducer 합법성 검사 시에 이 초기 action 을 직접 던져 넣어 2 회 실행하고, 1 개의 action case 를 절약, 더욱이 초기 환경의 식별 변수와 추가의 store.init 메소드도 절약
자신의 dispatch 메커니즘을 충분히 이용하며, 매우현명한做法
四.applyMiddleware
이 부분의 소스코드가 가장 challenge 되며, 비교적 혼란스럽게 보이고, 이해하기 어려움
다시 한번 미들웨어의 구조를 봄:
// 미들웨어 구조
// fn1 fn2 fn3
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
왜이렇게 추한 고계 함수를 사용할 필요가 있는가?
function applyMiddleware(...middlewares) {
// 각 middleware 에 {getState, dispatch} 를 주입 fn1 을 벗김
chain = middlewares.map(middleware => middleware(middlewareAPI))
// fn = compose(...chain) 는 reduceLeft 로 왼쪽으로부터 오른쪽에 체인식으로 조합
// fn(store.dispatch) 는 원래의 dispatch 를 전달, 마지막 next 로서 (최내층 파라미터)
// 상일보 파라미터 구치 프로세스는 오른쪽으로부터 왼쪽에 next 를 주입 fn2 를 벗김
// 개찬된 disoatch 를 호출할 때, 왼쪽으로부터 오른쪽에 action 을 전달
// action 은 먼저 next 체인 순서로 모든 middleware 를 흐르고, 마지막 고리는 원래의 dispatch, reducer 계산 프로세스에 들어감
dispatch = compose(...chain)(store.dispatch)
}
fn2 가 어떻게 벗겨지는지에 중점 주목:
// 파라미터 구치 프로세스는 오른쪽으로부터 왼쪽에 next 를 주입 fn2 를 벗김 dispatch = compose(...chain)(store.dispatch)
주석의通り:
-
fn = compose(...chain)는 reduceLeft 로 왼쪽으로부터 오른쪽에 체인식으로 조합 -
fn(store.dispatch)는 원래의dispatch를 전달, 마지막next로서 (최내층 파라미터) -
상일보 파라미터 구치 프로세스는 오른쪽으로부터 왼쪽에 next 를 주입 fn2 를 벗김
reduceLeft 파라미터 구치 프로세스를 이용하여 bind next
호출 프로세스를 다시 봄:
-
개찬된
disoatch를 호출할 때, 왼쪽으로부터 오른쪽에action을 전달 -
action은 먼저next체인 순서로 모든middleware를 흐르고, 마지막 고리는 원래의dispatch,reducer계산 프로세스에 들어감
따라서 미들웨어 구조 중의 고계 함수각 층에는 특정의 작용:
fn1 middlewareAPI 주입을 받아들임
fn2 next bind 를 받아들임
fn3 dispatch API 를 실현 (action 을 수신)
applyMiddleware 는 리팩토링되며, 더욱 명확한 버전은 pull request#2146 참조, 코어 로직은 이대로, 리팩토링은 break change 를 수행할지 여부, 경계 case 를 서포트할지 여부, 충분히 읽기 쉬운가 (많은 사람이 이 몇 행의 코드에 주목, 관련 issue/pr 은 적어도 수십 개) 등을 고려할 가능성이 있으며, Redux 유지 팀은 비교적 신중하며, 이 부분의 혼란성은 매우 많은 회 질의되어서 리팩토링을 결정
五.소스코드 분석
Git 주소:https://github.com/ayqy/redux-3.7.0
P.S.주석은 충분히 상세.최신的是 3.7.2 이지만, 큰 차이는 없으며, 4.0 은一波蓄謀已久的인 변화가 있을 가능성
아직 댓글이 없습니다