從 Redux 說起,到手寫,再到狀態管理

山頭人漢波發表於2022-04-03

先說結論

  1. Redux 是狀態管理庫,也是一種架構
  2. Redux 與 React 無關,但它是為了解決 React 元件中狀態無法共享而出的一種解決方案
  3. 單純的 Redux 只是一個狀態機, store 中存放了所有的狀態 state,要想改變裡面的狀態 state,只能 dispatch 一個動作
  4. 發出去的 action 需要用 reducer 來處理,傳入 state 和 action,返回新的 state
  5. subscribe 方法可以註冊回撥方法,當 dispatch action 的時候會執行裡面的回撥
  6. Redux 其實是一個釋出訂閱模式
  7. Redux 支援 enhancer,enhancer 其實就是一個裝飾器模式,傳入當前的 createStore,返回一個增強的 createStore
  8. Redux 使用 applyMiddleware 函式支援中介軟體,它的返回值其實就是一個 enhancer
  9. Redux 的中介軟體也是一個裝飾器模式,傳入當前的 dispatch,返回一個增強了的 dispatch
  10. 單純的 Redux 是沒有 View 層的

為什麼出現 Redux?

我們預設使用 React 技術棧,當頁面少且簡單時,完全沒必要使用 Redux。Redux 的出現,是為了應對複雜元件的情況。即當元件複雜到三層甚至四層時(如下圖),元件 4 想改變元件 1 的狀態

react 元件樹

按照 React 的做法,狀態提升,將狀態提升至同一父元件(在圖中為祖父元件)。但層級一多,根元件要管理的 state 就很多了,不方便管理。

所以當初有了 context(React 0.14 確定引入),通過 context 能實現”遠房元件“的資料共享。但它也有缺點,使用 context 意味著所有的元件都可以修改 context 裡面的狀態,就像誰都可以修改共享狀態一樣,導致程式執行的不可預測,這不是我們想要的

facebook 提出了 Flux 解決方案,它引入了單向資料流的概念(沒錯,React 沒有單向資料流的概念,Redux 是整合了 Flux 的單向資料流理念),架構如下圖所示:

Flux 流程圖

這裡不表 Flux。簡單理解,在 Flux 架構中,View 要通過 Action (動作)通知 Dispatcher(派發器),Dispatcher 來修改 Store,Store 再修改 View

Flux 的問題或者說缺點在哪?

store 之間存在依賴關係、難以進行伺服器端渲染、 stores 混雜了邏輯和狀態

筆者在學習的 React 技術棧時是 2018 年,那是已然流行 React + Redux 的解決方案,Flux 已經被淘汰了,瞭解 Flux 是為了引出 Redux

Redux 的出現

Redux 主要解決狀態共享問題

官網:Redux 是 JavaScript 狀態容器,它提供可預測的狀態管理

它的作者是 Dan Abramov

其架構為:

Redux 流程圖

可以看得出,Redux 只是一個狀態機,沒有 View 層。其過程可以這樣描述:

  • 自己寫一個 reducer(純函式,表示做什麼動作會返回什麼資料)
  • 自己寫一個 initState(store 初始值,可寫可不寫)
  • 通過 createStore 生成 store,此變數包含了三個重要的屬性

    • store.getState:得到唯一值(使用了閉包老哥)
    • store.dispatch:動作行為(改變 store 中資料的唯一指定屬性)
    • store.subscribe:訂閱(釋出訂閱模式)
  • 通過 store.dispatch 派發一個 action
  • reducer 處理 action 返回一個新的 store
  • 如果你訂閱過,當資料改變時,你會收到通知

按照行為過程,我們可手寫一個 Redux,下文在表,先說特點

三大原則

  • 單一資料來源

    • 整個應用的 全域性 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中
  • State 是隻讀的

    • 唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生時間的普通物件
  • 使用純函式來執行修改

    • 為了描述 action 如何改變 state tree,你需要編寫純的 reducers

三大原則是為了更好地開發,按照單向資料流的理念,行為變得可回溯

讓我們動手寫一個 Redux 吧

手寫 redux

按照行為過程和原則,我們要避免資料的隨意修改、行為的可回溯等問題

基礎版:23 行程式碼讓你使用 redux

export const createStore = (reducer, initState) => {
  let state = initState
  let listeners = []

  const subscribe = (fn) => {
    listeners.push(fn)
  }

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach((fn) => fn())
  }

  const getState = () => {
    return state
  }

  return {
    getState,
    dispatch,
    subscribe,
  }
}

