手寫實現redux,react-redux的api,中間件原理
redux源碼參考
準備工作
首先建一個React的腳手架項目, 刪除src下的所有東西,保留一個空的
index.js
當前目錄
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
src/ index.js
// 空 的js
需求1:js 實現渲染(風影嘎拉)三個字到 div 容器中
let state = {
name: '風影嘎拉',
color: '#f00',
}
function render(obj) {
const dom = document.getElementById('root')
dom.innerHTML = obj.name
dom.style.color = obj.color
}
render(state)
啓動服務:
當前的設計呢,存在諸多問題, 比如 state 變量誰都可以更改,不安全, 而每次更改時,不同的人可能會更改不同的值,
那麼,是不是可以通過派發動作的方式,來改變具體的屬性, 比如只改變名字, 只改變顏色, 接着下邊改進
- 改成通過派發一個動作,讓狀態改變,一個動作幹一件事
let state = {
name: '風影嘎拉',
color: '#f00',
}
function render(obj) {
const dom = document.getElementById('root')
dom.innerHTML = obj.name
dom.style.color = obj.color
}
// 添加動態改寫邏輯
const CHANGE_NAME = 'CHANGE_NAME'
const CHANGE_COLOR = 'CHANGE_COLOR'
function dispatch(action) {
switch (action.type) {
case CHANGE_NAME:
state.name = '手鞠'
break
case CHANGE_COLOR:
state.color = action.color
break
default:
break
}
}
// 派發動作改變名字
dispatch({ type: CHANGE_NAME })
//派發動作改變顏色, 並傳參
dispatch({ type: CHANGE_COLOR, color: 'green' })
render(state)
雖然改進了,一個動作做一件事, 但還是那個問題,全局的state 變量可以隨意更改,
比如說,我可以手動把state 的值置位 null, 這顯然是不合理的, 我希望變量能私有化
變量私有化, 可以採用 IIFE 函數(明顯此處不合適), 也可以採用函數閉包的形勢
創建一個 createStore
方法, 用來創建數據源
function render(obj) {
const dom = document.getElementById('root')
dom.innerHTML = obj.name
dom.style.color = obj.color
}
// 添加動態改寫邏輯
const CHANGE_NAME = 'CHANGE_NAME'
/**
*
* 進行變量私有化
* 函數,閉包
*/
function createStore() {
let store = {
name: '風影嘎啦',
color: '#f26',
}
// 獲取狀態
function getState() {
return store
}
// 派發動作改變數據源
function dispatch(action) {
switch (action.type) {
case CHANGE_NAME:
store.name = '手鞠'
break
default:
break
}
}
// 導出
return {
getState,
dispatch,
}
}
// 創建 store 數據源
let store = createStore()
// 派發 action 改變名字
store.dispatch({
type: CHANGE_NAME,
})
// 調用 store.getState() 獲取數據源
render(store.getState())
到這裏,前邊的問題算是解決了, 但是又有一個新問題了,狀態外部無法改變,內部直接寫死了,簡單畫個草圖
此時,A, B, C 三個組件,可能每個組件都有自己的特有的數據源,但此時的數據源可能只適合 A 組件
所以,截下來要解決的問題就是,能動態的數據源, 固定的輸入,得到固定的輸出
- 在 createStore() 方法時,傳入一個純函數
- 解決數據狀態由用戶自己具體指定
- 固定的輸入,得到固定的輸出
function render(obj) {
const dom = document.getElementById('root')
dom.innerHTML = obj.name
dom.style.color = obj.color
}
/**
*
* 進行變量私有化
* 函數,閉包
*/
function createStore(reducer) {
// 默認初始狀態
let state
// 得到總的狀態樹
function getState() {
return state
}
// 派發動作
function dispatch(action) {
// 得到新狀態
state = reducer(state, action)
}
// 由於默認 state 樹是沒有狀態的, 是一個控制,所以先派發一次action,給初始狀態賦值,
// 由於用戶自己定義的reducer 默認返回初始狀態,由於派發的這個type類型找不到,所以默認返回了初始值
dispatch({ type: '@@REDUX_INIT' })
return {
getState,
dispatch,
}
}
// 初始狀態
let initState = {
name: '我愛羅',
color: '#f26',
}
// 添加動態改寫邏輯
const CHANGE_NAME = 'CHANGE_NAME'
// 狀態用戶自己定義,傳入老的狀態,返回新狀態
function reducer(state = initState, action) {
switch (action.type) {
case CHANGE_NAME:
return {
...state,
name: action.name,
}
default:
return state
}
}
// 得到數據源
let store = createStore(reducer)
// 派發action動作
store.dispatch({
type: CHANGE_NAME,
name: '7代目止水',
})
render(store.getState())
此時,基本實現了簡化版redux, 然而還有一個問題, 就是如果你先渲染,然後再派發動作的話, 視圖是不會改變的,你只能先派發action , 纔會立馬更新視圖
let store = createStore(reducer)
// 如果在這不會視圖更新
render(store.getState())
store.dispatch({
type: CHANGE_NAME,
name: '手鞠',
})
// 如果先派發動作,視圖會更新
// render(store.getState())
那如果我就想先渲染更新,接着再渲染後,繼續提交action,還要引起視圖的更新,就像上邊代碼一樣,先渲染,再render
此時可以使用發佈訂閱模式,來監聽你的改變
一個簡單的發佈訂閱模式:
接着實現發佈訂閱,那既然可以訂閱, 就可以取消訂閱
function render(obj) {
const dom = document.getElementById('root')
dom.innerHTML = obj.name
dom.style.color = obj.color
}
/**
*
* 進行變量私有化
* 函數,閉包
*/
function createStore(reducer) {
// 默認初始狀態
let state
let listeners = [] // 存儲所有的監聽函數
// 得到總的狀態樹
function getState() {
return state
}
// 派發動作
function dispatch(action) {
// 得到新狀態
state = reducer(state, action)
// 訂閱
listeners.forEach((fn) => fn())
}
// 由於默認 state 樹是沒有狀態的, 是一個控制,所以先派發一次action,給初始狀態賦值,
// 由於用戶自己定義的reducer 默認返回初始狀態,由於派發的這個type類型找不到,所以默認返回了初始值
dispatch({ type: '@@REDUX_INIT' })
// 添加發布訂閱模式
function subscribe(listener) {
listeners.push(listener)
// 返回一個取消訂閱方法(就是個高階函數)
return function () {
listeners = listeners.filter((fn) => fn !== listener)
}
}
return {
getState,
dispatch,
subscribe,
}
}
let app = {
name: '我愛羅',
color: '#f26',
}
// 添加動態改寫邏輯
const CHANGE_NAME = 'CHANGE_NAME'
const CHANGE_COLOR = 'CHANGE_COLOR'
function reducer(state = app, action) {
switch (action.type) {
case CHANGE_NAME:
return {
...state,
name: action.name,
}
case CHANGE_COLOR:
return {
...state,
color: action.color,
}
default:
return state
}
}
let store = createStore(reducer)
// 必選先派發action , 再去觸發渲染,不能先渲染,再派發動作
/**
* 解決辦法: 添加發布訂閱模式
*/
const AppRender = () => render(store.getState())
// 先調用渲染一次
AppRender()
let unSubscribe = store.subscribe(AppRender)
// 模擬延時渲染
setTimeout(() => {
store.dispatch({
type: CHANGE_NAME,
name: '手鞠',
})
// 取消訂閱, 下邊的不會在執行了
// unSubscribe()
store.dispatch({
type: CHANGE_COLOR,
color: 'blue',
})
}, 1500)
最後總結代碼也就20多行吧,暴露出3個api
function createStore(reducer) {
let state
let listeners = []
function getState() {
return state
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach((fn) => fn())
}
dispatch({ type: '@@REDUX_INIT' })
function subscribe(listener) {
listeners.push(listener)
return function () {
listeners = listeners.filter((fn) => fn !== listener)
}
}
return {
getState,
dispatch,
subscribe,
}
}