资讯专栏INFORMATION COLUMN

redux源码解读--createStore源码解析

tianren124 / 2455人阅读

摘要:源码解析是最核心的模块。比如,当我们需要使用中间件的时候,就会像第三个参数传递一个返回值是一个。后续的源码解读和测试例子可以关注源码解读仓库

createStore源码解析

createStoreredux最核心的模块。这个模块就是用于创建一个store对象,同时,对外暴露出dispatch,getState,subscribereplaceReducer方法。(源码中关于observable的部分可以忽略,这个是redux内部使用的。我们在开发中几乎几乎用不到)

先看一下这个模块的基本结构:

依赖

lodash/isPlainObject

symbol-observable

对外输出

dispatch

getState

subscribe

replaceReducer

[$$observable](几乎不用)

redux源码中使用的lodash/isPlainObject依赖。在IE6-8中性能很差,其实现方式和jQuery3.x的实现相似,在旧版本的IE中支持不了。最后会和大家一起探讨。

源码注释

// 判断是不是纯粹对象的模块({})
import isPlainObject from "lodash/isPlainObject"
// 引入observable支持
import $$observable from "symbol-observable"
export const ActionTypes = {
  INIT: "@@redux/INIT"
}

上面这个是redux内部使用的一个action。主要用于内部测试和渲染初始的state。记住,我们自己编写action的时候,action.type不能为@@redux/INIT。因为,这个action会在redux的内部自动调用。比如,下面的捣蛋代码:

import {createStore, combineReducers, applyMiddleware} from "../src"
import logger from "redux-logger"

const actionTypes = "@@redux/INIT"
const reducers = (state = {}, action) => {
  switch(action.type) {
    case actionTypes:
      console.log("hello @@redux/INIT")
      return {
        "type": actionTypes
      }
    default:
      return state
  }
}
const store = createStore(reducers, applyMiddleware(logger))
console.log("*************************************")
console.log(store.getState()) //会渲染为 {"type": "@@redux/INIT"}
console.log("*************************************")

下面就是createStore方法的实现:

export default function createStore(reducer, preloadedState, enhancer){
  //...初始条件的判断和设定
  function getState() {
    // getState方法的实现
  }
  function subscribe() {
    // subscribe方法的实现
  }
  function dispatch() {
    // dispatch方法的实现
  }
  function replaceReducer() {
    // replaceReducer方法的实现
  }
  function observable() {
    // observable方法的实现
  }
  // store被创建后,自动分发一个"INIT" action。渲染出初始化的state树。
  dispatch({ type: ActionTypes.INIT })
}

下面就来一点点分析每一行代码到底做什么:

if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
  enhancer = preloadedState
  preloadedState = undefined
}

在调用createStore的方法的时候,可以传递三个参数createStore(reducer, preloadedState, enhancer)。其中,各参数属性如下:

reducer必需参数,function类型

preloadedState可选参数,object类型

enhancer可选参数,function类型

在平常的使用中,我们一般会省略第二个参数。比如,当我们需要使用redux中间件的时候,就会像第三个参数传递一个applyMiddleware()[返回值是一个function]。如果,我们没有初始状态,则会省略第二个参数。这个时候,我们的函数调用形式为:

const store = createStore(reducer, applyMiddleware(...))

这个时候就会执行上面源码中的代码,使函数调用满足最基本的形式调用。也就是函数在传递两个或者三个参数的情况下,其内部处理逻辑都是一样的。

// 如果我们指定了reducer增强器enhancer
if (typeof enhancer !== "undefined") {
  // enhancer必须是一个函数
  if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }
    // 这个函数接收createStore作为参数,并且返回一个函数,这个函数接收的参数是reducer,preloadedState
    // 直接返回经过enhancer包装的对象
    return enhancer(createStore)(reducer, preloadedState)
  }

想更好的理解这段代码,可以参考applyMiddleware内部的实现。

// 要求传递给createStore的第一个参数必须是一个函数
if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.")
 }
// 保存初始的reducer
let currentReducer = reducer
// 保存初始的state
let currentState = preloadedState
// 保存所有的事件监听器
let currentListeners = []
// 获取当前监听器的一个副本(相同的引用)
let nextListeners = currentListeners
// 是否正在派发action
let isDispatching = false

function ensureCanMutateNextListeners() {
  // 如果nextListeners和currentListeners具有相同的引用,则获取一份当前事件监听器集合的一个副本保存到nextListeners中
  if (nextListeners === currentListeners) {
    nextListeners = currentListeners.slice()
  }
}

