本文從將從Redux原理出發,一步步本身實現一個簡單的Redux,主要目的是瞭解Redux內部之間的聯繫。看本文以前先要知道Redux是怎麼用的,對Redux用法不會講解太多。javascript
首先要知道的是,Redux 和 React 沒有關係,Redux 能夠用在任何框架中。 Redux 是一個JavaScript 狀態管理器,是一種新型的前端「架構模式」。html
還有一般與redux一塊兒用的一個庫——react-redux, 它就是把 Redux 這種架構模式和 React.js 結合起來的一個庫,就是 Redux 架構在 React.js 中的體現。前端
從組建的角度看:java
從這個流程中能夠看出,Redux 的核心就是一個 觀察者 模式。一旦 store 發生了變化就會通知全部的訂閱者,視圖(在這裏是react組件)接收到通知以後會進行從新渲染。react
爲了簡化說明,我用和官網差很少的例子改寫來做案例 官網redux demogit
新建一個文件redux.js,而後直接引入,觀察控制檯輸出github
import { createStore } from 'redux'
const defaultState = {
value: 10
}
// reducer處理函數
function reducer (state = defaultState, action) {
console.log(state, action)
switch (action.type) {
case 'INCREMENT':
return {
...state,
value: state.value + 1
}
case 'DECREMENT':
return {
...state,
value: state.value - 1
}
default:
return state
}
}
const store = createStore(reducer)
const init = store.getState()
console.log(`一開始數字爲:${init.value}`)
function listener () {
const current = store.getState()
console.log(`當前數字爲:${current.value}`)
}
store.subscribe(listener) // 監聽state的改變
store.dispatch({ type: 'INCREMENT' })
// 當前數字爲:11
store.dispatch({ type: 'INCREMENT' })
// 當前數字爲:12
store.dispatch({ type: 'DECREMENT' })
// 當前數字爲:11
export default store
複製代碼
輸出結果:redux
{value: 10} {type: "@@redux/INIT1.a.7.g.7.t"}
一開始數字爲:10
{value: 10} {type: "INCREMENT"}
當前數字爲:11
{value: 11} {type: "INCREMENT"}
當前數字爲:12
{value: 12} {type: "DECREMENT"}
當前數字爲:11
複製代碼
全部對數據的操做必須經過 dispatch 函數,它接受一個參數action,action是一個普通的JavaScript對象,action必須包含一個type
字段,告訴它要修改什麼,只有它容許才能修改。數組
在每次調用進來reducer函數咱們都打印了state和action,咱們手動經過store.dispatch方法派發了三次action,但你會發現輸出了四次.這是由於Redux內部初始化就自動執行了一次dispatch方法,能夠看到第一次執行它的type對咱們數據來講是沒有影響的(由於type取值@@redux/INIT1.a.7.g.7.t
,咱們本身redux的數據type不會取名成這個樣子,因此不會跟它重複),即默認輸出state值bash
import { createStore } from 'redux'
// 傳入reducer
const store = createStore(reducer)
複製代碼
createStore 會返回一個對象,這個對象包含三個方法,因而咱們能夠列出Redux雛形。
新建mini-redux.js
export function createStore (reducer) {
const getState = () => {}
const subscribe = () => {}
const dispatch = () => {}
return {
getState,
subscribe,
dispatch
}
}
複製代碼
export function createStore (reducer) {
let currentState = {}
const getState = () => currentState
return {
getState
}
}
複製代碼
reducer
返回一個新狀態export function createStore (reducer) {
let currentState = {}
const getState = () => currentState
const dispatch = (action) => {
currentState = reducer(currentState, action) // 覆蓋原來的state
}
return {
getState,
dispatch
}
}
複製代碼
怎麼實現呢?咱們能夠直接使用subscribe函數把你要監聽的事件添加到數組, 而後執行dispatch方法的時候把listeners數組的監聽函數給執行一遍。
export function createStore (reducer) {
let currentState = {}
let currentListeners = [] // 監聽函數,可添加多個
const getState = () => currentState
const subscribe = (listener) => {
currentListeners.push(listener)
}
const dispatch = (action) => {
currentState = reducer(currentState, action) // 覆蓋原來的state
currentListeners.forEach(listener => listener())
}
return {
getState,
subscribe,
dispatch
}
}
複製代碼
翻開一開始咱們那個Redux例子,其實就是把store.getState()
添加進來,dispatch派發一個action後,reducer執行返回新的state,並執行了監聽函數store.getState()
,state的值就發生變化了。
function listener () {
const current = store.getState()
console.log(`當前數字爲:${current.value}`)
}
store.subscribe(listener) // 監聽state的改變
複製代碼
上述代碼,跟React依然沒有關係,只是純屬Redux例子。但想想當咱們把Redux和React一塊兒用的時候,還會多作這麼一步。
constructor(props) {
super(props)
this.state = store.getState()
this.storeChange = this.storeChange.bind(this)
store.subscribe(this.storeChange)
}
storeChange () {
this.setState(store.getState())
}
複製代碼
在React裏面監聽的方法,還要用this.setState()
, 這是由於React中state的改變必須依賴於this.setState
方法。因此對於 React 項目,就是組件的render方法或setState方法放入listen(監聽函數),纔會實現視圖的自動渲染,改變頁面中的state值。
最後一步,注意咱們上面說的,當初始化的時候,dispatch會先自動執行一次,繼續改代碼
export function createStore (reducer) {
let currentState = {}
let currentListeners = [] // 監聽器,可監聽多個事件
const getState = () => currentState
const subscribe = (listener) => {
currentListeners.push(listener)
}
const dispatch = (action) => {
currentState = reducer(currentState, action) // 覆蓋原來的state
currentListeners.forEach(listener => listener())
}
// 儘可能寫得複雜,使不會與咱們自定義的action有重複可能
dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' })
return {
getState,
subscribe,
dispatch
}
}
複製代碼
寫到這裏,咱們把引入的redux替換咱們寫的文件
import { createStore } from './mini-redux'
複製代碼
當咱們執行的時候,發現結果並不如咱們所願:
{} {type: "@@mini-redux/~GSDG4%FDG#*&"}
一開始數字爲:undefined
{} {type: "INCREMENT"}
當前數字爲:NaN
{type: "INCREMENT"}
當前數字爲:NaN
{value: NaN} {type: "DECREMENT"}
當前數字爲:NaN
複製代碼
這個怎麼回事呢?由於咱們寫的redux一開始就給state賦值爲{},在事實state初始值是由外部傳入的,一般咱們本身寫的時候會設置默認值
const defaultState = {
value: 10
}
function reducer (state = defaultState, action) {
switch (action.type) {
// ...
default:
return state
}
}
複製代碼
但在咱們Redux實現中卻把它手動置爲空對象,在這裏咱們暫時解決方法就是不給它賦值,讓它爲undefined,這樣reducer的默認參數就會生效。redux初始化第一次dispatch時,就會讓它自動賦值爲reducer傳入的第一個參數state默認值(ES6函數默認賦值),因此修改以下:
export function createStore (reducer) {
let currentState
let currentListeners = [] // 監聽器,可監聽多個事件
const getState = () => currentState
const subscribe = (listener) => {
currentListeners.push(listener)
}
const dispatch = (action) => {
currentState = reducer(currentState, action) // 覆蓋原來的state
currentListeners.forEach(listener => listener())
}
dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' })
return {
getState,
subscribe,
dispatch
}
}
複製代碼
這個mini-redux.js,咱們就能夠實現跟原來的redux徹底同樣的輸出效果了。
接下來咱們繼續補充知識點
createStore(reducer, [preloadedState], enhancer)
複製代碼
第二個參數 [preloadedState] (any)
是可選的: initial state
第三個參數enhancer(function)
也是可選的:用於添加中間件的
一般狀況下,經過 preloadedState 指定的 state 優先級要高於經過 reducer 指定的 state。這種機制的存在容許咱們在 reducer 能夠經過指明默認參數來指定初始數據,並且還爲經過服務端或者其它機制注入數據到 store 中提供了可能。
第三個參數咱們下篇會說,先繼續完善一下代碼,咱們須要對第二個和第三個可選參數進行判斷。
export function createStore (reducer, preloadedState, enhancer) {
// 當第二個參數沒有傳preloadedState,而直接傳function的話,就會直接把這個function當成enhancer
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// 當第三個參數傳了但不是function也會報錯
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
// reducer必須爲函數
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentState = preloadedState // 第二個參數沒傳默認就是undefined賦給currentState
let currentListeners = [] // 監聽器,可監聽多個事件
// ...
}
複製代碼
關於第三個參數判斷爲何返回 return enhancer(createStore)(reducer, preloadedState)
咱們下篇會說,這篇先忽略。 2. 咱們實現了store.subscribe()方法,但仍是不完整的,subscribe方法能夠添加監聽函數listener,它還有返回值,返回一個移除listener的函數;另外咱們依然要對類型進行判斷。
export function createStore (reducer, preloadedState, enhancer) {
// ...
let currentListeners = [] // 監聽器,可監聽多個事件
const subscribe = (listener) => {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
currentListeners.push(listener)
// 經過filter過濾,執行的時候將以前自己已經添加進數組的事件名移除數組
return () => {
currentListeners = currentListeners.filter(l => l !== listener);
}
}
// ...
}
複製代碼
也能夠經過找數組下標的方式移除listener
const subscribe = (listener) => {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
currentListeners.push(listener)
// 經過filter過濾,執行的時候將以前自己已經添加進數組的事件名移除數組
return () => {
let index = currentListeners.indexOf(listener)
currentListeners.splice(index, 1)
}
}
複製代碼
移除listener實際就是取消訂閱,使用方式以下:
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
unsubscribe(); // 取消監聽
複製代碼
export function createStore (reducer, preloadedState, enhancer) {
// ...
let isDispatching = false
const dispatch = (action) => {
// 用於判斷action是否爲一個普通對象
if (!isPlainObject(action)) {
throw new Error('Actions must be plain objects. ')
}
// 防止屢次dispatch請求同時改狀態,必定是前面的dispatch結束以後,才dispatch下一個
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = reducer(currentState, action) // 覆蓋原來的state
} finally {
isDispatching = false
}
currentListeners.forEach(listener => listener())
return action
}
}
// 用於判斷一個值是否爲一個普通的對象(普通對象即直接以字面量形式或調用 new Object() 所建立的對象)
export function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
// ...
複製代碼
isPlainObject函數中經過 while 不斷地判斷 Object.getPrototypeOf(proto) !== null 並執行, 最終 proto 會指向 Object.prototype. 這時再判斷 Object.getPrototypeOf(obj) === proto, 若是爲 true 的話就表明 obj 是經過字面量或調用 new Object() 所建立的對象了。
保持action對象是簡單對象的做用是方便reducer進行處理,不用處理其餘的狀況(好比function/class實例等)
至此,咱們實現了最基本能用的Redux代碼,下篇再繼續完善Redux代碼,最後放出基礎版Redux全部代碼:
export function createStore (reducer, preloadedState, enhancer) {
// 當第二個參數沒有傳preloadedState,而直接傳function的話,就會直接把這個function當成enhancer
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// 當第三個參數傳了但不是function也會報錯
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
// reducer必須爲函數
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentState = preloadedState // 第二個參數沒傳默認就是undefined賦給currentState
let currentListeners = [] // 監聽器,可監聽多個事件
let isDispatching = false
const getState = () => currentState
const subscribe = (listener) => {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
currentListeners.push(listener)
// 經過filter過濾,執行的時候將以前自己已經添加進數組的事件名移除數組
return () => {
currentListeners = currentListeners.filter(l => l !== listener);
}
}
const dispatch = (action) => {
// 用於判斷action是否爲一個普通對象
if (!isPlainObject(action)) {
throw new Error('Actions must be plain objects. ')
}
// 防止屢次dispatch請求同時改狀態,必定是前面的dispatch結束以後,才dispatch下一個
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = reducer(currentState, action) // 覆蓋原來的state
} finally {
isDispatching = false
}
currentListeners.forEach(listener => listener())
return action
}
dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' })
return {
getState,
subscribe,
dispatch
}
}
// 用於判斷一個值是否爲一個普通的對象(普通對象即直接以字面量形式或調用 new Object() 所建立的對象)
export function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
複製代碼
參考資料: