• 阅读react-redux源码 - 零
  • 阅读react-redux源码 - 一
  • 阅读react-redux源码(二) - createConnect、match函数的实现

阅读react-redux源码零中准备了一些react、redux和react-redux的基础知识。从使用的例子中可以看出来顶层的代码中需要用一个来自react-redux的Provider组件提供redux的store,然后Provider的后代组件通过connect组件连接自己的业务组件就可以获取到通过Provider组件跨组件传递过来的store中的state值。

所以我们先从Provider开始看源码的实现。因为源码很短,直接先贴出来整个源码如下:

import React, { useMemo, useEffect } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'
import Subscription from '../utils/Subscription'function Provider({ store, context, children }) {const contextValue = useMemo(() => {const subscription = new Subscription(store)subscription.onStateChange = subscription.notifyNestedSubsreturn {store,subscription}}, [store])const previousState = useMemo(() => store.getState(), [store])useEffect(() => {const { subscription } = contextValuesubscription.trySubscribe()if (previousState !== store.getState()) {subscription.notifyNestedSubs()}return () => {subscription.tryUnsubscribe()subscription.onStateChange = null}}, [contextValue, previousState])const Context = context || ReactReduxContextreturn <Context.Provider value={contextValue}>{children}</Context.Provider>
}if (process.env.NODE_ENV !== 'production') {Provider.propTypes = {store: PropTypes.shape({subscribe: PropTypes.func.isRequired,dispatch: PropTypes.func.isRequired,getState: PropTypes.func.isRequired}),context: PropTypes.object,children: PropTypes.any}
}export default Provider

可以看到在顶部引入了 ReactReduxContext 这个Context也很简单就是一个React.createContext创建出来的context,用于跨层级向后代组件提供 contextValue。这个contextValue将在下面被定义。

再下面引人注意的就是 Subscription 这个函数,从名字上可以看出是一个实现发布订阅的类。这将是本文的重点。

再往下就定义了我们今天的主角 Provider组件。

Provider组件指接收三个props,分别为store、context和children。而它的返回值为:

<Context.Provider value={contextValue}>{children}
</Context.Provider>

这个组件是React的context的Provider,用于向后代组件跨层级传递值,这里的值就是contextValue。

获取该值的方法就是通过<Context.Consumer>{contextValue => null}</Context.Consumer>或者使用hooks的useContext也可以拿到contextValue

contextValue

const contextValue = useMemo(() => {const subscription = new Subscription(store)subscription.onStateChange = subscription.notifyNestedSubsreturn {store,subscription}}, [store])

contextValue的值只依赖store,如果store没变那么contextValue的值则不会变。

可以看出来contextValue是一个对象其中有store和一个subscription对象。subscription对象是一个监听器(监听到某些事件然后通知自己的监听者),监听store中的state的变化,只要state变化了那么subscription对象的onStateChange则会执行,由此可见onStateChange这个名字也是很能说明这个方法是做什么的,就是监听store的state改变事件。

subscription.onStateChange = subscription.notifyNestedSubs这样就表示state发生变化则subscription.notifyNestedSubs则会被调用,用来通知自身的监听者。

再下面得到 contextValue 的值为 {store, subscription}。

const previousState = useMemo(() => store.getState(), [store])

暂存一下首次进来的state。(暂时只知道这里做了什么,但是为什么这么做还不是很清楚)

useEffect(() => {const { subscription } = contextValuesubscription.trySubscribe()if (previousState !== store.getState()) {subscription.notifyNestedSubs()}return () => {subscription.tryUnsubscribe()subscription.onStateChange = null}}, [contextValue, previousState])

一个执行副作用的钩子,执行subscription.trySubscribe()尝试监听某个对象,当前上下文中是store中的state的改变。之后返回一个函数用于在卸载的时候做一些清理工作,例如卸载监听和去除onStateChange的关联。

const Context = context || ReactReduxContext

获取Context,这个Context可以不用react-redux提供的默认Context也可以自己提供context。

return <Context.Provider value={contextValue}>{children}</Context.Provider>

