欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解JavaScript状态容器Redux

程序员文章站 2022-06-25 09:09:09
目录一、why redux二、redux data flow三、three principles(三大原则)四、redux源码解析4.2、createstore.js4.3、combinereduce...

一、why redux

在说为什么用 redux 之前,让我们先聊聊组件通信有哪些方式。常见的组件通信方式有以下几种:

  • 父子组件:props、state/callback回调来进行通信
  • 单页面应用:路由传值
  • 全局事件比如eventemitter监听回调传值
  • react中跨层级组件数据传递context(上下文)

在小型、不太复杂的应用中,一般用以上几种组件通信方式基本就足够了。

但随着应用逐渐复杂,数据状态过多(比如服务端响应数据、浏览器缓存数据、ui状态值等)以及状态可能会经常发生变化的情况下,使用以上组件通信方式会很复杂、繁琐以及很难定位、调试相关问题。

因此状态管理框架(如 vuex、mobx、redux等)就显得十分必要了,而 redux 就是其中使用最广、生态最完善的。

二、redux data flow

在一个使用了 redux 的 app应用里面会遵循下面四步:

第一步:通过store.dispatch(action)来触发一个action,action就是一个描述将要发生什么的对象。如下:

{ type: 'like_article', articleid: 42 }
{ type: 'fetch_user_success', response: { id: 3, name: 'mary' } }
{ type: 'add_todo', text: '金融前端.' }

第二步:redux会调用你提供的 reducer函数。

第三步:根 reducer 会将多个不同的 reducer 函数合并到单独的状态树中。

第四步:redux store会保存从根 reducer 函数返回的完整状态树。

所谓一图胜千言,下面我们结合 redux 的数据流图来熟悉这一过程。

详解JavaScript状态容器Redux

三、three principles(三大原则)

1、single source of truth:单一数据源,整个应用的state被存储在一个对象树中,并且只存在于唯一一个store中。

2、state is read-only:state里面的状态是只读的,不能直接去修改state,只能通过触发action来返回一个新的state。

3、changes are made with pure functions:要使用纯函数来修改state。

四、redux源码解析

redux 源码目前有js和ts版本,本文先介绍 js 版本的 redux 源码。redux 源码行数不多,所以对于想提高源码阅读能力的开发者来说,很值得前期来学习。

redux源码主要分为6个核心js文件和3个工具js文件,核心js文件分别为index.js、createstore.js、compose.js、combineruducers.js、bindactioncreators.js和applymiddleware.js文件。

接下来我们来一一学习。

4.1、index.js

index.js是入口文件,提供核心的api,如createstore、combinereducers、applymiddleware等。

export {
  createstore,
  combinereducers,
  bindactioncreators,
  applymiddleware,
  compose,
  __do_not_use__actiontypes
}

4.2、createstore.js

createstore是 redux 提供的api,用来生成唯一的store。store提供getstate、dispatch、subscibe等方法,redux 中的store只能通过dispatch一个action,通过action来找对应的 reducer函数来改变。

export default function createstore(reducer, preloadedstate, enhancer) {
...
}

从源码中可以知道,createstore接收三个参数:reducer、preloadedstate、enhancer。

reducer是action对应的一个可以修改store中state的纯函数。

preloadedstate代表之前state的初始化状态。

enhancer是中间件通过applymiddleware生成的一个加强函数。store中的getstate方法是获取当前应用中store中的状态树。

/**
 * reads the state tree managed by the store.
 *
 * @returns {any} the current state tree of your application.
 */
function getstate() {
  if (isdispatching) {
    throw new error(
      'you may not call store.getstate() while the reducer is executing. ' +
        'the reducer has already received the state as an argument. ' +
        'pass it down from the top reducer instead of reading it from the store.'
    )
  }
  return currentstate
}

dispatch方法是用来分发一个action的,这是唯一的一种能触发状态发生改变的方法。subscribe是一个监听器,当一个action被dispatch的时候或者某个状态发生改变的时候会被调用。

4.3、combinereducers.js

/**
 * turns an object whose values are different reducer functions, into a single
 * reducer function. it will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 */
export default function combinereducers(reducers) {
  const reducerkeys = object.keys(reducers)
     ...
  return function combination(state = {}, action) {
     ...
    let haschanged = false
    const nextstate = {}
    for (let i = 0; i < finalreducerkeys.length; i++) {
      const key = finalreducerkeys[i]
      const reducer = finalreducers[key]
      const previousstateforkey = state[key]
      const nextstateforkey = reducer(previousstateforkey, action)
      if (typeof nextstateforkey === 'undefined') {
        const errormessage = getundefinedstateerrormessage(key, action)
        throw new error(errormessage)
      }
      nextstate[key] = nextstateforkey
      //判断state是否发生改变
      haschanged = haschanged || nextstateforkey !== previousstateforkey
    }
    //根据是否发生改变,来决定返回新的state还是老的state
    return haschanged ? nextstate : state
  }
}