上面就是createStore方法中的一些初始参数,这里有一个地方值得思考:为什么要维护两份事件监听器列表(nextListeners,currentListeners)?。下面,我们会解释。

// 直接返回当前store的state
function getState() {
  return currentState
}
  function subscribe(listener) {
    // 事件监听器必须是函数,否则会抛出异常
    if (typeof listener !== "function") {
      throw new Error("Expected listener to be a function.")
    }
    // 这个事件监听器是否已经被取消的标志(个人感觉:这个初始值应该被设置为false,语意化更好一些。)
    let isSubscribed = true
    // 调用这个函数的结果就是生成一份当前事件监听器的一个副本保存到nextListeners中
    ensureCanMutateNextListeners()
    // 将新的事件监听器添加到nextListeners中
    nextListeners.push(listener)
    
    // 返回一个取消监听的函数
    return function unsubscribe() {
      // 如果这个监听器已经被取消了,则直接return
      if (!isSubscribed) {
        return
      }
      // 将监听器是否取消的标志设置为false
      isSubscribed = false
      // 再次生成一份事件监听器集合的副本
      ensureCanMutateNextListeners()
      // 获取到需要取消的事件监听器的索引
      const index = nextListeners.indexOf(listener)
      // 从事件监听器集合中删除这个事件监听器
      nextListeners.splice(index, 1)
    }
  }

subscribe方法的源码中可以看出,每次在进行监听器的添加/删除之前,都会基于当前的监听器集合生成一个副本保存到nextListeners中。这个时候还是不能准确的回答上面的问题,下面我们继续研究dispatch的源码:

function dispatch(action) {
    // dispatch的参数就是我们需要派发的action,一定要保证这个action是一个纯粹的对象
    // 如果不是一个纯粹的对象,则会抛出异常。
    if (!isPlainObject(action)) {
      // 这个方法有坑,在低版本的IE浏览器中性能很差,最后我们会研究这个方法的源码。
      throw new Error(
        "Actions must be plain objects. " +
        "Use custom middleware for async actions."
      )
    }

    // 所派发的action必须有一个type属性(我们可以将这个属性认为就是action的身份证,这样redux才知道你派发的是哪个action,你需要做什么,该怎么为你做)
    // 如果没有这个属性则会抛出异常
    if (typeof action.type === "undefined") {
      throw new Error(
        "Actions may not have an undefined "type" property. " +
        "Have you misspelled a constant?"
      )
    }
    
    // 如果redux正在派发action,则抛出异常?什么时候会出现这种情况???
    if (isDispatching) {
      throw new Error("Reducers may not dispatch actions.")
    }

    try {
      isDispatching = true
      // 派发action
      // 实质就是将当前的state和你需要派发的action传递给reducer函数病返回一个新的state
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 这一块也是一个十分关键的地方,哈哈哈哈哈,又多了一份事件监听器的列表,简单的说一下这三份列表的作用
      // nextListeners: 保存这次dispatch后,需要触发的所有事件监听器的列表
    // currentListeners: 保存一份nextListeners列表的副本
      // listeners: 需要执行的列表
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      // 调用所有的事件监听器
      listener()
    }
    //  dispatch的返回值也是十分重要的,如果没有这个返回值,就不可能引入强大的中间件机制。
    return action
  }

到这里,我们就可以回答这个问题了:为什么要维护两份事件监听器列表(nextListeners,currentListeners)?

首先,我们必须要知道的事情就是:我们的监听器在什么时候会执行?在我们的调用dispatch派发action之后。ok,看下面的这个图:

这个图表示,当dispatch方法执行到这行代码的时候,listenerscurrentListenersnextListeners这三个变量引用内存中的同一份数组,只要其中一个发生变化,另外两个立马改变。和下面的这个例子一样的含义:

所以,在这种情况下。如果我在某个事件监听器函数中调用了取消了某个监听器,那么在这次dispatch后,被取消的这个事件监听器就不会被执行了(?????是吗????)。

import {createStore, combineReducers, applyMiddleware} from "../src"
import logger from "redux-logger"

const actionTypes = "@@redux/INIT"
const reducers = (state = {}, action) => {
  switch(action.type) {
    case actionTypes:
      return {
        "type": actionTypes
      }
    default:
      return state
  }
}
const store = createStore(reducers, applyMiddleware(logger))