搞個測試用例

import { createStore } from '../redux/index.js'

const initState = {
  count: 0,
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1,
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      }
    default:
      return state
  }
}

const store = createStore(reducer, initState)

store.subscribe(() => {
  let state = store.getState()
  console.log('state', state)
})

store.dispatch({
  type: 'INCREMENT',
})
PS:俺是在 node 中使用 ES6 模組,需要升級 Node 版本至 13.2.0

第二版:難點突破:中介軟體

普通的 Redux 只能做最基礎地根據動作返回資料,dispatch 只是一個取資料的命令,例如:

dispatch({
  type: 'INCREMENT',
})
// store 中的 count + 1

但在開發中,我們有時候要檢視日誌、非同步呼叫、記錄日常等

怎麼辦,做成外掛

在 Redux 中,類似的概念叫中介軟體

中介軟體

Redux 的 createStore 共有三個引數

createStore([reducer], [initial state], [enhancer]);

第三個引數為 enhancer,意為增強器。它的作用就是代替普通的 createStore,轉變成為附加上中介軟體的 createStore。打幾個比方:

  • 託尼·斯塔克本來是一個普通有錢人,加上增強器(盔甲)後,成了鋼鐵俠
  • 中央下發一筆救災款,加上增強器(大小官員的打點)後,到災民手上的錢只有一丟丟
  • 路飛用武裝色打人,武裝色就是一箇中介軟體

enhancer 要做的就是:東西還是那個東西,只是經過了一些工序,加強了它。這些工序由 applyMiddleware 函式完成。按照行業術語,它是一個裝飾器模式。它的寫法大致是:

applyMiddleware(...middlewares)
// 結合 createStore,就是
const store = createStore(reudcer, initState, applyMiddleware(...middlewares))

所以我們需要先對 createStore 進行改造,判斷當有 enhancer 時,我們需傳值給中介軟體

export const createStore = (reducer, initState, enhancer) => {
    if (enhancer) {
        const newCreateStore = enhancer(createStore)
        return newCreateStore(reducer, initState)
    }

    let state = initState;
    let listeners = [];
    ...
}

如果有 enhancer 的話,先傳入 createStore 函式,生成的 newCreateStore 和原來的 createStore 一樣,會根據 reducer, initState 生成 store。可簡化為:

if (enhancer) {
  return enhancer(createStore)(reducer, initState)
}

PS:為什麼要寫成這樣,因為 redux 是按照函式式寫法來寫的

為什麼 createStore 可以被傳值,因為函式也是物件,也可以作為引數傳遞(老鐵閉包了)

這樣我們的 applyMiddleware 自然就明確了

const applyMiddleware = (...middlewares) => {
    return (oldCreateStore) => {
        return (reducer, initState) => {
            const store = oldCreateStore(reducer, initState)
            ...
        }
    }
}

這裡的 store 表示的是普通版中的 store,接下來我們要增強 store 中的屬性

我願稱之為:五行程式碼讓女人為我花了 18 萬

export const applyMiddleware = (...middlewares) => {
  return (oldCreateStore) => {
    return (reducer, initState) => {
      const store = oldCreateStore(reducer, initState)
      // 以下為新增
      const chain = middlewares.map((middleware) => middleware(store))
      // 獲得老 dispatch
      let dispatch = store.dispatch
      chain.reverse().map((middleware) => {
        // 給每個中介軟體傳入原派發器,賦值中介軟體改造後的dispatch
        dispatch = middleware(dispatch)
      })
      // 賦值給 store 上的 dispatch
      store.dispatch = dispatch
      return store
    }
  }
}

現在寫幾個中介軟體來測試一下

// 記錄日誌
export const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this.state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

// 記錄異常
export const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action)
  } catch (error) {
    console.log('錯誤報告', error)
  }
}

// 時間戳
export const timeMiddleware = (store) => (next) => (action) => {
  console.log('time', new Date().getTime())
  next(action)
}

引入專案中,並執行

import { createStore, applyMiddleware } from '../redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'

const initState = {
  count: 0,
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1,
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      }
    default:
      return state
  }
}

const store = createStore(
  reducer,
  initState,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)

store.subscribe(() => {
  let state = store.getState()
  console.log('state', state)
})

store.dispatch({
  type: 'INCREMENT',
})