从源码可以知道,入参是 reducers,返回一个function。combinereducers就是将所有的 reducer合并成一个大的 reducer 函数。核心关键的地方就是每次 reducer 返回新的state的时候会和老的state进行对比,如果发生改变,则haschanged为true,触发页面更新。反之,则不做处理。

4.4、bindactioncreators.js

/**
 * turns an object whose values are action creators, into an object with the
 * same keys, but with every function wrapped into a `dispatch` call so they
 * may be invoked directly. this is just a convenience method, as you can call
 * `store.dispatch(myactioncreators.dosomething())` yourself just fine.
 */
function bindactioncreator(actioncreator, dispatch) {
  return function() {
    return dispatch(actioncreator.apply(this, arguments))
  }
}
 
export default function bindactioncreators(actioncreators, dispatch) {
  if (typeof actioncreators === 'function') {
    return bindactioncreator(actioncreators, dispatch)
  }
    ...
    ...
  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
}

bindactioncreator是将单个actioncreator绑定到dispatch上,bindactioncreators就是将多个actioncreators绑定到dispatch上。

bindactioncreator就是将发送actions的过程简化,当调用这个返回的函数时就自动调用dispatch,发送对应的action。

bindactioncreators根据不同类型的actioncreators做不同的处理,actioncreators是函数就返回函数,是对象就返回一个对象。主要是将actions转化为dispatch(action)格式,方便进行actions的分离,并且使代码更加简洁。

4.5、compose.js

/**
 * composes single-argument functions from right to left. the rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...function} funcs the functions to compose.
 * @returns {function} a function obtained by composing the argument functions
 * from right to left. for example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */
 
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
 
  if (funcs.length === 1) {
    return funcs[0]
  }
 
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose是函数式变成里面非常重要的一个概念,在介绍compose之前,先来认识下什么是 reduce?官方文档这么定义reduce:reduce()方法对累加器和数组中的每个元素(从左到右)应用到一个函数,简化为某个值。compose是柯里化函数,借助于reduce来实现,将多个函数合并到一个函数返回,主要是在middleware中被使用。

4.6、applymiddleware.js

/**
 * creates a store enhancer that applies middleware to the dispatch method
 * of the redux store. this is handy for a variety of tasks, such as expressing
 * asynchronous actions in a concise manner, or logging every action payload.
 */
export default function applymiddleware(...middlewares) {
  return createstore => (...args) => {
    const store = createstore(...args)
    ...
    ...
    return {
      ...store,
      dispatch
    }
  }
}

applymiddleware.js文件提供了middleware中间件重要的api,middleware中间件主要用来对store.dispatch进行重写,来完善和扩展dispatch功能。

那为什么需要中间件呢?

首先得从reducer说起,之前 redux三大原则里面提到了reducer必须是纯函数,下面给出纯函数的定义:

  • 对于同一参数,返回同一结果
  • 结果完全取决于传入的参数
  • 不产生任何副作用

至于为什么reducer必须是纯函数,可以从以下几点说起?

  • 因为 redux 是一个可预测的状态管理器,纯函数更便于 redux进行调试,能更方便的跟踪定位到问题,提高开发效率。
  • redux 只通过比较新旧对象的地址来比较两个对象是否相同,也就是通过浅比较。如果在 reducer 内部直接修改旧的state的属性值,新旧两个对象都指向同一个对象,如果还是通过浅比较,则会导致 redux 认为没有发生改变。但要是通过深比较,会十分耗费性能。最佳的办法是 redux返回一个新对象,新旧对象通过浅比较,这也是 reducer是纯函数的重要原因。

reducer是纯函数,但是在应用中还是会需要处理记录日志/异常、以及异步处理等操作,那该如何解决这些问题呢?

这个问题的答案就是中间件。可以通过中间件增强dispatch的功能,示例(记录日志和异常)如下:

const store = createstore(reducer);
const next = store.dispatch;
 
// 重写store.dispatch
store.dispatch = (action) => {
    try {
        console.log('action:', action);
        console.log('current state:', store.getstate());
        next(action);
        console.log('next state', store.getstate());
    } catch (error){
        console.error('msg:', error);
    }
}

五、从零开始实现一个简单的redux