const listener1 = store.subscribe(() => {
    console.log("listener1")
})


const listener2 = store.subscribe(() => {
    // 取消listener3
    listener3()
    console.log("listener2")
})


const listener3 = store.subscribe(() => {
    console.log("listener3")
})

store.dispatch({type: actionTypes})

结果是:

listener1
listener2
listener3

结果,就是:即使你在某个事件监听器中,取消了其它的事件监听器,那么被取消的这个事件监听器,在这次dispatch后仍然会执行。也就是说。redux会保证在某个dispatch后,会保证在这个dispatch之前的所有事件监听器全部执行。

这是个bug还是个feature。无从而知,但是从redux源码中,可以知道,这是一个bug。所以,redux作者就利用上面的方法很巧妙的避免了这种情况。其实实现的方法很简单:切断nextListeners和currentListener,listeners相同的引用关系

下面接着扯:

// 提换reducer的方法。(动态加载reducers的时候才用)
function replaceReducer(nextReducer) {
    if (typeof nextReducer !== "function") {
      throw new Error("Expected the nextReducer to be a function.")
    }

    currentReducer = nextReducer
    // 替换结束后,重新初始化
    dispatch({ type: ActionTypes.INIT })
  }
// 触发预设action,主要就是为了生成初始的state tree的结构
dispatch({ type: ActionTypes.INIT })
// 这就很熟悉了吧
return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
      // 尼玛 忽略这个
    [$$observable]: observable
  }

这就是对createStore源码的一个整体解读,水平有限,欢迎拍砖。后续的源码解读和测试例子可以关注:redux源码解读仓库

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89454.html

相关文章

  • redux源码解读--applyMiddleware源码解析

    摘要:的中间件主要是通过模块实现的。返回的也是一个对象这个其实就是,各个中间件的最底层第三层的哪个函数组成的圆环函数构成的这就是对源码的一个整体解读,水平有限,欢迎拍砖。后续的源码解读和测试例子可以关注源码解读仓库 applyMiddleware源码解析 中间件机制在redux中是强大且便捷的,利用redux的中间件我们能够实现日志记录,异步调用等多种十分实用的功能。redux的中间件主要是...

    Atom 评论0 收藏0
  • Redux 入坑进阶 - 源码解析

    摘要:否则的话,认为只是一个普通的,将通过也就是进一步分发。在本组件内的应用传递给子组件源码解析期待一个作为传入,里面是如果只是传入一个,则通过返回被绑定到的函数遍历并通过分发绑定至将其声明为的属性之一接收的作为传入。 原文链接:https://github.com/ecmadao/Co...转载请注明出处 本文不涉及redux的使用方法,因此可能更适合使用过redux的玩家翻阅? 预热...

    BothEyes1993 评论0 收藏0
  • 重读redux源码(二)

    摘要:函数组合,科里化的串联结合示例源码,实现也很优雅,对于返回的,将等参数传递进去,然后执行,等待回调异步完成再。对于正常对象则进行下一步。前言 作为前端状态管理器,这个比较跨时代的工具库redux有很多实现和思想值得我们思考。在深入源码之前,我们可以相关注下一些常见问题,这样带着问题去看实现,也能更加清晰的了解。 常见问题 大概看了下主要有这么几个: redux三大原则 这个可以直接参考...

    dingda 评论0 收藏0
  • 不一样的redux源码解读

    摘要:这里还有一个疑问点就是的嵌套,最开始也我不明白,看了源码才知道,这里返回的也是接受也就是一个所以可以正常嵌套。以作为参数,调用上一步返回的函数以为参数进行调用。 1、本文不涉及redux的使用方法,因此可能更适合使用过 redux 的同学阅读2、当前redux版本为4.0.13、更多系列文章请看 Redux作为大型React应用状态管理最常用的工具。虽然在平时的工作中很多次的用到了它...

    Salamander 评论0 收藏0
  • redux源码阅读--主模块

    摘要:主模块的入口模块就是。主要就做两件事引入个功能模块,并挂载至同一个对象上,对外暴露。在非环境下压缩代码,给予警告。后续的源码解读和测试例子可以关注源码解读仓库 主模块 redux的入口模块就是src/index.js。这个文件的代码十分简单。主要就做两件事: 引入个功能模块,并挂载至同一个对象上,对外暴露。 在非production环境下压缩代码,给予警告。 下面是模块的源码(只包...

    testHs 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<