在这一篇文章中,笔者将带大家编写一个完整的Redux,深度剖析Redux的方方面面,读完本篇文章后,大家对Redux会有一个深刻的认识。

核心API

这套代码是笔者阅读完Redux源码,理解其设计思路后,自行总结编写的一套代码,API的设计遵循与原始一致的原则,省略掉了一些不必要的API。

createStore

这个方法是Redux核心中的核心,它将所有其他的功能连接在一起,暴露操作的API供开发者调用。

const INIT = '@@redux/INIT_' + Math.random().toString(36).substring(7)

export default function createStore (reducer, initialState, enhancer) {  if (typeof initialState === 'function') {
    enhancer = initialState
    initialState = undefined
  }  let state = initialState  const listeners = []  const store = {
    getState () {      return state
    },
    dispatch (action) {      if (action && action.type) {
        state = reducer(state, action)
        listeners.forEach(listener => listener())
      }
    },
    subscribe (listener) {      if (typeof listener === 'function') {
        listeners.push(listener)
      }
    }
  }  if (typeof initialState === 'undefined') {
    store.dispatch({ type: INIT })
  }  if (typeof enhancer === 'function') {    return enhancer(store)
  }  return store
}

在初始化时,createStore会主动触发一次dispach,它的action.type是系统内置的INIT,所以在reducer中不会匹配到任何开发者自定义的action.type,它走的是switch中default的逻辑,目的是为了得到初始化的状态。

当然我们也可以手动指定initialState,笔者在这里做了一层判断,当initialState没有定义时,我们才会dispatch,而在源码中是都会执行一次dispatch,笔者认为没有必要,这是一次多余的操作。因为这个时候,监听流中没有注册函数,走了一遍reducer中的default逻辑,得到新的state和initialState是一样的。

第三个参数enhancer只有在使用中间件时才会用到,通常情况下我们搭配applyMiddleware来使用,它可以增强dispatch的功能,如常用的logger和thunk,都是增强了dispatch的功能。

同时createStore会返回一些操作API,包括:

  • getState:获取当前的state值

  • dispatch:触发reducer并执行listeners中的每一个方法

  • subscribe:将方法注册到listeners中,通过dispatch来触发

applyMiddleware

这个方法通过中间件来增强dispatch的功能。

在写代码前,我们先来了解一下函数的合成,这对后续理解applyMiddleware的原理大有裨益。

函数的合成

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做函数的合成(compose)

举个例子

function add (a) {  return function (b) {    return a + b
  }
}// 得到合成后的方法let add6 = compose(add(1), add(2), add(3)) add6(10) // 16

下面我们通过一个非常巧妙的方法来写一个函数的合成(compose)。

export 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)))
}

上述代码巧妙的地方在于:通过数组的reduce方法,将两个方法合成一个方法,然后用这个合成的方法再去和下一个方法合成,直到结束,这样我们就得到了一个所有方法的合成函数。

有了这个基础,applyMiddleware就会变得非常简单。

import { compose } from './utils'export default function applyMiddleware (...middlewares) {  return store => {    const chains = middlewares.map(middleware => middleware(store))
    store.dispatch = compose(...chains)(store.dispatch)    return store
  }
}

光看这段代码可能有点难懂,我们配合中间件的代码结构来帮助理解

function middleware (store) {  return function f1 (dispatch) {    return function f2 (action) {      // do something
      dispatch(action)      // do something
    }
  }
}

可以看出,chains是函数f1的数组,通过compose将所欲f1合并成一个函数,暂且称之为F1,然后我们将原始dispatch传入F1,经过f2函数一层一层地改造后,我们得到了一个新的dispatch方法,这个过程和Koa的中间件模型(洋葱模型)原理是一样的。

为了方便大家理解,我们再来举个例子,有以下两个中间件

function middleware1 (store) {  return function f1 (dispatch) {    return function f2 (action) {      console.log(1)
      dispatch(action)      console.log(1)
    }
  }
}function middleware2 (store) {  return function f1 (dispatch) {    return function f2 (action) {      console.log(2)
      dispatch(action)      console.log(2)
    }
  }
}// applyMiddleware(middleware1, middleware2)

大家猜一猜以上的log输出顺序是怎样的?

好了,答案揭晓:1, 2, (原始dispatch), 2, 1。

为什么会这样呢?因为middleware2接收的dispatch是最原始的,而middleware1接收的dispatch是经过middleware1改造后的,我把它们写成如下的样子,大家应该就清楚了。

console.log(1)/* middleware1返回给middleware2的dispatch */console.log(2)
dispatch(action)console.log(2)/* end */console.log(1)

