メインコンテンツへ移動

Redux ソースコード解説

無料2017-08-27#JS#redux原理#redux源码分析#redux souce analysis#redux内部原理#redux与vuex

700 行以上、それほど難しくない

前置き

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 つは createStoreapplyMiddleware で、地位は coreplugin に相当します

二.設計理念

コア思路は Flux と同じ:

(state, action) => state

ソースコード(createStore/dispatch())での体现:

try {
  isDispatching = true
  // state を再計算
  // (state, action) => state の Flux 基本思路
  currentState = currentReducer(currentState, action)
} finally {
  isDispatching = false
}

currentStateaction をトップレベル 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(...)
}

コード混淆は isCrushedname を変更し、検出基準として使用

干渉なし 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 ブランチを開き(ない場合)、読み取り時に devmaster に 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 つの作用:

  • 特殊 typecombineReducer 中で 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 を流れ、最後の環は元の dispatchreducer 計算プロセスに入る

したがってミドルウェア構造中の高階関数各層には特定の作用

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 は一波蓄謀已久的な変化がある可能性

コメント

コメントはまだありません

コメントを書く