Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
rccoder opened this issue Oct 10, 2016 · 1 comment
Open

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

rccoder opened this issue Oct 10, 2016 · 1 comment

Comments

@rccoder
Copy link
Owner

rccoder commented Oct 10, 2016

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)

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

  • 无法复用异步代码,尤其是用 Promise 等的时候会出现一堆的同质代码(理论上我们可以抽象,见后面的 ActionCreater)
  • 多个异步不好统一处理
2.5.3.2 middleware 上做文章

前面在介绍 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:

  • dispatch(action)
  • dispatch(thunk)

如上,同样的 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 库有:

2.6 与 React 合作 —— react-redux

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:

系列文章

@rccoder rccoder added the React label Oct 14, 2016
@rccoder rccoder changed the title Redux 学习总结 听说你需要这样了解 Redux Oct 14, 2016
@rccoder rccoder changed the title 听说你需要这样了解 Redux 听说你需要这样了解 Redux(一) Feb 8, 2017
@rccoder rccoder added the Redux label Feb 18, 2017
@wq1308786830
Copy link

niubility

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants