如何读三方库源码——以阅读react-redux源码为例

相信不少初级程序员 / 学生 对读源码有一定的困难和心理压力,事实上,读懂源码并没有想象中那么困难。

本文将以阅读react-redux的源码为案例,分享一些读源码的小技巧。




读源码前该做什么

读源码前一定要沐浴更衣,焚香净手,冥想半小时~~~

37db6a12321f316d82b01a1f3bfcfdaf.jpg

手动狗头 /doge

读源码前,必须先了解这个库的基本背景知识:

  1. 解决了什么问题
  2. 基本的使用方法

有了这些背景知识,才容易找到阅读源码的切入点,更容易理解源码的意图。


以 react-redux 为例,查看源码前,我们先看看 react-redux 是什么,它的基本使用方法。

react-redux 简介

什么是react-redux?

React Redux is the official  React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.

事实上,redux只是一个单纯的JavaScript状态管理库,跟react并没有直接联系,在react代码中也难以直接使用。

而 react-redux 就是一座连接了 redux 和 react 的桥梁,它可以让React组件从 Redux store 中读取数据/状态 和 dispatch action 到 Redux store 中更改数据/状态


在 react-redux 官方文档Quick Start中,基本使用方法如下

先使用 Provider 包裹住 React App 的根组件

import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

使用 connect 函数,连接组件到 Redux store

import { connect } from 'react-redux'
import { increment, decrement, reset } from './actionCreators'

// const Counter = ...

const mapStateToProps = (state /*, ownProps*/) => {
  return {
    counter: state.counter
  }
}

const mapDispatchToProps = { increment, decrement, reset }

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

此外,react-redux 也支持 Hooks (Hooks | React Redux )

useSelector Hook

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}

useDispatch Hook

import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}


到目前为止,我们了解了 react-redux 的基本背景知识。

根据 基本使用方法,我们可以从 Provider 作为阅读react-redux 的切入口。


读源码时不要急于纠结细节,先总后分,先整体后局部

阅读 react-redux Provider 源码 (版本 v7.2.2)

import React, { useMemo, useEffect } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'
import Subscription from '../utils/Subscription'

