rccoder / blog

😛 个人博客 🤐 订阅是 watch 是 watch 是 watch 是 watch
580 stars 36 forks source link

听说你需要这样了解 Redux(一) #18

Open rccoder opened 7 years ago

rccoder commented 7 years ago

Redux 学习总结

1. 写在前面

对于复杂的 Web 交互应用,相继出现了一系列的 MV* 框架,09 年的 Angular 带动了一系列的发展。

在后期的开发过程中,大家发现 Angular 太过于重,双向绑定的数据流容易让数据变得不可控制。

在任何应用中管理数据都是必不可少的。通过用户界面来编排数据流是一项具有挑战的工程。现代的 Web 应用会涉及复杂的 UI 交互,比如改变了一个地方的 UI 数据,需要直接或者间接的改变其他地方的 UI 数据。一些双向绑定的框架(比如:Angular.js 等)都是对这种场景比较好的解决方案。

对于一些应用(尤其是数据流比较简单的),这种双向绑定是非常快速和足够的解决方案。但是对于一些更加复杂的应用场景,数据双向绑定已经被证明是不够的,它会妨碍用户界面的设计。实际上,React 并没有解决这样一个复杂应用中比较复杂的数据流问题(虽然后面出现了 Flux 这种解决方案),但是他确实解决了一个组件中数据流的问题

—— React 数据阵营之 State 与 Props

React 的横空出世一度让 Web App 的概念如火如荼,这种单向的数据流也就需要新的模式来组织。

在这个过程中对状态的管理相继出现了 Flux 等解决方案,在后来的发展中,Redux 由于单一的 Store 让大家更是喜欢。

2. Just Do

2.1 抛开全家桶

React 开发和全家桶离不开关系,但全家桶极有可能阻碍我们对 Redux 的理解。

这里我们抛开全家桶,用 jsfiddle 来学习 Redux。

2.2 几个概念与一个状态机

1. store:

Redux 是用来管理 App 中状态的。 和 Flux 相比,Redux 最鲜明的特点就是只有一个 Store。那这个 Store 到底是用来干什么的?

很直观的, Store 就是用来储存整个应用的状态的,也就是 React 中的 state。

Store 除了储存整个应用的状态之外,还提供几个接口,包括: 组件获取状态的接口 getState()、组件通过 action 和数据进行 dispath 更新 Store 的接口 dispatch(action)、Store 发生改变之后进行 subscribe 改变组件 view 的接口 subscribe()。

2. action:

action,翻译过来就是 动作 的意思。他用来接受一个指示和数据,然后由 action 进行 dispatch 来根据这个动作和数据进行 Store 的更新。

常见的action格式为如下,包括一个 typetext(可以为空):

{
    type: 'Action1',
    text: {
        content1: 'content1_val',
        content2: 'content2_val'
    }
}

3. reducer:

Store 中数据的更新通过 action 进行 dispatch,但 action 指示提供了 action_type 和 action_text,并没有去决定 Store 应该如何更新。

reducer 可以理解为 action 和 Store 进行关联的时候中间的一层,他接收 action_type 和 action_text,然后 switch action_type,在相关的条件下对相关的数据进行整合,最后结合原 Store 的 state 与 action 里面的 type/text 生成新的 state 给 Store,进行 Store 的更新。

4. Redux 工作状态机:

image

2.3 使用梗概

Redux is a predictable state container for JavaScript apps

Redux 只有一个单一的数据源,并且这个数据源是只读的,修改这个数据源必须通过 dispatch action,action 只是告知了 type 和 text,这个时候 reducer 结合上一个 state 和 action 做出自己的响应,返回新的 state 送入 store。

在 reducer 中,他接收上一个 state 与 action,返回一个新的 state。这个过程是纯函数式的,返回的结果由进去的参数决定,不会随着外界的变化而发生改变。

2.4 入门小例子

jsfiddle 见: https://jsfiddle.net/rccoder/shmxthbo/2/

const { createStore } = Redux

const reducer = (state, action) => {  
  let result = state
  switch (action.type) {
    case "TO_NUM":
      result.value = action.text
      break;
    case "TO_WORD":
      result.value = action.text
      break;
  }
  return result
}

const store = createStore(reducer, {value: ''})

store.subscribe(() => {
  console.log(store.getState())
})

store.dispatch({type: 'TO_NUM', text: 100})
store.dispatch({type: 'TO_WORD', text: "ABC"})
store.dispatch({type: 'TO_FLOAT', text: 100.1})

上述的例子最后会出现结果:

Object {value: 100}
Object {value: "ABC"}
Object {value: "ABC"}

如我们所愿,subscribe 监听 store 的变化得到上面的结果。

其中,createStore 接收的两个参数分别是 reducer 和 initState,为了让代码更加优雅,我们还可以把 initState 抽离出来:

const { createStore } = Redux

const initState = {
  value: ''
}

const reducer = (state, action) => {  
  let result = state
  switch (action.type) {
    case "TO_NUM":
      result.value = action.text
      break;
    case "TO_WORD":
      result.value = action.text
      break;
  }
  return result
}

const store = createStore(reducer, initState)

store.subscribe(() => {
  console.log(store.getState())
})

store.dispatch({type: 'TO_NUM', text: 100})
store.dispatch({type: 'TO_WORD', text: "ABC"})
store.dispatch({type: 'TO_FLOAT', text: 100.1})

再其次,在项目中为了项目的规范化,我们需要对所有的 action_type 常量进行统一的管理,放到单独的文件中,这样可以进一步的抽离:

const { createStore } = Redux

const initState = {
  value: ''
}

const TO_NUM = 'TO_NUM'
const TO_WORD = 'TO_WORD'
const TO_FLOAT = 'TO_FLOAT'

const reducer = (state, action) => {  
  let result = state
  switch (action.type) {
    case "TO_NUM":
      result.value = action.text
      break;
    case "TO_WORD":
      result.value = action.text
      break;
  }
  return result
}

const store = createStore(reducer, initState)

store.subscribe(() => {
  console.log(store.getState())
})

store.dispatch({type: TO_NUM, text: 100})
store.dispatch({type: TO_WORD, text: "ABC"})
store.dispatch({type: TO_FLOAT, text: 100.1})

2.5 小进阶

2.5.1 middleware

Redux 解决的正是状态的问题。在平时开发过程中,我们可能需要知道上一个状态以及现在的状态,以及发生这个改变的 action 与数据是什么。

这个时候就需要 middlerware 出厂了,middlerware 发生在 action 之后,reducer 之前,在 middlerware 中,我们调用 next(action) 函数就可以把 action 传递给 reducer。

middlerware 不仅仅能实现查看当前 action 与 action 旁边的两个状态,还能对特定的 action 最一些特定的处理,然后再传给 reducer。这也正是 middlerware 的本意。

我们在上面的代码上加上 middlerware,打印一下这个小程序的日志:

jsfiddle 见:https://jsfiddle.net/rccoder/shmxthbo/3/

const { createStore, applyMiddleware } = Redux

const reducer = (state, action) => {  
  let result = state
  switch (action.type) {
    case "TO_NUM":
      result.value = action.text
      break;
    case "TO_WORD":
      result.value = action.text
      break;
  }
  return result
}

const log = store => next => action => {
  console.log('PRE_STATE:')
  console.log(store.getState())

  console.log(`ACTION_TYPE: ${action.type}`)

  console.log('ACTION_TEXT:')
  console.log(action.text)

  let result = next(action)

  console.log('NEXT_STATE:')
  console.log(store.getState())

  console.log('----')
  return result
}

const store = createStore(reducer, {value: ''}, applyMiddleware(log))

store.subscribe(() => {
  //console.log(store.getState())
})

store.dispatch({type: 'TO_NUM', text: 100})
store.dispatch({type: 'TO_WORD', text: "ABC"})
store.dispatch({type: 'TO_FLOAT', text: 100.1})

结果如下:

image

可以说这个日志比较明显的反应了程序中状态的转移与转移条件。

加上 middlerware 之后整个 Redux 的运行状态转移图如下:

image

2.5.2 拆分 reducer

上面例子中 Store 的 State 是很简单的:

{
    value: ''
}

在真是环境中,state 往往是复杂的。对于 reducer 而言,我们也可能根据相应的业务需求拆分出不同的 reducer,这样就避免了在一个文件里面写出一堆的 case。

对于下面的 Store

