redux启示与实践

简介: redux启示与实践,redux三原则及data flow

redux三原则及data flow

  • 单一数据源
  • 状态只读
  • 使用纯函数来改变状态

稍微注意一下,这些原则,在redux的实现中是一点也没体现出来。

In a very real sense, each one of those statements is a lie!
      -- 摘自tao-of-redux

而这些原则是指导你如何使用redux

数据流

data_flow

counter demo

function counter(state = { num: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { num: state.num + 1 }
    case 'DECREMENT':
      return { num: state.num - 1 }
    default:
      return state
  }
}
const store = createStore(counter)

store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
// { num: 1 }
console.log(store.getState())
复制代码

因为介绍redux的文章太多,这里就略过了
建议看下官方文档的基础部分

Action Creator

为什么要使用action creator?
可以阅读
基础部分的actions#action-creators
技巧部分的reducing-boilerplate#action-creatorswhy-use-action-creators

示例见 常用工具的使用 -> redux-actions

Naive Implement

其实想一下,createStore创建出来的对象,无非包含几个方法,dispatch, getState, subscribe...

createStore很好实现了

function createStore(reducer, preloadState) {
  let currentState = preloadState
  let listener = []

  function getState() {
    return currentState
  }

  function subscribe(listener) {
    listener.push(listener)
    return function unsubscribe() {
      let index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    currentState = currentReducer(state, action)
    listeners.forEach(listener => listener())

    return action
  }

  return {
    dispatch,
    subscribe,
    getState
  }
}
复制代码

大概不到30行的代码,让你想不到的是,redux的确就是类似这样的方式来实现的。
你之前或许会想,应该搞一个factory创建一个类, 或者其他一些技巧尽量避开闭包。。。

所以有时候,写代码的时候,真的不要想太多,在代码的"样子"还没有对性能,可读性...产生影响的时候,越简单越好

Middleware

官方文档的middleware的介绍

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer

其实它是一种装饰者模式,一种可以动态添加功能的模式
详细可以阅读
JS 5种不同的方法实现装饰者模式
js实现装饰者模式,有几种方法,为了配合文档,这里说一下monkeypatch和middleware的方式

monkeypatch -- 猴补丁
为什么叫猴补丁呢? 想了解可以搜索一下

猴补丁怎么体现在代码上呢? 在运行时替换方法、属性等 当然,既然已经称为模式,肯定是不能修改原代码的,要不违反开闭原则

let p = {
  sayHello(name) {
    return 'hello, ' + name
  }
}

function decorateWithUpperFirst(obj) {
  let originSay = obj.sayHello
  obj.sayHello = (name) => {
    return originSay(name.charAt(0).toUpperCase() + name.substr(1).toLowerCase())
  }
}

function decorateWithLongName(obj) {
  let originSay = obj.sayHello
  obj.sayHello = (name) => {
      if (name.length > 4) {
        return originSay(name)
      } else {
        console.log('sorry...')
      }
  } 
}

decorateWithUpperFirst(p)
decorateWithLongName(p)

// 返回hello, Xiangwangdeshenghuo
p.sayHello('xiangwangdeshenghuo')
// 控制台输出sorry...,返回
p.sayHello('jxtz')
复制代码

第一次调用sayHello时,会进入decorateWithLongName方法中定义的sayHello,由于name长度大于4,会调用它外层的originSay, 即是decrateWithUpperFirst方法中定义的sayHello, 将首字母大写,最后调用它外层的originSay,即最初的p.sayHello

middleware

let p = {
  prefix: 'hello, ',

  sayHello(name) {
    return this.prefix + name
  }
}

function addDecorators(obj, decorators) {
  let sayHello = obj.sayHello

  decorators.slice().reverse().forEach((decorator) => {
    sayHello = decorator(obj)(sayHello)
  })

  obj.sayHello = sayHello
}

function decorateWithUpperFirst(obj) {
  return (nextSay) => {
    return function sayHello1(name) {
      nextSay.call(obj, name.charAt(0).toUpperCase() + name.substr(1).toLowerCase())
    }
  }
}

function decorateWithShortName(obj) {
  return (nextSay) => {
    return function sayHello2(name) {
      if (name.length <= 4) {
        nextSay.call(obj, name)
      } else {
        console.log('substr...')
        obj.sayHello(name.substr(0, 3))
      }
    }
  }
}

addDecorators(p, [decorateWithUpperFirst, decorateWithShortName])


// hello, Jxtz
p.sayHello('jxtz')
// hello, Xia
p.sayHello('xiangwangdeshenghuo')
复制代码

上面两次客户端调用sayHello, sayHello1函数,分别调用了几次?

其实middleware只是把monkey patch隐藏起来 官方文档的middleware 介绍的很详细
至于redux真实情况是怎么实现middleware的, applyMiddleware, 其利用了compose函数,熟悉函数式的应该特别熟悉这个组合函数

Split Reducer

技巧里的splitting-reducer-logic
拆分就要考虑重用,以及其他(如slice reducer之间的状态获取)...
refactoring-reducers-example

由于我们的state,往往是嵌套层级的(当然redux希望你去标准化它),由于这个需求太过于普遍性,redux提供了combineReducers这个工具方法,但是redux对很多实践都是unbiased, 对此也是,你甚至可以不用combineReducers

由于使用combineReducers是redux的common practice
下面看combineReducers的使用

function postsById(state = {}, action) {
  let { id, post } = action
  switch(action.type) {
    case 'ADD_POST':
      return Object.assign({}, state, { [id]: post })
      break
    default:
      return state
  }
}

function postsallIds(state = [], action) {
  let { id } = action
  switch(action.type) {
    case 'ADD_POST':
      return state.concat(id)
      break
    default:
      return state
  }
}

const posts = combineReducers({
  byId: postsById,
  allIds: postsallIds
})


// 类似posts...
function commentsById(state = {}, action) {
  let { id, comment } = action
  switch(action.type) {
    case 'ADD_COMMENT':
      Object.assign({}, state, { [id]: comment })
      break
    default:
      return state
  }
}

function commentsAllIds(state = [], action) {
  let { id } = action
  switch(action.type) {
    case 'ADD_COMMENT':
      return state.concat(id)
      break
    default:
      return state
  }
}

const comments = combineReducers({
  byId: commentsById,
  allIds: commentsAllIds
})

const rootReducer = combineReducers({
  posts,
  comments
})

// 使用
let store = createStore(rootReducer)
// 其实在createStore已经建立了初始值
// 聪明的读者,你能知道createStore是如何建立这个初始值的吗?
// {
//   posts: { byId: {}, allIds: [] },
//   comments: { byId: {}, allIds: [] }
// }
console.log(store.getState())

// dispatch会触发所有的reducer执行, 这里的slice reducer, case reducer
store.dispatch({ type: 'ADD_POST', id: 1,  post: '这是一篇文章哦' })

// {
//   posts: { byId: {1: '这是一篇文章哦'}, allIds: [1] },
//   comments: { byId: {}, allIds: [] }
// }
console.log(store.getState())
复制代码

使用了之后,自然可以去看redux的实现

显而易见,combineReducer也并不神秘,返回的仅仅也是一个reducer函数, 它将key值与state对应起来,从而在调用combine后的reducer时将state[key]值传入对应的slice reducer函数,从而slice reducer只处理自身感兴趣的state部分

在这里applyMiddleware做了一个优化,由于我们的action.type为'ADD_POST', 所以对comments部分的状态是没有改变的, 所以这部分comments状态会直接返回之前的引用 并不会返回新对象

Async Actions

如果没有middleware, 我们只能在组件中调用ajax 然后就会重复代码,我们需要重用逻辑 async-action-creators

Middleware lets us write more expressive, potentially async action creators.

介绍redux-thunk 见 常用工具使用 -> redux-thunk

一些常用工具的使用

redux-actions

Flux Standard Action

let defaultState = { num: 10 }

let addNum = createAction('ADD', (n) => {
  return {
    n
  }
})

let subtractNum = createAction('SUBTRACT', (n) => {
  return {
    n
  }
})

const rootReducer = handleActions({
  'ADD': (state, action) => {
    let { payload: { n } } = action
    return { ...state, num: state.num + n }
  },
  'SUBTRACT': (state, action) => {
    let { payload: { n } } = action
    return { ...state, num: state.num - n }
  }
}, defaultState)

let store = createStore(rootReducer)

store.dispatch(addNum(2))
// { num: 12 }
console.log(store.getState())
store.dispatch(subtractNum(1))
// { num: 11 }
console.log(store.getState())
复制代码

当然你可以使用更简洁的combineActions,见其repo

这里简单说一下,handleAction的实现, 他提供了next, throw的api,其实是查看action.error来判断调用next还是throw, 至于他内部也是判断action.type是否被include在它的第一个type参数(强制被转成数组)里, 从而决定是否执行此reducer.

redux-thunk(redux-promise, redux-saga)

“Thunk” middleware lets you write action creators as “thunks”, that is, functions returning functions. This inverts the control: you will get dispatch as an argument, so you can write an action creator that dispatches many times.

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;
复制代码

你看的没错,这就是redux-thunk的全部代码,仅仅判断如果action是函数,即action creator中返回的函数,那么将调用此函数并将dispatch和getState的传入。
特别注意,正如前面middleware中的代码,此时传入的dispatch,是applyMiddleware的middlewareAPI对象中的dispatch, 那么调用这个dispatch, 会让整个middleware chain都从头调用一遍, 就如前面decorateWithShortName的else部分

更多, 建议看看如下文档及代码 官方实例asyncreal world

当然你可以选择使用promise,而不是function, 那么你可以用redux-promise
也可以选择generator的方式, redux-saga

reselect

Reselect is a simple library for creating memoized, composable selector functions. Reselect selectors can be used to efficiently compute derived data from the Redux store.

官方文档computing-derived-data 看过文档,对他的使用也有所了解

这里,关注一下, 他到底做了啥优化?
memorize函数, 应该也见过很多次, 复习下

function defaultEqualityCheck(a, b) {
  return a === b
}

function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
  if (prev === null || next === null || prev.length !== next.length) {
    return false
  }

  // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
  const length = prev.length
  for (let i = 0; i < length; i++) {
    if (!equalityCheck(prev[i], next[i])) {
      return false
    }
  }

  return true
}

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null
  let lastResult = null
  // we reference arguments instead of spreading them for performance reasons
  return function () {
    if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
      // apply arguments instead of spreading for performance.
      lastResult = func.apply(null, arguments)
    }

    lastArgs = arguments
    return lastResult
  }
}
复制代码