返回Context.Provider包裹的组件。被包裹的组件可以通过对应的Context来获取被传入的value(contextValue)。

小结

组件Provider中一共使用了三个生命周期,useMemo、useMemo和useEffect,这三个生命周期都是直接或者间接监听store的改变。所以可以看出来这些逻辑是为了处理在运行过程中Provider的父组件改变store的行为。在父组件改变store的时候可以及时卸载旧store上的监听设置新store的监听,并且通知后代组件有新的state产生。

./utils/Subscription.js

在组件Provider中使用Subscription类的实例来监听store中state的改变,并且通知改变给自己的监听者。

Subscription的源码如下:

import { getBatch } from './batch'// encapsulates the subscription logic for connecting a component to the redux store, as
// well as nesting subscriptions of descendant components, so that we can ensure the
// ancestor components re-render before descendantsconst nullListeners = { notify() {} }function createListenerCollection() {const batch = getBatch()let first = nulllet last = nullreturn {clear() {first = nulllast = null},notify() {batch(() => {let listener = firstwhile (listener) {listener.callback()listener = listener.next}})},get() {let listeners = []let listener = firstwhile (listener) {listeners.push(listener)listener = listener.next}return listeners},subscribe(callback) {let isSubscribed = truelet listener = (last = {callback,next: null,prev: last})if (listener.prev) {listener.prev.next = listener} else {first = listener}return function unsubscribe() {if (!isSubscribed || first === null) returnisSubscribed = falseif (listener.next) {listener.next.prev = listener.prev} else {last = listener.prev}if (listener.prev) {listener.prev.next = listener.next} else {first = listener.next}}}}
}export default class Subscription {constructor(store, parentSub) {this.store = storethis.parentSub = parentSubthis.unsubscribe = nullthis.listeners = nullListenersthis.handleChangeWrapper = this.handleChangeWrapper.bind(this)}addNestedSub(listener) {this.trySubscribe()return this.listeners.subscribe(listener)}notifyNestedSubs() {this.listeners.notify()}handleChangeWrapper() {if (this.onStateChange) {this.onStateChange()}}isSubscribed() {return Boolean(this.unsubscribe)}trySubscribe() {if (!this.unsubscribe) {this.unsubscribe = this.parentSub? this.parentSub.addNestedSub(this.handleChangeWrapper): this.store.subscribe(this.handleChangeWrapper)this.listeners = createListenerCollection()}}tryUnsubscribe() {if (this.unsubscribe) {this.unsubscribe()this.unsubscribe = nullthis.listeners.clear()this.listeners = nullListeners}}
}

整个Subscription实现了一个事件链:

------ subscriptionA ------ subscriptionB\ ------ subscriptionC
-------------- |---------------------|---------------------------- |
-------------- | - listenerA1 — | - listenerB1 ----------- | - listenerB1
-------------- | - listenerA2 — | - listenerB2 ----------- | - listenerC2
-------------- | - listenerA3 — | - listenerB3 ----------- | - listenerC3

​使用Subscription实现上面的事件链:

const eventOrigin = {listeners: [],subscribe(fn) {eventOrigin.listeners.push(fn)return function() {eventOrigin.listeners = eventOrigin.listeners.filter(item => item !== fn)}},notify() {let i = 0while (i < eventOrigin.listeners.length) eventOrigin.listeners[i++]()}
}const subscriptionA = new Subscription(eventOrigin)
subscriptionA.onStateChange = subscriptionA.notifyNestedSubs
subscriptionA.trySubscribe()
subscriptionA.addNestedSub(function listenerA1() {console.log('listenerA1')
})
subscriptionA.addNestedSub(function listenerA2() {console.log('listenerA2')
})
subscriptionA.addNestedSub(function listenerA3() {console.log('listenerA3')
})const subscriptionB = new Subscription(undefined, subscriptionA)
subscriptionB.onStateChange = subscriptionB.notifyNestedSubs
subscriptionB.trySubscribe()
subscriptionB.addNestedSub(function listenerA1() {console.log('listenerB1')
})
subscriptionB.addNestedSub(function listenerA2() {console.log('listenerB2')
})
subscriptionB.addNestedSub(function listenerA3() {console.log('listenerB3')
})const subscriptionC = new Subscription(undefined, subscriptionB)
subscriptionC.onStateChange = subscriptionC.notifyNestedSubs
subscriptionC.trySubscribe()
subscriptionC.addNestedSub(function listenerA1() {console.log('listenerC1')
})
subscriptionC.addNestedSub(function listenerA2() {console.log('listenerC2')
})
subscriptionC.addNestedSub(function listenerA3() {console.log('listenerC3')
})// 测试,触发事件源的notify
eventOrigin.notify()

打印出如下结果:

listenerA1
listenerA2
listenerA3
listenerB1
listenerB2
listenerB3
listenerC1
listenerC2
listenerC3

每个subscription实例就是一个事件的节点,每个节点上面有很多事件的监听器,事件沿着subscription实例组成的链传递,挨个通知每个subscription节点,然后subscription节点的事件监听器监听到事件之后挨个执行回调。

可以想象成DOM的原生事件,事件沿着DOM传递,每个DOM上可以addEventListener多个事件回调。

createListenerCollection

其中用到的函数createListenerCollection创建的对象也是一个监听器,监听某个事件发生通知自己的监听者。

createListenerCollection返回的是一个双向链表,这个数据结构方便修改,删除某一项十分快捷,不需要遍历链表中的每一个,直接将上一个的next指针指向下一个就完成了自身的删除。源码如下:

import { getBatch } from './batch'// encapsulates the subscription logic for connecting a component to the redux store, as
// well as nesting subscriptions of descendant components, so that we can ensure the
// ancestor components re-render before descendantsconst nullListeners = { notify() {} }function createListenerCollection() {const batch = getBatch()let first = nulllet last = nullreturn {clear() {first = nulllast = null},notify() {batch(() => {let listener = firstwhile (listener) {listener.callback()listener = listener.next}})},get() {let listeners = []let listener = firstwhile (listener) {listeners.push(listener)listener = listener.next}return listeners},subscribe(callback) {let isSubscribed = truelet listener = (last = {callback,next: null,prev: last})if (listener.prev) {listener.prev.next = listener} else {first = listener}return function unsubscribe() {if (!isSubscribed || first === null) returnisSubscribed = falseif (listener.next) {listener.next.prev = listener.prev} else {last = listener.prev}if (listener.prev) {listener.prev.next = listener.next} else {first = listener.next}}}}
}
const batch = getBatch()

这个batch表示的是批量更新的意思。这个是react的知识点,简单描述已备忘。

react自身发起的更新是默认批量的。例如onClick函数里setState两次只会引起一次render,componentDidMount里面setState两次也只会引起一次render,但是setTimeout里面两次setState就会引起两次跟新,想要避免这个事情就可以用batch来规避。

原理就类似于react自己张开了一个袋子,需要更新的组件都会被收到这个袋子里,装完之后一起处理组件的更新。那么为什么setTimeout里面的装不到袋子里里呢?因为setTimeout是异步的并不归react管,不在一个调用栈内。或者说setTimeout的回调函数执行之前张开的袋子已经闭合了。

const batch = getBatch()
let first = null
let last = null

首先拿到batch函数,定义头指针和尾指针,这是一个双向链表,和单链表不同,不仅可以从first到last遍历,也可以从last到first遍历。

并且删除双向链表中的一个节点也十分方便,可以将当前节点的上一个节点的next指针指向当前节点的下一个节点就直接将当前节点删除了。如果是单链表需要从头开始遍历链表才可以。从空间上来说链表不需要连续的内存空间,相较于数组这方面也是更加灵活。

return {// 清理当前所有监听者clear() {},// 通知监听者事件发生notify() {},// 返回所有监听者组成的数组get() {},// 设置监听者subscribe(callback) {}
}
clear() {first = nulllast = null
}

