├── .babelrc ├── .gitignore ├── README.md ├── code-with-my-commit ├── applyMiddleware.js ├── bindActionCreators.js ├── combineReducers.js ├── compose.js └── createStore.js ├── demo1 └── index.js ├── demo2 └── index.js ├── demo3 └── index.js ├── demo4 └── index.js ├── demo5 ├── index.js └── reducers │ ├── counter.js │ ├── index.js │ └── todos.js ├── demo6 └── index.js ├── demo7 └── index.js ├── demo8 └── index.js ├── package.json ├── posts ├── applyMiddleware.md ├── bindActionCreators.md ├── combineReducers.md ├── compose.md └── createStore.md ├── source-code ├── applyMiddleware.js ├── bindActionCreators.js ├── combineReducers.js ├── compose.js ├── createStore.js ├── index.js └── utils │ └── warning.js ├── yarn-error.log └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux 源码阅读历程 2 | ![](https://camo.githubusercontent.com/f28b5bc7822f1b7bb28a96d8d09e7d79169248fc/687474703a2f2f692e696d6775722e636f6d2f4a65567164514d2e706e67) 3 | 接触 Redux 一段时间了,对其用法也稍有熟悉。但本着宝贵的探索精神,要知其然,更要知其所以然,我决定从今天(2017-08-03)开始,以自己的角度阅读和探索 Redux 源码,学习其编码思想,长在自己的知识树上。 4 | 5 | ## 写在开始 6 | 我不管,自己先给自己来颗✨ 7 | 8 | 第一次写源码阅读计划,没啥经验,就试着规划一下本项目将要做的事情吧。 9 | 10 | Redux 库十分简洁,只有 5 个 API 文件,我根据关联度,对它们排个序,后面也会按照这个顺序书写心得。 11 | 12 | - `createStore.js` 13 | - `combineReducers.js` 14 | - `bindActionCreators.js` 15 | - `compose.js` 16 | - `applyMiddleware.js` 17 | 18 | 既然没啥经验,就以自己认为的重要度一一分析这几个文件吧。围绕着每个文件,我会首先通过预览抽取出文件中使用得比较精髓的原生 API,然后再站在熟悉这些原生 API 的基础上分析文件,最后把理解思路整理成文章。总而言之我希望自己基本做到以下几个步骤: 19 | 1. 预习文件中的 API 20 | 2. 回顾 Redux API 的用法 21 | 3. 分析 Redux API 22 | 4. 总结可以用于实战的思想 23 | 24 | 这几个步骤可能会同时出现在一篇文章中,也有可能限于篇幅拆分至两篇或多篇文章中,反正按小爷我自己的节奏。😄 25 | 26 | ## 上车指南 27 | 1. ES6 28 | 2. 熟悉 Redux (可以看我的[另外一篇文章](https://www.yuque.com/u116378/vtu6np/aufm35)) 29 | 3. A little bit 函数式编程 30 | 31 | ## 文章列表 32 | - [ createStore —— 繁华的起点(2017.08.07)](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/createStore.md) 33 | - [ combineReucers —— 涓涓溪流,可成江海(2017.08.15)](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/combineReducers.md) 34 | - [ bindActionCreators —— 箭在弦上(2017.08.22)](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/bindActionCreators.md) 35 | - [ compose —— 管道工(2017.08.30)](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/compose.md) 36 | - [applyMiddleware —— Redux 的拓展坞(2017.09.03)](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/applyMiddleware.md) 37 | 38 | ## 文章提及 demo 使用方式 39 | 1. 克隆仓库:`git clone https://github.com/pobusama/redux-source-code-chewing.git && cd redux-source-code-chewing` 40 | 2. 安装依赖包:`npm i` 41 | 3. 运行 demo(如 demo1):`npm run demo1` 42 | -------------------------------------------------------------------------------- /code-with-my-commit/applyMiddleware.js: -------------------------------------------------------------------------------- 1 | import compose from './compose' 2 | 3 | /** 4 | * Creates a store enhancer that applies middleware to the dispatch method 5 | * of the Redux store. This is handy for a variety of tasks, such as expressing 6 | * asynchronous actions in a concise manner, or logging every action payload. 7 | * 8 | * See `redux-thunk` package as an example of the Redux middleware. 9 | * 10 | * Because middleware is potentially asynchronous, this should be the first 11 | * store enhancer in the composition chain. 12 | * 13 | * Note that each middleware will be given the `dispatch` and `getState` functions 14 | * as named arguments. 15 | * 16 | * @param {...Function} middlewares The middleware chain to be applied. 17 | * @returns {Function} A store enhancer applying the middleware. 18 | */ 19 | 20 | /** 21 | * 用法: 22 | * applyMiddleware(middlewareA, middlewareB, middlewareC)(createStore)(reducer, preloadedState, enhancer) 23 | * 1. applyMiddleware(middlewareA, middlewareB, middlewareC) 返回一个方法用来增强 createStore 24 | * 2. 该方法接收原 createStore,返回增强后的 createStore。 25 | * 3. 增强后的 createStore 形参不变,依然是 reducer、preloadedState、enhancer。 26 | * 4. 增强后的 createStore 还是返回一个标准的 store 对象(包括 getState、subscribe、增强后的 dispatch 等方法) 27 | * 另外: 28 | * middleware 函数一般定义如下 29 | * const middleware = store => next => action => { 30 | * // doSomething... 31 | * } 32 | */ 33 | export default function applyMiddleware(...middlewares) { 34 | //返回 Redux Enhancer 函数(接收 store 对象、返回 store 对象) 35 | return (createStore) => (reducer, preloadedState, enhancer) => { 36 | // 获得原始 createStore 生成的 store 37 | var store = createStore(reducer, preloadedState, enhancer) 38 | // 获得原始 dispatch 39 | var dispatch = store.dispatch 40 | var chain = [] 41 | // 给中间件传入可用的 store API 42 | var middlewareAPI = { 43 | getState: store.getState, 44 | /** 45 | * (action) => dispatch(action) 等价于 (action) => {return dispatch(action)} 46 | * 函数内部的 dispatch 指向外层定义的 dispatch 47 | * 当 middleware 内部执行 middlewareAPI.dispatch 方法时 48 | * dispatch 已经是 compose 之后的 dispatch 49 | */ 50 | dispatch: (action) => dispatch(action) 51 | } 52 | // 为 middleware 分配统一的 middlewareAPI 后,将 middleware 返回的函数(这里称作 handler)收集在 chain 数组中 53 | chain = middlewares.map(middleware => middleware(middlewareAPI)) 54 | // dispatch = handlerA(handlerB(handlerC(store.dispatch))) 55 | dispatch = compose(...chain)(store.dispatch) 56 | 57 | return { 58 | ...store, 59 | dispatch //middleware handler 处理后的 dispatch 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /code-with-my-commit/bindActionCreators.js: -------------------------------------------------------------------------------- 1 | function bindActionCreator(actionCreator, dispatch) { 2 | /** 3 | * 实际上对 actionCreator 和 dispatch 进行绑定的地方 4 | * 使用 "..." 解构语法将传入 actionCreator 的实参 5 | * 原封不动传给绑定后的函数 6 | */ 7 | return (...args) => dispatch(actionCreator(...args)) 8 | } 9 | 10 | /** 11 | * Turns an object whose values are action creators, into an object with the 12 | * same keys, but with every function wrapped into a `dispatch` call so they 13 | * may be invoked directly. This is just a convenience method, as you can call 14 | * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. 15 | * 16 | * For convenience, you can also pass a single function as the first argument, 17 | * and get a function in return. 18 | * 19 | * @param {Function|Object} actionCreators An object whose values are action 20 | * creator functions. One handy way to obtain it is to use ES6 `import * as` 21 | * syntax. You may also pass a single function. 22 | * 23 | * @param {Function} dispatch The `dispatch` function available on your Redux 24 | * store. 25 | * 26 | * @returns {Function|Object} The object mimicking the original object, but with 27 | * every action creator wrapped into the `dispatch` call. If you passed a 28 | * function as `actionCreators`, the return value will also be a single 29 | * function. 30 | */ 31 | export default function bindActionCreators(actionCreators, dispatch) { 32 | if (typeof actionCreators === 'function') { 33 | // 如果形参 actionCreators 传入的是单个 function 直接返回绑定后的函数。 34 | return bindActionCreator(actionCreators, dispatch) 35 | } 36 | // 无效参数校验 37 | if (typeof actionCreators !== 'object' || actionCreators === null) { 38 | throw new Error( 39 | `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` + 40 | `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` 41 | ) 42 | } 43 | 44 | var keys = Object.keys(actionCreators) 45 | var boundActionCreators = {} 46 | // 遍历 actionCreators 对象上的函数集合,每个都进行 dispatch 绑定。 47 | for (var i = 0; i < keys.length; i++) { 48 | var key = keys[i] 49 | var actionCreator = actionCreators[key] 50 | if (typeof actionCreator === 'function') { 51 | boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) 52 | } 53 | } 54 | // 返回绑定后的函数集合 55 | return boundActionCreators 56 | } 57 | -------------------------------------------------------------------------------- /code-with-my-commit/combineReducers.js: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from './createStore' 2 | import isPlainObject from 'lodash/isPlainObject' 3 | import warning from './utils/warning' 4 | 5 | /** 6 | * 用来提示使用者,哪个 action 经过哪个 Reducer 函数处理时没有返回正常的 state 7 | * 根据 redux 的规则,Reducer 函数处理每个 action 的后都必须返回一个 state 8 | * 而不是 undefined。 9 | */ 10 | function getUndefinedStateErrorMessage(key, action) { 11 | var actionType = action && action.type 12 | var actionName = actionType && `"${actionType.toString()}"` || 'an action' 13 | 14 | return ( 15 | `Given action ${actionName}, reducer "${key}" returned undefined. ` + 16 | `To ignore an action, you must explicitly return the previous state.` 17 | ) 18 | } 19 | 20 | function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) { 21 | var reducerKeys = Object.keys(reducers) 22 | var argumentName = action && action.type === ActionTypes.INIT ? 23 | 'preloadedState argument passed to createStore' : 24 | 'previous state received by the reducer' 25 | 26 | if (reducerKeys.length === 0) { 27 | return ( 28 | 'Store does not have a valid reducer. Make sure the argument passed ' + 29 | 'to combineReducers is an object whose values are reducers.' 30 | ) 31 | } 32 | 33 | if (!isPlainObject(inputState)) { 34 | return ( 35 | `The ${argumentName} has unexpected type of "` + 36 | ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + 37 | `". Expected argument to be an object with the following ` + 38 | `keys: "${reducerKeys.join('", "')}"` 39 | ) 40 | } 41 | 42 | var unexpectedKeys = Object.keys(inputState).filter(key => 43 | !reducers.hasOwnProperty(key) && 44 | !unexpectedKeyCache[key] 45 | ) 46 | 47 | unexpectedKeys.forEach(key => { 48 | unexpectedKeyCache[key] = true 49 | }) 50 | 51 | if (unexpectedKeys.length > 0) { 52 | return ( 53 | `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + 54 | `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` + 55 | `Expected to find one of the known reducer keys instead: ` + 56 | `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.` 57 | ) 58 | } 59 | } 60 | 61 | function assertReducerSanity(reducers) { 62 | // 遍历 reducers 对象上的 reducer 属性以及相应的 reducer 函数 63 | Object.keys(reducers).forEach(key => { 64 | var reducer = reducers[key] 65 | /** 66 | * 一般 reducer 的函数签名为:reducerA (state = initialState, action) 67 | * 1. 给 reducer 的 state 形参传入 undefined, 68 | * 这样 reducer 函数就会使用缺省值 initialState, 69 | * 2. 给 reducer 的 action 形参传入 `{ type: ActionTypes.INIT }`, 70 | * 由于一般使用者定义的 reducer 不会为 `ActionTypes.INIT` 这个 case 做特殊处理, 71 | * 所以 reducer 就会走 default case。也就是 return 当前 state,即 initialState。 72 | */ 73 | var initialState = reducer(undefined, { type: ActionTypes.INIT }) 74 | 75 | /** 76 | * 检验当前遍历的 reducer 在定义时有没有为 state 参数设置缺省值。 77 | */ 78 | if (typeof initialState === 'undefined') { 79 | throw new Error( 80 | `Reducer "${key}" returned undefined during initialization. ` + 81 | `If the state passed to the reducer is undefined, you must ` + 82 | `explicitly return the initial state. The initial state may ` + 83 | `not be undefined.` 84 | ) 85 | } 86 | 87 | /** 88 | * 使用探针检查验当前遍历的 reducer 有没有 default case。 89 | * 做法是将一个拥有随机 actionType 的 action 对象传入 reducer, 90 | * 然后判断 reducer 处理 action 后 return 的 state 是否为 undefined。 91 | */ 92 | var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.') 93 | if (typeof reducer(undefined, { type }) === 'undefined') { 94 | throw new Error( 95 | `Reducer "${key}" returned undefined when probed with a random type. ` + 96 | `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + 97 | `namespace. They are considered private. Instead, you must return the ` + 98 | `current state for any unknown actions, unless it is undefined, ` + 99 | `in which case you must return the initial state, regardless of the ` + 100 | `action type. The initial state may not be undefined.` 101 | ) 102 | } 103 | }) 104 | } 105 | 106 | /** 107 | * 把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数, 108 | * 然后就可以对这个 reducer 调用 createStore。合并后的 reducer 可以调用各个子 reducer, 109 | * 并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定。 110 | * 111 | * @param {Object} reducers 一个对象,它的值(value) 对应不同的 reducer 函数,这些 112 | * reducer 函数后面会被合并成一个。建议在 reducers/index.js 里使用 combineReducers() 来对 113 | * 外输出一个 reducer。 114 | * 115 | * @returns {Function} 一个调用 reducers 对象里所有 reducer 的 reducer,并且构造一个 116 | * 与 reducers 对象结构相同的 state 对象。 117 | */ 118 | export default function combineReducers(reducers) { 119 | // 取 reducers 对象的属性,存到 reducerKeys 数组中 120 | var reducerKeys = Object.keys(reducers) 121 | // 定义一个用来存放筛选后 reducers 属性的对象。 122 | var finalReducers = {} 123 | /** 124 | * 遍历 reducers 对象上的属性, 125 | * 1. 做非空校验 126 | * 2. 保留属性为 function 类型的 reducer 127 | */ 128 | for (var i = 0; i < reducerKeys.length; i++) { 129 | var key = reducerKeys[i] 130 | if (process.env.NODE_ENV !== 'production') { 131 | // 属性空警告 132 | if (typeof reducers[key] === 'undefined') { 133 | warning(`No reducer provided for key "${key}"`) 134 | } 135 | } 136 | // 筛选为类型函数的 reducer 137 | if (typeof reducers[key] === 'function') { 138 | finalReducers[key] = reducers[key] 139 | } 140 | } 141 | // 这里 finalReducers 是筛选完毕后的 reducers 对象 142 | var finalReducerKeys = Object.keys(finalReducers) 143 | 144 | // 开发环境下用于 warning 信息,暂不细究 145 | if (process.env.NODE_ENV !== 'production') { 146 | var unexpectedKeyCache = {} 147 | } 148 | 149 | // 定义该异常变量用于完善性检测 150 | var sanityError 151 | try { 152 | /** 153 | * 完善性检测 154 | * 检验 finalReducers 对象上的每一个子 reducer 能否按照 redux 定义的规则处理 action, 155 | * 并 return 正确的 state。 156 | * 如果不满足完善性检测,则抛出异常 157 | */ 158 | assertReducerSanity(finalReducers) 159 | } catch (e) { 160 | sanityError = e 161 | } 162 | 163 | /** 164 | * 最终返回的 rootReducer 函数组合了 finalReducers 对象上的子 reducer 逻辑。 165 | */ 166 | return function combination(state = {}, action) { 167 | // 如果未通过完善性检测,则中断并抛出异常。 168 | if (sanityError) { 169 | throw sanityError 170 | } 171 | 172 | // 开发环境下用于 warning 信息,暂不细究 173 | if (process.env.NODE_ENV !== 'production') { 174 | var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache) 175 | if (warningMessage) { 176 | warning(warningMessage) 177 | } 178 | } 179 | 180 | // 用以判断 state 是否被改变的标识位 181 | var hasChanged = false 182 | // 定义该对象用于保存更改后的 state 树 183 | var nextState = {} 184 | //再一次遍历 finalReducerKeys 对象上的子 reducer 属性。 185 | for (var i = 0; i < finalReducerKeys.length; i++) { 186 | var key = finalReducerKeys[i] 187 | var reducer = finalReducers[key] 188 | /** 189 | * 获取变更前 state 上与 reducer key 相对应的部分 state。 190 | * 如果是初始化阶段,该 state 分支的值是 undefined。 191 | * 如果非初始化阶段,则是获取 state 分支的值。 192 | */ 193 | var previousStateForKey = state[key] 194 | // 用当前 reducer 计算 action,返回计算后的部分 state。 195 | var nextStateForKey = reducer(previousStateForKey, action) 196 | // 检测一下当前 reducer 有没有正确返回 state。 197 | if (typeof nextStateForKey === 'undefined') { 198 | var errorMessage = getUndefinedStateErrorMessage(key, action) 199 | throw new Error(errorMessage) 200 | } 201 | // 将子 reducer 计算后的部分 state 挂在对应的 state 树属性上 202 | nextState[key] = nextStateForKey 203 | /** 204 | * 这里是对比每个 nextStateForKey 与 previousStateForKey,通过短路写法可以提高效率。 205 | * 对象相比比地址,如果 nextStateForKey 是 reducer 返回的新的 state, 206 | * 与 previousStateForKey 地址不同 hasChanged 就会变 true。 207 | * 208 | * reducer 的规则是,若要在 reducer 里面更新 state,不是直接修改 state, 209 | * 而是开辟新的地址修改并存放 newState,然后最终作为 reducer 的返回值。 210 | * 根据该规则,如果 state “变更” 了,其 nextStateForKey 和 previousStateForKey 地址 211 | * 不相等。 212 | */ 213 | hasChanged = hasChanged || nextStateForKey !== previousStateForKey 214 | } 215 | // 如果未改变,则返回旧 state,相应地 UI 方面也不用回流。 216 | return hasChanged ? nextState : state 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /code-with-my-commit/compose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Composes single-argument functions from right to left. The rightmost 3 | * function can take multiple arguments as it provides the signature for 4 | * the resulting composite function. 5 | * 6 | * @param {...Function} funcs The functions to compose. 7 | * @returns {Function} A function obtained by composing the argument functions 8 | * from right to left. For example, compose(f, g, h) is identical to doing 9 | * (...args) => f(g(h(...args))). 10 | */ 11 | /** 12 | * 用法:`compose(f, g, h)` 相当于 `(...args) => f(g(h(...args)))` 13 | */ 14 | export default function compose(...funcs) { 15 | /** 16 | * 当 compose 不接收函数实参时,返回一个**返回第一个实参的函数**。 17 | * 例如:`compose()(123, 456)` 等效于 `(x => x)(123, 456)` 18 | * 返回 123 19 | * 注:参考 demo7 20 | */ 21 | if (funcs.length === 0) { 22 | return arg => arg 23 | } 24 | /** 25 | * 当 compose 接收 1 个函数实参时返回【该函数】。 26 | * 例如:`compose(Math.pow)(4, 2)` 等效于 `Math.pow(4, 2)` 27 | * 返回 16 28 | * 注:参考 demo8 29 | */ 30 | if (funcs.length === 1) { 31 | return funcs[0] 32 | } 33 | /** 34 | * 当 compose 接收 1 个以上函数实参时返回一个【函数组合】。 35 | * 例如:`compose(a, b, c, d)(1, 2, 3)` 36 | * 等效于 `( (...args) => a(b(c(d(...args)))) )(1, 2, 3)` 37 | * 注:参考 demo6 38 | */ 39 | const last = funcs[funcs.length - 1] // 取倒数第 1 个被 compose 的函数 40 | const rest = funcs.slice(0, -1) // 取 0 至倒数第 2 个被 compose 的函数(数组) 41 | // 返回组合后的函数 42 | return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args)) 43 | 44 | /** 45 | * 例如: 46 | * compose(a, b, c, d) 时,last 为 d,rest 为 [a, b, c]。 47 | * 48 | * Array.prototype.reduce 函数,第一个参数为计算函数,第二个参数为初始值,例如: 49 | * ['1', '2', '3'].reduce((accumulator, currentValue) => accumulator + currentValue, '0') 50 | * currentValue 的获取顺序从左到右 51 | * 所以返回的是 ((('0' + '1') + '2') + '3') 即:"0123" 52 | * reduceRight 和 reduce 的区别就是 currentValue 的获取顺序从右到左 53 | * 所以返回的是 ((('0' + '3') + '2') + '1') 即:"0321" 54 | * 55 | * 回到 compose: 56 | * `rest.reduceRight((composed, f) => f(composed), last(...args))` 57 | * 等效于 `[a, b, c].reduceRight((composed, f) => f(composed), d(...args))` 58 | * accumulator(即 composed)接收:d(...args)) 59 | * currentValue (即 f)接收:c 60 | * 61 | * 看下 reduceRight 循环每次做了什么: 62 | * 第一次 composed 参数接收初始值 `d(...args)`,f 参数接收 c,所以返回的是 `c(d(...args))` 63 | * 第二次 composed 参数接收上一次的累积值 `c(d(...args))`, 64 | * f 参数接收 b,所以返回的是 `b(c(d(...args)))` 65 | * 第三次 composed 参数接收上一次的累积值`b(c(d(...args)))`, 66 | * f 参数接收 a,所以返回的是 `a(b(c(d(...args))))` 67 | * 68 | * 所以 `rest.reduceRight((composed, f) => f(composed), last(...args))` 69 | * 等效于 `[a, b, c].reduceRight((composed, f) => f(composed), d(...args))` 70 | * 等效于 a(b(c(d(...args)))) 71 | * 72 | * 因此 compose(a, b, c, d) 73 | * 等效于 (...args) => a(b(c(d(...args)))) 74 | */ 75 | } 76 | -------------------------------------------------------------------------------- /code-with-my-commit/createStore.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject' 2 | import $$observable from 'symbol-observable' 3 | 4 | /** 5 | * These are private action types reserved by Redux. 6 | * For any unknown actions, you must return the current state. 7 | * If the current state is undefined, you must return the initial state. 8 | * Do not reference these action types directly in your code. 9 | */ 10 | export var ActionTypes = { 11 | INIT: '@@redux/INIT' 12 | } 13 | 14 | /** 15 | * 创建一个用于管理 state 树的 Redux store 对象。 16 | * 修改 store 中数据的唯一方式就是调用 store 对象上的 `dispatch()` 方法。 17 | * 18 | * 一个应用只能拥有单一 store。 19 | * state 树的不同部分会根据 action 作出响应,为了区分这些不同,你可以使用 `combineReducers` 20 | * 函数将多个 reducer 函数拼装到一个单一的 reducer 函数上。 21 | * 22 | * @param {Function} reducer 一个 return 下一个 state 树的函数, 23 | * 这个函数接收当前 state 和 action,用来处理当前 state 并产生下一个 state。 24 | * 25 | * @param {any} [preloadedState] 初始 state。可选参数,在一般的应用中,该参数可以用于 26 | * 整合来自服务端的状态,也可以用来保存前一次的用户会话记录(session)。 27 | * 如果你使用 `combineReducers` 来生产 root reducer 函数,该参数必须是一个和 28 | * `combineReducers` 的属性的形式一样的对象。 29 | * 30 | * @param {Function} enhancer store 的增强器。可选参数,该参数用于增强 store,我们可以通过 31 | * 第三方例如中间件、时间旅行、持久化等功能来增强 store。`applyMiddleware()` 是唯一由 Redux 32 | * 提供的增强器。 33 | * 34 | * @returns {Store} 一个 Redux store 对象,通过该对象你可以读取 state,触发 action, 35 | * 并订阅(监听)state 的更新。 36 | */ 37 | export default function createStore(reducer, preloadedState, enhancer) { 38 | /* -------------- createStore 参数校验部分 -------------- */ 39 | /** 40 | * 参数校验,如果第二个形参传入的是函数,且第三个形参不传,则第二个实参代表 enhancer。 41 | * 换句话说,preloadedState 是可选配置。 42 | */ 43 | if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { 44 | enhancer = preloadedState 45 | preloadedState = undefined 46 | } 47 | // 校验传入的 enhancer 实参是否是函数。 48 | if (typeof enhancer !== 'undefined') { 49 | if (typeof enhancer !== 'function') { 50 | throw new Error('Expected the enhancer to be a function.') 51 | } 52 | /** 53 | * 从这里可以看出,enhancer 的一般形式是: 54 | * const enhancer = (createStore) => { 55 | * //返回一个函数 `finalCreateStore`,用于接收 reducer 和 preloadedState 56 | * return function finalCreateStore (reducer, preloadedState) { 57 | * //这里可以拿到原 createStore、reducer 和 preloadedState 58 | * //然后添加自定义逻辑 59 | * //最终返回 store 对象 60 | * return createStore(reducer, preloadedState); 61 | * } 62 | * } 63 | */ 64 | return enhancer(createStore)(reducer, preloadedState) 65 | } 66 | // 校验传入的 reducer 实参是否是函数。 67 | if (typeof reducer !== 'function') { 68 | throw new Error('Expected the reducer to be a function.') 69 | } 70 | /* -------------- createStore 正片部分 -------------- */ 71 | /** 72 | * 定义的几个变量: 73 | * 1. currentReducer:当前 store 应用的 reducer,默认使用传入的 reducer 参数,可通过 74 | * replaceReducer 函数来热替换 currentReducer。 75 | * 2. currentState:默认为传入的 preloadedState 参数,可通过 dispatch 函数改变。 76 | * 3. currentListeners:当前订阅队列,用以存放通过 subscribe 函数执行的订阅。 77 | * 4. isDispatching:dispatch 函数的标志位,作用后面会讲到。 78 | */ 79 | var currentReducer = reducer 80 | var currentState = preloadedState 81 | var currentListeners = [] 82 | var nextListeners = currentListeners 83 | var isDispatching = false 84 | 85 | function ensureCanMutateNextListeners() { 86 | if (nextListeners === currentListeners) { 87 | nextListeners = currentListeners.slice() 88 | } 89 | } 90 | 91 | /** 92 | * Reads the state tree managed by the store. 93 | * 94 | * @returns {any} The current state tree of your application. 95 | */ 96 | function getState() { 97 | return currentState 98 | } 99 | 100 | /** 101 | * 添加一个订阅 state 变更的监听函数(listener)。该监听函数将会在 action 分发后, 102 | * state 树完成可能的变更之后被调用。接着你可以在这个回调中通过调用 `getState()` 103 | * 来读取当前 state。 104 | * 105 | * 你可能会在一个监听函数中调用 `dispatch()`,请知晓以下注意事项: 106 | * 107 | * 1. 监听函数只应当在响应用户的 actions 或者特殊的条件限制下(比如:在 store 有一个 108 | * 特殊字段时 dispatch action)才能调用 dispatch()。虽然不作任何条件限制而在监听函数中 109 | * 调用 dispatch() 在技术上是可行的,但是随着每次 dispatch() 改变 store 可能会导致陷 110 | * 入无穷的循环。 111 | * 112 | * 2. 在每次调用 `dispatch()` 之前,订阅队列(subscriptions)会保存一份快照。如果你在 113 | * 订阅函数正在执行的时候订阅或者取消订阅,那这次订阅或取消订阅并不会影响本次 `dispatch()` 114 | * 过程。但下次调用 `dispatch()` 时,无论其是否嵌套,它都会应用订阅列表里最近的一次快照。 115 | * 116 | * 3. 因为在监听函数执行前,state 有可能在一个嵌套的 `dispatch()` 中改变多次,所以监听 117 | * 函数不一定能跟踪到所有的 state 变更。保证所有的监听器都注册在 dispatch() 启动之前, 118 | * 这样,在调用监听器的时候就会传入监听器所存在时间里最新的一次 state。 119 | * 120 | * @param {Function} listener 每当 dispatch action 的时候都会执行的回调函数。 121 | * @returns {Function} 一个用来移除函数变化监听器的函数。 122 | */ 123 | function subscribe(listener) { 124 | /* -------------- subscribe 参数校验部分 -------------- */ 125 | // listener 必须是函数类型(state 变更以后调用) 126 | if (typeof listener !== 'function') { 127 | throw new Error('Expected listener to be a function.') 128 | } 129 | 130 | /* -------------- subscribe 正片部分 -------------- */ 131 | // 每次订阅都会维护一个标志位,以便在重复取消订阅的时候提高性能 132 | var isSubscribed = true 133 | 134 | /** 135 | * 为了方便阅读,这里把源码中的 `ensureCanMutateNextListeners()` 替换成其实际代码 136 | */ 137 | if (nextListeners === currentListeners) { 138 | nextListeners = currentListeners.slice() 139 | } 140 | nextListeners.push(listener)// 向 listeners 队列中添加订阅函数 141 | 142 | /** 143 | * 取消订阅 144 | */ 145 | return function unsubscribe() { 146 | // 防止重复取消订阅时,再次进行下面比较耗费性能的运算 147 | if (!isSubscribed) { 148 | return 149 | } 150 | // 取消订阅先把标志位置 false 151 | isSubscribed = false 152 | 153 | ensureCanMutateNextListeners() 154 | // 找到订阅函数在订阅队列中的位置 155 | var index = nextListeners.indexOf(listener) 156 | // 删除订阅队列中的相应订阅函数。 157 | nextListeners.splice(index, 1) 158 | } 159 | } 160 | 161 | /** 162 | * Dispatches an action. It is the only way to trigger a state change. 163 | * 164 | * The `reducer` function, used to create the store, will be called with the 165 | * current state tree and the given `action`. Its return value will 166 | * be considered the **next** state of the tree, and the change listeners 167 | * will be notified. 168 | * 169 | * The base implementation only supports plain object actions. If you want to 170 | * dispatch a Promise, an Observable, a thunk, or something else, you need to 171 | * wrap your store creating function into the corresponding middleware. For 172 | * example, see the documentation for the `redux-thunk` package. Even the 173 | * middleware will eventually dispatch plain object actions using this method. 174 | * 175 | * @param {Object} action A plain object representing “what changed”. It is 176 | * a good idea to keep actions serializable so you can record and replay user 177 | * sessions, or use the time travelling `redux-devtools`. An action must have 178 | * a `type` property which may not be `undefined`. It is a good idea to use 179 | * string constants for action types. 180 | * 181 | * @returns {Object} For convenience, the same action object you dispatched. 182 | * 183 | * Note that, if you use a custom middleware, it may wrap `dispatch()` to 184 | * return something else (for example, a Promise you can await). 185 | */ 186 | function dispatch(action) { 187 | /* -------------- dispatch 参数校验部分 -------------- */ 188 | // action 要求是一个简单对象(plain object) 189 | if (!isPlainObject(action)) { 190 | throw new Error( 191 | 'Actions must be plain objects. ' + 192 | 'Use custom middleware for async actions.' 193 | ) 194 | } 195 | // 最基础的 dispatch 函数(没有接入三方中间件)接收的 action 对象必须要带 type 参数。 196 | if (typeof action.type === 'undefined') { 197 | throw new Error( 198 | 'Actions may not have an undefined "type" property. ' + 199 | 'Have you misspelled a constant?' 200 | ) 201 | } 202 | /** 203 | * 标识位,用来锁定 reducer 计算过程, 204 | * 如果 reducer 计算过程中调用了 dispatch 函数则会报错(为什么不能调用用?请接着往下看)。 205 | */ 206 | if (isDispatching) { 207 | throw new Error('Reducers may not dispatch actions.') 208 | } 209 | /* -------------- dispatch 正片部分 -------------- */ 210 | try { 211 | /** 212 | * 这是 Redux 的灵魂部分 213 | * 作用是将当前 state 和 action 交给 reducer 函数处理,计算出**新的 state** 214 | * 注意!在 reducer 函数中要避免调用 dispatch 215 | * 原因类似银行取钱:假设你和女朋友共存了 100 元,在某时刻,你取 10 块钱 216 | * 此时银行系统便会对你的账户计算:`100 - 10 = 90` 217 | * 如果计算过程中你女朋友取 20 元,那么银行系统又会计算:`100 - 20 = 80` 218 | * 那结果到底是 90 还是 80 呢? 219 | * 当然是 70 ! 220 | * 银行家的做法是在你 “取钱 -> 结算完毕” 过程中冻结其他存取操作(在本源码中是置 isDispatching 标识位为 true), 221 | * 你女朋友只能在你 “取钱 -> 结算完毕” 过程以外的时间里取钱。 222 | */ 223 | isDispatching = true // 更改过程锁 224 | currentState = currentReducer(currentState, action) // 将当前 state 和 action 交给 reducer 计算 225 | } finally { 226 | // 无论计算成功还是报错,最终都将标志位置为 false,以免阻碍下一个 action 的 dipatch。 227 | isDispatching = false 228 | } 229 | /** 230 | * 此时 state 已经更新完毕,我们将订阅队列中的函数一一执行 231 | * 我们在这些函数里可以拿到更新后的 state。 232 | */ 233 | var listeners = currentListeners = nextListeners 234 | for (var i = 0; i < listeners.length; i++) { 235 | listeners[i]() 236 | } 237 | // 此处设伏笔,在 applyMiddleware 里有妙用。 238 | return action 239 | } 240 | 241 | /** 242 | * Replaces the reducer currently used by the store to calculate the state. 243 | * 244 | * You might need this if your app implements code splitting and you want to 245 | * load some of the reducers dynamically. You might also need this if you 246 | * implement a hot reloading mechanism for Redux. 247 | * 248 | * @param {Function} nextReducer The reducer for the store to use instead. 249 | * @returns {void} 250 | */ 251 | function replaceReducer(nextReducer) { 252 | if (typeof nextReducer !== 'function') { 253 | throw new Error('Expected the nextReducer to be a function.') 254 | } 255 | 256 | /** 257 | * 1. 替换当前 Reducer 258 | * 2. 初始化 state 259 | */ 260 | currentReducer = nextReducer 261 | dispatch({ 262 | type: ActionTypes.INIT 263 | }) 264 | } 265 | 266 | /** 267 | * Interoperability point for observable/reactive libraries. 268 | * @returns {observable} A minimal observable of state changes. 269 | * For more information, see the observable proposal: 270 | * https://github.com/zenparsing/es-observable 271 | */ 272 | function observable() { 273 | var outerSubscribe = subscribe 274 | return { 275 | /** 276 | * The minimal observable subscription method. 277 | * @param {Object} observer Any object that can be used as an observer. 278 | * The observer object should have a `next` method. 279 | * @returns {subscription} An object with an `unsubscribe` method that can 280 | * be used to unsubscribe the observable from the store, and prevent further 281 | * emission of values from the observable. 282 | */ 283 | subscribe(observer) { 284 | if (typeof observer !== 'object') { 285 | throw new TypeError('Expected the observer to be an object.') 286 | } 287 | 288 | function observeState() { 289 | if (observer.next) { 290 | observer.next(getState()) 291 | } 292 | } 293 | 294 | observeState() 295 | var unsubscribe = outerSubscribe(observeState) 296 | return { 297 | unsubscribe 298 | } 299 | }, 300 | 301 | [$$observable]() { 302 | return this 303 | } 304 | } 305 | } 306 | 307 | /** 308 | * 触发一个内部 action,这样每个 reducer 都返回其 intial state, 309 | * 我们用各个子 raducer 的 initial state 初始化 state 整个树。 310 | */ 311 | dispatch({ 312 | type: ActionTypes.INIT 313 | }) 314 | 315 | return { 316 | dispatch, 317 | subscribe, 318 | getState, 319 | replaceReducer, 320 | [$$observable]: observable 321 | } 322 | } -------------------------------------------------------------------------------- /demo1/index.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'redux'; 2 | 3 | // actionTypes 4 | const ADD = 'add'; 5 | 6 | // reducer 7 | const todos = (state = [], action) => { 8 | switch (action.type) { 9 | case ADD: 10 | return [...state, {...action.payload}]; 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | // store 17 | const store = createStore(todos); 18 | const printState = store => 19 | console.log(`current state:`, JSON.stringify(store.getState())); 20 | printState(store); 21 | 22 | // subscribe 23 | store.subscribe(() => { 24 | printState(store); 25 | }); 26 | 27 | // dispatch 28 | store.dispatch({ 29 | type: ADD, 30 | payload: { 31 | text: 'learn Redux', 32 | completed: false 33 | } 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /demo2/index.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'redux'; 2 | 3 | // actionTypes 4 | const ADD = 'add'; 5 | 6 | // reducer 7 | const todos = (state = [], action) => { 8 | switch (action.type) { 9 | case ADD: 10 | return [...state, {...action.payload}]; 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | // store 17 | const store = createStore(todos); 18 | const printState = store => 19 | console.log(`current state:`, JSON.stringify(store.getState())); 20 | printState(store); 21 | 22 | // subscribe 23 | const subscribeA = store.subscribe(() => { 24 | console.log('subscribeA do this:') 25 | printState(store); 26 | }); 27 | const subscribeB = store.subscribe(() => { 28 | console.log('subscribeB do this:') 29 | printState(store); 30 | }); 31 | 32 | // dispatch 33 | store.dispatch({ 34 | type: ADD, 35 | payload: { 36 | text: 'learn Redux', 37 | completed: false 38 | } 39 | }); 40 | 41 | // unsubscribe 42 | subscribeB(); 43 | 44 | // dispatch 45 | store.dispatch({ 46 | type: ADD, 47 | payload: { 48 | text: 'learn React', 49 | completed: false 50 | } 51 | }); -------------------------------------------------------------------------------- /demo3/index.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'redux'; 2 | 3 | // actionTypes 4 | const ADD = 'add'; 5 | 6 | // reducer 7 | const todos = (state = [], action) => { 8 | switch (action.type) { 9 | case ADD: 10 | return [...state, {...action.payload}]; 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | // store 17 | const store = createStore(todos); 18 | const printState = store => 19 | console.log(`current state:`, JSON.stringify(store.getState())); 20 | printState(store); // 打印:current state: [] 21 | 22 | // subscribe 23 | const subscribeA = store.subscribe(() => { 24 | printState(store); 25 | subscribeA();//取消 subscribeA 的监听 26 | const subscribeB = store.subscribe(() => console.log('subscribeB'));//增加 subscribeB 的监听 27 | }); 28 | 29 | 30 | // dispatch 31 | store.dispatch({ 32 | type: ADD, 33 | payload: { 34 | text: 'learn Redux', 35 | completed: false 36 | } 37 | }); 38 | // 这里执行完后打印:current state: [{"text":"learn Redux","completed":false}] 39 | 40 | store.dispatch({ 41 | type: ADD, 42 | payload: { 43 | text: 'learn React', 44 | completed: false 45 | } 46 | }); 47 | // 这里执行完后打印:subscribeB -------------------------------------------------------------------------------- /demo4/index.js: -------------------------------------------------------------------------------- 1 | import {createStore} from 'redux'; 2 | 3 | // actionTypes 4 | const ADD = 'add'; 5 | 6 | // reducer 7 | const todos = (state = [], action) => { 8 | switch (action.type) { 9 | case ADD: 10 | return [...state, {...action.payload}]; 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | // store 17 | const store = createStore(todos); 18 | const printState = store => 19 | console.log(`current state:`, JSON.stringify(store.getState())); 20 | printState(store); // 打印:current state: [] 21 | 22 | // subscribe 23 | const subscribeA = store.subscribe(() => { 24 | subscribeA(); 25 | // dispatch 26 | store.dispatch({ 27 | type: ADD, 28 | payload: { 29 | text: 'learn Redux', 30 | completed: false 31 | } 32 | }); 33 | }); 34 | 35 | // dispatch 36 | store.dispatch({ 37 | type: ADD, 38 | payload: { 39 | text: 'learn React', 40 | completed: false 41 | } 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /demo5/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | import reducer from './reducers/index' 3 | 4 | let store = createStore(reducer) 5 | console.log(store.getState()) 6 | // { 7 | // counter: 0, 8 | // todos: [] 9 | // } 10 | 11 | store.dispatch({ 12 | type: 'ADD_TODO', 13 | text: 'Use Redux' 14 | }) 15 | console.log(store.getState()) 16 | // { 17 | // counter: 0, 18 | // todos: [ 'Use Redux' ] 19 | // } -------------------------------------------------------------------------------- /demo5/reducers/counter.js: -------------------------------------------------------------------------------- 1 | export default function counter(state = 0, action) { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1 5 | case 'DECREMENT': 6 | return state - 1 7 | default: 8 | return state 9 | } 10 | } -------------------------------------------------------------------------------- /demo5/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos' 3 | import counter from './counter' 4 | 5 | export default combineReducers({ 6 | todos, 7 | counter 8 | }) -------------------------------------------------------------------------------- /demo5/reducers/todos.js: -------------------------------------------------------------------------------- 1 | export default function todos(state = [], action) { 2 | switch (action.type) { 3 | case 'ADD_TODO': 4 | return state.concat([action.text]) 5 | default: 6 | return state 7 | } 8 | } -------------------------------------------------------------------------------- /demo6/index.js: -------------------------------------------------------------------------------- 1 | //demo6 2 | import {compose} from 'redux'; 3 | 4 | const fnA = (obj) => { 5 | console.log('fnA begin'); 6 | obj.a = 'a'; 7 | return obj; 8 | } 9 | const fnB = (obj) => { 10 | console.log('fnB begin'); 11 | obj.b = 'b'; 12 | return obj; 13 | } 14 | const fnC = (obj) => { 15 | console.log('fnC begin'); 16 | obj.c = 'c'; 17 | return obj; 18 | } 19 | const fnD = (obj) => { 20 | console.log('fnD begin'); 21 | obj.d = 'd'; 22 | return obj; 23 | } 24 | 25 | 26 | let obj = {} 27 | 28 | const composedFns = compose(fnA, fnB, fnC, fnD); 29 | 30 | console.log( composedFns(obj) ); 31 | //fnD begin 32 | //fnC begin 33 | //fnB begin 34 | //fnA begin 35 | //{ d: 'd', c: 'c', b: 'b', a: 'a' } -------------------------------------------------------------------------------- /demo7/index.js: -------------------------------------------------------------------------------- 1 | //demo7 2 | import {compose} from 'redux'; 3 | 4 | const result = compose()(123, 456); 5 | console.log(result); //123 -------------------------------------------------------------------------------- /demo8/index.js: -------------------------------------------------------------------------------- 1 | //demo8 2 | import {compose} from 'redux'; 3 | 4 | const result = compose(Math.pow)(4, 2); 5 | console.log(result); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-source-code-chewing", 3 | "version": "1.0.0", 4 | "description": "the analysis of redux source code", 5 | "main": "index.js", 6 | "scripts": { 7 | "demo1": "babel-node demo1/index.js", 8 | "demo2": "babel-node demo2/index.js", 9 | "demo3": "babel-node demo3/index.js", 10 | "demo4": "babel-node demo4/index.js", 11 | "demo5": "babel-node demo5/index.js", 12 | "demo6": "babel-node demo6/index.js", 13 | "demo7": "babel-node demo7/index.js", 14 | "demo8": "babel-node demo8/index.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/pobusama/redux-source-code-chewing.git" 19 | }, 20 | "keywords": [ 21 | "redux", 22 | "sourcecode" 23 | ], 24 | "author": "pobusama@gmail.com", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/pobusama/redux-source-code-chewing/issues" 28 | }, 29 | "homepage": "https://github.com/pobusama/redux-source-code-chewing#readme", 30 | "dependencies": { 31 | "babel-cli": "^6.24.1", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-stage-0": "^6.24.1", 34 | "redux": "^3.7.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /posts/applyMiddleware.md: -------------------------------------------------------------------------------- 1 | # applyMiddleware —— Redux 的拓展坞 2 | 3 | 这是本 [Redux 源码阅读历程](https://github.com/pobusama/redux-source-code-chewing)的第五篇文章,我们聊聊 applyMiddleware。在此假设你已经对 Redux 中间件有一定认识,建议先阅读[官方文档](http://cn.redux.js.org/docs/advanced/Middleware.html)。 4 | 5 | ## 为什么有 applyMiddleware ? 6 | 在[对 createStore 的 API —— dispatch 进行分析](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/createStore.md#触发-state-的变化--dispatch)时,我们知道 dispatch 只接收限定类型的 action 对象: 7 | 1. 必须是纯对象(plain object)。 8 | 2. 必须拥有 type 属性。 9 | 因为 dispatch 规定的 action 对象是用于 reducer 计算这一 “特殊用途”。 10 | 11 | 我们先讨论一个问题: 12 | 13 | 在纵览 Redux 的实现机制后,我们发现,从 action 到 reducer 再到 state 变更,整个流程都是同步的。显然在实际应用中,会有一些异步操作。那么这些异步操作应该放在哪里才合适呢? 14 | 15 | 我们知道,更新 Redux store 数据的关键之一是 action 对象。action 的 type 和携带的其他数据都决定 reducer 中的计算结果。我们仔细思考一下分发 action 对象的过程,如果分发一个 action 的时候,不是直接调用 reducer 计算,而是先去进行异步操作,异步操作拿到结果以后再调用 reducer 计算,是不是在 Redux 体系中完成异步需求的一种方式呢? 16 | 17 | 如果要实现上面的假设,我们需要做到两件事情。 18 | 1. dispatch API 需要 “定制化”,不再拿着 action 去执行 reducer 等原有操作。 19 | 2. 为了区分定制化的 dispatch 和普通的 dispatch,action 也需要定制化。即一种 action 对应着一种 dispatch。 20 | 21 | applyMiddleware 的主要能力就是为这两点提供支持。做到了这两点,意味着你可以自由定制 action - dispatch 的组合(这个则是中间件的功能),异步需求只是可完成的任务之一(其他的功能任由你拓展,比如打印 action 日志)。 22 | 23 | ## 如何使用 applyMiddleware? 24 | 既然有 “applyMiddleware” 那就有 “middleware”,Redux 中,中间件的接口定义十分明确,一个典型的中间件格式如下: 25 | ```js 26 | const aMiddware = {getState, dispatch} => next => action => { 27 | // 检查 action,看下是不是符合本中间件的要求 28 | if ( isRightAction(action) ) { 29 | // 拿到 getState、dispatch、action,做本中间件的特定逻辑 30 | } else { 31 | // 如果不是本中间件需要的 action 对象,将 action 交给下一个中间件处理 32 | return next(action) 33 | } 34 | } 35 | ``` 36 | 而启用自定义的中间件,只需要在 createStore 的时候做一些变化。 37 | ```js 38 | const store = createStore(reducer, initState, applyMiddleware( 39 | middlewareA, 40 | middlewareB, 41 | middlewareC, 42 | // more ... 43 | )); 44 | ``` 45 | 在[讲 createStore 的参数时](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/createStore.md#createstore-的参数),我们知道了 createStore 的第三个参数是 Enhancer 函数,它接收原始 store 返回加强后的 store,applyMiddleware **返回的函数**就是典型的 Redux Enhancer 函数 —— 它接收 store 对象,返回加强后的 store 对象。 46 | 47 | 梳理一下: 48 | 1. applyMiddleware 接收中间件函数(可以多个)作为参数。 49 | 2. applyMiddleware 返回 Redux Enhancer 函数。 50 | 2. Redux Enhancer 函数接收 store 对象、返回 store 对象。 51 | 52 | ## applyMiddleware 源码分析 53 | 54 | ```js 55 | export default function applyMiddleware(...middlewares) { 56 | //返回 Redux Enhancer 函数(接收 store 对象、返回 store 对象) 57 | return (createStore) => (reducer, preloadedState, enhancer) => { 58 | // 获得原始 createStore 生成的 store 59 | var store = createStore(reducer, preloadedState, enhancer) 60 | // 获得原始 dispatch 61 | var dispatch = store.dispatch 62 | var chain = [] 63 | // 给中间件传入可用的 store API 64 | var middlewareAPI = { 65 | getState: store.getState, 66 | /** 67 | * (action) => dispatch(action) 等价于 (action) => {return dispatch(action)} 68 | * 函数内部的 dispatch 指向外层定义的 dispatch 69 | * 当 middleware 内部执行 middlewareAPI.dispatch 方法时 70 | * dispatch 已经是 compose 之后的 dispatch 71 | */ 72 | dispatch: (action) => dispatch(action) 73 | } 74 | // 为 middleware 分配统一的 middlewareAPI 后,将 middleware 返回的函数(这里称作 handler)收集在 chain 数组中 75 | chain = middlewares.map(middleware => middleware(middlewareAPI)) 76 | // dispatch = handlerA(handlerB(handlerC(store.dispatch))) 77 | dispatch = compose(...chain)(store.dispatch) 78 | 79 | return { 80 | ...store, 81 | dispatch //middleware handler 处理后的 dispatch 82 | } 83 | } 84 | } 85 | ``` 86 | 回过头来看中间件的定义,我们可以对可用的 API 更加明确: 87 | ```js 88 | const aMiddware = {getState, dispatch} => next => action => { 89 | // 检查 action,看下是不是符合本中间件的要求 90 | if ( isRightAction(action) ) { 91 | // 1. getState:来自原始 store 对象的 API 92 | // 2. dispatch:compose 以后的 dispatch,意味着在这里分发 action 对象,action 会完整地经过所有中间件。 93 | // 3. action:上一个中间件未捕获的 action 对象。 94 | 95 | } else { 96 | // 如果不是本中间件需要的 action 对象,将 action 交给下一个中间件(准确来说是中间件的返回函数 handler)处理 97 | return next(action) 98 | } 99 | } 100 | ``` 101 | 102 | ## compose 在这中间起的作用 103 | 在源码中有一行把 middleware 函数的最外面一层执行了,产生我们称之为 "handler" 的函数,再对这些函数进行 compose。 104 | ```js 105 | chain = middlewares.map(middleware => middleware(middlewareAPI)) 106 | dispatch = compose(...chain)(store.dispatch) 107 | ``` 108 | handler 的形状如下: 109 | ```js 110 | next => action => { 111 | //... 112 | return next(action); 113 | } 114 | ``` 115 | 源码中把每个 middleware 生成的 handler 通过 compose 组装起来,原始的 dispatch 函数通过 handler 组成的 “管道”,成为新的函数。这里依然运用了函数式编程中 “函数是一等公民” 的原则,把 dispatch 函数作为 “管道” 的数据源,对其做自定义的改造。 116 | **注意:** 由于 middware 是由开发者决定的,所以开发者可以决定是否 `return next(action);`,这句话是管道衔接的关键,如果移除这句话,就是要断开 “管道”,即到当前 handler 为止,不再交给下一层 handler 处理了,所以需要谨慎。 117 | 118 | 最终分发 action 的时候, action 会被重新定义的 dispatch 函数处理,而这个函数根据 action 的特征决定将其交给哪一层的 handler 处理。 119 | 120 | 梳理: 121 | 1. compose 实际组合了中间件的逻辑。 122 | 2. 由于管道的特性,我们要谨慎处理每个管道区间的返回值。 123 | 124 | ## 总结 125 | applyMiddleware 区区十几行代码,为 redux 提供了对外接口,我们可以自由设计 action - dispatch 的搭配,让 action 不再受限于 reducer 的范围。 126 | 127 | 然而,这种 “自由” 依然是有限的,applyMiddleware 把这份自由限制在了分发 action 的过程中。不过,这正是 Redux 的智慧,毕竟过度的自由带来的是维护的灾难。 128 | 129 | 但我们还是有权利获取更多的自由的,第一章里提到的 Enhancer 就是这个 “黑洞” 的入口。毕竟,applyMiddleware 也只是无数 Redux Enhancer 的一种。不过,这不在我们本次源码研究的讨论范围内。 130 | 131 | [禁止转载] -------------------------------------------------------------------------------- /posts/bindActionCreators.md: -------------------------------------------------------------------------------- 1 | # bindActionCreators —— 箭在弦上 2 | 3 | 这是本 [Redux 源码阅读历程](https://github.com/pobusama/redux-source-code-chewing)的第三篇文章,第一篇中我们提及了分发 action 对象的方式:`dispatch()` 一个 action 生成器。 4 | 5 | ## 有个小麻烦 6 | 如果需要分发的 action 多了,你会见到类似下面的情况: 7 | ```js 8 | // 分发 actionA 9 | dispatch(actionCreatorA()); 10 | // 第二次分发 actionA 11 | dispatch(actionCreatorA()); 12 | // 分发 actionB 13 | dispatch(actionCreatorB()); 14 | ``` 15 | 16 | 我们知道重复的逻辑是可以封装的,所以我们试着定义一个函数来抽出公共部分: 17 | ```js 18 | const bindActionCreator = (dispatch, actionCreator) => { 19 | return (...arg) => dispatch(actionCreator(...arg)); 20 | } 21 | ``` 22 | 这样如果我们在第一次 dispatch 时将相应的 actionCreator 和 dispatch 绑定,后面使用就方便多了。 23 | ```js 24 | // 改写一下例子1 25 | actionCreatorA = bindActionCreator(dispatch, actionCreatorA); 26 | actionCreatorB = bindActionCreator(dispatch, actionCreatorB); 27 | 28 | // 分发 actionA 29 | actionCreatorA(); 30 | // 第二次分发 actionA 31 | actionCreatorA(); 32 | // 分发 actionB 33 | actionCreatorB(); 34 | ``` 35 | 没优化前,我们每次都需要把 “箭” (action)放在 “弓” (dispatch)上,然后再 “拉弓射箭” (`dispatch(action)`),而优化后,我们只需要射箭就可以了,可谓 “箭在弦上”。 36 | redux 开发者也为我们想到了这点,提供了一个工具函数 bindActionCreators。下面我们刷一遍它的源码。 37 | 38 | ## 源码分析 39 | 40 | ```js 41 | function bindActionCreator(actionCreator, dispatch) { 42 | /** 43 | * 实际对 actionCreator 和 dispatch 进行绑定的地方 44 | * 使用 "..." 解构语法将传入 actionCreator 的实参 45 | * 原封不动传给绑定后的函数 46 | */ 47 | return (...args) => dispatch(actionCreator(...args)) 48 | } 49 | 50 | export default function bindActionCreators(actionCreators, dispatch) { 51 | if (typeof actionCreators === 'function') { 52 | // 如果形参 actionCreators 传入的是单个 function 直接返回绑定后的函数。 53 | return bindActionCreator(actionCreators, dispatch) 54 | } 55 | // 无效参数校验 56 | if (typeof actionCreators !== 'object' || actionCreators === null) { 57 | throw new Error( 58 | `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` + 59 | `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` 60 | ) 61 | } 62 | 63 | var keys = Object.keys(actionCreators) 64 | var boundActionCreators = {} 65 | // 遍历 actionCreators 对象上的函数集合,每个都进行 dispatch 绑定。 66 | for (var i = 0; i < keys.length; i++) { 67 | var key = keys[i] 68 | var actionCreator = actionCreators[key] 69 | if (typeof actionCreator === 'function') { 70 | boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) 71 | } 72 | } 73 | // 返回绑定后的函数集合 74 | return boundActionCreators 75 | } 76 | ``` 77 | 核心逻辑和我此前提到的一样,不过源码比我们讨论的更进一步:它不但提供了绑定单一 actionCreator 函数的方式(actionCreators 形参接收一个函数),还提供了绑定多个函数的方式(actionCreators 形参接收一个对象)。此外依然做了严谨的参数校验,写工具函数时,我们可以借鉴这种思路。 78 | 79 | [暂不允许转载] -------------------------------------------------------------------------------- /posts/combineReducers.md: -------------------------------------------------------------------------------- 1 | # combineReducers —— 涓涓溪流,可成江海 2 | 3 | 这是本 [Redux 源码阅读历程](https://github.com/pobusama/redux-source-code-chewing)的第二篇文章,在第一篇文章[我简单提到了 reducer](https://github.com/pobusama/redux-source-code-chewing/blob/master/posts/createStore.md#触发-state-的变化--dispatch),这也是 Redux 里的一个重要概念。 4 | 5 | 我们常常会在营销活动广告的角落里找到一行字:“最终解释权归主办方所有”。也就是说你按照活动规则完成任务以后,怎么获得奖励以及获得多少奖励,由 “主办方” 说了算。而在 Redux 中,触发了一个 action,这个 action 通过什么方案改变 state,改变 state 的哪些部分,由 reducer 说了算,它就是所谓的 “主办方“,或者更具体来说是 ”主办方“ 的解释规则。 6 | 7 | 然而,随着 Redux 应用的复杂度提升(比如模块的增加),与之对应的 state 树变得更加庞大,相应的 reducer 函数也会随之变得越来越冗长。于是 Redux 给我们提供了 combineReducers 函数,让我们按模块拆分 reducer,再将拆分后的 reducer 组合起来。看起来,这是一个由一到多,再由多归一的过程。 8 | 9 | 我们来一探究竟。 10 | 11 | ## 用法回顾 12 | ```js 13 | // reducers/todos.js 14 | export default function todos(state = [], action) { 15 | switch (action.type) { 16 | case 'ADD_TODO': 17 | return state.concat([action.text]) 18 | default: 19 | return state 20 | } 21 | } 22 | // reducers/counter.js 23 | export default function counter(state = 0, action) { 24 | switch (action.type) { 25 | case 'INCREMENT': 26 | return state + 1 27 | case 'DECREMENT': 28 | return state - 1 29 | default: 30 | return state 31 | } 32 | } 33 | //reducers/index.js 34 | import { combineReducers } from 'redux' 35 | import todos from './todos' 36 | import counter from './counter' 37 | 38 | export default combineReducers({ 39 | todos, 40 | counter 41 | }) 42 | // App.js 43 | import { createStore } from 'redux' 44 | import reducer from './reducers/index' 45 | 46 | let store = createStore(reducer) 47 | console.log(store.getState()) 48 | // state 初始化后,会与 reducers 对象的 key 相同。 49 | // { 50 | // counter: 0, 51 | // todos: [] 52 | // } 53 | 54 | store.dispatch({ 55 | type: 'ADD_TODO', 56 | text: 'Use Redux' 57 | }) 58 | console.log(store.getState()) 59 | // { 60 | // counter: 0, 61 | // todos: [ 'Use Redux' ] 62 | // } 63 | ``` 64 | 65 | ## 源码分析 66 | 67 | ### combineReducers 的参数 68 | 首先看 combineReducers 的参数文档: 69 | ```js 70 | /** 71 | * 把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数, 72 | * 然后就可以对这个 reducer 调用 createStore。合并后的 reducer 可以调用各个子 reducer, 73 | * 并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定。 74 | * 75 | * @param {Object} reducers 一个对象,它的值(value) 对应不同的 reducer 函数,这些 76 | * reducer 函数后面会被合并成一个。建议在 reducers/index.js 里使用 combineReducers() 来 77 | * 对外输出一个 reducer。 78 | * 79 | * @returns {Function} 一个调用 reducers 对象里所有 reducer 的 reducer,并且构造一个 80 | * 与 reducers 对象结构相同的 state 对象。 81 | */ 82 | export default function combineReducers(reducers) { 83 | //... 84 | } 85 | ``` 86 | 了解了基本参数、返回值和使用场景,我们看具体的实现代码。同样,处于严谨,有一段代码用于校验、筛选 reducers 参数。 87 | ```js 88 | export default function combineReducers(reducers) { 89 | // 取 reducers 对象的属性,存到 reducerKeys 数组中 90 | var reducerKeys = Object.keys(reducers) 91 | // 定义一个用来存放筛选后 reducers 属性的对象。 92 | var finalReducers = {} 93 | /** 94 | * 遍历 reducers 对象上的属性, 95 | * 1. 做非空校验 96 | * 2. 保留属性为 function 类型的 reducer 97 | */ 98 | for (var i = 0; i < reducerKeys.length; i++) { 99 | var key = reducerKeys[i] 100 | if (process.env.NODE_ENV !== 'production') { 101 | // 属性空警告 102 | if (typeof reducers[key] === 'undefined') { 103 | warning(`No reducer provided for key "${key}"`) 104 | } 105 | } 106 | // 筛选为类型函数的 reducer 107 | if (typeof reducers[key] === 'function') { 108 | finalReducers[key] = reducers[key] 109 | } 110 | } 111 | // 这里 finalReducers 是筛选完毕后的 reducers 对象 112 | var finalReducerKeys = Object.keys(finalReducers) 113 | 114 | // 开发环境下用于 warning 信息,暂不细究 115 | if (process.env.NODE_ENV !== 'production') { 116 | var unexpectedKeyCache = {} 117 | } 118 | 119 | // 定义该异常变量用于完善性检测 120 | var sanityError 121 | try { 122 | /** 123 | * 完善性检测 124 | * 检验 finalReducers 对象上的每一个子 reducer 能否按照 redux 定义的规则处理 action, 125 | * 并 return 正确的 state。 126 | * 如果不满足完善性检测,则抛出异常 127 | */ 128 | assertReducerSanity(finalReducers) 129 | } catch (e) { 130 | sanityError = e 131 | } 132 | 133 | //... 134 | } 135 | ``` 136 | 到这里,如果不出意外的话 `finalReducers` 就是筛选后的 `reducers` 对象。我们接着用 `finalReducers` 137 | 完成 combineReducers 逻辑。 138 | 139 | 我们得到的结论是: 140 | - `finalReducers` 对象中的 reducer 都为函数类型。 141 | - `finalReducers` 对象中的 reducer 都能正常计算并返回 state。 142 | 143 | ### combineReducers 的实现 144 | 我们继续看源码: 145 | ```js 146 | export default function combineReducers(reducers) { 147 | //参数校验... 148 | /** 149 | * 最终返回的 rootReducer 函数组合了 finalReducers 对象上的子 reducer 逻辑。 150 | */ 151 | return function combination(state = {}, action) { 152 | // 如果未通过完善性检测,则中断并抛出异常。 153 | if (sanityError) { 154 | throw sanityError 155 | } 156 | 157 | // 开发环境下用于 warning 信息,暂不细究 158 | if (process.env.NODE_ENV !== 'production') { 159 | var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache) 160 | if (warningMessage) { 161 | warning(warningMessage) 162 | } 163 | } 164 | 165 | // 用以判断 state 是否被改变的标识位 166 | var hasChanged = false 167 | // 定义该对象用于保存更改后的 state 树 168 | var nextState = {} 169 | //再一次遍历 finalReducerKeys 对象上的子 reducer 属性。 170 | for (var i = 0; i < finalReducerKeys.length; i++) { 171 | var key = finalReducerKeys[i] 172 | var reducer = finalReducers[key] 173 | /** 174 | * 获取变更前 state 上与 reducer key 相对应的部分 state。 175 | * 如果是初始化阶段,该 state 分支的值是 undefined。 176 | * 如果非初始化阶段,则是获取 state 分支的值。 177 | */ 178 | var previousStateForKey = state[key] 179 | // 用当前 reducer 计算 action,返回计算后的部分 state。 180 | var nextStateForKey = reducer(previousStateForKey, action) 181 | // 检测一下当前 reducer 有没有正确返回 state。 182 | if (typeof nextStateForKey === 'undefined') { 183 | var errorMessage = getUndefinedStateErrorMessage(key, action) 184 | throw new Error(errorMessage) 185 | } 186 | // 将子 reducer 计算后的部分 state 挂在对应的 state 树属性上 187 | nextState[key] = nextStateForKey 188 | /** 189 | * 这里是对比每个 nextStateForKey 与 previousStateForKey,通过短路写法可以提高效率。 190 | * 对象相比比地址,如果 nextStateForKey 是 reducer 返回的新的 state, 191 | * 与 previousStateForKey 地址不同 hasChanged 就会变 true。 192 | * 193 | * reducer 的规则是,若要在 reducer 里面更新 state,不是直接修改 state, 194 | * 而是开辟新的地址修改并存放 newState,然后最终作为 reducer 的返回值。 195 | * 根据该规则,如果 state “变更” 了,其 nextStateForKey 和 previousStateForKey 地址 196 | * 不相等。 197 | */ 198 | hasChanged = hasChanged || nextStateForKey !== previousStateForKey 199 | } 200 | // 如果未改变,则返回旧 state,相应地 UI 方面也不用回流。 201 | return hasChanged ? nextState : state 202 | } 203 | } 204 | ``` 205 | 206 | 其实 combineReducers 工具函数我们也可以自己实现,只不过 redux 提供给我们的这个工具实现了比较完善的排错机制。我们这里就不详细讨论这些排错机制了,如果你有兴趣和我一起理解,请移步[我的注释的版本](https://github.com/pobusama/redux-source-code-chewing/blob/master/code-with-my-commit/combineReducers.js)~ 207 | 208 | [暂不允许转载] -------------------------------------------------------------------------------- /posts/compose.md: -------------------------------------------------------------------------------- 1 | # compose —— 管道工 2 | 3 | 这是本 [Redux 源码阅读历程](https://github.com/pobusama/redux-source-code-chewing)的第四篇文章。这次我们依然讲一个工具函数, 这个工具函数是 redux 中间件系统的重要基础,它就是 compose。 4 | 5 | ## compose 的用法 6 | compose 工具函数的作用非常简单明了,就是把一组函数组合成一个函数。我们先来看它是怎么使用的。 7 | 8 | ```js 9 | //demo6 10 | import {compose} from 'redux'; 11 | 12 | const fnA = (obj) => { 13 | console.log('fnA begin'); 14 | obj.a = 'a'; 15 | return obj; 16 | } 17 | const fnB = (obj) => { 18 | console.log('fnB begin'); 19 | obj.b = 'b'; 20 | return obj; 21 | } 22 | const fnC = (obj) => { 23 | console.log('fnC begin'); 24 | obj.c = 'c'; 25 | return obj; 26 | } 27 | const fnD = (obj) => { 28 | console.log('fnD begin'); 29 | obj.d = 'd'; 30 | return obj; 31 | } 32 | let obj = {} 33 | const composedFns = compose(fnA, fnB, fnC, fnD); 34 | console.log( composedFns(obj) ); 35 | //fnD begin 36 | //fnC begin 37 | //fnB begin 38 | //fnA begin 39 | //{ d: 'd', c: 'c', b: 'b', a: 'a' } 40 | ``` 41 | 42 | `npm run demo6`可以验证结果,通过上面的代码,将 fnA、fnB、fnC、fnD 四个函数组装成了 composedFns 函数。这里的拼装形式十分特殊,是将一个函数的输出(返回值)作为另一个函数的输入(实参),而 compose 的顺序是从右往左,也就是将 fnD 作为第一个函数,接收 composedFns 函数的 obj 实参,它的返回值将作为 fnC 函数的实参,fnC 的返回值作为 fnB 的实参,以此类推,最终 fnA 函数的返回值即是 `composedFns(obj)` 的返回值。 43 | 44 | 如果有点晕,我们来看下等效代码: 45 | ```js 46 | composedFns(obj); 47 | // 等价于 48 | fnA(fnB(fnC(fnD(obj)))); 49 | // 等价于 50 | ((obj) => { 51 | console.log('fnD begin'); 52 | obj.d = 'd'; 53 | // fnD 处理完 obj 后通过 return 移交给 fnC 54 | console.log('fnC begin'); 55 | obj.c = 'c'; 56 | // fnC 处理完 obj 后通过 return 移交给 fnB 57 | console.log('fnB begin'); 58 | obj.b = 'b'; 59 | // fnB 处理完 obj 后通过 return 移交给 fnA 60 | console.log('fnA begin'); 61 | obj.a = 'a'; 62 | return obj; 63 | })(obj) 64 | 65 | ``` 66 | 67 | 为啥要把函数拆分成这样的形式,再组装起来呢?大家知道 Redux 遵循函数式编程风格,函数式编程要求细化每个函数的功能,再把拥有不同功能的函数组合成特定功能的函数,以此实现需求。 68 | 69 | 这里的 compose 只是函数组合的一种形式,它适用于处理 “管道” 数据流需求。我们的 demo6 就是一个例子,一个本来是 `{}` 的 obj 对象,在 “流经” fnD、 fnC、 fnB、fnA 函数 “管道” 后,最终被加工为 `{ d: 'd', c: 'c', b: 'b', a: 'a' }`。 70 | 71 | 这和中间件的模式是不是有些相似呢?我们把这个问题留到下一篇讲,接下来,我们看看 compose 到底是如何组合函数的。 72 | 73 | ## compose 源码分析 74 | 75 | ### reduce 和 reduceRight 76 | 磨刀不误砍柴功,理解 compose 源码之前,充分理解一个原生数组 API —— reduce(reduceRight)十分关键。对这个 API 比较陌生的同学理解起来还是会比较扰的,我们通过例子来理解: 77 | 78 | ```js 79 | ['1', '2', '3'].reduce((accumulator, currentValue) => accumulator + currentValue, '0') 80 | // => 0123 81 | ``` 82 | 这行代码发生了什么? 83 | 首先,reduce 接收两个参数,第一个是函数(我们称为累加器函数),第二个是初始值(如果不传初始值就以 arr[0] 作为初始值)。 84 | 接着,累加器函数接收两个参数,第一个是 accumulator 即累计值,第二个是 currentValue 即当前值。 85 | 在首次计算中,累加器会以初始值(这里为 '0')为 accumulator,以 `arr[0]` 作为 currentValue,**累加器返回的结果作为下一次累加的 accumulator,而下一次累加的 currentValue 为从左到右的数组元素**,这里的顺序是 `arr[1]` 到 `arr[length - 1]`。 86 | 87 | 所以这行代码实际上可以看作: 88 | ```js 89 | ((('0' + '1') + '2') + '3') 90 | // => 0123 91 | ``` 92 | 93 | 而 reduceRight 和 reduce 的区别是取 currentValue 的顺序,从数组的最后一个元素开始取,即从右往左。 94 | 95 | ```js 96 | ['1', '2', '3'].reduceRight((accumulator, currentValue) => accumulator + currentValue, '0') 97 | //等效于 98 | ((('0' + '3') + '2') + '1') 99 | // => 0321 100 | ``` 101 | 这里初始值 '0' 不变,而 currentValue 取值顺序改变。 102 | 103 | ### 回到 compose 104 | 105 | 首先,compose 函数区分了不同参数个数的情况: 106 | ```js 107 | export default function compose(...funcs) { 108 | /** 109 | * 当 compose 不接收函数实参时,返回一个**返回第一个实参的函数**。 110 | * 例如:`compose()(123, 456)` 等效于 `(x => x)(123, 456)` 111 | * 返回 123 112 | * 注:参考 demo7 113 | */ 114 | if (funcs.length === 0) { 115 | return arg => arg 116 | } 117 | /** 118 | * 当 compose 接收 1 个函数实参时返回【该函数】。 119 | * 例如:`compose(Math.pow)(4, 2)` 等效于 `Math.pow(4, 2)` 120 | * 返回 16 121 | * 注:参考 demo8 122 | */ 123 | if (funcs.length === 1) { 124 | return funcs[0] 125 | } 126 | /** 127 | * 当 compose 接收 1 个以上函数实参时返回一个【函数组合】。 128 | * 例如:`compose(a, b, c, d)(1, 2, 3)` 129 | * 等效于 `( (...args) => a(b(c(d(...args)))) )(1, 2, 3)` 130 | * 注:参考 demo6 131 | */ 132 | //... 133 | } 134 | ``` 135 | 136 | 接着,我们着重看一下接收 1 个以上参数时的情况: 137 | ```js 138 | export default function compose(...funcs) { 139 | //... 140 | /** 141 | * 当 compose 接收 1 个以上函数实参时返回一个【函数组合】。 142 | * 例如:`compose(a, b, c, d)(1, 2, 3)` 143 | * 等效于 `( (...args) => a(b(c(d(...args)))) )(1, 2, 3)` 144 | * 注:参考 demo6 145 | */ 146 | const last = funcs[funcs.length - 1] // 取倒数第 1 个被 compose 的函数 147 | const rest = funcs.slice(0, -1) // 取 0 至倒数第 2 个被 compose 的函数(数组) 148 | // 返回组合后的函数 149 | return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args)) 150 | 151 | /** 152 | * 例如: 153 | * compose(a, b, c, d) 时,last 为 d,rest 为 [a, b, c]。 154 | * 155 | * `rest.reduceRight((composed, f) => f(composed), last(...args))` 156 | * 等效于 `[a, b, c].reduceRight((composed, f) => f(composed), d(...args))` 157 | * composed(即 accumulator)接收:d(...args)) 158 | * f(即 currentValue)接收:c 159 | * 160 | * 看下 reduceRight 循环每次做了什么: 161 | * 第一次 composed 参数接收初始值 `d(...args)`,f 参数接收 c,所以返回的是 `c(d(...args))` 162 | * 第二次 composed 参数接收上一次的累积值 `c(d(...args))`, 163 | * f 参数接收 b,所以返回的是 `b(c(d(...args)))` 164 | * 第三次 composed 参数接收上一次的累积值`b(c(d(...args)))`, 165 | * f 参数接收 a,所以返回的是 `a(b(c(d(...args))))` 166 | * 167 | * 所以 `rest.reduceRight((composed, f) => f(composed), last(...args))` 168 | * 等效于 `[a, b, c].reduceRight((composed, f) => f(composed), d(...args))` 169 | * 等效于 a(b(c(d(...args)))) 170 | * 171 | * 因此 compose(a, b, c, d) 172 | * 等效于 (...args) => a(b(c(d(...args)))) 173 | */ 174 | } 175 | ``` 176 | 177 | ## 总结 178 | 通过探索 Redux 实现的 compose 工具函数,我主要收获了以下两点 179 | 1. 函数式编程中存在着一种 “管道” 思想,即通过多个函数组合成的 “管道”,对 “流过” 的数据进行处理,最终输出处理后的数据。 180 | 2. reduce/reduceRight 的用法实践。由于 “函数是一等公民”,其地位等同于其他数据类型,所以 reduce/reduceRight 也可以用来累计一组函数。 181 | 182 | [暂不允许转载] -------------------------------------------------------------------------------- /posts/createStore.md: -------------------------------------------------------------------------------- 1 | # createStore —— 繁华的起点 2 | 3 | 这是本 [Redux 源码阅读历程](https://github.com/pobusama/redux-source-code-chewing)的第一篇文章,我们就从 Redux 库的核心文件 —— createStore.js 开始研读。讲道理,Redux 库跟外面其他妖艳的 JS 库不一样,[配套注释十分详细](https://github.com/pobusama/redux-source-code-chewing/blob/master/source-code/createStore.js),阅读下来就跟读思路清晰的文章感受差不多。嗯,是个正经的 JS 库。 4 | 5 | 进入正题,store 是 Redux 的核心概念,那么它的创造者 —— createStore 函数就应该是核心 API 之一了,你可以[预览](https://github.com/pobusama/redux-source-code-chewing/blob/master/source-code/createStore.js)一下它的源码。不出所料,createStore.js 输出的函数只有一个 —— `createStore`。而拉到源码文件的最底部,我们发现这个函数最终返回一个对象,对象上面包含 5 个 API:dispatch、subscribe、getState、replaceReducer 和 [$$observable],我们主要分析前三个。这个对象也就是我们所谓的 `store`。 6 | 7 | 那么接下来,我们先回顾一下 `createStore` 的用法。 8 | 9 | **提示:** 本文基于 Redux 的 3.6.0 版本 10 | 11 | 12 | 13 | 如果用一句话总结 Redux 的基本用法,那便是**创建 store,监听 state 变化,触发 action 使 state 变更**。写一段你不能再熟悉的代码: 14 | ```js 15 | // demo1 16 | import {createStore} from 'redux'; 17 | // actionTypes 18 | const ADD = 'add'; 19 | // reducer 20 | const todos = (state = [], action) => { 21 | switch (action.type) { 22 | case ADD: 23 | return [...state, {...action.payload}]; 24 | default: 25 | return state; 26 | } 27 | } 28 | // store 29 | const store = createStore(todos); 30 | // 这里定义一个用来打印 state 的函数 31 | const printState = store => 32 | console.log(`current state:`, JSON.stringify(store.getState())); 33 | printState(store); 34 | /** 35 | * 这里打印出 state 初始值: 36 | * current state: [] 37 | */ 38 | 39 | // 订阅 state 的变化并打印变化后的 state 40 | store.subscribe(() => { 41 | printState(store); 42 | }); 43 | // dispatch 44 | store.dispatch({ 45 | type: ADD, 46 | payload: { 47 | text: 'learn Redux', 48 | completed: false 49 | } 50 | }); 51 | /** 52 | * 这里打印: 53 | * current state: [{"text":"learn Redux","completed":false}] 54 | */ 55 | ``` 56 | 运行一下这段代码(`npm run demo1`),控制台输出: 57 | ``` 58 | current state: [] 59 | current state: [{"text":"learn Redux","completed":false}] 60 | ``` 61 | 由第一行可知,`store` 创建成功,state 初始化完成。第二行则是说明我们成功使用 `dispatch` API 改变了 state。 62 | 回顾完毕,接下来来看看 Redux 是怎么实现这几个 API 的。 63 | 64 | ## 源码分析 65 | ### createStore 的参数 66 | 67 | 读懂文档是读懂源码的第一步,我们先看下 `createStore` 的 API 文档(酱油翻译,轻喷⇁_⇁)说了些啥: 68 | 69 | ```js 70 | /** 71 | * 创建一个用于管理 state 树的 Redux store 对象。 72 | * 修改 store 中数据的唯一方式就是调用 store 对象上的 `dispatch()` 方法。 73 | * 74 | * 一个应用只能拥有单一 store。 75 | * state 树的不同部分会根据 action 作出响应,为了区分这些不同,你可以使用 `combineReducers` 76 | * 函数将多个 reducer 函数拼装到一个单一的 reducer 函数上。 77 | * 78 | * @param {Function} reducer 一个 return 下一个 state 树的函数, 79 | * 这个函数接收当前 state 和 action,用来处理当前 state 并产生下一个 state。 80 | * 81 | * @param {any} [preloadedState] 初始 state。可选参数,在一般的应用中,该参数可以用于 82 | * 整合来自服务端的状态,也可以用来保存前一次的用户会话记录(session)。 83 | * 如果你使用 `combineReducers` 来生产 root reducer 函数,该参数必须是一个和 84 | * `combineReducers` 的属性的形式一样的对象。 85 | * 86 | * @param {Function} enhancer store 的增强器。可选参数,该参数用于增强 store,我们可以通过 87 | * 第三方例如中间件、时间旅行、持久化等功能来增强 store。`applyMiddleware()` 是唯一由 Redux 88 | * 提供的增强器。 89 | * 90 | * @returns {Store} 一个 Redux store 对象,通过该对象你可以读取 state,触发 action, 91 | * 并订阅(监听)state 的更新。 92 | */ 93 | export default function createStore(reducer, preloadedState, enhancer) { 94 | //... 95 | } 96 | ``` 97 | 好了,现在我们对 createStore 函数的基本用法、参数作用和返回值有了大致的认识,接下来,我们看看 createStore 函数的内部逻辑。 98 | 99 | 首先我们从参数校验部分巩固对参数的理解: 100 | ```js 101 | export default function createStore(reducer, preloadedState, enhancer) { 102 | /* -------------- createStore 参数校验部分 -------------- */ 103 | /** 104 | * 参数校验,如果第二个形参传入的是函数,且第三个形参不传,则第二个实参代表 enhancer。 105 | * 换句话说,preloadedState 是可选配置。 106 | */ 107 | if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { 108 | enhancer = preloadedState 109 | preloadedState = undefined 110 | } 111 | // 校验传入的 enhancer 实参是否是函数。 112 | if (typeof enhancer !== 'undefined') { 113 | if (typeof enhancer !== 'function') { 114 | throw new Error('Expected the enhancer to be a function.') 115 | } 116 | /** 117 | * 从这里可以看出,enhancer 的一般形式是: 118 | * const enhancer = (createStore) => { 119 | * //返回一个函数 `finalCreateStore`,用于接收 reducer 和 preloadedState 120 | * return function finalCreateStore (reducer, preloadedState) { 121 | * //这里可以拿到原 createStore、reducer 和 preloadedState 122 | * //然后添加自定义逻辑 123 | * //最终返回 store 对象 124 | * return createStore(reducer, preloadedState); 125 | * } 126 | * } 127 | */ 128 | return enhancer(createStore)(reducer, preloadedState) 129 | } 130 | // 校验传入的 reducer 实参是否是函数。 131 | if (typeof reducer !== 'function') { 132 | throw new Error('Expected the reducer to be a function.') 133 | } 134 | //主体部分... 135 | } 136 | ``` 137 | 我们得出的结论是: 138 | 1. `reducer` 是必传参数,类型必须为 `function`。 139 | 2. `preloadedState` 和 `enhancer` 是可选参数。 140 | 3. `enhancer` 可选参数的类型必须为 `function`。 141 | 142 | ### createStore 内部变量 143 | 144 | 接下来是主体部分,这部分比较多,咱们一步步来,我们先看 createStore 函数内部维护了哪些变量。 145 | ```js 146 | export default function createStore(reducer, preloadedState, enhancer) { 147 | //...参数校验部分 148 | /* -------------- createStore 正片部分 -------------- */ 149 | var currentReducer = reducer 150 | var currentState = preloadedState 151 | var currentListeners = [] 152 | var nextListeners = currentListeners 153 | var isDispatching = false 154 | //... 155 | } 156 | ``` 157 | 1. currentReducer:当前 store 应用的 reducer,默认使用传入的 reducer 参数,可通过 replaceReducer 函数来热替换 currentReducer。 158 | 2. currentState:默认为传入的 preloadedState 参数,可通过 dispatch 函数改变。 159 | 3. currentListeners:当前订阅队列,用以存放通过 subscribe 函数执行的订阅。 160 | 4. nextListeners:subscribe 函数可以订阅或取消订阅,`nextListeners` 用来存放订阅或取消订阅后的队列。 161 | 5. isDispatching:dispatch 函数的标志位,作用后面会讲到。 162 | 163 | 我们大致了解了这些变量的基本用处。接下来你一定以为我会顺着源码聊到 `getState()`,哈哈那怎么是我的风格! 164 | 165 | ### 触发 state 的变化 —— dispatch 166 | 167 | 现在我们再次纵观整个 `createStore.js` 文件,发现定义 5 个变量之后紧接着定义了几个 API 函数,而**真正执行内部函数的地方,只有靠近文件最底部的 `dispatch({ type: ActionTypes.INIT })` 这段代码**,我先告诉你这是初始化整个 `state` 的关键步骤。至于它是怎么初始化 `state` 的,且看 `dispatch` 做了些什么: 168 | ```js 169 | //... 170 | function dispatch(action) { 171 | /* -------------- dispatch 参数校验部分 -------------- */ 172 | // action 要求是一个简单对象(plain object) 173 | if (!isPlainObject(action)) { 174 | throw new Error( 175 | 'Actions must be plain objects. ' + 176 | 'Use custom middleware for async actions.' 177 | ) 178 | } 179 | // 最基础的 dispatch 函数(没有接入三方中间件)接收的 action 对象必须要带 type 参数。 180 | if (typeof action.type === 'undefined') { 181 | throw new Error( 182 | 'Actions may not have an undefined "type" property. ' + 183 | 'Have you misspelled a constant?' 184 | ) 185 | } 186 | /** 187 | * 标识位,用来锁定 reducer 计算过程, 188 | * 如果 reducer 计算过程中调用了 dispatch 函数则会报错(为什么不能调用用?请接着往下看)。 189 | */ 190 | if (isDispatching) { 191 | throw new Error('Reducers may not dispatch actions.') 192 | } 193 | /* -------------- dispatch 正片部分 -------------- */ 194 | try { 195 | /** 196 | * 这是 Redux 的灵魂部分 197 | * 作用是将当前 state 和 action 交给 reducer 函数处理,计算出**新的 state** 198 | * 注意!在 reducer 函数中要避免调用 dispatch 199 | * 原因类似银行取钱:假设你和女朋友共存了 100 元,在某时刻,你取 10 块钱 200 | * 此时银行系统便会对你的账户计算:`100 - 10 = 90` 201 | * 如果计算过程中你女朋友取 20 元,那么银行系统又会计算:`100 - 20 = 80` 202 | * 那结果到底是 90 还是 80 呢? 203 | * 当然是 70 ! 204 | * 银行家的做法是在你 “取钱 -> 结算完毕” 过程中冻结其他存取操作(在本源码中是置 isDispatching 标识位为 true), 205 | * 你女朋友只能在你 “取钱 -> 结算完毕” 过程以外的时间里取钱。 206 | */ 207 | isDispatching = true // 更改过程锁 208 | currentState = currentReducer(currentState, action) // 将当前 state 和 action 交给 reducer 计算 209 | } finally { 210 | // 无论计算成功还是报错,最终都将标志位置为 false,以免阻碍下一个 action 的 dipatch。 211 | isDispatching = false 212 | } 213 | /** 214 | * 此时 state 已经更新完毕,我们将订阅队列中的函数一一执行 215 | * 我们在这些函数里可以拿到更新后的 state。 216 | */ 217 | var listeners = currentListeners = nextListeners 218 | for (var i = 0; i < listeners.length; i++) { 219 | listeners[i]() 220 | } 221 | // 此处设伏笔,在 applyMiddleware 里有妙用。 222 | return action 223 | } 224 | //... 225 | ``` 226 | 227 | 现在回头再看看 `dispatch({ type: ActionTypes.INIT })` 这段代码,你一定已经对他了若指掌了。 228 | 拿 `demo1` 中设计的 reducer 函数举例子: 229 | ```js 230 | // reducer 231 | const todos = (state = [], action) => { 232 | switch (action.type) { 233 | case ADD: 234 | return [...state, {...action.payload}]; 235 | default: 236 | return state; 237 | } 238 | } 239 | ``` 240 | 作为使用者我们肯定不会针对 `ActionTypes.INIT` 这个 Redux 内部的 action type 做相应计算,那么 reducer 函数就会直接走 default case 从而返回 currentState。毫无疑问,在 store 初始化的时候,这个 state 要么是 reducer 函数定义的缺省 state(这里是 `[]`),要么是 createStore 函数中传入的第二个参数 `preloadedState`。这样我们就完成了 state 的初始化。 241 | 242 | 在初始化之后,在其他地方触发 `dispatch` 函数,就是我们熟悉的过程了:通过 action 携带的信息和 currentState 计算 nextState,更新 store 内部的 currentState。 243 | 244 | 所以我们通过 createStore 函数拿到 store 对象意味着什么呢?结论是: 245 | 1. 我们在 store 内部已经有了一个 state(currentState)。 246 | 2. 我们有 5 个 API 来管理这个 state。 247 | 248 | ### 获取当前 state —— getState 249 | 250 | 如何获取当前 state?想必你已经能预测到它的源码是怎么写的了: 251 | ```js 252 | //... 253 | function getState() { 254 | return currentState 255 | } 256 | //... 257 | ``` 258 | 259 | ### 订阅(监听)state 的变化 —— subscribe 260 | 我们目前有了初始 state,有了更新 state 的方式,还有获取当前 state 的方式。但我们如何监听 state 的变化呢?细心的你肯定已在 dispatch 函数的实现代码中初见端倪了。 261 | 好我们来看 subscribe 函数: 262 | 263 | ```js 264 | function subscribe(listener) { 265 | /* -------------- subscribe 参数校验部分 -------------- */ 266 | // listener 必须是函数类型(state 变更以后调用) 267 | if (typeof listener !== 'function') { 268 | throw new Error('Expected listener to be a function.') 269 | } 270 | 271 | /* -------------- subscribe 正片部分 -------------- */ 272 | // 每次订阅都会维护一个标志位,以便在重复取消订阅的时候提高性能 273 | var isSubscribed = true 274 | 275 | ensureCanMutateNextListeners() 276 | nextListeners.push(listener)// 向 listeners 队列中添加订阅函数 277 | 278 | /** 279 | * 取消订阅 280 | */ 281 | return function unsubscribe() { 282 | // 防止重复取消订阅时,再次进行下面比较耗费性能的运算 283 | if (!isSubscribed) { 284 | return 285 | } 286 | // 取消订阅先把标志位置 false 287 | isSubscribed = false 288 | 289 | ensureCanMutateNextListeners() 290 | // 找到订阅函数在订阅队列中的位置 291 | var index = nextListeners.indexOf(listener) 292 | // 删除订阅队列中的相应订阅函数。 293 | nextListeners.splice(index, 1) 294 | } 295 | } 296 | ``` 297 | 粗略看下来逻辑还是比较清晰的,该 API 提供了订阅和取消订阅的功能,订阅时,向内部维护的订阅队列(nextListeners)中 push 订阅函数。这时候我们回顾一下 dispatch 的部分代码: 298 | ```js 299 | //... 300 | var listeners = currentListeners = nextListeners 301 | for (var i = 0; i < listeners.length; i++) { 302 | listeners[i]() 303 | } 304 | //... 305 | ``` 306 | 这里在 state 变更后将 nextListeners 数组中的订阅函数按顺序执行,这就完成了订阅 -> 执行订阅函数的流程。 307 | 308 | 此外,subscribe 返回一个 unsubscribe 函数用于取消订阅。 unsubscribe 利用 subscribe 函数闭包变量 listener,定位到订阅队列的相应位置,然后删除相应订阅函数。 309 | 310 | 我在 [`demo2`](https://github.com/pobusama/redux-source-code-chewing/tree/master/demo2) 中简单地演示了一下取消订阅的用法,请运行 `npm run demo2` 查看结果。 311 | 312 | ```js 313 | // demo2 314 | // ... 以上是相同的代码 315 | // 这回添加了两个监听函数 316 | const subscribeA = store.subscribe(() => { 317 | console.log('subscribeA do this:') 318 | printState(store); 319 | }); 320 | const subscribeB = store.subscribe(() => { 321 | console.log('subscribeB do this:') 322 | printState(store); 323 | }); 324 | // 执行 dispatch 后,监听函数依次执行 325 | store.dispatch({ 326 | type: ADD, 327 | payload: { 328 | text: 'learn Redux', 329 | completed: false 330 | } 331 | }); 332 | /** 333 | * 这一步打印: 334 | * subscribeA do this: 335 | * current state: [{"text":"learn Redux","completed":false}] 336 | * subscribeB do this: 337 | * current state: [{"text":"learn Redux","completed":false}] 338 | */ 339 | // 取消 subscribeB 订阅函数 340 | subscribeB(); 341 | // 现在执行 dispatch 后,只有 subscribeA 订阅函数会执行。 342 | store.dispatch({ 343 | type: ADD, 344 | payload: { 345 | text: 'learn React', 346 | completed: false 347 | } 348 | }); 349 | /** 350 | * 这一步打印: 351 | * subscribeA do this: 352 | * current state: [{"text":"learn Redux","completed":false},{"text":"learn React", 353 | * "completed":false}] 354 | */ 355 | ``` 356 | 357 | 慢着!我们好像漏看了两行代码!subscribe 函数中出现了两次 `ensureCanMutateNextListeners()`,它们是干什么用的呢?从字面理解,这行代码用于 “确认可以修改 nextListeners 变量”。还是不懂?没关系! 358 | 我们把 `ensureCanMutateNextListeners()` 替换成这个函数具体代码: 359 | 360 | ```js 361 | function subscribe(listener) { 362 | //... 363 | var isSubscribed = true 364 | 365 | // 替换 ensureCanMutateNextListeners() 366 | if (nextListeners === currentListeners) { 367 | nextListeners = currentListeners.slice() 368 | } 369 | // 向 listeners 队列中添加订阅函数 370 | nextListeners.push(listener) 371 | 372 | return function unsubscribe() { 373 | if (!isSubscribed) { 374 | return 375 | } 376 | isSubscribed = false 377 | 378 | // 替换 ensureCanMutateNextListeners() 379 | if (nextListeners === currentListeners) { 380 | nextListeners = currentListeners.slice() 381 | } 382 | // 找到订阅函数在订阅队列中的位置 383 | var index = nextListeners.indexOf(listener) 384 | // 删除订阅队列中的相应订阅函数。 385 | nextListeners.splice(index, 1) 386 | } 387 | } 388 | ``` 389 | 结合函数名 “ensureCanMutateNextListeners”,和函数代码,我们可以提出以下两个问题: 390 | 1. “确认” 有什么用处呢? 391 | 2. 为什么要复制一份 currentListeners 到 nextListeners 上修改,而不是直接在 currentListeners 上修改呢? 392 | 393 | 带着这两个问题,我们回头看一下 subscribe 函数的注释: 394 | ```js 395 | /** 396 | * 添加一个订阅 state 变更的监听函数(listener)。该监听函数将会在 action 分发后, 397 | * state 树完成可能的变更之后被调用。接着你可以在这个回调中通过调用 `getState()` 398 | * 来读取当前 state。 399 | * 400 | * 你可能会在一个监听函数中调用 `dispatch()`,请知晓以下注意事项: 401 | * 402 | * 1. 监听函数只应当在响应用户的 actions 或者特殊的条件限制下(比如:在 store 有一个 403 | * 特殊字段时 dispatch action)才能调用 dispatch()。虽然不作任何条件限制而在监听函数中 404 | * 调用 dispatch() 在技术上是可行的,但是随着每次 dispatch() 改变 store 可能会导致陷 405 | * 入无穷的循环。 406 | * 407 | * 2. 在每次调用 `dispatch()` 之前,订阅队列(subscriptions)会保存一份快照。如果你在 408 | * 订阅函数正在执行的时候订阅或者取消订阅,那这次订阅或取消订阅并不会影响本次 `dispatch()` 409 | * 过程。但下次调用 `dispatch()` 时,无论其是否嵌套,订阅队列应用都会应用订阅列表里最近的 410 | * 一次快照。 411 | * 412 | * 3. 因为在监听函数执行前,state 有可能在一个嵌套的 `dispatch()` 中改变多次,所以监听 413 | * 函数不一定能跟踪到所有的 state 变更。保证所有的监听器都注册在 dispatch() 启动之前, 414 | * 这样,在调用监听器的时候就会传入监听器所存在时间里最新的一次 state。 415 | * 416 | * @param {Function} listener 每当 dispatch action 的时候都会执行的回调函数。 417 | * @returns {Function} 一个用来移除函数变化监听器的函数。 418 | */ 419 | function subscribe(listener) { 420 | //... 421 | } 422 | ``` 423 | 我们仔细看第 2 条注意事项,它强调了 “如果你在订阅函数正在执行的时候订阅或者取消订阅,那这次订阅或取消订阅并不会影响本次 `dispatch()` 过程”。“订阅函数正在执行的时候” 对应的是 dispatch 函数中的代码: 424 | ```js 425 | var listeners = currentListeners = nextListeners 426 | for (var i = 0; i < listeners.length; i++) { 427 | listeners[i]() 428 | } 429 | ``` 430 | 设想一共有 10 个订阅函数,我们在第 5 个订阅函数执行过程中又增加一个订阅函数。我们知道这段代码是同步执行的,执行到第 5 个时,**循环没有执行完,后面 5 个订阅函数也没有执行**,此时若操作 “listeners”,“currentListeners”,“nextListeners” 任意一个数组变量(他们都指向同一个数组对象的地址),都会影响后面的循环。想要不影响后面的循环?`ensureCanMutateNextListeners()` 登场。 431 | ```js 432 | // ensureCanMutateNextListeners() 433 | if (nextListeners === currentListeners) { 434 | nextListeners = currentListeners.slice() 435 | } 436 | ``` 437 | 再回到 “第 5 个订阅函数”,如果我们在其执行时调用 subscribe 添加或取消订阅函数,此刻 `nextListeners === currentListeners` 为 true,我们通过 `nextListeners = currentListeners.slice()` **将当前订阅队列拷贝了一份,获得了新的数组对象地址**,然后赋值给 `nextListeners`(这里也就是源码注释里所说的 “快照”),用这个数组添加或取消订阅函数。这样丝毫没有影响 `listeners` 数组的循环过程,一直到执行订阅函数的循环结束。而下一次执行 `dispatch()` 时,`var listeners = currentListeners = nextListeners` 这段代码使订阅队列应用 “应用订阅列表里最近的一次快照”,更新了 listeners 变量,接着再循环执行订阅队列。至此,我们完成逻辑上的闭环。 438 | 439 | 这次我们用 demo3(`npm run demo3`)来解释这个过程。 440 | ```js 441 | //demo3 442 | // ... 以上是相同的代码 443 | // subscribe 444 | const subscribeA = store.subscribe(() => { 445 | printState(store); 446 | //在订阅函数中取消 subscribeA 订阅(改变订阅队列) 447 | subscribeA(); 448 | //增加 subscribeB 的监听(也会改变订阅队列) 449 | const subscribeB = store.subscribe(() => console.log('subscribeB')); 450 | }); 451 | // dispatch 452 | store.dispatch({ 453 | type: ADD, 454 | payload: { 455 | text: 'learn Redux', 456 | completed: false 457 | } 458 | }); 459 | /** 460 | * 第一次执行 dispatch,触发 subscribeA 订阅函数 461 | * 这里执行完后打印:current state: [{"text":"learn Redux","completed":false}] 462 | * 说明 dispatch 期间订阅队列没有受到影响 463 | */ 464 | 465 | store.dispatch({ 466 | type: ADD, 467 | payload: { 468 | text: 'learn React', 469 | completed: false 470 | } 471 | }); 472 | /** 473 | * 第二次 dispatch,应用最近的快照 474 | * 由于取消了 subscribeA 增加了 subscribeB 所以快照里只有 subscribeB。 475 | * 这里执行完后打印:subscribeB 476 | * 说明已经应用了最新的快照 477 | */ 478 | ``` 479 | 现在,我们回答一开始提出的两个问题 480 | 1. “确认” 有什么用处呢?—— 确认当前队列和 “快照“ 是否一致,若一致则开辟新的快照。 481 | 2. 为什么要复制一份 currentListeners 到 nextListeners 上修改,而不是直接在 currentListeners 上修改呢? —— 保存 “快照”,屏蔽订阅或取消订阅对当前循环的影响。 482 | 483 | 读到这里我们基本理解了 subscribe 的各种小心思。不过说实话作为框架使用者,我很少直接用到 subscribe 这个 API,它一般是 Redux 与其他库(比如 react)的桥接库(react-redux)的宠儿,参与管理 view 的顶层数据。换句话说,理解了 subscribe 的内部逻辑,以后读 react-redux 库的逻辑会更加轻车熟路,正所谓 “技多不压身” 嘛! 484 | 485 | ### 偷梁换柱 —— replaceReducer 486 | replaceReducer 只做了两件事情,首先用接收的 nextReducer 替换内部的 currentReducer,接着用 `dispatch({type: ActionTypes.INIT})` 来初始化 state(至于为什么这样初始化,我在 dispatch 小节中有提到)。 487 | 这个 API 的代码非常简单,但给应用提供的可能性是无穷的,目前我还没遇到直接使用 replaceReducer 的场景,等遇到了再回头来扩充。 488 | 489 | ```js 490 | function replaceReducer(nextReducer) { 491 | if (typeof nextReducer !== 'function') { 492 | throw new Error('Expected the nextReducer to be a function.') 493 | } 494 | /** 495 | * 1. 替换当前 Reducer 496 | * 2. 初始化 state 497 | */ 498 | currentReducer = nextReducer 499 | dispatch({ 500 | type: ActionTypes.INIT 501 | }) 502 | } 503 | ``` 504 | 505 | ### [$$observable] 506 | 这个 API 我并没有在官方文档中找到清晰的解释。同样等遇到了使用场景再回头来看。 507 | 508 | ## 总结 509 | 总的来说,createStore 是 Redux 最核心的部分。它提供创建 store 对象的方式。而 store 对象则是管理应用里唯一一个 state 树的工具。通读 createStore 的源码,我觉得下面几个要点比较重要: 510 | 1. createStore 通过函数内存的方式存储 store 需要的变量,可有效隔绝外部影响。 511 | 2. dispatch 通过触发监听函数队列的方式协助 subscribe 实现订阅机制。 512 | 3. rducer 必须要有默认返回对象(一般是当前 state)。 513 | 4. 在同一轮订阅函数队列的执行过程中,增加或取消订阅只会影响下次订阅队列执行。这个可以通过 “快照” 的方式来实现。 514 | 515 | That's all~ 516 | 517 | [暂不允许转载] -------------------------------------------------------------------------------- /source-code/applyMiddleware.js: -------------------------------------------------------------------------------- 1 | import compose from './compose' 2 | 3 | /** 4 | * Creates a store enhancer that applies middleware to the dispatch method 5 | * of the Redux store. This is handy for a variety of tasks, such as expressing 6 | * asynchronous actions in a concise manner, or logging every action payload. 7 | * 8 | * See `redux-thunk` package as an example of the Redux middleware. 9 | * 10 | * Because middleware is potentially asynchronous, this should be the first 11 | * store enhancer in the composition chain. 12 | * 13 | * Note that each middleware will be given the `dispatch` and `getState` functions 14 | * as named arguments. 15 | * 16 | * @param {...Function} middlewares The middleware chain to be applied. 17 | * @returns {Function} A store enhancer applying the middleware. 18 | */ 19 | export default function applyMiddleware(...middlewares) { 20 | return (createStore) => (reducer, preloadedState, enhancer) => { 21 | var store = createStore(reducer, preloadedState, enhancer) 22 | var dispatch = store.dispatch 23 | var chain = [] 24 | 25 | var middlewareAPI = { 26 | getState: store.getState, 27 | dispatch: (action) => dispatch(action) 28 | } 29 | chain = middlewares.map(middleware => middleware(middlewareAPI)) 30 | dispatch = compose(...chain)(store.dispatch) 31 | 32 | return { 33 | ...store, 34 | dispatch 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source-code/bindActionCreators.js: -------------------------------------------------------------------------------- 1 | function bindActionCreator(actionCreator, dispatch) { 2 | return (...args) => dispatch(actionCreator(...args)) 3 | } 4 | 5 | /** 6 | * Turns an object whose values are action creators, into an object with the 7 | * same keys, but with every function wrapped into a `dispatch` call so they 8 | * may be invoked directly. This is just a convenience method, as you can call 9 | * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. 10 | * 11 | * For convenience, you can also pass a single function as the first argument, 12 | * and get a function in return. 13 | * 14 | * @param {Function|Object} actionCreators An object whose values are action 15 | * creator functions. One handy way to obtain it is to use ES6 `import * as` 16 | * syntax. You may also pass a single function. 17 | * 18 | * @param {Function} dispatch The `dispatch` function available on your Redux 19 | * store. 20 | * 21 | * @returns {Function|Object} The object mimicking the original object, but with 22 | * every action creator wrapped into the `dispatch` call. If you passed a 23 | * function as `actionCreators`, the return value will also be a single 24 | * function. 25 | */ 26 | export default function bindActionCreators(actionCreators, dispatch) { 27 | if (typeof actionCreators === 'function') { 28 | return bindActionCreator(actionCreators, dispatch) 29 | } 30 | 31 | if (typeof actionCreators !== 'object' || actionCreators === null) { 32 | throw new Error( 33 | `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` + 34 | `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` 35 | ) 36 | } 37 | 38 | var keys = Object.keys(actionCreators) 39 | var boundActionCreators = {} 40 | for (var i = 0; i < keys.length; i++) { 41 | var key = keys[i] 42 | var actionCreator = actionCreators[key] 43 | if (typeof actionCreator === 'function') { 44 | boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) 45 | } 46 | } 47 | return boundActionCreators 48 | } 49 | -------------------------------------------------------------------------------- /source-code/combineReducers.js: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from './createStore' 2 | import isPlainObject from 'lodash/isPlainObject' 3 | import warning from './utils/warning' 4 | 5 | function getUndefinedStateErrorMessage(key, action) { 6 | var actionType = action && action.type 7 | var actionName = actionType && `"${actionType.toString()}"` || 'an action' 8 | 9 | return ( 10 | `Given action ${actionName}, reducer "${key}" returned undefined. ` + 11 | `To ignore an action, you must explicitly return the previous state.` 12 | ) 13 | } 14 | 15 | function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) { 16 | var reducerKeys = Object.keys(reducers) 17 | var argumentName = action && action.type === ActionTypes.INIT ? 18 | 'preloadedState argument passed to createStore' : 19 | 'previous state received by the reducer' 20 | 21 | if (reducerKeys.length === 0) { 22 | return ( 23 | 'Store does not have a valid reducer. Make sure the argument passed ' + 24 | 'to combineReducers is an object whose values are reducers.' 25 | ) 26 | } 27 | 28 | if (!isPlainObject(inputState)) { 29 | return ( 30 | `The ${argumentName} has unexpected type of "` + 31 | ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + 32 | `". Expected argument to be an object with the following ` + 33 | `keys: "${reducerKeys.join('", "')}"` 34 | ) 35 | } 36 | 37 | var unexpectedKeys = Object.keys(inputState).filter(key => 38 | !reducers.hasOwnProperty(key) && 39 | !unexpectedKeyCache[key] 40 | ) 41 | 42 | unexpectedKeys.forEach(key => { 43 | unexpectedKeyCache[key] = true 44 | }) 45 | 46 | if (unexpectedKeys.length > 0) { 47 | return ( 48 | `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + 49 | `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` + 50 | `Expected to find one of the known reducer keys instead: ` + 51 | `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.` 52 | ) 53 | } 54 | } 55 | 56 | function assertReducerSanity(reducers) { 57 | Object.keys(reducers).forEach(key => { 58 | var reducer = reducers[key] 59 | var initialState = reducer(undefined, { type: ActionTypes.INIT }) 60 | 61 | if (typeof initialState === 'undefined') { 62 | throw new Error( 63 | `Reducer "${key}" returned undefined during initialization. ` + 64 | `If the state passed to the reducer is undefined, you must ` + 65 | `explicitly return the initial state. The initial state may ` + 66 | `not be undefined.` 67 | ) 68 | } 69 | 70 | var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.') 71 | if (typeof reducer(undefined, { type }) === 'undefined') { 72 | throw new Error( 73 | `Reducer "${key}" returned undefined when probed with a random type. ` + 74 | `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + 75 | `namespace. They are considered private. Instead, you must return the ` + 76 | `current state for any unknown actions, unless it is undefined, ` + 77 | `in which case you must return the initial state, regardless of the ` + 78 | `action type. The initial state may not be undefined.` 79 | ) 80 | } 81 | }) 82 | } 83 | 84 | /** 85 | * Turns an object whose values are different reducer functions, into a single 86 | * reducer function. It will call every child reducer, and gather their results 87 | * into a single state object, whose keys correspond to the keys of the passed 88 | * reducer functions. 89 | * 90 | * @param {Object} reducers An object whose values correspond to different 91 | * reducer functions that need to be combined into one. One handy way to obtain 92 | * it is to use ES6 `import * as reducers` syntax. The reducers may never return 93 | * undefined for any action. Instead, they should return their initial state 94 | * if the state passed to them was undefined, and the current state for any 95 | * unrecognized action. 96 | * 97 | * @returns {Function} A reducer function that invokes every reducer inside the 98 | * passed object, and builds a state object with the same shape. 99 | */ 100 | export default function combineReducers(reducers) { 101 | var reducerKeys = Object.keys(reducers) 102 | var finalReducers = {} 103 | for (var i = 0; i < reducerKeys.length; i++) { 104 | var key = reducerKeys[i] 105 | 106 | if (process.env.NODE_ENV !== 'production') { 107 | if (typeof reducers[key] === 'undefined') { 108 | warning(`No reducer provided for key "${key}"`) 109 | } 110 | } 111 | 112 | if (typeof reducers[key] === 'function') { 113 | finalReducers[key] = reducers[key] 114 | } 115 | } 116 | var finalReducerKeys = Object.keys(finalReducers) 117 | 118 | if (process.env.NODE_ENV !== 'production') { 119 | var unexpectedKeyCache = {} 120 | } 121 | 122 | var sanityError 123 | try { 124 | assertReducerSanity(finalReducers) 125 | } catch (e) { 126 | sanityError = e 127 | } 128 | 129 | return function combination(state = {}, action) { 130 | if (sanityError) { 131 | throw sanityError 132 | } 133 | 134 | if (process.env.NODE_ENV !== 'production') { 135 | var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache) 136 | if (warningMessage) { 137 | warning(warningMessage) 138 | } 139 | } 140 | 141 | var hasChanged = false 142 | var nextState = {} 143 | for (var i = 0; i < finalReducerKeys.length; i++) { 144 | var key = finalReducerKeys[i] 145 | var reducer = finalReducers[key] 146 | var previousStateForKey = state[key] 147 | var nextStateForKey = reducer(previousStateForKey, action) 148 | if (typeof nextStateForKey === 'undefined') { 149 | var errorMessage = getUndefinedStateErrorMessage(key, action) 150 | throw new Error(errorMessage) 151 | } 152 | nextState[key] = nextStateForKey 153 | hasChanged = hasChanged || nextStateForKey !== previousStateForKey 154 | } 155 | return hasChanged ? nextState : state 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /source-code/compose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Composes single-argument functions from right to left. The rightmost 3 | * function can take multiple arguments as it provides the signature for 4 | * the resulting composite function. 5 | * 6 | * @param {...Function} funcs The functions to compose. 7 | * @returns {Function} A function obtained by composing the argument functions 8 | * from right to left. For example, compose(f, g, h) is identical to doing 9 | * (...args) => f(g(h(...args))). 10 | */ 11 | 12 | export default function compose(...funcs) { 13 | if (funcs.length === 0) { 14 | return arg => arg 15 | } 16 | 17 | if (funcs.length === 1) { 18 | return funcs[0] 19 | } 20 | 21 | const last = funcs[funcs.length - 1] 22 | const rest = funcs.slice(0, -1) 23 | return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args)) 24 | } 25 | -------------------------------------------------------------------------------- /source-code/createStore.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject' 2 | import $$observable from 'symbol-observable' 3 | 4 | /** 5 | * These are private action types reserved by Redux. 6 | * For any unknown actions, you must return the current state. 7 | * If the current state is undefined, you must return the initial state. 8 | * Do not reference these action types directly in your code. 9 | */ 10 | export var ActionTypes = { 11 | INIT: '@@redux/INIT' 12 | } 13 | 14 | /** 15 | * Creates a Redux store that holds the state tree. 16 | * The only way to change the data in the store is to call `dispatch()` on it. 17 | * 18 | * There should only be a single store in your app. To specify how different 19 | * parts of the state tree respond to actions, you may combine several reducers 20 | * into a single reducer function by using `combineReducers`. 21 | * 22 | * @param {Function} reducer A function that returns the next state tree, given 23 | * the current state tree and the action to handle. 24 | * 25 | * @param {any} [preloadedState] The initial state. You may optionally specify it 26 | * to hydrate the state from the server in universal apps, or to restore a 27 | * previously serialized user session. 28 | * If you use `combineReducers` to produce the root reducer function, this must be 29 | * an object with the same shape as `combineReducers` keys. 30 | * 31 | * @param {Function} enhancer The store enhancer. You may optionally specify it 32 | * to enhance the store with third-party capabilities such as middleware, 33 | * time travel, persistence, etc. The only store enhancer that ships with Redux 34 | * is `applyMiddleware()`. 35 | * 36 | * @returns {Store} A Redux store that lets you read the state, dispatch actions 37 | * and subscribe to changes. 38 | */ 39 | export default function createStore(reducer, preloadedState, enhancer) { 40 | if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { 41 | enhancer = preloadedState 42 | preloadedState = undefined 43 | } 44 | 45 | if (typeof enhancer !== 'undefined') { 46 | if (typeof enhancer !== 'function') { 47 | throw new Error('Expected the enhancer to be a function.') 48 | } 49 | 50 | return enhancer(createStore)(reducer, preloadedState) 51 | } 52 | 53 | if (typeof reducer !== 'function') { 54 | throw new Error('Expected the reducer to be a function.') 55 | } 56 | 57 | var currentReducer = reducer 58 | var currentState = preloadedState 59 | var currentListeners = [] 60 | var nextListeners = currentListeners 61 | var isDispatching = false 62 | 63 | function ensureCanMutateNextListeners() { 64 | if (nextListeners === currentListeners) { 65 | nextListeners = currentListeners.slice() 66 | } 67 | } 68 | 69 | /** 70 | * Reads the state tree managed by the store. 71 | * 72 | * @returns {any} The current state tree of your application. 73 | */ 74 | function getState() { 75 | return currentState 76 | } 77 | 78 | /** 79 | * Adds a change listener. It will be called any time an action is dispatched, 80 | * and some part of the state tree may potentially have changed. You may then 81 | * call `getState()` to read the current state tree inside the callback. 82 | * 83 | * You may call `dispatch()` from a change listener, with the following 84 | * caveats: 85 | * 86 | * 1. The listener should only call dispatch() either in response to user actions 87 | * or under specific conditions (e. g. dispatching an action when the store has 88 | * a specific field). Calling dispatch() without any conditions is technically 89 | * possible, however it leads to an infinite loop as every dispatch() call 90 | * usually triggers the listener again. 91 | * 92 | * 2. The subscriptions are snapshotted just before every `dispatch()` call. 93 | * If you subscribe or unsubscribe while the listeners are being invoked, this 94 | * will not have any effect on the `dispatch()` that is currently in progress. 95 | * However, the next `dispatch()` call, whether nested or not, will use a more 96 | * recent snapshot of the subscription list. 97 | * 98 | * 3. The listener should not expect to see all state changes, as the state 99 | * might have been updated multiple times during a nested `dispatch()` before 100 | * the listener is called. It is, however, guaranteed that all subscribers 101 | * registered before the `dispatch()` started will be called with the latest 102 | * state by the time it exits. 103 | * 104 | * @param {Function} listener A callback to be invoked on every dispatch. 105 | * @returns {Function} A function to remove this change listener. 106 | */ 107 | function subscribe(listener) { 108 | if (typeof listener !== 'function') { 109 | throw new Error('Expected listener to be a function.') 110 | } 111 | 112 | var isSubscribed = true 113 | 114 | ensureCanMutateNextListeners() 115 | nextListeners.push(listener) 116 | 117 | return function unsubscribe() { 118 | if (!isSubscribed) { 119 | return 120 | } 121 | 122 | isSubscribed = false 123 | 124 | ensureCanMutateNextListeners() 125 | var index = nextListeners.indexOf(listener) 126 | nextListeners.splice(index, 1) 127 | } 128 | } 129 | 130 | /** 131 | * Dispatches an action. It is the only way to trigger a state change. 132 | * 133 | * The `reducer` function, used to create the store, will be called with the 134 | * current state tree and the given `action`. Its return value will 135 | * be considered the **next** state of the tree, and the change listeners 136 | * will be notified. 137 | * 138 | * The base implementation only supports plain object actions. If you want to 139 | * dispatch a Promise, an Observable, a thunk, or something else, you need to 140 | * wrap your store creating function into the corresponding middleware. For 141 | * example, see the documentation for the `redux-thunk` package. Even the 142 | * middleware will eventually dispatch plain object actions using this method. 143 | * 144 | * @param {Object} action A plain object representing “what changed”. It is 145 | * a good idea to keep actions serializable so you can record and replay user 146 | * sessions, or use the time travelling `redux-devtools`. An action must have 147 | * a `type` property which may not be `undefined`. It is a good idea to use 148 | * string constants for action types. 149 | * 150 | * @returns {Object} For convenience, the same action object you dispatched. 151 | * 152 | * Note that, if you use a custom middleware, it may wrap `dispatch()` to 153 | * return something else (for example, a Promise you can await). 154 | */ 155 | function dispatch(action) { 156 | if (!isPlainObject(action)) { 157 | throw new Error( 158 | 'Actions must be plain objects. ' + 159 | 'Use custom middleware for async actions.' 160 | ) 161 | } 162 | 163 | if (typeof action.type === 'undefined') { 164 | throw new Error( 165 | 'Actions may not have an undefined "type" property. ' + 166 | 'Have you misspelled a constant?' 167 | ) 168 | } 169 | 170 | if (isDispatching) { 171 | throw new Error('Reducers may not dispatch actions.') 172 | } 173 | 174 | try { 175 | isDispatching = true 176 | currentState = currentReducer(currentState, action) 177 | } finally { 178 | isDispatching = false 179 | } 180 | 181 | var listeners = currentListeners = nextListeners 182 | for (var i = 0; i < listeners.length; i++) { 183 | listeners[i]() 184 | } 185 | 186 | return action 187 | } 188 | 189 | /** 190 | * Replaces the reducer currently used by the store to calculate the state. 191 | * 192 | * You might need this if your app implements code splitting and you want to 193 | * load some of the reducers dynamically. You might also need this if you 194 | * implement a hot reloading mechanism for Redux. 195 | * 196 | * @param {Function} nextReducer The reducer for the store to use instead. 197 | * @returns {void} 198 | */ 199 | function replaceReducer(nextReducer) { 200 | if (typeof nextReducer !== 'function') { 201 | throw new Error('Expected the nextReducer to be a function.') 202 | } 203 | 204 | currentReducer = nextReducer 205 | dispatch({ type: ActionTypes.INIT }) 206 | } 207 | 208 | /** 209 | * Interoperability point for observable/reactive libraries. 210 | * @returns {observable} A minimal observable of state changes. 211 | * For more information, see the observable proposal: 212 | * https://github.com/zenparsing/es-observable 213 | */ 214 | function observable() { 215 | var outerSubscribe = subscribe 216 | return { 217 | /** 218 | * The minimal observable subscription method. 219 | * @param {Object} observer Any object that can be used as an observer. 220 | * The observer object should have a `next` method. 221 | * @returns {subscription} An object with an `unsubscribe` method that can 222 | * be used to unsubscribe the observable from the store, and prevent further 223 | * emission of values from the observable. 224 | */ 225 | subscribe(observer) { 226 | if (typeof observer !== 'object') { 227 | throw new TypeError('Expected the observer to be an object.') 228 | } 229 | 230 | function observeState() { 231 | if (observer.next) { 232 | observer.next(getState()) 233 | } 234 | } 235 | 236 | observeState() 237 | var unsubscribe = outerSubscribe(observeState) 238 | return { unsubscribe } 239 | }, 240 | 241 | [$$observable]() { 242 | return this 243 | } 244 | } 245 | } 246 | 247 | // When a store is created, an "INIT" action is dispatched so that every 248 | // reducer returns their initial state. This effectively populates 249 | // the initial state tree. 250 | dispatch({ type: ActionTypes.INIT }) 251 | 252 | return { 253 | dispatch, 254 | subscribe, 255 | getState, 256 | replaceReducer, 257 | [$$observable]: observable 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /source-code/index.js: -------------------------------------------------------------------------------- 1 | import createStore from './createStore' 2 | import combineReducers from './combineReducers' 3 | import bindActionCreators from './bindActionCreators' 4 | import applyMiddleware from './applyMiddleware' 5 | import compose from './compose' 6 | import warning from './utils/warning' 7 | 8 | /* 9 | * This is a dummy function to check if the function name has been altered by minification. 10 | * If the function has been minified and NODE_ENV !== 'production', warn the user. 11 | */ 12 | function isCrushed() {} 13 | 14 | if ( 15 | process.env.NODE_ENV !== 'production' && 16 | typeof isCrushed.name === 'string' && 17 | isCrushed.name !== 'isCrushed' 18 | ) { 19 | warning( 20 | 'You are currently using minified code outside of NODE_ENV === \'production\'. ' + 21 | 'This means that you are running a slower development build of Redux. ' + 22 | 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 23 | 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 24 | 'to ensure you have the correct code for your production build.' 25 | ) 26 | } 27 | 28 | export { 29 | createStore, 30 | combineReducers, 31 | bindActionCreators, 32 | applyMiddleware, 33 | compose 34 | } 35 | -------------------------------------------------------------------------------- /source-code/utils/warning.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints a warning in the console if it exists. 3 | * 4 | * @param {String} message The warning message. 5 | * @returns {void} 6 | */ 7 | export default function warning(message) { 8 | /* eslint-disable no-console */ 9 | if (typeof console !== 'undefined' && typeof console.error === 'function') { 10 | console.error(message) 11 | } 12 | /* eslint-enable no-console */ 13 | try { 14 | // This error was thrown as a convenience so that if you enable 15 | // "break on all exceptions" in your console, 16 | // it would pause the execution at this line. 17 | throw new Error(message) 18 | /* eslint-disable no-empty */ 19 | } catch (e) { } 20 | /* eslint-enable no-empty */ 21 | } 22 | -------------------------------------------------------------------------------- /yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /Users/reven/.nvm/versions/node/v6.10.3/bin/node /usr/local/bin/yarn add react-scripts 3 | 4 | PATH: 5 | /Users/reven/.nvm/versions/node/v6.10.3/bin:/usr/local/bin:/usr/local/bin:/opt/subversion/bin/:/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 6 | 7 | Yarn version: 8 | 0.17.0 9 | 10 | Node version: 11 | 6.10.3 12 | 13 | Platform: 14 | darwin x64 15 | 16 | npm manifest: 17 | { 18 | "name": "redux-source-code-chewing", 19 | "version": "1.0.0", 20 | "description": "the analysis of redux source code", 21 | "main": "index.js", 22 | "scripts": { 23 | "demo1": "babel-node demo1/index.js" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/pobusama/redux-source-code-chewing.git" 28 | }, 29 | "keywords": [ 30 | "redux", 31 | "sourcecode" 32 | ], 33 | "author": "pobusama@gmail.com", 34 | "license": "ISC", 35 | "bugs": { 36 | "url": "https://github.com/pobusama/redux-source-code-chewing/issues" 37 | }, 38 | "homepage": "https://github.com/pobusama/redux-source-code-chewing#readme", 39 | "dependencies": { 40 | "babel-cli": "^6.24.1", 41 | "redux": "^3.7.2" 42 | }, 43 | "devDependencies": { 44 | "babel-preset-es2015": "^6.24.1", 45 | "babel-preset-stage-0": "^6.24.1" 46 | } 47 | } 48 | 49 | yarn manifest: 50 | No manifest 51 | 52 | Lockfile: 53 | No lockfile 54 | 55 | Trace: 56 | Error: read ETIMEDOUT 57 | at exports._errnoException (util.js:1018:11) 58 | at TLSWrap.onread (net.js:568:26) 59 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | abbrev@1: 6 | version "1.1.0" 7 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" 8 | 9 | ajv@^4.9.1: 10 | version "4.11.8" 11 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" 12 | dependencies: 13 | co "^4.6.0" 14 | json-stable-stringify "^1.0.1" 15 | 16 | ansi-regex@^2.0.0: 17 | version "2.1.1" 18 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 19 | 20 | ansi-styles@^2.2.1: 21 | version "2.2.1" 22 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 23 | 24 | anymatch@^1.3.0: 25 | version "1.3.2" 26 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" 27 | dependencies: 28 | micromatch "^2.1.5" 29 | normalize-path "^2.0.0" 30 | 31 | aproba@^1.0.3: 32 | version "1.1.2" 33 | resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" 34 | 35 | are-we-there-yet@~1.1.2: 36 | version "1.1.4" 37 | resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" 38 | dependencies: 39 | delegates "^1.0.0" 40 | readable-stream "^2.0.6" 41 | 42 | arr-diff@^2.0.0: 43 | version "2.0.0" 44 | resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" 45 | dependencies: 46 | arr-flatten "^1.0.1" 47 | 48 | arr-flatten@^1.0.1: 49 | version "1.1.0" 50 | resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" 51 | 52 | array-unique@^0.2.1: 53 | version "0.2.1" 54 | resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" 55 | 56 | asn1@~0.2.3: 57 | version "0.2.3" 58 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 59 | 60 | assert-plus@1.0.0, assert-plus@^1.0.0: 61 | version "1.0.0" 62 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 63 | 64 | assert-plus@^0.2.0: 65 | version "0.2.0" 66 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" 67 | 68 | async-each@^1.0.0: 69 | version "1.0.1" 70 | resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" 71 | 72 | asynckit@^0.4.0: 73 | version "0.4.0" 74 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 75 | 76 | aws-sign2@~0.6.0: 77 | version "0.6.0" 78 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" 79 | 80 | aws4@^1.2.1: 81 | version "1.6.0" 82 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" 83 | 84 | babel-cli@^6.24.1: 85 | version "6.24.1" 86 | resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.1.tgz#207cd705bba61489b2ea41b5312341cf6aca2283" 87 | dependencies: 88 | babel-core "^6.24.1" 89 | babel-polyfill "^6.23.0" 90 | babel-register "^6.24.1" 91 | babel-runtime "^6.22.0" 92 | commander "^2.8.1" 93 | convert-source-map "^1.1.0" 94 | fs-readdir-recursive "^1.0.0" 95 | glob "^7.0.0" 96 | lodash "^4.2.0" 97 | output-file-sync "^1.1.0" 98 | path-is-absolute "^1.0.0" 99 | slash "^1.0.0" 100 | source-map "^0.5.0" 101 | v8flags "^2.0.10" 102 | optionalDependencies: 103 | chokidar "^1.6.1" 104 | 105 | babel-code-frame@^6.22.0: 106 | version "6.22.0" 107 | resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" 108 | dependencies: 109 | chalk "^1.1.0" 110 | esutils "^2.0.2" 111 | js-tokens "^3.0.0" 112 | 113 | babel-core@^6.24.1: 114 | version "6.25.0" 115 | resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.25.0.tgz#7dd42b0463c742e9d5296deb3ec67a9322dad729" 116 | dependencies: 117 | babel-code-frame "^6.22.0" 118 | babel-generator "^6.25.0" 119 | babel-helpers "^6.24.1" 120 | babel-messages "^6.23.0" 121 | babel-register "^6.24.1" 122 | babel-runtime "^6.22.0" 123 | babel-template "^6.25.0" 124 | babel-traverse "^6.25.0" 125 | babel-types "^6.25.0" 126 | babylon "^6.17.2" 127 | convert-source-map "^1.1.0" 128 | debug "^2.1.1" 129 | json5 "^0.5.0" 130 | lodash "^4.2.0" 131 | minimatch "^3.0.2" 132 | path-is-absolute "^1.0.0" 133 | private "^0.1.6" 134 | slash "^1.0.0" 135 | source-map "^0.5.0" 136 | 137 | babel-generator@^6.25.0: 138 | version "6.25.0" 139 | resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.25.0.tgz#33a1af70d5f2890aeb465a4a7793c1df6a9ea9fc" 140 | dependencies: 141 | babel-messages "^6.23.0" 142 | babel-runtime "^6.22.0" 143 | babel-types "^6.25.0" 144 | detect-indent "^4.0.0" 145 | jsesc "^1.3.0" 146 | lodash "^4.2.0" 147 | source-map "^0.5.0" 148 | trim-right "^1.0.1" 149 | 150 | babel-helper-bindify-decorators@^6.24.1: 151 | version "6.24.1" 152 | resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330" 153 | dependencies: 154 | babel-runtime "^6.22.0" 155 | babel-traverse "^6.24.1" 156 | babel-types "^6.24.1" 157 | 158 | babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: 159 | version "6.24.1" 160 | resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" 161 | dependencies: 162 | babel-helper-explode-assignable-expression "^6.24.1" 163 | babel-runtime "^6.22.0" 164 | babel-types "^6.24.1" 165 | 166 | babel-helper-call-delegate@^6.24.1: 167 | version "6.24.1" 168 | resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" 169 | dependencies: 170 | babel-helper-hoist-variables "^6.24.1" 171 | babel-runtime "^6.22.0" 172 | babel-traverse "^6.24.1" 173 | babel-types "^6.24.1" 174 | 175 | babel-helper-define-map@^6.24.1: 176 | version "6.24.1" 177 | resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080" 178 | dependencies: 179 | babel-helper-function-name "^6.24.1" 180 | babel-runtime "^6.22.0" 181 | babel-types "^6.24.1" 182 | lodash "^4.2.0" 183 | 184 | babel-helper-explode-assignable-expression@^6.24.1: 185 | version "6.24.1" 186 | resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" 187 | dependencies: 188 | babel-runtime "^6.22.0" 189 | babel-traverse "^6.24.1" 190 | babel-types "^6.24.1" 191 | 192 | babel-helper-explode-class@^6.24.1: 193 | version "6.24.1" 194 | resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb" 195 | dependencies: 196 | babel-helper-bindify-decorators "^6.24.1" 197 | babel-runtime "^6.22.0" 198 | babel-traverse "^6.24.1" 199 | babel-types "^6.24.1" 200 | 201 | babel-helper-function-name@^6.24.1: 202 | version "6.24.1" 203 | resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" 204 | dependencies: 205 | babel-helper-get-function-arity "^6.24.1" 206 | babel-runtime "^6.22.0" 207 | babel-template "^6.24.1" 208 | babel-traverse "^6.24.1" 209 | babel-types "^6.24.1" 210 | 211 | babel-helper-get-function-arity@^6.24.1: 212 | version "6.24.1" 213 | resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" 214 | dependencies: 215 | babel-runtime "^6.22.0" 216 | babel-types "^6.24.1" 217 | 218 | babel-helper-hoist-variables@^6.24.1: 219 | version "6.24.1" 220 | resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" 221 | dependencies: 222 | babel-runtime "^6.22.0" 223 | babel-types "^6.24.1" 224 | 225 | babel-helper-optimise-call-expression@^6.24.1: 226 | version "6.24.1" 227 | resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" 228 | dependencies: 229 | babel-runtime "^6.22.0" 230 | babel-types "^6.24.1" 231 | 232 | babel-helper-regex@^6.24.1: 233 | version "6.24.1" 234 | resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz#d36e22fab1008d79d88648e32116868128456ce8" 235 | dependencies: 236 | babel-runtime "^6.22.0" 237 | babel-types "^6.24.1" 238 | lodash "^4.2.0" 239 | 240 | babel-helper-remap-async-to-generator@^6.24.1: 241 | version "6.24.1" 242 | resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" 243 | dependencies: 244 | babel-helper-function-name "^6.24.1" 245 | babel-runtime "^6.22.0" 246 | babel-template "^6.24.1" 247 | babel-traverse "^6.24.1" 248 | babel-types "^6.24.1" 249 | 250 | babel-helper-replace-supers@^6.24.1: 251 | version "6.24.1" 252 | resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" 253 | dependencies: 254 | babel-helper-optimise-call-expression "^6.24.1" 255 | babel-messages "^6.23.0" 256 | babel-runtime "^6.22.0" 257 | babel-template "^6.24.1" 258 | babel-traverse "^6.24.1" 259 | babel-types "^6.24.1" 260 | 261 | babel-helpers@^6.24.1: 262 | version "6.24.1" 263 | resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" 264 | dependencies: 265 | babel-runtime "^6.22.0" 266 | babel-template "^6.24.1" 267 | 268 | babel-messages@^6.23.0: 269 | version "6.23.0" 270 | resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" 271 | dependencies: 272 | babel-runtime "^6.22.0" 273 | 274 | babel-plugin-check-es2015-constants@^6.22.0: 275 | version "6.22.0" 276 | resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" 277 | dependencies: 278 | babel-runtime "^6.22.0" 279 | 280 | babel-plugin-syntax-async-functions@^6.8.0: 281 | version "6.13.0" 282 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" 283 | 284 | babel-plugin-syntax-async-generators@^6.5.0: 285 | version "6.13.0" 286 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" 287 | 288 | babel-plugin-syntax-class-constructor-call@^6.18.0: 289 | version "6.18.0" 290 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" 291 | 292 | babel-plugin-syntax-class-properties@^6.8.0: 293 | version "6.13.0" 294 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" 295 | 296 | babel-plugin-syntax-decorators@^6.13.0: 297 | version "6.13.0" 298 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" 299 | 300 | babel-plugin-syntax-do-expressions@^6.8.0: 301 | version "6.13.0" 302 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz#5747756139aa26d390d09410b03744ba07e4796d" 303 | 304 | babel-plugin-syntax-dynamic-import@^6.18.0: 305 | version "6.18.0" 306 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" 307 | 308 | babel-plugin-syntax-exponentiation-operator@^6.8.0: 309 | version "6.13.0" 310 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" 311 | 312 | babel-plugin-syntax-export-extensions@^6.8.0: 313 | version "6.13.0" 314 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" 315 | 316 | babel-plugin-syntax-function-bind@^6.8.0: 317 | version "6.13.0" 318 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46" 319 | 320 | babel-plugin-syntax-object-rest-spread@^6.8.0: 321 | version "6.13.0" 322 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" 323 | 324 | babel-plugin-syntax-trailing-function-commas@^6.22.0: 325 | version "6.22.0" 326 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" 327 | 328 | babel-plugin-transform-async-generator-functions@^6.24.1: 329 | version "6.24.1" 330 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db" 331 | dependencies: 332 | babel-helper-remap-async-to-generator "^6.24.1" 333 | babel-plugin-syntax-async-generators "^6.5.0" 334 | babel-runtime "^6.22.0" 335 | 336 | babel-plugin-transform-async-to-generator@^6.24.1: 337 | version "6.24.1" 338 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" 339 | dependencies: 340 | babel-helper-remap-async-to-generator "^6.24.1" 341 | babel-plugin-syntax-async-functions "^6.8.0" 342 | babel-runtime "^6.22.0" 343 | 344 | babel-plugin-transform-class-constructor-call@^6.24.1: 345 | version "6.24.1" 346 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9" 347 | dependencies: 348 | babel-plugin-syntax-class-constructor-call "^6.18.0" 349 | babel-runtime "^6.22.0" 350 | babel-template "^6.24.1" 351 | 352 | babel-plugin-transform-class-properties@^6.24.1: 353 | version "6.24.1" 354 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" 355 | dependencies: 356 | babel-helper-function-name "^6.24.1" 357 | babel-plugin-syntax-class-properties "^6.8.0" 358 | babel-runtime "^6.22.0" 359 | babel-template "^6.24.1" 360 | 361 | babel-plugin-transform-decorators@^6.24.1: 362 | version "6.24.1" 363 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d" 364 | dependencies: 365 | babel-helper-explode-class "^6.24.1" 366 | babel-plugin-syntax-decorators "^6.13.0" 367 | babel-runtime "^6.22.0" 368 | babel-template "^6.24.1" 369 | babel-types "^6.24.1" 370 | 371 | babel-plugin-transform-do-expressions@^6.22.0: 372 | version "6.22.0" 373 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz#28ccaf92812d949c2cd1281f690c8fdc468ae9bb" 374 | dependencies: 375 | babel-plugin-syntax-do-expressions "^6.8.0" 376 | babel-runtime "^6.22.0" 377 | 378 | babel-plugin-transform-es2015-arrow-functions@^6.22.0: 379 | version "6.22.0" 380 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" 381 | dependencies: 382 | babel-runtime "^6.22.0" 383 | 384 | babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: 385 | version "6.22.0" 386 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" 387 | dependencies: 388 | babel-runtime "^6.22.0" 389 | 390 | babel-plugin-transform-es2015-block-scoping@^6.24.1: 391 | version "6.24.1" 392 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576" 393 | dependencies: 394 | babel-runtime "^6.22.0" 395 | babel-template "^6.24.1" 396 | babel-traverse "^6.24.1" 397 | babel-types "^6.24.1" 398 | lodash "^4.2.0" 399 | 400 | babel-plugin-transform-es2015-classes@^6.24.1: 401 | version "6.24.1" 402 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" 403 | dependencies: 404 | babel-helper-define-map "^6.24.1" 405 | babel-helper-function-name "^6.24.1" 406 | babel-helper-optimise-call-expression "^6.24.1" 407 | babel-helper-replace-supers "^6.24.1" 408 | babel-messages "^6.23.0" 409 | babel-runtime "^6.22.0" 410 | babel-template "^6.24.1" 411 | babel-traverse "^6.24.1" 412 | babel-types "^6.24.1" 413 | 414 | babel-plugin-transform-es2015-computed-properties@^6.24.1: 415 | version "6.24.1" 416 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" 417 | dependencies: 418 | babel-runtime "^6.22.0" 419 | babel-template "^6.24.1" 420 | 421 | babel-plugin-transform-es2015-destructuring@^6.22.0: 422 | version "6.23.0" 423 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" 424 | dependencies: 425 | babel-runtime "^6.22.0" 426 | 427 | babel-plugin-transform-es2015-duplicate-keys@^6.24.1: 428 | version "6.24.1" 429 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" 430 | dependencies: 431 | babel-runtime "^6.22.0" 432 | babel-types "^6.24.1" 433 | 434 | babel-plugin-transform-es2015-for-of@^6.22.0: 435 | version "6.23.0" 436 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" 437 | dependencies: 438 | babel-runtime "^6.22.0" 439 | 440 | babel-plugin-transform-es2015-function-name@^6.24.1: 441 | version "6.24.1" 442 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" 443 | dependencies: 444 | babel-helper-function-name "^6.24.1" 445 | babel-runtime "^6.22.0" 446 | babel-types "^6.24.1" 447 | 448 | babel-plugin-transform-es2015-literals@^6.22.0: 449 | version "6.22.0" 450 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" 451 | dependencies: 452 | babel-runtime "^6.22.0" 453 | 454 | babel-plugin-transform-es2015-modules-amd@^6.24.1: 455 | version "6.24.1" 456 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" 457 | dependencies: 458 | babel-plugin-transform-es2015-modules-commonjs "^6.24.1" 459 | babel-runtime "^6.22.0" 460 | babel-template "^6.24.1" 461 | 462 | babel-plugin-transform-es2015-modules-commonjs@^6.24.1: 463 | version "6.24.1" 464 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe" 465 | dependencies: 466 | babel-plugin-transform-strict-mode "^6.24.1" 467 | babel-runtime "^6.22.0" 468 | babel-template "^6.24.1" 469 | babel-types "^6.24.1" 470 | 471 | babel-plugin-transform-es2015-modules-systemjs@^6.24.1: 472 | version "6.24.1" 473 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" 474 | dependencies: 475 | babel-helper-hoist-variables "^6.24.1" 476 | babel-runtime "^6.22.0" 477 | babel-template "^6.24.1" 478 | 479 | babel-plugin-transform-es2015-modules-umd@^6.24.1: 480 | version "6.24.1" 481 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" 482 | dependencies: 483 | babel-plugin-transform-es2015-modules-amd "^6.24.1" 484 | babel-runtime "^6.22.0" 485 | babel-template "^6.24.1" 486 | 487 | babel-plugin-transform-es2015-object-super@^6.24.1: 488 | version "6.24.1" 489 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" 490 | dependencies: 491 | babel-helper-replace-supers "^6.24.1" 492 | babel-runtime "^6.22.0" 493 | 494 | babel-plugin-transform-es2015-parameters@^6.24.1: 495 | version "6.24.1" 496 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" 497 | dependencies: 498 | babel-helper-call-delegate "^6.24.1" 499 | babel-helper-get-function-arity "^6.24.1" 500 | babel-runtime "^6.22.0" 501 | babel-template "^6.24.1" 502 | babel-traverse "^6.24.1" 503 | babel-types "^6.24.1" 504 | 505 | babel-plugin-transform-es2015-shorthand-properties@^6.24.1: 506 | version "6.24.1" 507 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" 508 | dependencies: 509 | babel-runtime "^6.22.0" 510 | babel-types "^6.24.1" 511 | 512 | babel-plugin-transform-es2015-spread@^6.22.0: 513 | version "6.22.0" 514 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" 515 | dependencies: 516 | babel-runtime "^6.22.0" 517 | 518 | babel-plugin-transform-es2015-sticky-regex@^6.24.1: 519 | version "6.24.1" 520 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" 521 | dependencies: 522 | babel-helper-regex "^6.24.1" 523 | babel-runtime "^6.22.0" 524 | babel-types "^6.24.1" 525 | 526 | babel-plugin-transform-es2015-template-literals@^6.22.0: 527 | version "6.22.0" 528 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" 529 | dependencies: 530 | babel-runtime "^6.22.0" 531 | 532 | babel-plugin-transform-es2015-typeof-symbol@^6.22.0: 533 | version "6.23.0" 534 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" 535 | dependencies: 536 | babel-runtime "^6.22.0" 537 | 538 | babel-plugin-transform-es2015-unicode-regex@^6.24.1: 539 | version "6.24.1" 540 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" 541 | dependencies: 542 | babel-helper-regex "^6.24.1" 543 | babel-runtime "^6.22.0" 544 | regexpu-core "^2.0.0" 545 | 546 | babel-plugin-transform-exponentiation-operator@^6.24.1: 547 | version "6.24.1" 548 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" 549 | dependencies: 550 | babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" 551 | babel-plugin-syntax-exponentiation-operator "^6.8.0" 552 | babel-runtime "^6.22.0" 553 | 554 | babel-plugin-transform-export-extensions@^6.22.0: 555 | version "6.22.0" 556 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653" 557 | dependencies: 558 | babel-plugin-syntax-export-extensions "^6.8.0" 559 | babel-runtime "^6.22.0" 560 | 561 | babel-plugin-transform-function-bind@^6.22.0: 562 | version "6.22.0" 563 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz#c6fb8e96ac296a310b8cf8ea401462407ddf6a97" 564 | dependencies: 565 | babel-plugin-syntax-function-bind "^6.8.0" 566 | babel-runtime "^6.22.0" 567 | 568 | babel-plugin-transform-object-rest-spread@^6.22.0: 569 | version "6.23.0" 570 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" 571 | dependencies: 572 | babel-plugin-syntax-object-rest-spread "^6.8.0" 573 | babel-runtime "^6.22.0" 574 | 575 | babel-plugin-transform-regenerator@^6.24.1: 576 | version "6.24.1" 577 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418" 578 | dependencies: 579 | regenerator-transform "0.9.11" 580 | 581 | babel-plugin-transform-strict-mode@^6.24.1: 582 | version "6.24.1" 583 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" 584 | dependencies: 585 | babel-runtime "^6.22.0" 586 | babel-types "^6.24.1" 587 | 588 | babel-polyfill@^6.23.0: 589 | version "6.23.0" 590 | resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" 591 | dependencies: 592 | babel-runtime "^6.22.0" 593 | core-js "^2.4.0" 594 | regenerator-runtime "^0.10.0" 595 | 596 | babel-preset-es2015@^6.24.1: 597 | version "6.24.1" 598 | resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" 599 | dependencies: 600 | babel-plugin-check-es2015-constants "^6.22.0" 601 | babel-plugin-transform-es2015-arrow-functions "^6.22.0" 602 | babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" 603 | babel-plugin-transform-es2015-block-scoping "^6.24.1" 604 | babel-plugin-transform-es2015-classes "^6.24.1" 605 | babel-plugin-transform-es2015-computed-properties "^6.24.1" 606 | babel-plugin-transform-es2015-destructuring "^6.22.0" 607 | babel-plugin-transform-es2015-duplicate-keys "^6.24.1" 608 | babel-plugin-transform-es2015-for-of "^6.22.0" 609 | babel-plugin-transform-es2015-function-name "^6.24.1" 610 | babel-plugin-transform-es2015-literals "^6.22.0" 611 | babel-plugin-transform-es2015-modules-amd "^6.24.1" 612 | babel-plugin-transform-es2015-modules-commonjs "^6.24.1" 613 | babel-plugin-transform-es2015-modules-systemjs "^6.24.1" 614 | babel-plugin-transform-es2015-modules-umd "^6.24.1" 615 | babel-plugin-transform-es2015-object-super "^6.24.1" 616 | babel-plugin-transform-es2015-parameters "^6.24.1" 617 | babel-plugin-transform-es2015-shorthand-properties "^6.24.1" 618 | babel-plugin-transform-es2015-spread "^6.22.0" 619 | babel-plugin-transform-es2015-sticky-regex "^6.24.1" 620 | babel-plugin-transform-es2015-template-literals "^6.22.0" 621 | babel-plugin-transform-es2015-typeof-symbol "^6.22.0" 622 | babel-plugin-transform-es2015-unicode-regex "^6.24.1" 623 | babel-plugin-transform-regenerator "^6.24.1" 624 | 625 | babel-preset-stage-0@^6.24.1: 626 | version "6.24.1" 627 | resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz#5642d15042f91384d7e5af8bc88b1db95b039e6a" 628 | dependencies: 629 | babel-plugin-transform-do-expressions "^6.22.0" 630 | babel-plugin-transform-function-bind "^6.22.0" 631 | babel-preset-stage-1 "^6.24.1" 632 | 633 | babel-preset-stage-1@^6.24.1: 634 | version "6.24.1" 635 | resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0" 636 | dependencies: 637 | babel-plugin-transform-class-constructor-call "^6.24.1" 638 | babel-plugin-transform-export-extensions "^6.22.0" 639 | babel-preset-stage-2 "^6.24.1" 640 | 641 | babel-preset-stage-2@^6.24.1: 642 | version "6.24.1" 643 | resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" 644 | dependencies: 645 | babel-plugin-syntax-dynamic-import "^6.18.0" 646 | babel-plugin-transform-class-properties "^6.24.1" 647 | babel-plugin-transform-decorators "^6.24.1" 648 | babel-preset-stage-3 "^6.24.1" 649 | 650 | babel-preset-stage-3@^6.24.1: 651 | version "6.24.1" 652 | resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395" 653 | dependencies: 654 | babel-plugin-syntax-trailing-function-commas "^6.22.0" 655 | babel-plugin-transform-async-generator-functions "^6.24.1" 656 | babel-plugin-transform-async-to-generator "^6.24.1" 657 | babel-plugin-transform-exponentiation-operator "^6.24.1" 658 | babel-plugin-transform-object-rest-spread "^6.22.0" 659 | 660 | babel-register@^6.24.1: 661 | version "6.24.1" 662 | resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" 663 | dependencies: 664 | babel-core "^6.24.1" 665 | babel-runtime "^6.22.0" 666 | core-js "^2.4.0" 667 | home-or-tmp "^2.0.0" 668 | lodash "^4.2.0" 669 | mkdirp "^0.5.1" 670 | source-map-support "^0.4.2" 671 | 672 | babel-runtime@^6.18.0, babel-runtime@^6.22.0: 673 | version "6.25.0" 674 | resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.25.0.tgz#33b98eaa5d482bb01a8d1aa6b437ad2b01aec41c" 675 | dependencies: 676 | core-js "^2.4.0" 677 | regenerator-runtime "^0.10.0" 678 | 679 | babel-template@^6.24.1, babel-template@^6.25.0: 680 | version "6.25.0" 681 | resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071" 682 | dependencies: 683 | babel-runtime "^6.22.0" 684 | babel-traverse "^6.25.0" 685 | babel-types "^6.25.0" 686 | babylon "^6.17.2" 687 | lodash "^4.2.0" 688 | 689 | babel-traverse@^6.24.1, babel-traverse@^6.25.0: 690 | version "6.25.0" 691 | resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.25.0.tgz#2257497e2fcd19b89edc13c4c91381f9512496f1" 692 | dependencies: 693 | babel-code-frame "^6.22.0" 694 | babel-messages "^6.23.0" 695 | babel-runtime "^6.22.0" 696 | babel-types "^6.25.0" 697 | babylon "^6.17.2" 698 | debug "^2.2.0" 699 | globals "^9.0.0" 700 | invariant "^2.2.0" 701 | lodash "^4.2.0" 702 | 703 | babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.25.0: 704 | version "6.25.0" 705 | resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.25.0.tgz#70afb248d5660e5d18f811d91c8303b54134a18e" 706 | dependencies: 707 | babel-runtime "^6.22.0" 708 | esutils "^2.0.2" 709 | lodash "^4.2.0" 710 | to-fast-properties "^1.0.1" 711 | 712 | babylon@^6.17.2: 713 | version "6.17.4" 714 | resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" 715 | 716 | balanced-match@^1.0.0: 717 | version "1.0.0" 718 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 719 | 720 | bcrypt-pbkdf@^1.0.0: 721 | version "1.0.1" 722 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" 723 | dependencies: 724 | tweetnacl "^0.14.3" 725 | 726 | binary-extensions@^1.0.0: 727 | version "1.9.0" 728 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.9.0.tgz#66506c16ce6f4d6928a5b3cd6a33ca41e941e37b" 729 | 730 | block-stream@*: 731 | version "0.0.9" 732 | resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" 733 | dependencies: 734 | inherits "~2.0.0" 735 | 736 | boom@2.x.x: 737 | version "2.10.1" 738 | resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" 739 | dependencies: 740 | hoek "2.x.x" 741 | 742 | brace-expansion@^1.1.7: 743 | version "1.1.8" 744 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 745 | dependencies: 746 | balanced-match "^1.0.0" 747 | concat-map "0.0.1" 748 | 749 | braces@^1.8.2: 750 | version "1.8.5" 751 | resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" 752 | dependencies: 753 | expand-range "^1.8.1" 754 | preserve "^0.2.0" 755 | repeat-element "^1.1.2" 756 | 757 | caseless@~0.12.0: 758 | version "0.12.0" 759 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 760 | 761 | chalk@^1.1.0: 762 | version "1.1.3" 763 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 764 | dependencies: 765 | ansi-styles "^2.2.1" 766 | escape-string-regexp "^1.0.2" 767 | has-ansi "^2.0.0" 768 | strip-ansi "^3.0.0" 769 | supports-color "^2.0.0" 770 | 771 | chokidar@^1.6.1: 772 | version "1.7.0" 773 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" 774 | dependencies: 775 | anymatch "^1.3.0" 776 | async-each "^1.0.0" 777 | glob-parent "^2.0.0" 778 | inherits "^2.0.1" 779 | is-binary-path "^1.0.0" 780 | is-glob "^2.0.0" 781 | path-is-absolute "^1.0.0" 782 | readdirp "^2.0.0" 783 | optionalDependencies: 784 | fsevents "^1.0.0" 785 | 786 | co@^4.6.0: 787 | version "4.6.0" 788 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 789 | 790 | code-point-at@^1.0.0: 791 | version "1.1.0" 792 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 793 | 794 | combined-stream@^1.0.5, combined-stream@~1.0.5: 795 | version "1.0.5" 796 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 797 | dependencies: 798 | delayed-stream "~1.0.0" 799 | 800 | commander@^2.8.1: 801 | version "2.11.0" 802 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 803 | 804 | concat-map@0.0.1: 805 | version "0.0.1" 806 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 807 | 808 | console-control-strings@^1.0.0, console-control-strings@~1.1.0: 809 | version "1.1.0" 810 | resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 811 | 812 | convert-source-map@^1.1.0: 813 | version "1.5.0" 814 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" 815 | 816 | core-js@^2.4.0: 817 | version "2.5.0" 818 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" 819 | 820 | core-util-is@1.0.2, core-util-is@~1.0.0: 821 | version "1.0.2" 822 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 823 | 824 | cryptiles@2.x.x: 825 | version "2.0.5" 826 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" 827 | dependencies: 828 | boom "2.x.x" 829 | 830 | dashdash@^1.12.0: 831 | version "1.14.1" 832 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 833 | dependencies: 834 | assert-plus "^1.0.0" 835 | 836 | debug@^2.1.1, debug@^2.2.0: 837 | version "2.6.8" 838 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" 839 | dependencies: 840 | ms "2.0.0" 841 | 842 | deep-extend@~0.4.0: 843 | version "0.4.2" 844 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" 845 | 846 | delayed-stream@~1.0.0: 847 | version "1.0.0" 848 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 849 | 850 | delegates@^1.0.0: 851 | version "1.0.0" 852 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 853 | 854 | detect-indent@^4.0.0: 855 | version "4.0.0" 856 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" 857 | dependencies: 858 | repeating "^2.0.0" 859 | 860 | ecc-jsbn@~0.1.1: 861 | version "0.1.1" 862 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" 863 | dependencies: 864 | jsbn "~0.1.0" 865 | 866 | escape-string-regexp@^1.0.2: 867 | version "1.0.5" 868 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 869 | 870 | esutils@^2.0.2: 871 | version "2.0.2" 872 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 873 | 874 | expand-brackets@^0.1.4: 875 | version "0.1.5" 876 | resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" 877 | dependencies: 878 | is-posix-bracket "^0.1.0" 879 | 880 | expand-range@^1.8.1: 881 | version "1.8.2" 882 | resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" 883 | dependencies: 884 | fill-range "^2.1.0" 885 | 886 | extend@~3.0.0: 887 | version "3.0.1" 888 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" 889 | 890 | extglob@^0.3.1: 891 | version "0.3.2" 892 | resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" 893 | dependencies: 894 | is-extglob "^1.0.0" 895 | 896 | extsprintf@1.3.0, extsprintf@^1.2.0: 897 | version "1.3.0" 898 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 899 | 900 | filename-regex@^2.0.0: 901 | version "2.0.1" 902 | resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" 903 | 904 | fill-range@^2.1.0: 905 | version "2.2.3" 906 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" 907 | dependencies: 908 | is-number "^2.1.0" 909 | isobject "^2.0.0" 910 | randomatic "^1.1.3" 911 | repeat-element "^1.1.2" 912 | repeat-string "^1.5.2" 913 | 914 | for-in@^1.0.1: 915 | version "1.0.2" 916 | resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" 917 | 918 | for-own@^0.1.4: 919 | version "0.1.5" 920 | resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" 921 | dependencies: 922 | for-in "^1.0.1" 923 | 924 | forever-agent@~0.6.1: 925 | version "0.6.1" 926 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 927 | 928 | form-data@~2.1.1: 929 | version "2.1.4" 930 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" 931 | dependencies: 932 | asynckit "^0.4.0" 933 | combined-stream "^1.0.5" 934 | mime-types "^2.1.12" 935 | 936 | fs-readdir-recursive@^1.0.0: 937 | version "1.0.0" 938 | resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" 939 | 940 | fs.realpath@^1.0.0: 941 | version "1.0.0" 942 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 943 | 944 | fsevents@^1.0.0: 945 | version "1.1.2" 946 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" 947 | dependencies: 948 | nan "^2.3.0" 949 | node-pre-gyp "^0.6.36" 950 | 951 | fstream-ignore@^1.0.5: 952 | version "1.0.5" 953 | resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" 954 | dependencies: 955 | fstream "^1.0.0" 956 | inherits "2" 957 | minimatch "^3.0.0" 958 | 959 | fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: 960 | version "1.0.11" 961 | resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" 962 | dependencies: 963 | graceful-fs "^4.1.2" 964 | inherits "~2.0.0" 965 | mkdirp ">=0.5 0" 966 | rimraf "2" 967 | 968 | gauge@~2.7.3: 969 | version "2.7.4" 970 | resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" 971 | dependencies: 972 | aproba "^1.0.3" 973 | console-control-strings "^1.0.0" 974 | has-unicode "^2.0.0" 975 | object-assign "^4.1.0" 976 | signal-exit "^3.0.0" 977 | string-width "^1.0.1" 978 | strip-ansi "^3.0.1" 979 | wide-align "^1.1.0" 980 | 981 | getpass@^0.1.1: 982 | version "0.1.7" 983 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 984 | dependencies: 985 | assert-plus "^1.0.0" 986 | 987 | glob-base@^0.3.0: 988 | version "0.3.0" 989 | resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" 990 | dependencies: 991 | glob-parent "^2.0.0" 992 | is-glob "^2.0.0" 993 | 994 | glob-parent@^2.0.0: 995 | version "2.0.0" 996 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" 997 | dependencies: 998 | is-glob "^2.0.0" 999 | 1000 | glob@^7.0.0, glob@^7.0.5: 1001 | version "7.1.2" 1002 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 1003 | dependencies: 1004 | fs.realpath "^1.0.0" 1005 | inflight "^1.0.4" 1006 | inherits "2" 1007 | minimatch "^3.0.4" 1008 | once "^1.3.0" 1009 | path-is-absolute "^1.0.0" 1010 | 1011 | globals@^9.0.0: 1012 | version "9.18.0" 1013 | resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" 1014 | 1015 | graceful-fs@^4.1.2, graceful-fs@^4.1.4: 1016 | version "4.1.11" 1017 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 1018 | 1019 | har-schema@^1.0.5: 1020 | version "1.0.5" 1021 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" 1022 | 1023 | har-validator@~4.2.1: 1024 | version "4.2.1" 1025 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" 1026 | dependencies: 1027 | ajv "^4.9.1" 1028 | har-schema "^1.0.5" 1029 | 1030 | has-ansi@^2.0.0: 1031 | version "2.0.0" 1032 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 1033 | dependencies: 1034 | ansi-regex "^2.0.0" 1035 | 1036 | has-unicode@^2.0.0: 1037 | version "2.0.1" 1038 | resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 1039 | 1040 | hawk@~3.1.3: 1041 | version "3.1.3" 1042 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" 1043 | dependencies: 1044 | boom "2.x.x" 1045 | cryptiles "2.x.x" 1046 | hoek "2.x.x" 1047 | sntp "1.x.x" 1048 | 1049 | hoek@2.x.x: 1050 | version "2.16.3" 1051 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 1052 | 1053 | home-or-tmp@^2.0.0: 1054 | version "2.0.0" 1055 | resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" 1056 | dependencies: 1057 | os-homedir "^1.0.0" 1058 | os-tmpdir "^1.0.1" 1059 | 1060 | http-signature@~1.1.0: 1061 | version "1.1.1" 1062 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" 1063 | dependencies: 1064 | assert-plus "^0.2.0" 1065 | jsprim "^1.2.2" 1066 | sshpk "^1.7.0" 1067 | 1068 | inflight@^1.0.4: 1069 | version "1.0.6" 1070 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 1071 | dependencies: 1072 | once "^1.3.0" 1073 | wrappy "1" 1074 | 1075 | inherits@2, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.3: 1076 | version "2.0.3" 1077 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 1078 | 1079 | ini@~1.3.0: 1080 | version "1.3.4" 1081 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" 1082 | 1083 | invariant@^2.2.0: 1084 | version "2.2.2" 1085 | resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" 1086 | dependencies: 1087 | loose-envify "^1.0.0" 1088 | 1089 | is-binary-path@^1.0.0: 1090 | version "1.0.1" 1091 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" 1092 | dependencies: 1093 | binary-extensions "^1.0.0" 1094 | 1095 | is-buffer@^1.1.5: 1096 | version "1.1.5" 1097 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" 1098 | 1099 | is-dotfile@^1.0.0: 1100 | version "1.0.3" 1101 | resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" 1102 | 1103 | is-equal-shallow@^0.1.3: 1104 | version "0.1.3" 1105 | resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" 1106 | dependencies: 1107 | is-primitive "^2.0.0" 1108 | 1109 | is-extendable@^0.1.1: 1110 | version "0.1.1" 1111 | resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" 1112 | 1113 | is-extglob@^1.0.0: 1114 | version "1.0.0" 1115 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" 1116 | 1117 | is-finite@^1.0.0: 1118 | version "1.0.2" 1119 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" 1120 | dependencies: 1121 | number-is-nan "^1.0.0" 1122 | 1123 | is-fullwidth-code-point@^1.0.0: 1124 | version "1.0.0" 1125 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 1126 | dependencies: 1127 | number-is-nan "^1.0.0" 1128 | 1129 | is-glob@^2.0.0, is-glob@^2.0.1: 1130 | version "2.0.1" 1131 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" 1132 | dependencies: 1133 | is-extglob "^1.0.0" 1134 | 1135 | is-number@^2.1.0: 1136 | version "2.1.0" 1137 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" 1138 | dependencies: 1139 | kind-of "^3.0.2" 1140 | 1141 | is-number@^3.0.0: 1142 | version "3.0.0" 1143 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" 1144 | dependencies: 1145 | kind-of "^3.0.2" 1146 | 1147 | is-posix-bracket@^0.1.0: 1148 | version "0.1.1" 1149 | resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" 1150 | 1151 | is-primitive@^2.0.0: 1152 | version "2.0.0" 1153 | resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" 1154 | 1155 | is-typedarray@~1.0.0: 1156 | version "1.0.0" 1157 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 1158 | 1159 | isarray@1.0.0, isarray@~1.0.0: 1160 | version "1.0.0" 1161 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 1162 | 1163 | isobject@^2.0.0: 1164 | version "2.1.0" 1165 | resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" 1166 | dependencies: 1167 | isarray "1.0.0" 1168 | 1169 | isstream@~0.1.2: 1170 | version "0.1.2" 1171 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 1172 | 1173 | js-tokens@^3.0.0: 1174 | version "3.0.2" 1175 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 1176 | 1177 | jsbn@~0.1.0: 1178 | version "0.1.1" 1179 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 1180 | 1181 | jsesc@^1.3.0: 1182 | version "1.3.0" 1183 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" 1184 | 1185 | jsesc@~0.5.0: 1186 | version "0.5.0" 1187 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" 1188 | 1189 | json-schema@0.2.3: 1190 | version "0.2.3" 1191 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 1192 | 1193 | json-stable-stringify@^1.0.1: 1194 | version "1.0.1" 1195 | resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" 1196 | dependencies: 1197 | jsonify "~0.0.0" 1198 | 1199 | json-stringify-safe@~5.0.1: 1200 | version "5.0.1" 1201 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 1202 | 1203 | json5@^0.5.0: 1204 | version "0.5.1" 1205 | resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" 1206 | 1207 | jsonify@~0.0.0: 1208 | version "0.0.0" 1209 | resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 1210 | 1211 | jsprim@^1.2.2: 1212 | version "1.4.1" 1213 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 1214 | dependencies: 1215 | assert-plus "1.0.0" 1216 | extsprintf "1.3.0" 1217 | json-schema "0.2.3" 1218 | verror "1.10.0" 1219 | 1220 | kind-of@^3.0.2: 1221 | version "3.2.2" 1222 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" 1223 | dependencies: 1224 | is-buffer "^1.1.5" 1225 | 1226 | kind-of@^4.0.0: 1227 | version "4.0.0" 1228 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" 1229 | dependencies: 1230 | is-buffer "^1.1.5" 1231 | 1232 | lodash-es@^4.2.1: 1233 | version "4.17.4" 1234 | resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" 1235 | 1236 | lodash@^4.2.0, lodash@^4.2.1: 1237 | version "4.17.4" 1238 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 1239 | 1240 | loose-envify@^1.0.0, loose-envify@^1.1.0: 1241 | version "1.3.1" 1242 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" 1243 | dependencies: 1244 | js-tokens "^3.0.0" 1245 | 1246 | micromatch@^2.1.5: 1247 | version "2.3.11" 1248 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" 1249 | dependencies: 1250 | arr-diff "^2.0.0" 1251 | array-unique "^0.2.1" 1252 | braces "^1.8.2" 1253 | expand-brackets "^0.1.4" 1254 | extglob "^0.3.1" 1255 | filename-regex "^2.0.0" 1256 | is-extglob "^1.0.0" 1257 | is-glob "^2.0.1" 1258 | kind-of "^3.0.2" 1259 | normalize-path "^2.0.1" 1260 | object.omit "^2.0.0" 1261 | parse-glob "^3.0.4" 1262 | regex-cache "^0.4.2" 1263 | 1264 | mime-db@~1.29.0: 1265 | version "1.29.0" 1266 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" 1267 | 1268 | mime-types@^2.1.12, mime-types@~2.1.7: 1269 | version "2.1.16" 1270 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" 1271 | dependencies: 1272 | mime-db "~1.29.0" 1273 | 1274 | minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: 1275 | version "3.0.4" 1276 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 1277 | dependencies: 1278 | brace-expansion "^1.1.7" 1279 | 1280 | minimist@0.0.8: 1281 | version "0.0.8" 1282 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 1283 | 1284 | minimist@^1.2.0: 1285 | version "1.2.0" 1286 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 1287 | 1288 | "mkdirp@>=0.5 0", mkdirp@^0.5.1: 1289 | version "0.5.1" 1290 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 1291 | dependencies: 1292 | minimist "0.0.8" 1293 | 1294 | ms@2.0.0: 1295 | version "2.0.0" 1296 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 1297 | 1298 | nan@^2.3.0: 1299 | version "2.6.2" 1300 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" 1301 | 1302 | node-pre-gyp@^0.6.36: 1303 | version "0.6.36" 1304 | resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" 1305 | dependencies: 1306 | mkdirp "^0.5.1" 1307 | nopt "^4.0.1" 1308 | npmlog "^4.0.2" 1309 | rc "^1.1.7" 1310 | request "^2.81.0" 1311 | rimraf "^2.6.1" 1312 | semver "^5.3.0" 1313 | tar "^2.2.1" 1314 | tar-pack "^3.4.0" 1315 | 1316 | nopt@^4.0.1: 1317 | version "4.0.1" 1318 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" 1319 | dependencies: 1320 | abbrev "1" 1321 | osenv "^0.1.4" 1322 | 1323 | normalize-path@^2.0.0, normalize-path@^2.0.1: 1324 | version "2.1.1" 1325 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" 1326 | dependencies: 1327 | remove-trailing-separator "^1.0.1" 1328 | 1329 | npmlog@^4.0.2: 1330 | version "4.1.2" 1331 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" 1332 | dependencies: 1333 | are-we-there-yet "~1.1.2" 1334 | console-control-strings "~1.1.0" 1335 | gauge "~2.7.3" 1336 | set-blocking "~2.0.0" 1337 | 1338 | number-is-nan@^1.0.0: 1339 | version "1.0.1" 1340 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 1341 | 1342 | oauth-sign@~0.8.1: 1343 | version "0.8.2" 1344 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 1345 | 1346 | object-assign@^4.1.0: 1347 | version "4.1.1" 1348 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 1349 | 1350 | object.omit@^2.0.0: 1351 | version "2.0.1" 1352 | resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" 1353 | dependencies: 1354 | for-own "^0.1.4" 1355 | is-extendable "^0.1.1" 1356 | 1357 | once@^1.3.0, once@^1.3.3: 1358 | version "1.4.0" 1359 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 1360 | dependencies: 1361 | wrappy "1" 1362 | 1363 | os-homedir@^1.0.0: 1364 | version "1.0.2" 1365 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 1366 | 1367 | os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: 1368 | version "1.0.2" 1369 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 1370 | 1371 | osenv@^0.1.4: 1372 | version "0.1.4" 1373 | resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" 1374 | dependencies: 1375 | os-homedir "^1.0.0" 1376 | os-tmpdir "^1.0.0" 1377 | 1378 | output-file-sync@^1.1.0: 1379 | version "1.1.2" 1380 | resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" 1381 | dependencies: 1382 | graceful-fs "^4.1.4" 1383 | mkdirp "^0.5.1" 1384 | object-assign "^4.1.0" 1385 | 1386 | parse-glob@^3.0.4: 1387 | version "3.0.4" 1388 | resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" 1389 | dependencies: 1390 | glob-base "^0.3.0" 1391 | is-dotfile "^1.0.0" 1392 | is-extglob "^1.0.0" 1393 | is-glob "^2.0.0" 1394 | 1395 | path-is-absolute@^1.0.0: 1396 | version "1.0.1" 1397 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 1398 | 1399 | performance-now@^0.2.0: 1400 | version "0.2.0" 1401 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" 1402 | 1403 | preserve@^0.2.0: 1404 | version "0.2.0" 1405 | resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" 1406 | 1407 | private@^0.1.6: 1408 | version "0.1.7" 1409 | resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" 1410 | 1411 | process-nextick-args@~1.0.6: 1412 | version "1.0.7" 1413 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 1414 | 1415 | punycode@^1.4.1: 1416 | version "1.4.1" 1417 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 1418 | 1419 | qs@~6.4.0: 1420 | version "6.4.0" 1421 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 1422 | 1423 | randomatic@^1.1.3: 1424 | version "1.1.7" 1425 | resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" 1426 | dependencies: 1427 | is-number "^3.0.0" 1428 | kind-of "^4.0.0" 1429 | 1430 | rc@^1.1.7: 1431 | version "1.2.1" 1432 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" 1433 | dependencies: 1434 | deep-extend "~0.4.0" 1435 | ini "~1.3.0" 1436 | minimist "^1.2.0" 1437 | strip-json-comments "~2.0.1" 1438 | 1439 | readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4: 1440 | version "2.3.3" 1441 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" 1442 | dependencies: 1443 | core-util-is "~1.0.0" 1444 | inherits "~2.0.3" 1445 | isarray "~1.0.0" 1446 | process-nextick-args "~1.0.6" 1447 | safe-buffer "~5.1.1" 1448 | string_decoder "~1.0.3" 1449 | util-deprecate "~1.0.1" 1450 | 1451 | readdirp@^2.0.0: 1452 | version "2.1.0" 1453 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" 1454 | dependencies: 1455 | graceful-fs "^4.1.2" 1456 | minimatch "^3.0.2" 1457 | readable-stream "^2.0.2" 1458 | set-immediate-shim "^1.0.1" 1459 | 1460 | redux@^3.7.2: 1461 | version "3.7.2" 1462 | resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" 1463 | dependencies: 1464 | lodash "^4.2.1" 1465 | lodash-es "^4.2.1" 1466 | loose-envify "^1.1.0" 1467 | symbol-observable "^1.0.3" 1468 | 1469 | regenerate@^1.2.1: 1470 | version "1.3.2" 1471 | resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" 1472 | 1473 | regenerator-runtime@^0.10.0: 1474 | version "0.10.5" 1475 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" 1476 | 1477 | regenerator-transform@0.9.11: 1478 | version "0.9.11" 1479 | resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283" 1480 | dependencies: 1481 | babel-runtime "^6.18.0" 1482 | babel-types "^6.19.0" 1483 | private "^0.1.6" 1484 | 1485 | regex-cache@^0.4.2: 1486 | version "0.4.3" 1487 | resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" 1488 | dependencies: 1489 | is-equal-shallow "^0.1.3" 1490 | is-primitive "^2.0.0" 1491 | 1492 | regexpu-core@^2.0.0: 1493 | version "2.0.0" 1494 | resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" 1495 | dependencies: 1496 | regenerate "^1.2.1" 1497 | regjsgen "^0.2.0" 1498 | regjsparser "^0.1.4" 1499 | 1500 | regjsgen@^0.2.0: 1501 | version "0.2.0" 1502 | resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" 1503 | 1504 | regjsparser@^0.1.4: 1505 | version "0.1.5" 1506 | resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" 1507 | dependencies: 1508 | jsesc "~0.5.0" 1509 | 1510 | remove-trailing-separator@^1.0.1: 1511 | version "1.0.2" 1512 | resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511" 1513 | 1514 | repeat-element@^1.1.2: 1515 | version "1.1.2" 1516 | resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" 1517 | 1518 | repeat-string@^1.5.2: 1519 | version "1.6.1" 1520 | resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 1521 | 1522 | repeating@^2.0.0: 1523 | version "2.0.1" 1524 | resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 1525 | dependencies: 1526 | is-finite "^1.0.0" 1527 | 1528 | request@^2.81.0: 1529 | version "2.81.0" 1530 | resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" 1531 | dependencies: 1532 | aws-sign2 "~0.6.0" 1533 | aws4 "^1.2.1" 1534 | caseless "~0.12.0" 1535 | combined-stream "~1.0.5" 1536 | extend "~3.0.0" 1537 | forever-agent "~0.6.1" 1538 | form-data "~2.1.1" 1539 | har-validator "~4.2.1" 1540 | hawk "~3.1.3" 1541 | http-signature "~1.1.0" 1542 | is-typedarray "~1.0.0" 1543 | isstream "~0.1.2" 1544 | json-stringify-safe "~5.0.1" 1545 | mime-types "~2.1.7" 1546 | oauth-sign "~0.8.1" 1547 | performance-now "^0.2.0" 1548 | qs "~6.4.0" 1549 | safe-buffer "^5.0.1" 1550 | stringstream "~0.0.4" 1551 | tough-cookie "~2.3.0" 1552 | tunnel-agent "^0.6.0" 1553 | uuid "^3.0.0" 1554 | 1555 | rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: 1556 | version "2.6.1" 1557 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" 1558 | dependencies: 1559 | glob "^7.0.5" 1560 | 1561 | safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 1562 | version "5.1.1" 1563 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 1564 | 1565 | semver@^5.3.0: 1566 | version "5.4.1" 1567 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" 1568 | 1569 | set-blocking@~2.0.0: 1570 | version "2.0.0" 1571 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 1572 | 1573 | set-immediate-shim@^1.0.1: 1574 | version "1.0.1" 1575 | resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" 1576 | 1577 | signal-exit@^3.0.0: 1578 | version "3.0.2" 1579 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 1580 | 1581 | slash@^1.0.0: 1582 | version "1.0.0" 1583 | resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 1584 | 1585 | sntp@1.x.x: 1586 | version "1.0.9" 1587 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" 1588 | dependencies: 1589 | hoek "2.x.x" 1590 | 1591 | source-map-support@^0.4.2: 1592 | version "0.4.15" 1593 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" 1594 | dependencies: 1595 | source-map "^0.5.6" 1596 | 1597 | source-map@^0.5.0, source-map@^0.5.6: 1598 | version "0.5.6" 1599 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 1600 | 1601 | sshpk@^1.7.0: 1602 | version "1.13.1" 1603 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" 1604 | dependencies: 1605 | asn1 "~0.2.3" 1606 | assert-plus "^1.0.0" 1607 | dashdash "^1.12.0" 1608 | getpass "^0.1.1" 1609 | optionalDependencies: 1610 | bcrypt-pbkdf "^1.0.0" 1611 | ecc-jsbn "~0.1.1" 1612 | jsbn "~0.1.0" 1613 | tweetnacl "~0.14.0" 1614 | 1615 | string-width@^1.0.1, string-width@^1.0.2: 1616 | version "1.0.2" 1617 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 1618 | dependencies: 1619 | code-point-at "^1.0.0" 1620 | is-fullwidth-code-point "^1.0.0" 1621 | strip-ansi "^3.0.0" 1622 | 1623 | string_decoder@~1.0.3: 1624 | version "1.0.3" 1625 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 1626 | dependencies: 1627 | safe-buffer "~5.1.0" 1628 | 1629 | stringstream@~0.0.4: 1630 | version "0.0.5" 1631 | resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" 1632 | 1633 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 1634 | version "3.0.1" 1635 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 1636 | dependencies: 1637 | ansi-regex "^2.0.0" 1638 | 1639 | strip-json-comments@~2.0.1: 1640 | version "2.0.1" 1641 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 1642 | 1643 | supports-color@^2.0.0: 1644 | version "2.0.0" 1645 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 1646 | 1647 | symbol-observable@^1.0.3: 1648 | version "1.0.4" 1649 | resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" 1650 | 1651 | tar-pack@^3.4.0: 1652 | version "3.4.0" 1653 | resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" 1654 | dependencies: 1655 | debug "^2.2.0" 1656 | fstream "^1.0.10" 1657 | fstream-ignore "^1.0.5" 1658 | once "^1.3.3" 1659 | readable-stream "^2.1.4" 1660 | rimraf "^2.5.1" 1661 | tar "^2.2.1" 1662 | uid-number "^0.0.6" 1663 | 1664 | tar@^2.2.1: 1665 | version "2.2.1" 1666 | resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" 1667 | dependencies: 1668 | block-stream "*" 1669 | fstream "^1.0.2" 1670 | inherits "2" 1671 | 1672 | to-fast-properties@^1.0.1: 1673 | version "1.0.3" 1674 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" 1675 | 1676 | tough-cookie@~2.3.0: 1677 | version "2.3.2" 1678 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" 1679 | dependencies: 1680 | punycode "^1.4.1" 1681 | 1682 | trim-right@^1.0.1: 1683 | version "1.0.1" 1684 | resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" 1685 | 1686 | tunnel-agent@^0.6.0: 1687 | version "0.6.0" 1688 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 1689 | dependencies: 1690 | safe-buffer "^5.0.1" 1691 | 1692 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 1693 | version "0.14.5" 1694 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 1695 | 1696 | uid-number@^0.0.6: 1697 | version "0.0.6" 1698 | resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" 1699 | 1700 | user-home@^1.1.1: 1701 | version "1.1.1" 1702 | resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" 1703 | 1704 | util-deprecate@~1.0.1: 1705 | version "1.0.2" 1706 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1707 | 1708 | uuid@^3.0.0: 1709 | version "3.1.0" 1710 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" 1711 | 1712 | v8flags@^2.0.10: 1713 | version "2.1.1" 1714 | resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" 1715 | dependencies: 1716 | user-home "^1.1.1" 1717 | 1718 | verror@1.10.0: 1719 | version "1.10.0" 1720 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 1721 | dependencies: 1722 | assert-plus "^1.0.0" 1723 | core-util-is "1.0.2" 1724 | extsprintf "^1.2.0" 1725 | 1726 | wide-align@^1.1.0: 1727 | version "1.1.2" 1728 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" 1729 | dependencies: 1730 | string-width "^1.0.2" 1731 | 1732 | wrappy@1: 1733 | version "1.0.2" 1734 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1735 | --------------------------------------------------------------------------------