意思就是传入一个函数的func,它只接受一个数组参数,memorize将返回一个函数,调用它时,会检查这个数组的每个元素,与之前的是否 "===", 如果均通过,则使用"记忆"的数据,不重新计算

剩下就是将最后一个函数前面所有的依赖函数调用的值,与之前进行比较,如果相同则使用原先的结果,不再调用最后一个函数

const state = {
  a : {
      first : 5
  },
  b : 10
};

const selectA = state => state.a;
const selectB = state => state.b;

const selectA1 = createSelector(
    [selectA],
    a => a.first
);

const selectResult = createSelector(
    [selectA1, selectB],
    (a1, b) => {
        console.log("Output selector running");
        return a1 + b;
    }
);

const result = selectResult(state);
// Log: "Output selector running"
console.log(result);
// 15

const secondResult = selectResult(state);
// No log output
console.log(secondResult);
// 15
复制代码

总之reselect,可以提升性能,一方面,一个复杂转换操作,其性能损耗大,那么仅在state.someData变化时,才执行,而state.someElseData变化,它只需返回缓存数据,另一方面,对react-redux, connect方法, 根据你返回的mapState的所有字段是否与之前"===", 来决定组件是否rerender, 而返回缓存数据,不会触发组件rerender
using-reselect-selectors