執行發現已經實現了 redux 最重要的功能——中介軟體

測試程式碼

來分析下中介軟體的函數語言程式設計,以 loggerMiddleware 為例:

export const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this.state', store.getState())
  console.log('action', action)
  next(action)
  console.log('next state', store.getState())
}

在 applyMiddleware 原始碼中,

const chain = middlewares.map((middleware) => middleware(store))

相當於給每個中介軟體傳值普通版的 store

let dispatch = store.dispatch
chain.reverse().map((middleware) => (dispatch = middleware(dispatch)))

相當於給每個中介軟體在傳入 store.dispatch,也就是 next,原 dispatch = next。這個時候的中介軟體已經本成品了,程式碼中的 (action) => {...} 就是函式 const dispatch = (action) => {}。當你執行 dispatch({ type: XXX }) 時執行中介軟體這段(action) => {...}

PS:柯里化一開始比較難理解,用多習慣就慢慢能懂

第三版:結構複雜化與拆分

中介軟體理解起來或許有些複雜,先看看其他的概念換換思路

一個應用做大後,單靠一個 JavaScript 檔案來維護程式碼顯然是不科學的,在 Redux 中,為避免這類情況,它提供了 combineReducers 來整個多個 reducer,使用方法如:

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

combinReducers 中傳入一個物件,什麼樣的 state 對應什麼樣的 reducer。這就好了,那麼 combinReducers 怎麼實現呢?因為比較簡單,不做多分析,直接上原始碼:

export const combinReducers = (...reducers) => {
  // 拿到 counter、info
  const reducerKey = Object.keys(reducers)
  // combinReducers 合併的是 reducer,返回的還是一個 reducer,所以返回一樣的傳參
  return (state = {}, action) => {
    const nextState = {}
    // 迴圈 reducerKey,什麼樣的 state 對應什麼樣的 reducer
    for (let i = 0; i < reducerKey.length; i++) {
      const key = reducerKey[i]
      const reducer = reducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey
    }
    return nextState
  }
}

同級目錄下新建一個 reducer 資料夾,並新建 reducer.jsinfo.jsindex.js

// reducer.js
export default (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
      }
    case 'DECREMENT': {
      return {
        count: state.count - 1,
      }
    }
    default:
      return state
  }
}
// info.js
export default (state, action) => {
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name,
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description,
      }
    default:
      return state
  }
}

合併匯出

import counterReducer from './counter.js'
import infoReducer from './info.js'

export { counterReducer, infoReducer }

我們現在測試一下

import { createStore, applyMiddleware, combinReducers } from '../redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'

const initState = {
  counter: {
    count: 0,
  },
  info: {
    name: 'johan',
    description: '前端之虎',
  },
}

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

const store = createStore(
  reducer,
  initState,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)

store.dispatch({
  type: 'INCREMENT',
})

combinReducers 也完成了

測試程式碼

既然拆分了 reducer,那麼 state 是否也能拆分,並且它是否需要傳,在我們平時的寫法中,一般都不傳 state。這裡需要兩點改造,一是每個 reducer 中包含了它的 state 和 reducer;二是改造 createStore,讓 initState 變得可傳可不傳,以及初始化資料

// counter.js 中寫入對應的 state 和 reducer
let initState = {
  counter: {
    count: 0,
  },
}

export default (state, action) => {
  if (!state) {
    state = initState
  }
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
      }
    case 'DECREMENT': {
      return {
        count: state.count - 1,
      }
    }
    default:
      return state
  }
}
// info.js
let initState = {
  info: {
    name: 'johan',
    description: '前端之虎',
  },
}

export default (state, action) => {
  if (!state) {
    state = initState
  }
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name,
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description,
      }
    default:
      return state
  }
}

改造 createStore

export const createStore = (reducer, initState, enhancer) => {

    if (typeof initState === 'function') {
        enhancer = initState;
        initState = undefined
    }
    ...
    const getState = () => {
        return state
    }
    // 用一個不匹配任何動作來初始化store
    dispatch({ type: Symbol() })

    return {
        getState,
        dispatch,
        subscribe
    }
}

主檔案中

import { createStore, applyMiddleware, combinReducers } from './redux/index.js'
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from './middleware.js'
import { counterReducer, infoReducer } from './reducer/index.js'

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
})

const store = createStore(
  reducer,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware),
)

console.dir(store.getState())

到此為止,我們已經實現了一個七七八八的 redux 了