既然是要从零开始实现一个redux(简易计数器),那么在此之前我们先忘记之前提到的store、reducer、dispatch等各种概念,只需牢记redux是一个状态管理器。

首先我们来看下面的代码:

let state = {
    count : 1
}
//修改之前
console.log (state.count);
//修改count的值为2
state.count = 2;
//修改之后
console.log (state.count);

我们定义了一个有count字段的state对象,同时能输出修改之前和修改之后的count值。但此时我们会发现一个问题?就是其它如果引用了count的地方是不知道count已经发生修改的,因此我们需要通过订阅-发布模式来监听,并通知到其它引用到count的地方。因此我们进一步优化代码如下:

let state = {
    count: 1
};
//订阅
function subscribe (listener) {
    listeners.push(listener);
}
function changestate(count) {
    state.count = count;
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i];
        listener();//监听
    }
}

此时我们对count进行修改,所有的listeners都会收到通知,并且能做出相应的处理。但是目前还会存在其它问题?比如说目前state只含有一个count字段,如果要是有多个字段是否处理方式一致。同时还需要考虑到公共代码需要进一步封装,接下来我们再进一步优化:

const createstore = function (initstate) {
    let state = initstate;
    //订阅
    function subscribe (listener) {
        listeners.push(listener);
    }
    function changestate (count) {
        state.count = count;
        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i];
            listener();//通知
        }
    }
    function getstate () {
        return state;
    }
    return {
        subscribe,
        changestate,
        getstate
    }
}

我们可以从代码看出,最终我们提供了三个api,是不是与之前redux源码中的核心入口文件index.js比较类似。但是到这里还没有实现redux,我们需要支持添加多个字段到state里面,并且要实现redux计数器。

let initstate = {
    counter: {
        count : 0
    },
    info: {
        name: '',
        description: ''
    }
}
let store = createstore(initstate);
//输出count
store.subscribe(()=>{
    let state = store.getstate();
    console.log(state.counter.count);
});
//输出info
store.subscribe(()=>{
    let state = store.getstate();
    console.log(`${state.info.name}:${state.info.description}`);
});

通过测试,我们发现目前已经支持了state里面存多个属性字段,接下来我们把之前changestate改造一下,让它能支持自增和自减。

//自增
store.changestate({
    count: store.getstate().count + 1
});
//自减
store.changestate({
    count: store.getstate().count - 1
});
//随便改成什么
store.changestate({
    count: 金融
});

我们发现可以通过changestate自增、自减或者随便改,但这其实不是我们所需要的。我们需要对修改count做约束,因为我们在实现一个计数器,肯定是只希望能进行加减操作的。所以我们接下来对changestate做约束,约定一个plan方法,根据type来做不同的处理。

function plan (state, action) => {
  switch (action.type) {
    case 'increment':
      return {
        ...state,
        count: state.count + 1
      }
    case 'decrement':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state
  }
}
let store = createstore(plan, initstate);
//自增
store.changestate({
    type: 'increment'
});
//自减
store.changestate({
    type: 'decrement'
});

我们在代码中已经对不同type做了不同处理,这个时候我们发现再也不能随便对state中的count进行修改了,我们已经成功对changestate做了约束。我们把plan方法做为createstore的入参,在修改state的时候按照plan方法来执行。到这里,恭喜大家,我们已经用redux实现了一个简单计数器了。

这就实现了 redux?这怎么和源码不一样啊

然后我们再把plan换成reducer,把changestate换成dispatch就会发现,这就是redux源码所实现的基础功能,现在再回过头看redux的数据流图是不是更加清晰了。

详解JavaScript状态容器Redux

六、redux devtools

redux devtools是redux的调试工具,可以在chrome上安装对应的插件。对于接入了redux的应用,通过 redux devtools可以很方便看到每次请求之后所发生的改变,方便开发同学知道每次操作后的前因后果,大大提升开发调试效率。

详解JavaScript状态容器Redux

如上图所示就是 redux devtools的可视化界面,左边操作界面就是当前页面渲染过程中执行的action,右侧操作界面是state存储的数据,从state切换到action面板,可以查看action对应的 reducer参数。切换到diff面板,可以查看前后两次操作发生变化的属性值。

七、总结

redux 是一款优秀的状态管理器,源码短小精悍,社区生态也十分成熟。如常用的react-redux、dva都是对 redux 的封装,目前在大型应用中被广泛使用。这里推荐通过redux官网以及来学习它核心的思想,进而提升阅读源码的能力。

以上就是详解javascript状态容器redux的详细内容,更多关于javascript状态容器redux的资料请关注其它相关文章!