小结

关于redux的内容,还有很多内容没有介绍,比如server-render, immutable(immer)结合, devtools, react-redux...

redux对很多使用规则都是无偏见的,只要你遵循他的思想, 所以还需要多实践它的common practice,找到适合自己的best practice

参考

这里有redux很多资料
https://redux.js.org/introduction/learning-resources
最好多看作者的stackoverflow和issue中的回答 



原文发布时间为:2018年06月30日

作者:小雨心情

本文来源:掘金 如需转载请联系原作者


相关文章
|
6天前
|
开发框架 前端开发 JavaScript
【总结】React 的发展情况总结及大厂选择与新手看法
【总结】React 的发展情况总结及大厂选择与新手看法
|
3月前
|
缓存 移动开发 JavaScript
vue核心面试题汇总【查缺补漏】(一)
vue核心面试题汇总【查缺补漏】(一)
107 0
|
3月前
|
缓存 JavaScript 前端开发
vue核心面试题汇总【查缺补漏】(二)
vue核心面试题汇总【查缺补漏】(二)
|
3月前
|
JavaScript 前端开发 编译器
【VueConf 2022】尤雨溪:Vue的进化历程
【VueConf 2022】尤雨溪:Vue的进化历程
|
8月前
|
前端开发 JavaScript 小程序
react技术问题十问十答
react技术问题十问十答
56 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-前端洋葱圈模型
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-前端洋葱圈模型
56 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react hook认知2
前端学习笔记202307学习笔记第六十天-react hook认知2
30 1
|
8月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react hook认知1
前端学习笔记202307学习笔记第六十天-react hook认知1
37 0
|
10月前
|
设计模式 缓存 前端开发
|
存储 前端开发 JavaScript
React修仙笔记,筑基初期之更新数据
在之前的一篇文章中我们有了解到react函数组件和class组件,以及react数据流,状态提升,以及react设计哲学,在我们了解了这些基本的知识后,我们需要了解react内部更深的一些知识
React修仙笔记,筑基初期之更新数据