{
    businessA:{
        m: 1,
        n: 2
    },
    businessB: [
        1,
        2
    ]

我们可以拆分为两个 reducer

const reducerA = (state = {m: 1, n: 2}, action) => {
    switch(action.type) {
        case "A_TYPE1":
            return {...state, m: 2}
            break;
        case "A_TYPE2":
            return {...state, k: 3}
            break;
        default:
            return state
            break;
    }
}

const reducerB = (state = [1, 2], action) => {
    switch(action.type) {
        case "B_TYPE1":
            return [].concat(state.slice(0))
            break;
        default:
            return state
            break;
    }
}

// 合并 reducer
const reducer = (state = {}, action) => {
    return {
        businessA: reducerA(state.businessA, action),
        businessB: reducerB(state.businessB, action)
    }
}

如此, reducer 不再负责整个 reducer,而是把它下分到 reducerA 和 reducerB 两个 reducer, 每个 reducer 互相独立。

这种做法是非常值得推荐的,尤其是在大型应用中, Redux 官方也给出了合并 reducer 的 API:

const {combineReducers} = redux

const reducer = combineReducers({
    businessA: reducerA,
    businessB: reducerB
})

结合上面的描述,对贯穿整个的例子做一个修改:

jsfiddle 见 https://jsfiddle.net/rccoder/shmxthbo/5/

const { createStore, applyMiddleware, combineReducers } = Redux

const TO_NUM = 'TO_NUM'
const TO_WORD = 'TO_WORD'
const PLUS = 'PLUS'

const NUM_WORD_reducer = (state = '', action) => {
    switch(action.type) {
        case TO_NUM:
            return action.text
            break
        case TO_WORD:
            return action.text
            break
        default:
            return 'ACTION_NUM_WORD_NO_TEXT'
            break
  }
}

const NUM_PLUS_reducer = (state = 0, action) => {
    switch(action.type) {
        case PLUS: 
            return state+action.text
            break
        default:
            return state
            break
  }
}

const reducer = combineReducers({
    NUM_WORD: NUM_WORD_reducer,
    NUM_PLUS: NUM_PLUS_reducer
})

const log = store => next => action => {
    console.log('PRE_STATE:')
    console.log(store.getState())

    console.log(`ACTION_TYPE: ${action.type}`)

    console.log('ACTION_TEXT:')
    console.log(action.text)

    let result = next(action)

    console.log('NEXT_STATE:')
    console.log(store.getState())

    console.log('----')
    return result
}

const store = createStore(reducer, {}, applyMiddleware(log))

store.subscribe(() => {
  //console.log(store.getState())
})

store.dispatch({type: 'TO_NUM', text: 100})
store.dispatch({type: 'TO_WORD', text: "ABC"})
store.dispatch({type: 'TO_FLOAT', text: 100.1})

2.5.3 Redux 异步

我们需要保持 reducer 是函数式的,这也就意味着在 reducer 中相同的输入一定有相同的返回,并且这个返回时及时的。

在应用开发中,我们经常会遇到一些异步的请求,这个需要显然是存在的一个问题。

通常的,我们有下面的几个解决方案:

2.5.3.1 理所当然

对于异步这种情况理所当然我们有着这样一种想法:在异步得到结果之后进行 dispatch。

setTimeout(() => {
    dispatch(action)
}, 2000)

在简单场景下,这种做法是完全接受的。但是当异步比较多比较复杂的时候,他的缺点也就暴露了出来:

前面在介绍 middleware 的时候,提到 middleware 是介于 Action 与 Reducer 之间的,所以当 action 经过 middleware 的时候,完全由 middleware 控制。

const m = store => next => action => {
    setTimeout(() => {
        ... // 针对老的 action 进行处理,生成新的 action
        next(newAction)
    }, 2000)
}

在 middleware 上做文章显然比理所当然那种方法好很多,我们可以封装相应的 middleware,然后做出不同的处理,把异步封装在 middleware 中。

无疑,在 middleware 上做文章是优雅的。

2.5.3.3 第三方组件

redux 本身不提供异步,不代表第三方也不支持,有下面的一些 thunk 能方便我们在 redux 中进行异步操作。

2.5.3.3.1 redux-thunk

redux-thunk 是 redux 官方钦定的异步组件,实质是 redux 的一个 middleware。

正如官方的介绍,redux-thunk 的目的就是让同步操作与异步操作以同一种方式来 dispatch:

如上,同样的 API 去实现异步操作。

thunk 是什么?

thunk 这是一个神秘的单词,在不同的场合其意义有所差别。在这里,如官方所言:“A thunk is a function that wraps an expression to delay its evaluation.”

他就是一个延时的函数,其意义与函数式编程中的惰性有点相同的意思

编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。(https://i5ting.github.io/asynchronous-flow-control/#702)

store.dispatch(dispatch => {
    fetch(...)
        .then(data){
            dispatch({action: 'xxx', text: JSON.parse(data)})
        }
})

如上,redux-thunk 判断 store.dispatch 传递进来的参数,如果是一个 thunk(延时函数),就处理 thunk 里面的东西,完事之后执行这个函数。

纵观 redux-thunk 的源码,也能感叹设计之美:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

在实际操作中,我们可能需要标记异步是否实现完毕,一般情况下我们可以设置两个 Action 来标记:

store.dispatch(dispatch => {
    dispatch({action: 'FETCH_START', text: ''})
    fetch(...)
        .then(data){
            dispatch({action: 'FETCH_END', text: JSON.parse(data)})
        }
})
2.5.3.3.2 其他

比较著名的 redux 异步组件还有 redux-premise、redux-saga 等,大家可以自行研究(我还没使用过😂)

2.5.4 Action Creater

尤其是在有异步操作的时候,比如我们 dispatch(thunk) 的时候,这个 thunk 是比叫大的,理论上我们需要对他封装一下:

// 原
store.dispatch(dispatch => {
    dispatch({action: 'FETCH_START', text: ''})
    fetch(...)
        .then(data){
            dispatch({action: 'FETCH_END', text: JSON.parse(data)})
        }
})

// 封装之后
store.dispatch(fetch_thunk(text))

const fetch_thunk = (text) => {
    return dispatch => {
        dispatch({action: 'FETCH_START', text: ''})
        fetch(...)
            .then(data){
                dispatch({action: 'FETCH_END', text: JSON.parse(data)})
            }
    }
}

我们发现,又可以优雅的 dispatch 了。 ✿

2.5.5 immutable

我们知道 Redux 的风格是函数式的,在函数式编程中经常会出现一个重要的概念: immutable。

在 Redux 中,Store 中的 state 应该就是 immutable 的,在上面的例子中,我们明显的看到每次不是修改 state, 而是返回一个新的 state。这个过程中,我们就希望 state 是 immutable 的。

在 JavaScript 中,没有这个特定。

immutable 要求的是值本身不被修改,这点也是和 const 的区别(“指针”不发生变化,比如用 const 声明一个 Object 之后,我们还能在给这个 Object 添加新的 key-value 对);最需要注意的是在 JavaScript 中引用类型相关(Object、Array)的变量。例如:

let a = 1
let b = a
b = 2
console.log(a)
console.log(b)

let c = {'m': 1}
let d = c
d.n = 2
console.log(c)
console.log(d)

在上面的例子中,我们会发现 a 不会随着 b 的改变而发生变化,而 c 会随着 d 的改变发生变化。显然这个是不符合 immutable 的!

对于这个问题,我们可以使用一些没有副作用的方法来解决,比如对 Array 使用 concat 生成新的数组、对 Object 使用解构等

let a = {m:1}
let b = {...a, n:2}
console.log(a)
console.log(b)

let c = [1, 2]
let d = [].concat(c)
c.push(3)
console.log(c)
console.log(d)

在实际开发过程中,我们可能会觉得像上面一样做可能容易犯错误,更重要的一点是直观地,我们会感觉到如果数据是 immutable 的,性能应该会更好,当然事实上也是如此。针对这个问题,我们可以使用一些库然后利用他的 API 来操作数据去保证数据是 immutable 的。

比较著名的一些 immutable 库有:

redux 是一个处理状态的库,尽管你听他的时候他经常和 react 出现,但这并不意味着 redux 只能和 react 一起用。他还可以和 Angular、backbone 等使用,甚至可以直接使用 redux,就和上面的描述一样。

严格来说他们是互相分离的,中间使用官方的 react-redux 就可以完美的把 redux 与 react 结合起来。

2.6.1 provider

Provider 是在原有的 APP 上面包一层,然后接收 store 作为 props,然后给 connect 用

2.6.2 connect

connect 应该是 react-redux 的核心。他接收 store 提供的 state 和 action,返回给我们的 react 组件;还有一个很重要的一点是在 connect 这层 react-redux 做了优化,保证给组件只传和他相关的 state,这也就保证了性能上的优势

2.6.3 实战

HTML:

JavaScript:

系列文章

wq1308786830 commented 7 years ago

niubility