完整體的 Redux

退訂

const subscribe = (fn) => {
  listeners.push(fn)
  return () => {
    const index = listeners.indexOf(listener)
    listeners.splice(index, 1)
  }
}

中介軟體拿到的 store

現在的中介軟體能拿到完整的 store,他甚至可以修改我們的 subscribe 方法。按照最小開放策略,我們只用給 getState 即可,修改下 applyMiddleware 中給中介軟體傳的 store

// const chain = middlewares.map(middleware => middleware(store))
const simpleStore = { getState: store.getState }
const chain = middlewares.map((middleware) => middleware(simpleStore))

compose

在我們的 applyMiddleware 中,把 [A, B, C] 轉換成 A(B(C(next))),效果是:

const chain = [A, B, C]
let dispatch = store.dispatch
chain.reverse().map((middleware) => {
  dispatch = middleware(dispatch)
})

Redux 提供了一個 compose ,如下

const compose = (...funcs) => {
  if (funcs.length === 0) {
    return (args) => args
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

2 行程式碼 replaceReducer

替換當前的 reudcer ,使用場景:

  • 程式碼分割
  • 動態載入
  • 實時 reloading 機制
const replaceReducer = (nextReducer) => {
  reducer = nextReducer
  // 重新整理一次,廣播 reducer 已經替換,也同樣把預設值換成新的 reducer
  dispatch({ type: Symbol() })
}

bindActionCreators

bindActionCreators 是做什麼的,他通過閉包,把 dispatch 和 actionCreator 隱藏起來,讓其他地方感知不到 redux 的存在。一般與 react-redux 的 connect 結合

這裡直接貼原始碼實現:

const bindActionCreator = (actionCreator, dispatch) => {
  return function () {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export const bindActionCreators = (actionCreators, dispatch) => {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error()
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

以上,我們就已經完成了 Redux 中所有的程式碼。大體上這裡 100 多行的程式碼就是 Redux 的全部,真 Redux 無非是加了些註釋和引數校驗

總結

我們把與 Redux 相關的名詞列出來,梳理它是做什麼的

  • createStore

    • 建立 store 物件,包含 getState、dispatch、subscribe、replaceReducer
  • reducer

    • 純函式,接受舊的 state、action,生成新的 state
  • action

    • 動作,是一個物件,必須包括 type 欄位,表示 view 發出通知告訴 store 要改變
  • dispatch

    • 派發,觸發 action ,生成新的 state。是 view 發出 action 的唯一方法
  • subscribe

    • 訂閱,只有訂閱了,當派發時,會執行訂閱函式
  • combineReducers

    • 合併 reducer 成一個 reducer
  • replaceReudcer

    • 代替 reducer 的函式
  • middleware

    • 中介軟體,擴充套件 dispatch 函式

磚家曾經畫過一張關於 Redux 的流程圖

流程圖

換種思考方式理解

我們說過, Redux 只是一個狀態管理庫,它是由資料來驅動,發起 action,會引發 reducer 的資料更新,從而更新到最新的 store

與 React 結合

拿著剛做好的 Redux,放到 React 中,試試什麼叫 Redux + React 集合,注意,這裡我們先不使用 React-Redux,單拿這兩個結合

先建立專案

npx create-react-app demo-5-react

引入手寫的 redux 庫

App.js 中引入 createStore,並寫好初始資料和 reducer,在 useEffect 中監聽資料,監聽好之後當發起一個 action 時,資料就會改變,看程式碼:

import React, { useEffect, useState } from 'react'
import { createStore } from './redux'
import './App.css'

const initState = {
  count: 0,
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1,
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      }
    default:
      return state
  }
}

const store = createStore(reducer, initState)

function App() {
  const [count, setCount] = useState(store.getState().count)

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setCount(store.getState().count)
    })
    return () => {
      if (unsubscribe) {
        unsubscribe()
      }
    }
  }, [])

  const onHandle = () => {
    store.dispatch({
      type: 'INCREMENT',
    })
    console.log('store', store.getState().count)
  }
  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onHandle}>add</button>
    </div>
  )
}

export default App

點選 button 後,資料跟著改變

效果圖

PS:雖然我們可以用這種方式訂閱 store 和改變資料,但是訂閱的程式碼重複過多,我們可以用高階元件將他提取出去。這也是 React-Redux 所做的事情

與原生 JS+HTML 結合