三个或三个以上的中间件,其原理也是如此。

至此,最复杂最难理解的中间件已经讲解完毕。

combineReducers

由于Redux是单一状态流管理的模式,因此如果有多个reducer,我们需要合并一下,这块的逻辑比较简单,直接上代码。

export default function combineReducers (reducers) {  const availableKeys = []  const availableReducers = {}  Object.keys(reducers).forEach(key => {    if (typeof reducers[key] === 'function') {
      availableKeys.push(key)
      availableReducers[key] = reducers[key]
    }
  })  return (state = {}, action) => {    const nextState = {}    let hasChanged = false     availableKeys.forEach(key => {
      nextState[key] = availableReducers[key](state[key], action)      if (!hasChanged) {
        hasChanged = state[key] !== nextState[key]
      }
    })    return hasChanged ? nextState : state
  }
}

combineReucers将单个reducer塞到一个对象中,每个reducer对应一个唯一键值,单个reducer状态改变时,对应键值的值也会改变,然后返回整个state。

bindActionCreators

这个方法就是将我们的action和dispatch连接起来。

function bindActionCreator (actionCreator, dispatch) {  return function () {
    dispatch(actionCreator.apply(this, arguments))
  }
} export default function bindActionCreators (actionCreators, dispatch) {  if (typeof actionCreators === 'function') {    return bindActionCreator(actionCreators, dispatch)
  }  const boundActionCreators = {}  Object.keys(actionCreators).forEach(key => {    let actionCreator = actionCreators[key]    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  })  return boundActionCreators
}

它返回一个方法集合,直接调用来触发dispatch。

中间件

在自己动手编写中间件时,你一定会惊奇的发现,原来这么好用的中间件代码竟然只有寥寥数行,却可以实现这么强大的功能。

logger

function getFormatTime () {  const date = new Date()  return date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() + ' ' + date.getMilliseconds()
} export default function logger ({ getState }) {  return next => action => {    /* eslint-disable no-console */
    console.group(`%caction %c${action.type} %c${getFormatTime()}`, 'color: gray; font-weight: lighter;', 'inherit', 'color: gray; font-weight: lighter;')    // console.time('time')
    console.log(`%cprev state`, 'color: #9E9E9E; font-weight: bold;', getState())    console.log(`%caction    `, 'color: #03A9F4; font-weight: bold;', action)     next(action)    console.log(`%cnext state`, 'color: #4CAF50; font-weight: bold;', getState())    // console.timeEnd('time')
    console.groupEnd()
  }
}

thunk

export default function thunk ({ getState }) {  return next => action => {    if (typeof action === 'function') {
      action(next, getState)
    } else {
      next(action)
    }
  }
}

这里要注意的一点是,中间件是有执行顺序的。像在这里,第一个参数是thunk,然后才是logger,因为假如logger在前,那么这个时候action可能是一个包含异步操作的方法,不能正常输出action的信息。

心得体会

到了这里,关于Redux的方方面面都已经讲完了,希望大家看完能够有所收获。

但是笔者其实还有一个担忧:每一次dispatch都会重新渲染整个视图,虽然React是在虚拟DOM上进行diff,然后定向渲染需要更新的真实DOM,但是我们知道,一般使用Redux的场景都是中大型应用,管理庞大的状态数据,这个时候整个虚拟DOM进行diff可能会产生比较明显的性能损耗(diff过程实际上是对象和对象的各个字段挨个比较,如果数据达到一定量级,虽然没有操作真实DOM,也可能产生可观的性能损耗,在小型应用中,由于数据较少,因此diff的性能损耗可以忽略不计)。

本文源码地址:https://github.com/ansenhuang/redux-demo

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者黄安程授权发布。