清除的时候直接将first和last设置为null这个链表没有被引用,自然就会被垃圾回收机制回收掉。

notify() {batch(() => {let listener = firstwhile (listener) {listener.callback()listener = listener.next}})
}

从第一个节点开始遍历这个链表,执行每个节点上的存储的回调函数。

get() {let listeners = []let listener = firstwhile (listener) {listeners.push(listener)listener = listener.next}return listeners
}

将双向链表转换成数组返回出去。

subscribe

设置事件发生的回调函数,notify通知的就是在这里subscribe的回调函数,这个方法相对复杂点,整体来说做了两件事情:

一件是将入参callback作为一个节点添加到双向链表中,以便notify的时候可以通知到

一件是返回一个函数用于在链表中删除该节点

subscribe(callback) {let isSubscribed = truelet listener = (last = {callback,next: null,prev: last})if (listener.prev) {listener.prev.next = listener} else {first = listener}return function unsubscribe() {if (!isSubscribed || first === null) returnisSubscribed = falseif (listener.next) {listener.next.prev = listener.prev} else {last = listener.prev}if (listener.prev) {listener.prev.next = listener.next} else {first = listener.next}}
}

一个标识符表示callback是否在链表中,为了防止返回的卸载监听的函数多次被调用:

let isSubscribed = true
let listener = (last = {callback,next: null,prev: last
})

定义链表中当前节点的值。从右往左看,新加进去的节点一定是最后一个节点,所以新加入节点的上一个节点一定是当前的last节点,所以prev的值是last。

而新加入节点的next值就是null了。

if (listener.prev) {listener.prev.next = listener
} else {first = listener
}

如果有上一个节点,表示新加入的节点不是第一个,也就是加入前的last节点不是null。新加入的节点是第一个就需要将当前节点作为头结点赋值给first变量,以便随时可以从头开始遍历链表。

return function unsubscribe() {if (!isSubscribed || first === null) returnisSubscribed = falseif (listener.next) {listener.next.prev = listener.prev} else {last = listener.prev}if (listener.prev) {listener.prev.next = listener.next} else {first = listener.next}
}

可以改变链表的不仅仅是被返回的函数,还有一个clear方法,会删除整个链表,所以在删除节点的时候首先要检查下这个链表是否还存在,然后看下当前节点是否还存在,如果链表存在并且还在监听中,那么可以执行卸载流程了。

首先修改刚才的订阅标识符,修改标识符为未订阅,因为马上要卸载了。

如果当前节点有下一个节点,就将当前节点的下一个节点的prev指针指向当前节点的prev。

如果当前节点没有下一个几点,直接将闭包中的last指向当前节点的prev,就删除了当前节点。

还需要改下节点的next指针,因为是双向链表。

注:subscribe函数设置监听的同时还会返回一个卸载监听的函数,这种风格和redux的store的subscribe的风格如出一辙。

小结

createListenerCollection()返回一个对象,这个对象有方法subscribe和notify,一个用来订阅事件的发生,一个用来通知订阅的回调事件发生了,内部的实现则是通过双向链表来完成的。

class Subscription

export default class Subscription {constructor(store, parentSub) {this.store = storethis.parentSub = parentSubthis.unsubscribe = nullthis.listeners = nullListenersthis.handleChangeWrapper = this.handleChangeWrapper.bind(this)}addNestedSub(listener) {this.trySubscribe()return this.listeners.subscribe(listener)}notifyNestedSubs() {this.listeners.notify()}handleChangeWrapper() {if (this.onStateChange) {this.onStateChange()}}isSubscribed() {return Boolean(this.unsubscribe)}trySubscribe() {if (!this.unsubscribe) {this.unsubscribe = this.parentSub? this.parentSub.addNestedSub(this.handleChangeWrapper): this.store.subscribe(this.handleChangeWrapper)this.listeners = createListenerCollection()}}tryUnsubscribe() {if (this.unsubscribe) {this.unsubscribe()this.unsubscribe = nullthis.listeners.clear()this.listeners = nullListeners}}
}

使用案例:


const subscriptionB = new Subscription(undefined, subscriptionA)
subscriptionB.onStateChange = subscriptionB.notifyNestedSubs
subscriptionB.trySubscribe()
subscriptionB.addNestedSub(function listenerA1() {console.log('listenerB1')
})
subscriptionB.addNestedSub(function listenerA2() {console.log('listenerB2')
})
subscriptionB.addNestedSub(function listenerA3() {console.log('listenerB3')
})

这个类做的事情在上面大体上说过,类似于浏览器的时间冒泡,现在看下具体是怎么实现的。


constructor(store, parentSub) {this.store = storethis.parentSub = parentSubthis.unsubscribe = nullthis.listeners = nullListenersthis.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}

Subscription的构造函数有两个入参,一个是store,一个是parentSub是同一个类的不同实例。

上面在使用这个类的时候,new了之后紧接着会关联实例的onStateChange到notifyNestedSubs中去,表示onStateChange执行的时候,实际上执行的是notifyNestedSubs。

紧接着调用实例trySubscribe方法,尝试订阅:

trySubscribe() {if (!this.unsubscribe) {this.unsubscribe = this.parentSub? this.parentSub.addNestedSub(this.handleChangeWrapper): this.store.subscribe(this.handleChangeWrapper)this.listeners = createListenerCollection()}
}

如果没有订阅,那么就去订阅事件。订阅的时候以parentSub优先,如果没有提供parentSub,那么就订阅store的事件。

parentSub和subscribe方法有同样的签名,需要一个入参函数,会返回一个取消订阅的函数。

返回的取消订阅的函数在Subscription会被用作是否订阅事件的标识符。

调用createListenerCollection初始化字段listeners,后面Subscription实例的监听函数都会被委托到listeners上。

handleChangeWrapper() {if (this.onStateChange) {this.onStateChange()}
}

毕竟onStateChange到notifyNestedSubs的关联是调用方手动关联的,如果没有关联的话直接调用会报错,为了不报错,做一次检查也是有必要的。

为什么要onStateChange = notifyNestedSubs?做一次关联?主观感觉应该是语义上的考虑。

这个类实例化出来的对象主要是监听store中state的改变的,所以对外onStateChange这个名字一听就懂。但是对内的话,其实响应事件之后是要通知自身的监听者,所以是notifyNestedSubs。

addNestedSub(listener) {this.trySubscribe()return this.listeners.subscribe(listener)
}

添加Subscription实例的监听函数,被委托给listeners。

notifyNestedSubs() {this.listeners.notify()
}

通知自身的订阅者们,事件发生了,你们要做点什么了。

总结

Provider组件跨组件向后代组件(主要是后面要提到的connect)提供了一个contextValue对象,其中包括了Subscription类的实例和store自身,其中Subscription的实例在监听到store中state的变化的时候就会通知自身的监听者,store的state变化了你们需要重新store.getState()重新渲染组件了。

  • 阅读react-redux源码 - 零
  • 阅读react-redux源码 - 一
  • 阅读react-redux源码(二) - createConnect、match函数的实现