// 接收三个参数,第一个是 redux store 的引用,第二个参数明显是可选的(比如上面基本用法里并没有传入 context
function Provider({ store, context, children }) {
  // 使用了 useMemo 缓存
  const contextValue = useMemo(() => {
    // 这边使用了一个内部自定义类 Subscription ,先不着急查看对应的源码,先看 Provider 的剩余代码 
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    // 返回一个对象
    return {
      store,
      subscription,
    }
  }, [store])

  // 又用了一个 useMemo 缓存
  const previousState = useMemo(() => store.getState(), [store])

  useEffect(() => {
    const { subscription } = contextValue
    subscription.trySubscribe() // 根据变量名和方法名,这边尝试订阅(大概率是订阅 redux store 的变化)

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

  // 如果参数 context 是false值,就使用默认的 ReactReduxContext
  const Context = context || ReactReduxContext

  // 划重点:这个用法很明显就是 React Context 
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

阅读 Provider 源码时,发现有个内部自定义类 Subscription ,因为Provider整体源码并不多,我们可以先看完 Provider 整体代码,而不着急查看 Subscription 类的源码。

看完Provider整体代码后,可以简单概况为:

  1. Provider 利用了React Context (不了解 context 的同学请参考官方文档 Context – React )
  2. 使用了 useMemo 缓存了 redux store 引用 和 自定义类 Subscription 的实例
  3. 使用 useEffect 做订阅和取消订阅操作


整体解读了 Provider 源码后,我们就可以开始阅读 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 descendants



export default class Subscription {
  constructor(store, parentSub) {
    this.store = store; // 存放 redux store 的引用
    this.parentSub = parentSub; // 父级订阅
    this.unsubscribe = null // 取消订阅的方法的引用
    this.listeners = nullListeners // 监听器链表 —— 随后会解读

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }

  // 添加嵌套订阅
  addNestedSub(listener) {
    this.trySubscribe() // 确保实例本身已经订阅了 
    return this.listeners.subscribe(listener) // 加入监听器链表
  }

  // 通知监听器链表中的所有监听器
  notifyNestedSubs() {
    this.listeners.notify()
  }

  handleChangeWrapper() {
    // 当这个Subscription实例有onStateChange方法时就触发该方法
    if (this.onStateChange) { 
      this.onStateChange()
    }
  }

  isSubscribed() {
    return Boolean(this.unsubscribe)
  }

  // 尝试订阅
  trySubscribe() {
    if (!this.unsubscribe) { // 避免重复订阅
      // 划重点:当这个Subscription实例有上一级的订阅时,往上一级的订阅中添加子订阅
      // 划重点:否则就直接在 redux store 中订阅
      this.unsubscribe = this.parentSub
          ? this.parentSub.addNestedSub(this.handleChangeWrapper)
          : this.store.subscribe(this.handleChangeWrapper)

      // 懒加载 —— 仅当成功调用 trySubscribe 时才创建 监听器链表 —— 随后会解读为什么是链表
      this.listeners = createListenerCollection()
    }
  }

  // 尝试取消订阅
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}


// 源码中,下面的代码是处于开头的,为了解读起来更好理解,我搬到结尾

// 一个无实现的监听器“链表” —— 作为Subcription实例中listeners的初始值 
const nullListeners = {
  notify() {
  }
}

// 工厂方法 —— 创建一个监听器链表结构
function createListenerCollection() {
  const batch = getBatch() // 获取批量更新的策略函数 —— react 和 react-native 不同
// 一个双向链表结构
  let first = null  // 链表头
  let last = null   // 链表尾

  return {
    clear() {
      first = null
      last = null
    },

    notify() {
      batch(() => {
        let listener = first
        while (listener) { // 遍历链表,触发回调
          listener.callback()
          listener = listener.next
        }
      })
    },

    get() { // 以数组形式返回监听器链表
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },

    // 订阅,即新增监听器
    subscribe(callback) {
      let isSubscribed = true

      // 构建新监听器,置于链表结尾
      let listener = (last = {
        callback,
        next: null,
        prev: last, // 链表结尾
      })

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

      // 返回取消订阅的方法
      return function unsubscribe() {
        if (!isSubscribed || first === null) return
        isSubscribed = false

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

根据源码,我们发现了以下几点:

  1. Subscription作为工具类,在底层调用了 redux store 的 subscribe 方法
  2. Subscription 的属性 listeners 维护了一个双向链表结构,用于支持嵌套的订阅

读源码时发现疑问,可以(在源码里)做实验验证自己的想法

阅读 Subscription 源码时,最让我困惑的是它的嵌套订阅设计

  1. 什么时候会发生嵌套订阅?
  2. 一个父组件监听了redux后,它的子组件如果也监听redux,那么子组件的订阅是父组件订阅的子级吗?


读源码时发现疑问时,通常有两种做法:

  1. 继续精读各部分代码,尝试理解 —— 通常比较难,比较费脑细胞
  2. 在源码里做实验验证自己的想法 —— 结果通常比较直观,但是不一定能直接理解,常用作辅助的证据

读者可能会问,怎么在源码里做实验呢?

其实步骤很简单:

  1. 把三方库的源码 git clone 到本地
  2. 使用 create-react-app 创建一个 react 项目
  3. 把三方库的源码复制到我们的 react 项目里
  4. 直接从本地 import 三方库来使用

上截图

97e08af57e85016e8bf339bc49f5832d.jpeg

react-redux-app 是我自己创建的react app,我把 react-redux 的源码复制粘贴到了 src 下 的 react-redux 文件夹

edf63780ff37f9b593b0813268984093.jpeg

注意,在使用 react-redux 时的 import 语句是直接从本地 import

import {useDispatch, useSelector} from './react-redux';

接下来,只要在本地的 react-redux 文件夹里“魔改”源码就能做实验了。

我在 Subscription.js 里createListenerCollection() 函数里添加了一个 length() 方法

    length() { // 计算当前链表的长度
      let listener = first;
      let counter = 0;
      while (listener) {
        counter++;
        listener = listener.next;
      }
      return counter;
    },

并在 subscribe 方法里输出日志

    subscribe(callback) {
      console.log('createListenerCollection subscribe');
      let isSubscribed = true

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

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

      console.log('current length ' + this.length());

      return function unsubscribe() {
        if (!isSubscribed || first === null) return
        isSubscribed = false

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

最后,在这个测试 React APP 里写了几个嵌套的组件

Counter.js

import React from 'react';
import {useDispatch, useSelector} from './react-redux';


const SubCounter = () => {
  const counter = useSelector(state => state);
  const dispatch = useDispatch();

  return (
      <div onClick={() => dispatch({type: 'increase'})}>
        Sub Counter: {counter}
      </div>
  );
};


const Counter = () => {
  const counter = useSelector(state => state);
  const dispatch = useDispatch();

  return (
      <div>
        <div onClick={() => dispatch({type: 'increase'})}>
          Counter: {counter}
        </div>
        <br/>
        <SubCounter/>
      </div>
  );
};

export default Counter;


Timer.js

import React from 'react';
import {useDispatch, useSelector} from './react-redux';


const SubTimer = () => {
  const counter = useSelector(state => state);
  const dispatch = useDispatch();

  return (
      <div onClick={() => dispatch({type: 'increase'})}>
        Sub Timer: {counter}
      </div>
  );
};


const Timer = () => {
  const counter = useSelector(state => state);
  const dispatch = useDispatch();

  return (
      <div>
        <div onClick={() => dispatch({type: 'increase'})}>
          Timer: {counter}
        </div>
        <br/>
        <SubTimer/>
      </div>
  );
};

export default Timer;


App.js

import store from './redux/store'
import {Provider} from './react-redux';
import './App.css';
import Counter from './Counter';
import Timer from './Timer';

function App() {
  return (
      <Provider store={store}>
        <div className="App">
          <header className="App-header">
            <Counter/>
            <br/>
            <Timer/>
          </header>
        </div>
      </Provider>
  );
}

export default App;

在 App 组件中,将会分别渲染一个 Counter组件 和 一个Timer组件, 而 Counter组件Timer组件里除了自身订阅了 redux ,还有子组件 SubCounter 和 SubTimer 也会订阅redux

效果如下

react-redux 源码解读 实验

我在前文提到的疑问

一个父组件监听了redux后,它的子组件如果也监听redux,那么子组件的订阅是父组件订阅的子级吗?

如果是如此的话,那么运行 App 后日志中应该会(至少)两次输出

createListenerCollection subscribe
current length 1

而事实上,打印的结果是

d27fcdcbb18ba6a0dbc1176c523a0527.jpeg

也就是说,以上4个订阅了redux的组件(即 使用了4次 useSelector) 背后产生了4个监听器并且处于同一个监听器链表中!

因此上面的疑问的答案是否定的 —— 订阅的父子级关系与组件的父子级不相关。


事实上,在一般情况下(即 Provider 里不设 context 属性) ,嵌套订阅只发生一次。

还记的 Provider 源码里见到的

const subscription = new Subscription(store)

吗? 这一级是父级订阅。

之后使用useSelector 产生的所有的订阅都是它的子级订阅,具体细节可以查看以下源码:

https://github.com/reduxjs/react-redux/blob/1df5622da1324320d6a1b2135aeba914f1873078/src/hooks/useSelector.js#L102

https://github.com/reduxjs/react-redux/blob/1df5622da1324320d6a1b2135aeba914f1873078/src/hooks/useSelector.js#L104

https://github.com/reduxjs/react-redux/blob/1df5622da1324320d6a1b2135aeba914f1873078/src/hooks/useSelector.js#L17




在读 react-redux 源码时发现的一些有趣的点

在阅读 useSelector.js 时,发现 useSelectorWithStoreAndSubscription 这个内部hook 非常有意思,解读如下

const refEquality = (a, b) => a === b

function useSelectorWithStoreAndSubscription(
  selector, // selector函数
  equalityFn, // 判断前后state是否相等的策略函数(参考设计模式中的策略模式)
  store, // redux store 引用
  contextSub // Context 中的 Subscription 实例
) {
  // 一个 特殊的 useReducer 的用法,调用 forceRender 时会触发组件重新渲染!
  // 拓展知识点:useState 底层实际调用了 useReducer 才导致了重新渲染
  const [, forceRender] = useReducer((s) => s + 1, 0)

  const subscription = useMemo(() => new Subscription(store, contextSub), [
    store,
    contextSub,
  ])

  const latestSubscriptionCallbackError = useRef()
  const latestSelector = useRef()
  const latestStoreState = useRef()
  const latestSelectedState = useRef()

  const storeState = store.getState()
  let selectedState

  try {
    if (
      selector !== latestSelector.current ||
      storeState !== latestStoreState.current ||
      latestSubscriptionCallbackError.current
    ) {
      selectedState = selector(storeState)
    } else {
      selectedState = latestSelectedState.current
    }
  } catch (err) {
    if (latestSubscriptionCallbackError.current) {
      err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
    }

    throw err
  }

  // useIsomorphicLayoutEffect 根据当前环境是 web 还是 react native 有不同的实现
  // 可以参考源码 https://github.com/reduxjs/react-redux/blob/v7.2.2/src/utils/useIsomorphicLayoutEffect.js 和 https://github.com/reduxjs/react-redux/blob/v7.2.2/src/utils/useIsomorphicLayoutEffect.native.js
  useIsomorphicLayoutEffect(() => {
    latestSelector.current = selector
    latestStoreState.current = storeState
    latestSelectedState.current = selectedState
    latestSubscriptionCallbackError.current = undefined
  })

  useIsomorphicLayoutEffect(() => {
    function checkForUpdates() {
      try {
        const newSelectedState = latestSelector.current(store.getState())

        if (equalityFn(newSelectedState, latestSelectedState.current)) {
          // 如果根据 equalityFn 判断前后的 state 不变,就不做任何事,直接返回
          return
        }

        latestSelectedState.current = newSelectedState
      } catch (err) {
        // we ignore all errors here, since when the component
        // is re-rendered, the selectors are called again, and
        // will throw again, if neither props nor store state
        // changed
        latestSubscriptionCallbackError.current = err
      }

      // 如果根据 equalityFn 判断前后的 state 发现变化,则重新渲染 
      forceRender()
    }

    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    checkForUpdates()

    return () => subscription.tryUnsubscribe()
  }, [store, subscription])

  return selectedState
}


还有另一个有趣的现象 —— 在 react-redux 源码中,我发现 connect 相关的源码非常复杂,相反,而Hooks 相关源码简单清晰很多。这是不是从某种程度上说明 React Hooks 的确比 HOC 更好/先进呢?

这边有篇讨论使用 react-redux时该用过 connect HOC 还是 hooks 的文章(英文),有兴趣的读者可以阅读看看 Blogged Answers: Thoughts on React Hooks, Redux, and Separation of Concerns




总结

授人予鱼,不如授人予渔。今天这边文章分享了我自己阅读源码的一些技巧,希望对你们有帮助,增强你们阅读源码的信心。如果你们觉得本文对你们有启发,有帮助,欢迎点赞、喜欢、收藏!



相关链接:

Quick Start | React Redux

reduxjs/react-redux

Blogged Answers: Thoughts on React Hooks, Redux, and Separation of Concerns


猜你喜欢

转载自blog.51cto.com/15064417/2569809