作者:网易云社区
链接:https://www.jianshu.com/p/059759653d57
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Redux其实很简单(原理篇)的更多相关文章

  1. 写你的shell,其实很简单[架构篇]

    引语:我本人以前并没有写过shell脚本,也许是因为懒,也许是没有被逼到要去写shell的地步.但是,前段时间,工作需求,要求重新跑几个月的脚本,这些脚本是每天定时进行跑的,而且每天是好几个脚本一起关 ...

  2. zabbix真的很简单 (安装篇)

    系统环境: Centos 6.4 一直觉得 zabbix 很简单,但是还是有好多人看了好多文档都搞不明白怎么用,我从2013年使用到现在也小有心得,如果时间允许,很高兴与大家一起分享我在使用过程中的一 ...

  3. 我在阿里这仨月 前端开发流程 前端进阶的思考 延伸学习的方式很简单:google 一个关键词你能看到十几篇优秀的博文,再这些博文中寻找新的关键字,直到整个大知识点得到突破

    我在阿里这仨月 Alibaba 试用期是三个月,转眼三个月过去了,也到了转正述职的时间.回想这三个月做过的事情,很多很杂,但还是有重点. 本文谈一谈工作中遇到的各种场景,需要用到的一些前端知识,以及我 ...

  4. v86.01 鸿蒙内核源码分析 (静态分配篇) | 很简单的一位小朋友 | 百篇博客分析 OpenHarmony 源码

    本篇关键词:池头.池体.节头.节块 内存管理相关篇为: v31.02 鸿蒙内核源码分析(内存规则) | 内存管理到底在管什么 v32.04 鸿蒙内核源码分析(物理内存) | 真实的可不一定精彩 v33 ...

  5. Hook任务栏时钟窗口(原理其实很简单,就是注入DLL到时钟窗口进程(explorer.exe))

    用过一些日历软件的小伙伴应该都知道它们都实现了在时钟窗口上的Hook,也就是屏蔽了系统原有的功能,实现自己的功能 某日历软件Hook时钟窗口后的效果 经过一番研究,发现原理其实很简单,就是注入DLL到 ...

  6. 很简单的Java断点续传实现原理

    原理解析 在开发当中,"断点续传"这种功能很实用和常见,听上去也是比较有"逼格"的感觉.所以通常我们都有兴趣去研究研究这种功能是如何实现的? 以Java来说,网 ...

  7. 依然是关于我空间那篇申请的日志《JavaScript axError:Unexpected token ILLEGAL 很简单的代码……》

    接下来要讲的日志现在的标题已经更改为<很简单的代码,但是无法--> 这篇日志地址:http://www.cnblogs.com/herbertchina/p/4475092.html 经过 ...

  8. extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”

    extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”

  9. Cesium原理篇:5最长的一帧之影像

    如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了.之前的几个系列,我们全面的介绍了Cesium的地形内容,详见: Cesium原理篇:1最长的一帧之渲染调度 Cesium原 ...

随机推荐

  1. 安全运维 -- 更改ssh端口

    环境:Ubuntu 16 前言 黑客遍地都是,ssh/pop3/ftp等爆破工具的流行让站长的日常运维工作量大大加重.Metasplot,Bruter等工具更是针对以上协议有专门 的破解方法,有字典破 ...

  2. Golang之Mysql操作

    话说当年武大郎对着电脑一顿噼里啪啦,,,对mysql增删改查 增加insert package main import ( "fmt" "github.com/jmoir ...

  3. layui学习<一>

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name ...

  4. 2018.09.28 bzoj3688: 折线统计(dp+树状数组)

    传送门 简单树状数组优化dp. 注意到k很小提示我们搜(d)(d)(d)索(p)(p)(p). 先按第一维排序. 用f[i][j][0/1]f[i][j][0/1]f[i][j][0/1]表示第i个点 ...

  5. 2018.08.16 洛谷P2029 跳舞(线性dp)

    传送门 简单的线性dp" role="presentation" style="position: relative;">dpdp. 直接推一推 ...

  6. Django入门与实践-第26章:个性化工具(完结)

    http://127.0.0.1:8000/boards/1/topics/62/reply/ 我觉得只添加内置的个性化(humanize)包就会很不错. 它包含一组为数据添加“人性化(human t ...

  7. 【Unity】2.0 第2章 Unity编辑器和基本操作

    分类:Unity.C#.VS2015 创建日期:2016-03-26 本章要点: 1.掌握Unity 5.3.4编辑器视图和菜单项及其含义,这是入门的最基础部分,必须掌握. 2.了解最基本的操作,先学 ...

  8. python面向对象-1方法、构造函数

    类是指:描述一种事物的定义,是个抽象的概念 实例指:该种事物的一个具体的个体,是具体的东西 打个比方: “人”是一个类.“张三”是人类的一个具体例子 在编程时也是同样的道理,你先自己定义一个“类”,当 ...

  9. UVa 11294 Wedding (TwoSat)

    题意:有 n-1 对夫妻参加一个婚宴,所有人都坐在一个长长的餐桌左侧或者右侧,新郎和新娘面做面坐在桌子的两侧.由于新娘的头饰很复杂,她无法看到和她坐在同一侧餐桌的人,只能看到对面餐桌的人.任意一对夫妻 ...

  10. (最小生成树) Arctic Network -- POJ --2349

    链接: http://poj.org/problem?id=2349 Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 1371 ...