我們說過,Redux 是個獨立於 Redux 的存在,它不僅可在 Redux 充當資料管理器,還可以在原生 JS + HTML 中充當起職位

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="container">
      <div id="count">1</div>
      <button id="btn">add</button>
    </div>
    <script type="module">
      import { createStore } from './redux/index.js'

      const initState = {
        count: 0,
      }

      const reducer = (state, action) => {
        switch (action.type) {
          case 'INCREMENT':
            return {
              ...state,
              count: state.count + 1,
            }
          case 'DECREMENT':
            return {
              ...state,
              count: state.count - 1,
            }
          default:
            return state
        }
      }

      const store = createStore(reducer, initState)

      let count = document.getElementById('count')
      let add = document.getElementById('btn')
      add.onclick = function () {
        store.dispatch({
          type: 'INCREMENT',
        })
      }
      // 渲染檢視
      function render() {
        count.innerHTML = store.getState().count
      }
      render()
      // 監聽資料
      store.subscribe(() => {
        let state = store.getState()
        console.log('state', state)
        render()
      })
    </script>
  </body>
</html>

效果如下:

效果圖

狀態生態

我們從 Flux 說到 Redux,再從 Redux 說了各種中介軟體,其中 React-saga 就是為解決非同步行為而生的中介軟體,它主要採用 Generator(生成器)概念,比起 React-thunk 和 React-promise,它沒有像其他兩者將非同步行為放在 action creator 上,而是把所有的非同步操作看成“執行緒”,通過 action 觸發它,當操作完成後再次發出 action 作為輸出

function* helloWorldGenerator() {
  yield 'hello'
  yield 'world'
  yield 'ending'
}

const helloWorld = helloWorldGenerator()

hewlloWorld.next() // { value: 'hello', done: false }
hewlloWorld.next() // { value: 'world', done: false }
hewlloWorld.next() // { value: 'ending', done: true }
hewlloWorld.next() // { value: undefined, done: true }

簡單來說:遇到 yield 表示式,就暫停執行後面的操作,並將緊跟 yield 後面的那個表示式的值,作為返回值 value,等著下一個呼叫 next 方法,再繼續往下執行

Dva

Dva 是什麼?

官網:Dva 首先是一個基於 Redux + Redux-saga 的資料流方案。為了簡化開發體驗,Dva 還額外內建了 react-router 和 fetch,所以可以理解為一個輕量級的應用框架

簡單來說,它是整合了現在最流行的資料流方案,即一個 React 技術棧:

dva = React-Router + Redux + Redux-saga + React-Redux

它的資料流圖為:

Dva 流程圖

view dispatch 一個動作,改變 state(即 store),state 與 view 繫結,響應 view

其他不表,可去 Dva 官網檢視,這裡講講 Model ,它包含了 5 個屬性

  • namespace

    • model 的名稱空間,同時也是他在全域性 state 上的屬性,只能用字串,不支援通過 . 的方式建立多層名稱空間
  • state

    • 初始值
  • reducers

    • 純函式,以 key/value 格式定義 reducer。用於處理同步擦做,唯一可以修改 state 的地方,由 action 觸發
    • 格式為:(state, action) => newState[(state, action) => newState, enhancer]
  • effects

    • 處理非同步操作和業務邏輯,以 key/value 格式定義 effect
    • 不直接修改 state。由 action 觸發
    • call:執行非同步操作
    • put:發出一個 Action,類似於 dispatch
  • subscriptions

    • 訂閱
    • app.start() 時被執行,資料來源可以是當前的時間、伺服器的 websocket 連結、 keyboard 輸入、history 路由變化、geolocation 變化等等

Mobx

View 通過訂閱也好,監聽也好,不同的框架有不同的技術,總之 store 變化, view 也跟著變化

Mobx 使用的是響應式資料流方案。後續會單獨寫一篇,此篇太長,先不寫

補充:單向資料流

先介紹 React 中資料傳遞,即通訊問題

  • 向子元件發訊息
  • 向父元件發訊息
  • 向其他元件發訊息

React 只提供了一種通訊方式:傳參。

即父傳值給子,子不能修改父傳的資料,props 具有不可修改性。子元件想把資料傳給父元件怎麼辦?通過 props 中的事件來傳值通知父元件

倉庫地址:https://github.com/johanazhu/...

本文參與了 SegmentFault 思否徵文「如何“反殺”面試官?」,歡迎正在閱讀的你也加入。

相關文章