阅读react-redux源码 - 一相关推荐

  1. 逐行阅读redux源码(二)combineReducers

    前情提要 逐行阅读redux源码(一)createStore 认识reducers 在我们开始学习源码之前,我们不妨先来看看何谓reducers: 如图所见,我们可以明白, reducer 是用来对初 ...

  2. [react] 你阅读了几遍React的源码?都有哪些收获?你是怎么阅读的?

    [react] 你阅读了几遍React的源码?都有哪些收获?你是怎么阅读的? 0遍 +1 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大家一起讨论 主目录 与歌 ...

  3. 学习 redux 源码整体架构,深入理解 redux 及其中间件原理

    如果觉得内容不错,可以设为星标置顶我的公众号 1. 前言 你好,我是若川.这是学习源码整体架构系列第八篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是 ...

  4. Redux 源码解读 —— 从源码开始学 Redux

    已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下:捡起旧知识的同时又有了一些新的收获,在这里作文以记之. 在阅读文章之前,最好已经知道如何使用 ...

  5. 中秋福利 | 漂亮的React后台源码真情大放送

    中秋快乐 每逢佳节倍思亲,一年一度的中秋,你和谁一起度过?如果你和小编一样,漂泊在外,别忘记给远在家乡的父母打个电话,祝福他们中秋快乐,告诉他们自己还好,勿让他们挂念.在此小编,祝各位粉丝们" ...

  6. 深入理解redux之从redux源码到react-redux的原理

    在使用react的过程中,用redux来管理应用中的状态,使应用流更清晰的同时也会有小小的疑惑,比如reducer在redux中时怎么发挥作用的,为什么只要写好reducer,和dispatch特定a ...

  7. 前端战五渣学React——JSX React.createElement() React.ReactElement()源码

    最近<一拳超人>动画更新第二季了,感觉打斗场面没有第一季那么烧钱了,但是剧情还挺好看的,就找了漫画来看.琦玉老师真的厉害!!!打谁都一拳,就喜欢看老师一拳把那些上来就吹牛逼的反派打的稀烂, ...

  8. rust墙壁升级点什么_分享:如何在阅读Rust项目源码中学习

    今天做了一个Substrate相关的小分享,公开出来. 因为我平时也比较忙,昨天才选定了本次分享的主题,准备比较仓促,细节可能不是很充足,但分享的目的也是给大家提供一个学习的思路,更多的细节大家可以在 ...

  9. redux源码分析之一:createStore.js

    欢迎关注redux源码分析系列文章: redux源码分析之一:createStore.js redux源码分析之二:combineReducers.js redux源码分析之三:bindActionC ...

  10. Redux源码分析(一)

    Redux源码分析(createStore) 使用redux都快3年了,到现在也没认真去了解一下源码罪过啊,所以需要对它进行一些分析和学习,一方面能更好的去使用它,另一方面也学习一下该框架的设计思路, ...

最新文章

  1. 微信公众号开发之准备工作
  2. VIM进阶-模式mode
  3. 这才是真正的,坐上来,自己动!| 今日趣图
  4. php @touch,touch - [ php中文手册 ] - 在线原生手册 - php中文网
  5. OpenAI新研究:通过非监督学习提升NLP任务表现
  6. Spark数据分析技术学习笔记(二)——DataFrame使用
  7. python多元线性回归_多元线性回归模型精度提升 虚拟变量
  8. win8 oracle 卸载,大神细说win8系统卸载oracle的法子
  9. UnionPay-银联支付-netcore(一)
  10. 2021牛客暑期多校训练营#10:F-Train Wreck
  11. 【bzoj2959】长跑【LCT+并查集】
  12. linux、ubuntu如何查看网速
  13. 计算机应用 行动计划范文,制定计算机学习计划范文3篇0001.docx
  14. UPDATE或者DELETE忘加WHERE条件的恢复
  15. Ceph Calamari安装问题汇总
  16. python 模拟鼠标,键盘点击
  17. word2016论文不同章节设置页眉页码方法
  18. i9级E52450处理器_来了!十代英特尔酷睿标压处理器,实测跑分首发
  19. sql---多表联查
  20. 笔记:Echarts地图 被选中更改颜色一系列配置

热门文章

  1. eclipse加载maven工程提示pom.xml无法解析org.apache.maven.plugins:maven-resources-plugin:2.4.3解决方案...
  2. USACO3.15stamps(dp)
  3. Unity3D 访问Access数据库
  4. ppt图片丢失_041 职场PPT实战:做好的PPT换个电脑就丢字体?三招解决!
  5. Qt配置VS2017
  6. Linux Shell——-if -eq,if -ne,if -gt[笔记]
  7. 启动rocketmq 报错_RocketMQ为什么要保证订阅关系的一致性?
  8. C++插入中文到mysql乱码
  9. AUTOSAR从入门到精通100讲(二十三)-AUTOSAR通信篇—PduR模块
  10. 计算机在智慧交通的应用论文,智能交通的毕业论文