<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Redux - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Redux - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 26 May 2026 20:24:23 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/redux/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 小游戏开发：使用 React 和 Redux Tool Kit 实现俄罗斯方块 ]]>
                </title>
                <description>
                    <![CDATA[ 俄罗斯方块是一款经典小游戏。通过从零开发一款小游戏，我们可以更好地了解开发使用的语言，同时也得到更强烈的“所见即所得”的反馈，所以开发小游戏会是不错的练手项目。 从一个主力语言是JavaScript的开发者角度来分析俄罗斯方块，不同形状的方块的下落、移动、消除其实是 复杂的状态管理和组件通信 。这不刚好就是React和Redux的射程范围嘛！（并没有说它们适合游戏开发） 我将通过这篇博文拆解实现俄罗斯方块的一些核心逻辑，以及Redux Tool Kit [https://redux-toolkit.js.org/]的使用。 你可以在我的Github仓库 [https://github.com/PapayaHUANG/react-redux-tetris]上找到项目代码，也可以通过 Github Page [https://papayahuang.github.io/react-redux-tetris/]来试玩这个游戏。 本文涉及的技术  * JavaScript数组高阶函数  * 使用Redux Tool Kit对React单页面项目进行状态管理  * 使用request ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-tetris-game-with-react-and-redux-tool-kit/</link>
                <guid isPermaLink="false">63fcae780687e3060be26c4b</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Redux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 01 Mar 2023 03:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/03/nik-lUbIun4IL38-unsplash--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>俄罗斯方块是一款经典小游戏。通过从零开发一款小游戏，我们可以更好地了解开发使用的语言，同时也得到更强烈的“所见即所得”的反馈，所以开发小游戏会是不错的练手项目。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/----2023-02-28-14.36.54.gif" alt="游戏动态展示" width="340" height="608" loading="lazy"></p>
<p>从一个主力语言是JavaScript的开发者角度来分析俄罗斯方块，不同形状的方块的下落、移动、消除其实是 <em>复杂的状态管理和组件通信</em>。这不刚好就是React和Redux的射程范围嘛！（并没有说它们适合游戏开发）</p>
<p>我将通过这篇博文拆解实现俄罗斯方块的一些核心逻辑，以及<a href="https://redux-toolkit.js.org/">Redux Tool Kit</a>的使用。</p>
<p>你可以在<a href="https://github.com/PapayaHUANG/react-redux-tetris">我的Github仓库</a>上找到项目代码，也可以通过<a href="https://papayahuang.github.io/react-redux-tetris/">Github Page</a>来试玩这个游戏。</p>
<h2 id="">本文涉及的技术</h2>
<ul>
<li>JavaScript数组高阶函数</li>
<li>使用Redux Tool Kit对React单页面项目进行状态管理</li>
<li>使用<code>requestAnimationFrame</code>实现动画效果</li>
</ul>
<h2 id="">文件结构</h2>
<h3 id="">项目总结构</h3>
<pre><code>|-- src
    |-- App.js
	|-- index.js
	|-- store.js
	|-- features/
		|-- game/
			|-- components/
			|-- game-slice.js
	|-- styles/
	|-- utils/
</code></pre>
<p>我是通过<code>create-react-app</code>创建的项目，游戏界面及功能所有代码都在<code>src</code>路径下：</p>
<ul>
<li>我保留了<code>index.js</code>和<code>App.js</code>，同时添加了<code>store.js</code>来处理Redux store的配置代码；</li>
<li>样式全部放在了<code>styles</code>文件夹中，游戏运行的核心逻辑放到了<code>utils</code>文件夹中；</li>
<li>Redux官方推荐在使用RTK（Redux Tool Kit）时，创建一个<code>features</code>目录，并把不同的功能模块放在这个目录下，每一个功能模块包含自己的组件、reducer和其他相关文件（如与后端通信的代码），所以我在<code>features</code>目录下创建了<code>game</code>文件夹，并在其下创建了<code>components</code>目录和<code>game-slice.js</code>。</li>
</ul>
<h3 id="">组件结构</h3>
<p>我规划的组件包括：</p>
<pre><code>|-- components/
	|-- Board.js
	|-- Control.js
	|-- MessagePopup.js
	|-- NextBlock.js
	|-- ScoreBoard.js
	|-- Square.js
</code></pre>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/----.jpeg" alt="组件展示" width="727" height="1024" loading="lazy"></p>
<ul>
<li><code>Board.js</code>是主游戏区</li>
<li><code>Control.js</code>是按钮控制面板</li>
<li><code>MessagePopup.js</code>是游戏结束时弹出的消息框</li>
<li><code>NextBlock.js</code>是下一个形状提示框</li>
<li><code>ScoreBoard.js</code>显示当前分数和历史最高分数</li>
<li><code>Square.js</code>是组成主游戏区和提示框的小方块</li>
</ul>
<h2 id="ui">核心UI组件搭建的逻辑</h2>
<h3 id="">游戏区域的实现</h3>
<h4 id="">主游戏区框架的实现</h4>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/10-18---1.png" alt="10*18网格" width="499" height="809" loading="lazy"><br>
将主游戏区看作一个10*18的网格，可以通过单项数据为0的<strong>二维数组</strong>实现：</p>
<pre><code class="language-js">//src/utils/index.js

const boardDefault = () =&gt; {
    const rows = 18; //共18行
	const cols = 10; //共10列	
	const array = Array.from(Array(rows), () =&gt; Array(cols).fill(0));
	return array;
};
</code></pre>
<blockquote>
<p>注：在示意图中，我标注了x和y象限，因为之后会使用x和y的值来判断形状在主游戏区的位置。</p>
</blockquote>
<h4 id="">形状的实现</h4>
<p>既然主游戏区可以通过数组来实现，那么不同形状的方块也可以通过数组来实现：</p>
<p>设定每一种形状都用一个4*4的网格作为容器：</p>
<pre><code class="language-js">[

	[0, 0, 0, 0],
	[0, 0, 0, 0],
	[0, 0, 0, 0],
	[0, 0, 0, 0],

],
</code></pre>
<p>在俄罗斯方块游戏中，不同的形状的颜色不同，所以可以用整数<code>1,2,3,4...</code>来区分它们。</p>
<p>使用整数的<strong>另一个好处</strong>是可以和<strong>样式绑定</strong>，先提前设置好每个颜色对应的编号：</p>
<pre><code class="language-css">:root {
	--color-0: #282c34;
	--color-1: #ff6600;
	--color-2: #eec900;
	...
	--color-7: #ff0000;
}

.color-0 {
	background-color: var(--color-0);
}

.color-1 {
	background-color: var(--color-1);
}

...
  
.color-7 {
	background-color: var(--color-7);
}
</code></pre>
<p>然后就可以通过数字<code>1,2,3,4...</code>在前文的4*4网格数组中的排列来表达不同的形状了，需要注意的是每个形状还有旋转后的样式也要考虑进去。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/02/--2023-02-28-15.55.33.png" alt="长条形状" width="124" height="107" loading="lazy"></p>
<p>比方说一个长条形状及它旋转后可以表示为：</p>
<pre><code class="language-js">//I

[
	[
		[0, 0, 0, 0],
		[1, 1, 1, 1],
		[0, 0, 0, 0],
		[0, 0, 0, 0],
	],

	[
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
	],
],
</code></pre>
<ul>
<li>把单个形状及其旋转后的样式放在同一个数组中，引用时就可以通过<strong>索引号</strong>来获得形状的旋转效果。</li>
<li>再把所有形状的数组汇总到一个数组中，这样通过<strong>索引号</strong>就把表示颜色的数字和形状挂钩了。</li>
</ul>
<h3 id="">形状到主游戏区的映射</h3>
<p>我们完成了主游戏区和形状分别的实现，那么当游戏进行时，如何把形状映射到主游戏区呢？</p>
<p>在React中，每一次渲染都相当于一次快照，那么就可以把游戏进行时，形状在主游戏区域的每一次移动都看作独立的一次<em>形状数组到主游戏区数组的映射</em>：</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/03/--2023-03-01-09.54.06.png" alt="快照" width="276" height="385" loading="lazy"><br>
<img src="https://chinese.freecodecamp.org/news/content/images/2023/03/----.jpeg" alt="快照数组" width="734" height="1024" loading="lazy"></p>
<p>具像化我们映射过程就是从以上第一幅图到第二幅图。</p>
<p>代码实现如下：</p>
<pre><code class="language-js">//src/features/game/components/Board.js

//这里的board变量是10*18的由0组成的二位数组
const boardSquare = board.map((rowArray, row) =&gt; {
	return rowArray.map((square, col) =&gt; {
	//col（0-9）row（0-17)
	const blockX = col - x;
	const blockY = row - y;
	//square为0
	let color = square;
	//生成移动的block的颜色
	if (
		blockX &gt;= 0 &amp;&amp;
		blockX &lt; block.length &amp;&amp;
		blockY &gt;= 0 &amp;&amp;
		blockY &lt; block.length
	) {
    //这里的block变量引用了从函数外部取得的表示形状的数组
		color = block[blockY][blockX] === 0 ? color : blockColor;
	}
	//生成key
	const k = row * board[0].length + col;
	return &lt;Square key={k} color={color} /&gt;;
	});
});
</code></pre>
<p>让我们看看上面代码发生了什么：</p>
<ul>
<li><code>x</code>,<code>y</code>是形状起始的横轴（行）和纵轴（列）的坐标，我设定的初始值为<code>x=4, y=-5</code>，这样每一个形状的初始位置就位于主游戏区差不多正上方，随着形状的移动，这两个值也会发生变化；</li>
<li><code>boardSquare</code>函数实际上做了两件事：将我们用数组表达的10*8主游戏区展示到UI；根据主游戏区纵坐标（0-17）、横坐标（0-9）与<code>x</code>、<code>y</code>的差值来定位，将形状数组映射到主游戏区；</li>
<li>首先通过一组嵌套结构的<code>Array.map()</code>的方法，遍历主游戏区所有网格，并将所有网格的<code>color</code>设置为<code>0</code>；</li>
<li>在遍历主游戏区的同时，通过blockX和blockY坐标来锁定位于形状数组内部的方块，如果它的值不为<code>0</code>的话，就返回对应数字的颜色；</li>
<li>因为React要求遍历的组件包含一个独一无二的<code>key</code>，所以通过<code>row * board[0].length + col</code>生成key。</li>
</ul>
<h2 id="">判断边界的逻辑</h2>
<h3 id="">移动范围边界</h3>
<p>形状在主游戏区的移动遵守一定的规则：</p>
<ol>
<li>左右下边都不可越界</li>
<li>如果在主游戏区碰到其他的形状，也不得移动。</li>
</ol>
<p>我们只用检查形状内部每一个方块是否符合以上规则，就可以判断形状是否可以移动了。</p>
<p>此处还需要注意的是，每一形状都被放置在4*4的网格中，所以要排除方块值为<code>0</code>情况：</p>
<pre><code class="language-js">//src/utils/index.js

const canMoveTo = (shape, board, x, y, rotation) =&gt; {
	const currentShape = shapes[shape][rotation];
	for (let row = 0; row &lt; currentShape.length; row++) {
		for (let col = 0; col &lt; currentShape[row].length; col++) {
			if (currentShape[row][col] !== 0) {
				const proposedX = col + x;
				const proposedY = row + y;
			if (proposedY &lt; 0) {
				continue;
				}
			const possibleRow = board[proposedY];
			if (possibleRow) {
				if (
					possibleRow[proposedX] === undefined ||
					possibleRow[proposedX] !== 0
					) {
						return false;
					}
				} else {
				//超越底线
						return false;
					}
				}
			}
		}
	//默认可以移动
	return true;
};
</code></pre>
<ul>
<li>使用<code>canMoveTo</code>函数来判断是否可以移动，函数接受<code>shape</code>（形状数组）、<code>board</code>（主游戏区数组）、<code>x,y</code>（形状定位坐标）、<code>rotation</code>（旋转索引号）作为参数，返回布尔值。</li>
<li>首先默认可以移动；</li>
<li>利用两个嵌套<code>Array.map()</code>遍历形状数组内部的方块，如果方块的值为非<code>0</code>，就根据<code>x</code>和<code>y</code>的值来判断方块位于游戏主区的位置，即<code>proposedX</code>和<code>proposedY</code>；</li>
<li>因为形状的初始情况是位于游戏主区上方，不在主区内，所以存在<code>proposedY</code>小于<code>0</code>的情况，这个时候继续遍历就好；</li>
<li>根据<code>propsedY</code>可以推断出方块位于主游戏区的行数<code>possibleRow</code>，如果不存在<code>possibleRow</code>的话，则说明方块已经超越了主游戏区的<strong>底边</strong>，返回<code>false</code>；</li>
<li>用<code>possibleRow[proposedX]</code>来定位形状方块位于主游戏区该行的哪一个方块，如果值不为<code>0</code>或者为<code>undefined</code>的话，说明主游戏区上<strong>已经有其他形状的方块</strong>或者<strong>超越主游戏区左右边界</strong>，返回<code>false</code>。</li>
</ul>
<h3 id="">消除条件</h3>
<p>当主游戏区一整行填满形状方块就会消除，放在数组的框架下思考，就是检查一行是否全不为<code>0</code>。如果符合条件就删除一整行，然后在数组最开始重新添加一行：</p>
<pre><code class="language-js">//src/utils/index.js

const checkRows = (board) =&gt; {
	for (let row = 0; row &lt; board.length; row++) {
	//检查是否一整行都不为'0'
		if (board[row].indexOf(0) === -1) {
		//如果是，删除这一行
				board.splice(row, 1); 
				//同时在数组最开始添加一行新的数组
				board.unshift(Array(10).fill(0));
			}
	}
}
</code></pre>
<h2 id="">状态管理</h2>
<h3 id="reduxtoolkit">使用Redux Tool Kit</h3>
<p>在这个应用中，我使用Redux Tool Kit（后文简称RTK）来实现状态管理。</p>
<p>RTK对比传统的Redux来说的<strong>优势</strong>在于：</p>
<ol>
<li>更少的样板代码</li>
<li>RTK通过Immer库来实现不可变数据，提高应用程序的性能。</li>
</ol>
<h4 id="slice">创建Slice</h4>
<p>在RTK中，reducer和action集合到slice中，可以通过引用<code>createSlice</code>来实现：</p>
<pre><code class="language-js">//src/features/game/game-slice.js

import {createSlice} from '@reduxjs/toolkit'

const gameSlice = createSlice({
	name:'game',
	initialState: {...},
	reducer: {
		rotate:(state, action) =&gt;{
			...
			},
		moveRight: (state, action) =&gt;{
			...
			},
			...
		},
})

export const {rotate, moveRight} = gameSlice.actions
export default gameSlice.reducer;
</code></pre>
<blockquote>
<p>注：在传统方式中若要修改不可变数据，需要手动复制原始数据，并在副本上进行修改，由于immer库的存在，在编写reducer的时候，可以直接修改<code>state</code>，详细代码可以查看github仓库。</p>
</blockquote>
<h4 id="store">创建store</h4>
<pre><code class="language-js">//src/store.js

import { configureStore } from '@reduxjs/toolkit';
import gameReducer from './features/game/game-slice';

export const store = configureStore({ reducer: { game: gameReducer}})
</code></pre>
<h4 id="">将数据提供给应用</h4>
<pre><code class="language-js">//src/index.js

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

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
	&lt;React.StrictMode&gt;
		&lt;Provider store={store}&gt;
			&lt;App /&gt;
		&lt;/Provider&gt;
	&lt;/React.StrictMode&gt;
);
</code></pre>
<h4 id="">使用数据</h4>
<pre><code class="language-js">//src/features/game/components/Control.js

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

export default function Controls() {
	const dispatch = useDispatch();
	const isRunning = useSelector((state) =&gt; state.game.isRunning);
    const gameOver = useSelector((state) =&gt; state.game.gameOver);

	const handleRotate = () =&gt; {
		if (!isRunning || gameOver) return;
		dispatch(rotate());
	};

	return (
		&lt;div className="controls"&gt;
		{/* rotation */}
			&lt;button
				disabled={!isRunning || gameOver}
				className="control-button"
				onClick={handleRotate}
			&gt;						
			 Rotate
			&lt;/button&gt;
</code></pre>
<h3 id="">下落动画的实现</h3>
<h4 id="deltatime">Delta Time</h4>
<p>Delta time（Δt）是一个计算两个时间点之间经过的时间差的量。在游戏开发中，delta time 指的是每一帧之间的时间间隔。</p>
<p>实现方块的下落动画，会运用到<code>delta time</code>。</p>
<p>这里我们需要计算两次调用<code>requestAnimationFrame</code>的时间差的值，如果累计差值比我们设定的游戏速度要大，就触发<code>moveDown</code>action。</p>
<p>形状的下落实际上就是在一定时间间隔下更新画面（触发moveDown这个action），不就是定时器嘛！</p>
<h4 id="requestanimationframeuserefuseeffect">使用<code>requestAnimationFrame</code>、<code>useRef</code>和<code>useEffect</code>实现定时器的原因</h4>
<ul>
<li><code>requestAnimationFrame</code></li>
</ul>
<p>一般想到定时器会想到<code>setInterval</code>和<code>setTimeout</code>，但实际上使用<code>requestAnimationFrame</code>的性能更好。</p>
<p>这是因为<code>requestAnimationFrame</code>更符合屏幕的刷新率，通常屏幕的刷新率是60Hz，也就意味着浏览器每间隔16.7毫秒（1000毫秒/60）就会更新一次页面。<code>requestAnimationFrame</code>的回调函数会在浏览器准备好下一帧时调用，从而确保每一帧都能够在屏幕刷新之前完成绘制。</p>
<p>但<code>setTimeout</code>和<code>setInterval</code>的时间并不精准，可能会出现卡顿、延迟等性能问题。</p>
<ul>
<li><code>useRef</code></li>
</ul>
<p>React中的函数组件就像一个快照，每次渲染完成后值就会丢失。所以如果需要让函数“记住”值的话，就需要使用额外的钩子。</p>
<p>在俄罗斯方块游戏中，我们需要对形状下落的帧进行记录，这个值不受组件的生命周期影响，因此需要<code>useRef</code>。</p>
<ul>
<li><code>useEffect</code></li>
</ul>
<p>游戏中只需要在特定时间调用控制下落的函数，并且这个过程是<code>Board.js</code>组件的一个副作用，因此需要使用<code>useEffect</code>。</p>
<pre><code class="language-js">//src/utils/useTimer.js

import { useEffect, useRef } from 'react';
import { useDispatch } from 'react-redux';

export function useTimer(flagVal, benchmark, func) {
	const requestRef = useRef();
	const lastUpdateTimeRef = useRef(0);
	const progressTimeRef = useRef(0);
	
	const dispatch = useDispatch();

	const update = (time) =&gt; {
		requestRef.current = requestAnimationFrame(update);
		if (!flagVal) return;
		if (!lastUpdateTimeRef.current) {
			lastUpdateTimeRef.current = time;
		}
	
		const deltaTime = time - lastUpdateTimeRef.current;
		progressTimeRef.current += deltaTime;
	
		if (progressTimeRef.current &gt; benchmark) {
		dispatch(func());
		progressTimeRef.current = 0;
		}
		lastUpdateTimeRef.current = time;
	};

	useEffect(() =&gt; {
		requestRef.current = requestAnimationFrame(update);
		return () =&gt; cancelAnimationFrame(requestRef.current);
	}, [flagVal]);
}
</code></pre>
<ul>
<li>使用<code>useRef</code>创建了三个引用变量<code>requestRef</code>、<code>lastUpdateTimeRef</code>和<code>progressTimeRef</code>，用于存储定时器的状态和进度。</li>
<li><code>requestRef</code>用于存储当前动画帧的引用， <code>lastUpdateTimeRef</code>用于存储上一次更新的时间戳，<code>progressTimeRef</code>用于存储已经过去的时间。</li>
<li>在<code>update</code>函数中实现定时器逻辑。</li>
<li>首先使用<code>requestAnimationFrame</code>来获取下一帧的引用，并存储到<code>requestRef.current</code>中，以实现流畅的动画效果；</li>
<li>然后通过<code>flagVal</code>的值，来决定是否退出函数；</li>
<li>接着如果没有<code>lastUpdateTimeRef.current</code>的值，就将当前的时间戳(<code>time</code>)存储到里面。</li>
<li>创建<code>deltaTime</code>，它的值为当前时间戳和<code>lastUpdateTimeRef.current</code>的时间差，将每次更新得到的<code>deltaTime</code>都添加到<code>progressTimeRef.current</code>中，记录累计过去的时间；</li>
<li>如果这个累计值大于我们设定的时间间隔<code>benchmark</code>，就执行传入的函数，执行完毕后，将累计时间清零；</li>
<li>最后将当前时间戳存储在<code>lastUpdateTimeRef.current</code>中，以便下一次调用时使用。</li>
<li>在使用这个定时器时，我们传入应用的状态属性<code>isRunning</code>作为<code>flagVal</code>来判断是否需要退出函数，<code>speed</code>作为<code>benchmark</code>来和累积值做对比，控制形状的下移动画的速度，<code>moveDown</code>作为<code>func</code>来实现方块下移的动作。</li>
</ul>
<h2 id="">总结</h2>
<p>本项目通过数组实现了基本的游戏静态画面，动画部分使用了<code>requestAnimationFrame</code>，游戏复杂的状态管理借助了更简洁的RTK。</p>
<p>如果你仔细观察项目代码，会发现我还使用了第三方库Redux Persist来存储最高分数。</p>
<p>如果你想要动手实验一下我在文章中提到的逻辑和状态管理，我准备了<a href="https://github.com/PapayaHUANG/react-redux-tetris-starter">starter仓库</a>，它包含了基础的框架和样式，你可以在此基础上搭建属于你的俄罗斯方块，当然你也可以在我的项目基础上添加新的内容，如不同的关卡（下落速度不同），将按键和键盘绑定等。</p>
<p>希望阅读这篇文章让你有所收获，祝你实验愉快！</p>
<h2 id="">代码仓库</h2>
<ul>
<li><a href="https://github.com/PapayaHUANG/react-redux-tetris">完整项目</a></li>
<li><a href="https://github.com/PapayaHUANG/react-redux-tetris-starter">仅UI部分</a></li>
<li><a href="https://papayahuang.github.io/react-redux-tetris/">试玩</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 通过创建计数器应用程序来学习 Redux ]]>
                </title>
                <description>
                    <![CDATA[ Redux 是前端应用的状态管理库。开发者通常会通过 react-redux 包来使用它，不过它也可以在任意前端框架或库中单独使用，如 Vanilla JavaScript。 刚接触 Redux 可能会被吓到，你可能需要一段时间才能适应它，而且可能最终要翻看以前的代码才能找到解决问题的办法。 常遭人诟病的是，Redux 需要使用很多样板代码来管理应用状态。确实如此——在小型应用中使用 Redux 犹如拿大炮打蚂蚁。在 React 项目中，还有其他可用方案，如属性（prop）钻取和上下文（context）；Vue项目中可以使用 VueX；Angular 项目中可以使用 NGRX。 在 React 应用中，Redux 解决的主要问题是，将应用的所有状态保存在一个全局的 store 中，而不是通过（单向的）属性传递来管理状态。这样就可以在任意需要的地方访问到全局状态了，非常方便！ 本文会教你使用 Redux 实现一个 React 计数器应用，让你有足够的基础知识在项目中使用 Redux。 项目设置 首先，运行 npm i redux react-redux  命令，安装 redux ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-redux-by-making-a-counter-application/</link>
                <guid isPermaLink="false">60d98aec240b4e0653a3e052</guid>
                
                    <category>
                        <![CDATA[ Redux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Humilitas ]]>
                </dc:creator>
                <pubDate>Mon, 28 Jun 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/06/reduxpic.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Redux 是前端应用的状态管理库。开发者通常会通过 react-redux 包来使用它，不过它也可以在任意前端框架或库中单独使用，如 Vanilla JavaScript。</p>
<p>刚接触 Redux 可能会被吓到，你可能需要一段时间才能适应它，而且可能最终要翻看以前的代码才能找到解决问题的办法。</p>
<p>常遭人诟病的是，Redux 需要使用很多样板代码来管理应用状态。确实如此——在小型应用中使用 Redux 犹如拿大炮打蚂蚁。在 React 项目中，还有其他可用方案，如属性（prop）钻取和上下文（context）；Vue项目中可以使用 VueX；Angular 项目中可以使用 NGRX。</p>
<p>在 React 应用中，Redux 解决的主要问题是，将应用的所有状态保存在一个全局的 store 中，而不是通过（单向的）属性传递来管理状态。这样就可以在任意需要的地方访问到全局状态了，非常方便！</p>
<p>本文会教你使用 Redux 实现一个 React 计数器应用，让你有足够的基础知识在项目中使用 Redux。</p>
<h2 id="">项目设置</h2>
<p>首先，运行 <code>npm i redux react-redux</code> 命令，安装 redux 和 react-redux。Redux 是独立的包，react-redux 则提供了一些方便使用的钩子（hook）。</p>
<h2 id="">创建目录和文件</h2>
<p>接下来，创建 action 和 reducer。顾名思义，action 就是决定要干什么的对象。另一方面，reducer 会检查执行了哪个 action 并根据 action 更新 state，它接收 state 和 action 作为参数。</p>
<p>我喜欢创建一个名为 <code>Redux</code> 的目录，再在其中创建创建 actions 和 reducers 目录，分别保存 action 和 reducer 文件。</p>
<p>在 reducers 目录中创建 <code>counterReducer.js</code> 文件。很多人通常会使用 switch 语句来实现 reducer，不过你也可以使用 if 语句。示例中会使用 switch。</p>
<p>所以，在 <code>counterReducer.js</code> 中写入以下代码：</p>
<pre><code class="language-js">// src/Redux/reducers/counterReducer.js
const counterReducer = (state = 1, action) =&gt; {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    case "RESET":
      return (state = 0);
    default:
      return state;
  }
};
export default counterReducer;
</code></pre>
<p>这里我将 state 初始值硬编码为 1，你也可以选择从 0 开始计数。</p>
<p>在 switch 语句中，<code>INCREMENT</code> 表示让计数器加 1，<code>DECREMENT</code> 表示让计数器减 1，<code>RESET</code> 表示将计数器重置为 0。</p>
<p>按照惯例会将 action 的类型（action.type）全部大写，但不作强制要求——大小写都行。需要把 counterReducer 导出，以便在其他文件中使用它。</p>
<h2 id="">加入虚拟的身份验证</h2>
<p>为计数器应用添加虚拟的身份验证，让应用功能更进一步。我会在那里向你展示一个关于我的小秘密。所以，除了 <code>counterReducer</code>，还要创建一个 <code>authReducer</code>，其代码如下：</p>
<pre><code class="language-javascript">// src/Redux/reducers/authReducer.js
const authReducer = (state = false, action) =&gt; {
  switch (action.type) {
    case "LOG_IN":
      return true;
    case "LOG_OUT":
      return false;
    default:
      return state;
  }
};
export default authReducer;
</code></pre>
<p>在 <code>authReducer</code> 中，state 被初始化为 false，<code>LOG_IN</code> 会将它设置为 true，<code>LOG_OUT</code> 会将它设置为 false。</p>
<h2 id="combinereducer">如何使用 <code>combineReducer</code> 辅助函数</h2>
<p>因为有多个 reducer，所以需要引入 Redux 提供的 <code>combineReducer</code> 辅助函数。这个函数可以将多个 reducer 合并成单个 reducer，以便传入 <code>createStore</code> API。不必为不了解 <code>createStore</code> API 而困惑——我稍后会介绍。</p>
<p><code>combineReducer</code> 使用方式如下：</p>
<p><code>combineReducer({reducer-a, reducer-b, reducer-c})</code></p>
<p>在 reducer 目录中创建 <code>index</code> 文件（译注：文中提及的默认都是 JavaScript 文件，此处指的是 <code>index.js</code>），引入 <code>combineReducer</code> 和 reducer，并把这些 reducer 组合在一起。</p>
<p>其代码如下：</p>
<pre><code class="language-js">// src/Redux/reducers/index.js
import counterReducer from "./counterReducer";
import authReducer from "./authReducer";
import {combineReducers} from "redux";

const allReducers = combineReducers({
    counter: counterReducer,
    auth: authReducer,
});
export default allReducers;
</code></pre>
<h2 id="store">如何创建全局 Store</h2>
<p>接下来要做的是创建一个 store，我喜欢在 React 的 <code>index</code> 文件中创建它。</p>
<p>需要先从 Redux 中引入 <code>createStore</code> API：</p>
<p><code>import { createStore } from "redux";</code></p>
<p>还需要引入已经组合好的 reducer，然后就可以使用它们来创建 store 了。</p>
<p>为了让我们的计数器功能可用，必须把 Redux 连接到应用中。</p>
<p>在 index 文件里，首先从 react-redux 引入 <code>Provider</code>，并用它包裹整个组件树。Provider 将全局的 state 连接到应用中。<code>Provider</code> 接收一个名为 store 的参数，我们在其中传入之前创建的 store。</p>
<p>index 文件的代码如下：</p>
<pre><code class="language-js">// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { createStore } from "redux";
import allReducers from "./redux/reducers";
import { Provider } from "react-redux";

// 创建 store
const store = createStore(
  allReducers,
);
ReactDOM.render(
  &lt;React.StrictMode&gt;
    &lt;Provider store={store}&gt;
      &lt;App /&gt;
    &lt;/Provider&gt;
  &lt;/React.StrictMode&gt;,
  document.getElementById("root")
);
</code></pre>
<h2 id="action">如何定义 Action</h2>
<p>为了实现增加计数、减少计数、登入、登出，我们必须回到 actions 目录，定义相应的 action，并将它们导出。dispatch 函数可以执行我们定义的 action。</p>
<p>我在 reducer 文件中实现了 <code>INCREMENT</code>、<code>DECREMENT</code>、<code>RESET</code>、<code>LOG_IN</code> 和 <code>LOG_OUT</code> action 的逻辑。actions 目录中的 index 文件里要以同样的标识符（即 <code>type</code>）来使用它们。</p>
<p>actions 目录中，index 文件的代码如下：</p>
<pre><code class="language-js">// src/Redux/actions/index.js
export const increment = () =&gt; {
  return {
    type: "INCREMENT",
  };
};

export const decrement = () =&gt; {
  return {
    type: "DECREMENT",
  };
};

export const reset = () =&gt; {
  return {
    type: "RESET",
  };
};

export const logIn = () =&gt; {
  return {
    type: "LOG_IN",
  };
};

export const logOut = () =&gt; {
  return {
    type: "LOG_OUT",
  };
};

</code></pre>
<h2 id="reducerdispatchaction">如何获取 Reducer 以及 Dispatch Action</h2>
<p>从 <code>react-redux</code> 中引入 <code>useSelector</code>（在其中可以访问整个 state），以便获取 <code>counterReducer</code>。为了看到我之前说过的关于我的小秘密，我们还需要获取 <code>authReducer</code>。</p>
<p>我会在 App.js 文件中操作：</p>
<pre><code class="language-js">import { useSelector } from "react-redux";
</code></pre>
<p>我们需要引入定义好的 action，并从 react-redux 引入 <code>useDispatch</code> 钩子，以便 dispatch action。</p>
<pre><code class="language-js">import { useDispatch } from "react-redux";
</code></pre>
<p>现在 <code>useDispatch</code> 钩子和其它东西都是可用的，我们需要设置“增加”、“减少”、“重置”、“登入”和“登出”按钮。</p>
<p>需要为每个按钮绑定点击事件处理程序，来 dispatch 相应的 action。现在 app.js 的完整代码如下：</p>
<pre><code class="language-js">// src/App.js
import "./App.css";
import { useSelector, useDispatch } from "react-redux";
import {
  decrement,
  increment,
  reset,
  logIn,
  logOut,
} from "./redux/actions/index";

function App() {
  const counter = useSelector((state) =&gt; state.counter);
  const auth = useSelector((state) =&gt; state.auth);
  const dispatch = useDispatch();

  return (
    &lt;div className="App"&gt;
      &lt;h1&gt;
         Hello World &lt;br /&gt; A little Redux Project. YaaY!
      &lt;/h1&gt;
      &lt;h3&gt;Counter&lt;/h3&gt;
      &lt;h3&gt;{counter}&lt;/h3&gt;
      &lt;button onClick={() =&gt; dispatch(increment())}&gt;Increase&lt;/button&gt;
      &lt;button onClick={() =&gt; dispatch(reset())}&gt;Reset&lt;/button&gt;
      &lt;button onClick={() =&gt; dispatch(decrement())}&gt;Decrease&lt;/button&gt;

      &lt;h2&gt;For Logged in users only&lt;/h2&gt;
      &lt;p&gt;Log in to see a secret about me&lt;/p&gt;
      &lt;button onClick={() =&gt; dispatch(logIn())}&gt;Login&lt;/button&gt;
      &lt;button onClick={() =&gt; dispatch(logOut())}&gt;Logout&lt;/button&gt;
      {auth ? (
        &lt;div&gt;
          &lt;p&gt;
            I don't know more than 50% of redux. But if you know 50% of it, you're like a Superman.
          &lt;/p&gt;
        &lt;/div&gt;
      ) : (
        ""
      )}
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>最终结果如下：<br>
<img src="https://www.freecodecamp.org/news/content/images/2021/06/redux.gif" alt="redux" width="600" height="400" loading="lazy"></p>
<p>希望你能找到那个关于我的小秘密。</p>
<h2 id="">总结</h2>
<p>感谢你的阅读，希望你通过本文能够对 Redux 有基本的了解，进而使用它开发一些自己的东西。</p>
<p>想和我联系的话，可以关注我的 <a href="http://twitter.com/koladechris">Twitter</a>，我经常会在上面讨论编码以及 web 开发相关的话题。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/learn-redux-by-making-a-counter-application/">Learn Redux by Making a Counter Application</a>，作者：<a href="https://www.freecodecamp.org/news/author/kayode/">Kayode, Kolade Christopher</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Redux 中的 reducer 到底是什么，以及它为什么叫 reducer？ ]]>
                </title>
                <description>
                    <![CDATA[ Redux 有 3 大核心概念：  * Action  * Reducer  * Store 其中 Action 和 Store 都非常好理解，我们可以直接按照其字面意思，将他们理解为动作和储存。 Action 表示应用中的各类动作或操作，不同的操作会改变应用相应的 state 状态，说白了就是一个带 type 属性的对象。 Store 则是我们储存state 的地方。我们通过 redux 当中的 createStore 方法来创建一个 store，它提供 3 个主要的方法，在这里我们可以模拟一下 createStore 的源码： // 以下代码示例来自redux官方教程 const createStore = (reducer) => {   let state;   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-exactly-is-reducer-in-redux/</link>
                <guid isPermaLink="false">5d357931fbfdee429dc5f08f</guid>
                
                    <category>
                        <![CDATA[ Redux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 余博伦 ]]>
                </dc:creator>
                <pubDate>Mon, 17 May 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/07/web-developer.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><strong>Redux </strong>有 3 大核心概念：</p><ul><li>Action</li><li>Reducer</li><li>Store</li></ul><p>其中 <strong>Action </strong>和 <strong>Store </strong>都非常好理解，我们可以直接按照其字面意思，将他们理解为<strong>动作</strong>和<strong>储存</strong>。</p><p><strong>Action </strong>表示应用中的各类动作或操作，不同的操作会改变应用相应的 <strong>state </strong>状态，说白了就是一个带 <strong>type </strong>属性的对象。</p><p><strong>Store </strong>则是我们储存<strong>state </strong>的地方。我们通过 <strong>redux </strong>当中的 <strong>createStore </strong>方法来创建一个 <strong>store</strong>，它提供 3 个主要的方法，在这里我们可以模拟一下 <strong>createStore </strong>的源码：</p><pre><code class="language-js">// 以下代码示例来自redux官方教程
const createStore = (reducer) =&gt; {
  let state;
  let listeners = [];
  // 用来返回当前的state
  const getState = () =&gt; state;
  // 根据action调用reducer返回新的state并触发listener
  const dispatch = (action) =&gt; {
      state = reducer(state, action);
      listeners.forEach(listener =&gt; listener());
    };
  /* 这里的subscribe有两个功能
   * 调用 subscribe(listener) 会使用listeners.push(listener)注册一个listener
   * 而调用 subscribe 的返回函数则会注销掉listener
   */
  const subscribe = (listener) =&gt; {
      listeners.push(listener);
      return () =&gt; {
        listeners = listeners.filter(l =&gt; l !== listener);
      };
    };

  return { getState, dispatch, subscribe };
};
</code></pre><p>那么剩下的这个 <strong>reducer </strong>连翻译都很难翻译的东西应该怎么理解呢？</p><p>我们注意到 <strong>redux </strong>的官方文档里专门有一句对 reducer 命名的解释：</p><p>It's called a reducer because it's the type of function you would pass to <a href="https://link.zhihu.com/?target=https%3A//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce" rel="nofollow noreferrer">Array.prototype.reduce(reducer, ?initialValue)</a></p><p>中文版的文档把这一句话翻译成了：</p><p>之所以称作 <strong>reducer</strong> 是因为它将被传递给 <a href="https://link.zhihu.com/?target=https%3A//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce" rel="nofollow noreferrer">Array.prototype.reduce(reducer, ?initialValue)</a> 方法。</p><p>我们要注意到这里的中文翻译理解其实是错误的。原文的本意并不是说 <strong>redux </strong>里的 <strong>reducer </strong>会被传入到 <strong>Array.prototype.reduce</strong> 这个方法中。真的要翻译的话，应该翻译为：</p><p>之所以将这样的函数称之为 reducer，是因为这种函数与被传入 <strong>Array.prototype.reduce(reducer, ?initialValue)</strong> 的回调函数属于相同的类型。</p><p>为什么这么讲呢？我们来看一下 <strong>array </strong>使用 <strong>reduce </strong>方法的具体例子：</p><pre><code class="language-js">// 以下代码示例来自 MDN JavaScript 文档

/* 这里的callback是和reducer非常相似的函数
 * arr.reduce(callback, [initialValue])
 */

var sum = [0, 1, 2, 3].reduce(function(acc, val) {
  return acc + val;
}, 0);
// sum = 6

/* 注意这当中的回调函数 (prev, curr) =&gt; prev + curr
 * 与我们redux当中的reducer模型 (previousState, action) =&gt; newState 看起来是不是非常相似呢
 */
[0, 1, 2, 3, 4].reduce( (prev, curr) =&gt; prev + curr );
</code></pre><p>我们再来看一个简单的具体的 reducer 的例子：</p><pre><code class="language-js">// 以下代码示例来自redux官方教程

// reducer接受state和action并返回新的state
const todos = (state = [], action) =&gt; {
  // 根据不同的action.type对state进行不同的操作，一般都是用switch语句来实现，当然你要用if...else我也拦不住你
  switch (action.type) {
    case 'ADD_TODO':
      return [
        // 这里是ES7里的对象展开运算符语法
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    // 不知道是什么action类型的话则返回默认state
    default:
      return state;
  }
};
</code></pre><p>如果非要翻译 <strong>reducer </strong>的话，可以将其翻译为缩减器或者折叠器？</p><p>为了进一步加深理解，我们再了解一下 <strong>reduce </strong>是什么东西，这个名词其实是函数式编程当中的一个术语，在更多的情况下，<strong>reduce </strong>操作被称为 <strong>Fold </strong>折叠（下图来自维基百科）。</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-a32cb02859ea4ac0f8f50f1ec885d85c_hd.png" class="kg-image" alt="v2-a32cb02859ea4ac0f8f50f1ec885d85c_hd" width="600" height="400" loading="lazy"></figure><p>直观起见，我们还是拿 JavaScript 来理解。<strong>reduce </strong>属于一种高阶函数，它将其中的回调函数 <strong>reducer </strong>递归应用到数组的所有元素上并返回一个独立的值。这也就是“缩减”或“折叠”的意义所在了。</p><p><strong>总而言之一句话，redux 当中的 reducer 之所以叫做 reducer，是因为它和 Array.prototype.reduce 当中传入的回调函数非常相似。</strong></p><p>当然，如果你认为这种命名不完美容易产生歧义，你完全可以去给 <strong>redux </strong>提交一个 PR，提供一种更加恰当的命名方式。</p><p>有任何好的意见或者是建议欢迎在评论区参与讨论，如果文中有任何错误也欢迎在评论区批评指正。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Redux 源码整体架构 ]]>
                </title>
                <description>
                    <![CDATA[ 1. 本文阅读最佳方式 把我的redux源码仓库 git clone https://github.com/lxchuan12/redux-analysis.git克隆下来，顺便 star一下我的redux源码学习仓库 (opens new window) [https://github.com/lxchuan12/redux-analysis]^_^。跟着文章节奏调试和示例代码调试，用chrome 动手调试印象更加深刻。文章长段代码不用细看，可以调试时再细看。看这类源码文章百遍，可能不如自己多调试几遍。也欢迎加我微信交流ruochuan12。 2. git subtree 管理子仓库 写了很多源码文章，vuex、axios、koa等都是使用新的仓库克隆一份源码在自己仓库中。 虽然电脑可以拉取最新代码，看到原作者的git信息。但上传到 github后。读者却看不到原仓库作者的git信息了。于是我找到了git submodules 方案，但并不是很适合。再后来发现了git subtree。 简单说下 npm package和git subtree的区别。 npm package是单向 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-the-overall-architecture-of-redux-source-code/</link>
                <guid isPermaLink="false">6016ba226183a70540156321</guid>
                
                    <category>
                        <![CDATA[ Redux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Sat, 30 Jan 2021 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/safar-safarov-MSN8TFhJ0is-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="1-">1. 本文阅读最佳方式</h2><p>把我的<code>redux</code>源码仓库 <code>git clone https://github.com/lxchuan12/redux-analysis.git</code>克隆下来，顺便<code>star</code>一下<a href="https://github.com/lxchuan12/redux-analysis" rel="noopener noreferrer">我的redux源码学习仓库 (opens new window)</a>^_^。<strong>跟着文章节奏调试和示例代码调试，用<code>chrome</code>动手调试印象更加深刻</strong>。文章长段代码不用细看，可以调试时再细看。看这类源码文章百遍，可能不如自己多调试几遍。也欢迎加我微信交流<code>ruochuan12</code>。</p><h2 id="2-git-subtree-">2. git subtree 管理子仓库</h2><p>写了很多源码文章，<code>vuex</code>、<code>axios</code>、<code>koa</code>等都是使用新的仓库克隆一份源码在自己仓库中。 虽然电脑可以拉取最新代码，看到原作者的git信息。但上传到<code>github</code>后。读者却看不到原仓库作者的<code>git</code>信息了。于是我找到了<code>git submodules</code> 方案，但并不是很适合。再后来发现了<code>git subtree</code>。</p><p>简单说下 <code>npm package</code>和<code>git subtree</code>的区别。 <code>npm package</code>是单向的。<code>git subtree</code>则是双向的。</p><p>具体可以查看这篇文章<a href="https://segmentfault.com/a/1190000003969060" rel="noopener noreferrer">@德来（原有赞大佬）：用 Git Subtree 在多个 Git 项目间双向同步子项目，附简明使用手册(opens new window)</a></p><p>学会了<code>git subtree</code>后，我新建了<code>redux-analysis</code>项目后，把<code>redux</code>源码<code>4.x</code>（截止至2020年06月13日，<code>4.x</code>分支最新版本是<code>4.0.5</code>，<code>master</code>分支是<code>ts</code>，文章中暂不想让一些不熟悉<code>ts</code>的读者看不懂）分支克隆到了我的项目里的一个子项目，得以保留<code>git</code>信息。</p><p>对应命令则是：</p><pre><code class="language-bash">git subtree add --prefix=redux https://github.com/reduxjs/redux.git 4.x
</code></pre><h2 id="3-redux-">3. 调试 redux 源码准备工作</h2><p>之前，我在知乎回答了一个问题<a href="https://www.zhihu.com/question/350289336/answer/910970733" rel="noopener noreferrer">若川：一年内的前端看不懂前端框架源码怎么办？ (opens new window)</a>推荐了一些资料，阅读量还不错，大家有兴趣可以看看。主要有四点：<br>1.借助调试<br>2.搜索查阅相关高赞文章<br>3.把不懂的地方记录下来，查阅相关文档<br>4.总结</p><p>看源码调试很重要，所以我的每篇源码文章都详细描述（也许有人看来是比较啰嗦...）如何调试源码。</p><p><strong>断点调试要领：</strong><br><strong>赋值语句可以一步按<code>F10</code>跳过，看返回值即可，后续详细再看。</strong><br><strong>函数执行需要断点按<code>F11</code>跟着看，也可以结合注释和上下文倒推这个函数做了什么。</strong><br><strong>有些不需要细看的，直接按<code>F8</code>走向下一个断点</strong><br><strong>刷新重新调试按<code>F5</code></strong></p><p>调试源码前，先简单看看 <code>redux</code> 的工作流程，有个大概印象。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-26.png" class="kg-image" alt="image-26" width="844" height="379" loading="lazy"></figure><h3 id="3-1-rollup-sourcemap-">3.1 rollup 生成 sourcemap 便于调试</h3><p>修改<code>rollup.config.js</code>文件，<code>output</code>输出的配置生成<code>sourcemap</code>。</p><pre><code class="language-js">// redux/rollup.config.js 有些省略
const sourcemap = {
  sourcemap: true,
};

output: {
    // ...
    ...sourcemap,
}
</code></pre><p>安装依赖</p><pre><code class="language-bash">git clone http://github.com/lxchuan12/redux-analysis.git
cd redux-analysi/redux
npm i
npm run build
# 编译结束后会生成 sourcemap .map格式的文件到 dist、es、lib 目录下。
</code></pre><p>仔细看看<code>redux/examples</code>目录和<code>redux/README</code>。</p><p>这时我在根路径下，新建文件夹<code>examples</code>，把原生<code>js</code>写的计数器<code>redux/examples/counter-vanilla/index.html</code>，复制到<code>examples/index.html</code>。同时把打包后的包含<code>sourcemap</code>的<code>redux/dist</code>目录，复制到<code>examples/dist</code>目录。</p><p>修改<code>index.html</code>的<code>script</code>的<code>redux.js</code>文件为<code>dist中的路径</code>。</p><p>为了便于区分和调试后续<code>html</code>文件，我把<code>index.html</code>重命名为<code>index.1.redux.getState.dispatch.html</code>。</p><pre><code class="language-bash"># redux-analysis 根目录
# 安装启动服务的npm包
npm i -g http-server
cd examples
hs -p 5000
</code></pre><p>就可以开心的调试啦。可以直接克隆我的项目<code>git clone http://github.com/lxchuan12/redux-analysis.git</code>。本地调试，动手实践，容易消化吸收。</p><h2 id="4-redux-">4. 通过调试计数器例子的学习 redux 源码</h2><p>接着我们来看<code>examples/index.1.redux.getState.dispatch.html</code>文件。先看<code>html</code>部分。只是写了几个 <code>button</code>，比较简单。</p><pre><code class="language-html">&lt;div&gt;
    &lt;p&gt;
    Clicked: &lt;span id="value"&gt;0&lt;/span&gt; times
    &lt;button id="increment"&gt;+&lt;/button&gt;
    &lt;button id="decrement"&gt;-&lt;/button&gt;
    &lt;button id="incrementIfOdd"&gt;Increment if odd&lt;/button&gt;
    &lt;button id="incrementAsync"&gt;Increment async&lt;/button&gt;
    &lt;/p&gt;
&lt;/div&gt;
</code></pre><p><code>js部分</code>，也比较简单。声明了一个<code>counter</code>函数，传递给<code>Redux.createStore(counter)</code>，得到结果<code>store</code>，而<code>store</code>是个对象。<code>render</code>方法渲染数字到页面。用<code>store.subscribe(render)</code>订阅的<code>render</code>方法。还有<code>store.dispatch({type: 'INCREMENT' })</code>方法，调用<code>store.dispatch</code>时会触发<code>render</code>方法。这样就实现了一个计数器。</p><pre><code class="language-js">function counter(state, action) {
    if (typeof state === 'undefined') {
        return 0
    }

    switch (action.type) {
        case 'INCREMENT':
        return state + 1
        case 'DECREMENT':
        return state - 1
        default:
        return state
    }
}

var store = Redux.createStore(counter)
var valueEl = document.getElementById('value')

function render() {
    valueEl.innerHTML = store.getState().toString()
}
render()
store.subscribe(render)

document.getElementById('increment')
.addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT' })
})

// 省略部分暂时无效代码...
</code></pre><p>思考：看了这段代码，你会在哪打断点来调试呢。</p><pre><code class="language-js">// 四处可以断点来看
// 1.
var store = Redux.createStore(counter)
// 2.
function render() {
valueEl.innerHTML = store.getState().toString()
}
render()
// 3.
store.subscribe(render)
// 4.
store.dispatch({ type: 'INCREMENT' })
</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-27.png" class="kg-image" alt="image-27" width="1312" height="649" loading="lazy"></figure><p>图中的右边<code>Scope</code>，有时需要关注下，会显示闭包、全局环境、当前环境等变量，还可以显示函数等具体代码位置，能帮助自己理解代码。</p><p>断点调试，按<code>F5</code>刷新页面后，按<code>F8</code>，把鼠标放在<code>Redux</code>和<code>store</code>上。</p><p>可以看到<code>Redux</code>上有好几个方法。分别是：</p><ul><li>__DO_NOT_USE__ActionTypes: {INIT: "@@redux/INITu.v.d.u.6.r", REPLACE: "@@redux/REPLACEg.u.u.7.c", PROBE_UNKNOWN_ACTION: ƒ}</li><li>applyMiddleware: ƒ applyMiddleware() 函数是一个增强器，组合多个中间件，最终增强<code>store.dispatch</code>函数，<code>dispatch</code>时，可以串联执行所有中间件。</li><li>bindActionCreators: ƒ bindActionCreators(actionCreators, dispatch) 生成actions，主要用于其他库，比如<code>react-redux</code>。</li><li>combineReducers: ƒ combineReducers(reducers) 组合多个<code>reducers</code>，返回一个总的<code>reducer</code>函数。</li><li>compose: ƒ compose() 组合多个函数，从右到左，比如：compose(f, g, h) 最终得到这个结果 (...args) =&gt; f(g(h(...args))).</li><li>createStore: ƒ createStore(reducer, preloadedState, enhancer) 生成 <code>store</code> 对象</li></ul><p>再看<code>store</code>也有几个方法。分别是：</p><ul><li>dispatch: ƒ dispatch(action) 派发动作，也就是把<code>subscribe</code>收集的函数，依次遍历执行</li><li>subscribe: ƒ subscribe(listener) 订阅收集函数存在数组中，等待触发<code>dispatch</code>依次执行。返回一个取消订阅的函数，可以取消订阅监听。</li><li>getState: ƒ getState() 获取存在<code>createStore</code>函数内部闭包的对象。</li><li>replaceReducer: ƒ replaceReducer(nextReducer) 主要用于<code>redux</code>开发者工具，对比当前和上一次操作的异同。有点类似时间穿梭功能。</li><li>Symbol(observable): ƒ observable()</li></ul><p>也就是<a href="https://redux.org.js/" rel="noopener noreferrer">官方文档redux.org.js (opens new window)</a>上的 <code>API</code>。</p><p>暂时不去深究每一个<code>API</code>的实现。重新按<code>F5</code>刷新页面，断点到<code>var store = Redux.createStore(counter)</code>。一直按<code>F11</code>，先走一遍主流程。</p><h3 id="4-1-redux-createsotre">4.1 Redux.createSotre</h3><p><code>createStore</code> 函数结构是这样的，是不是看起来很简单，最终返回对象<code>store</code>，包含<code>dispatch</code>、<code>subscribe</code>、<code>getState</code>、<code>replaceReducer</code>等方法。</p><pre><code class="language-js">// 省略了若干代码
export default function createStore(reducer, preloadedState, enhancer) {
    // 省略参数校验和替换
    // 当前的 reducer 函数
    let currentReducer = reducer
    // 当前state
    let currentState = preloadedState
    // 当前的监听数组函数
    let currentListeners = []
    // 下一个监听数组函数
    let nextListeners = currentListeners
    // 是否正在dispatch中
    let isDispatching = false
    function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice()
        }
    }
    function getState() {
        return currentState
    }
    function subscribe(listener) {}
    function dispatch(action) {}
    function replaceReducer(nextReducer) {}
    function observable() {}
    // ActionTypes.INIT @@redux/INITu.v.d.u.6.r
    dispatch({ type: ActionTypes.INIT })
    return {
        dispatch,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
    }
}
</code></pre><h3 id="4-2-store-dispatch-action-">4.2 store.dispatch(action)</h3><pre><code class="language-js">function dispatch(action) {
    // 判断action是否是对象，不是则报错
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // 判断action.type 是否存在，没有则报错
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 不是则报错
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
        // 调用完后置为 false
      isDispatching = false
    }
    //  把 收集的函数拿出来依次调用
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i &lt; listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    // 最终返回 action
    return action
  }
</code></pre><pre><code class="language-js">var store = Redux.createStore(counter)
</code></pre><p>上文调试完了这句。</p><p>继续按<code>F11</code>调试。</p><pre><code class="language-js">function render() {
    valueEl.innerHTML = store.getState().toString()
}
render()
</code></pre><h3 id="4-3-store-getstate-">4.3 store.getState()</h3><p><code>getState</code>函数实现比较简单。</p><pre><code class="language-js">function getState() {
    // 判断正在dispatch中，则报错
    if (isDispatching) {
        throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
            'The reducer has already received the state as an argument. ' +
            'Pass it down from the top reducer instead of reading it from the store.'
        )
    }
    // 返回当前的state
    return currentState
}
</code></pre><h3 id="4-4-store-subscribe-listener-">4.4 store.subscribe(listener)</h3><p>订阅监听函数，存放在数组中，<code>store.dispatch(action)</code>时遍历执行。</p><pre><code class="language-js">function subscribe(listener) {
    // 订阅参数校验不是函数报错
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 正在dispatch中，报错
    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
      )
    }
    // 订阅为 true
    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    // 返回一个取消订阅的函数
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }
      // 正在dispatch中，则报错
      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
        )
      }
      // 订阅为 false
      isSubscribed = false

      ensureCanMutateNextListeners()
    //   找到当前监听函数
      const index = nextListeners.indexOf(listener)
    //   在数组中删除
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }
</code></pre><p>到这里，我们就调试学习完了<code>Redux.createSotre</code>、<code>store.dispatch</code>、<code>store.getState</code>、<code>store.subscribe</code>的源码。</p><p>接下来，我们写个中间件例子，来调试中间件相关源码。</p><h2 id="5-redux-">5. Redux 中间件相关源码</h2><p>中间件是重点，面试官也经常问这类问题。</p><h3 id="5-1-redux-applymiddleware-middlewares-">5.1 Redux.applyMiddleware(...middlewares)</h3><h4 id="5-1-1-logger-">5.1.1 准备 logger 例子调试</h4><p>为了调试<code>Redux.applyMiddleware(...middlewares)</code>，我在<code>examples/js/middlewares.logger.example.js</code>写一个简单的<code>logger</code>例子。分别有三个<code>logger1</code>，<code>logger2</code>，<code>logger3</code>函数。由于都是类似，所以我在这里只展示<code>logger1</code>函数。</p><pre><code class="language-js">// examples/js/middlewares.logger.example.js
function logger1({ getState }) {
  return next =&gt; action =&gt; {
      console.log('will dispatch--1--next, action:', next, action)

      // Call the next dispatch method in the middleware chain.
      const returnValue = next(action)

      console.log('state after dispatch--1', getState())

      // This will likely be the action itself, unless
      // a middleware further in chain changed it.
      return returnValue
  }
}
// 省略 logger2、logger3
</code></pre><p><code>logger</code>中间件函数做的事情也比较简单，返回两层函数，<code>next</code>就是下一个中间件函数，调用返回结果。为了让读者能看懂，我把<code>logger1</code>用箭头函数、<code>logger2</code>则用普通函数。</p><p><code>写好例子后</code>，我们接着来看怎么调试<code>Redux.applyMiddleware(...middlewares))</code>源码。</p><pre><code class="language-bash">cd redux-analysis &amp;&amp; hs -p 5000
# 上文说过npm i -g http-server
</code></pre><p>打开<code>http://localhost:5000/examples/index.2.redux.applyMiddleware.compose.html</code>，按<code>F12</code>打开控制台，</p><p>先点击加号操作+1，把结果展示出来。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-28.png" class="kg-image" alt="image-28" width="1316" height="605" loading="lazy"></figure><p>从图中可以看出，<code>next</code>则是下一个函数。先1-2-3，再3-2-1这样的顺序。</p><p>这种也就是我们常说的中间件，面向切面编程（AOP）。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-29.png" class="kg-image" alt="image-29" width="470" height="411" loading="lazy"></figure><p>接下来调试，在以下语句打上断点和一些你觉得重要的地方打上断点。</p><pre><code class="language-js">// examples/index.2.redux.applyMiddleware.compose.html
var store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))
</code></pre><h4 id="5-1-2-redux-applymiddleware-middlewares-">5.1.2 Redux.applyMiddleware(...middlewares) 源码</h4><pre><code class="language-js">// redux/src/applyMiddleware.js
/**
 * ...
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
export default function applyMiddleware(...middlewares) {
  return createStore =&gt; (...args) =&gt; {
    const store = createStore(...args)
    let dispatch = () =&gt; {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) =&gt; dispatch(...args)
    }
    const chain = middlewares.map(middleware =&gt; middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
</code></pre><pre><code class="language-js">// redux/src/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
  // 省略参数校验
  // 如果第二个参数`preloadedState`是函数，并且第三个参数`enhancer`是undefined，把它们互换一下。
  if (typeof preloadedState === 'function' &amp;&amp; typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // enhancer 也就是`Redux.applyMiddleware`返回的函数
    // createStore 的 args 则是 `reducer, preloadedState`
    /**
     * createStore =&gt; (...args) =&gt; {
            const store = createStore(...args)
            return {
              ...store,
               dispatch,
            }
        }
     ** /
    // 最终返回增强的store对象。
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 省略后续代码
}
</code></pre><p>把接收的中间件函数<code>logger1</code>, <code>logger2</code>, <code>logger3</code>放入到 了<code>middlewares</code>数组中。<code>Redux.applyMiddleware</code>最后返回两层函数。 把中间件函数都混入了参数<code>getState</code>和<code>dispatch</code>。</p><pre><code class="language-js">// examples/index.2.redux.applyMiddleware.compose.html
var store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))
</code></pre><p>最后这句其实是返回一个增强了<code>dispatch</code>的<code>store</code>对象。</p><p>而增强的<code>dispatch</code>函数，则是用<code>Redux.compose(...functions)</code>进行串联起来执行的。</p><h3 id="5-2-redux-compose-functions-">5.2 Redux.compose(...functions)</h3><pre><code class="language-js">export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg =&gt; arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) =&gt; (...args) =&gt; a(b(...args)))
}
</code></pre><pre><code class="language-js">// applyMiddleware.js
dispatch = compose(...chain)(store.dispatch)
// compose
funcs.reduce((a, b) =&gt; (...args) =&gt; a(b(...args)))
</code></pre><p>这两句可能不是那么好理解，可以断点多调试几次。我把箭头函数转换成普通函数。</p><pre><code class="language-js">funcs.reduce(function(a, b){
  return function(...args){
    return a(b(...args));
  };
});
</code></pre><p>其实<code>redux</code>源码中注释很清晰了，这个<code>compose</code>函数上方有一堆注释，其中有一句：组合多个函数，从右到左，比如：<code>compose(f, g, h)</code> 最终得到这个结果 <code>(...args) =&gt; f(g(h(...args)))</code>.</p><h4 id="5-2-1-compose-">5.2.1 compose 函数演化</h4><p>看<code>Redux.compose(...functions)</code>函数源码后，还是不明白，不要急不要慌，吃完鸡蛋还有汤。仔细来看如何演化而来，先来简单看下如下需求。</p><p>传入一个数值，计算数值乘以10再加上10，再减去2。</p><p>实现起来很简单。</p><pre><code class="language-js">const calc = (num) =&gt; num * 10 + 10 - 2;
calc(10); // 108
</code></pre><p>但这样写有个问题，不好扩展，比如我想乘以<code>10</code>时就打印出结果。 为了便于扩展，我们分开写成三个函数。</p><pre><code class="language-js">const multiply = (x) =&gt; {
   const result = x * 10;
   console.log(result);
   return result;
};
const add = (y) =&gt; y + 10;
const minus = (z) =&gt; z - 2;

// 计算结果
console.log(minus(add(multiply(10))));
// 100
// 108
// 这样我们就把三个函数计算结果出来了。
</code></pre><p>再来实现一个相对通用的函数，计算这三个函数的结果。</p><pre><code class="language-js">const compose = (f, g, h) =&gt; {
  return function(x){
    return f(g(h(x)));
  }
}
const calc = compose(minus, add, multiply);
console.log(calc(10));
// 100
// 108
</code></pre><p>这样还是有问题，只支持三个函数。我想支持多个函数。 我们了解到数组的<code>reduce</code>方法就能实现这样的功能。 前一个函数</p><pre><code class="language-js">// 我们常用reduce来计算数值数组的总和
[1,2,3,4,5].reduce((pre, item, index, arr) =&gt; {
  console.log('(pre, item, index, arr)', pre, item, index, arr);
  // (pre, item, index, arr) 1 2 1 (5) [1, 2, 3, 4, 5]
  // (pre, item, index, arr) 3 3 2 (5) [1, 2, 3, 4, 5]
  // (pre, item, index, arr) 6 4 3 (5) [1, 2, 3, 4, 5]
  // (pre, item, index, arr) 10 5 4 (5) [1, 2, 3, 4, 5]
  return pre + item;
});
// 15
</code></pre><p><code>pre</code> 是上一次返回值，在这里是数值<code>1,3,6,10</code>。在下一个例子中则是匿名函数。</p><pre><code class="language-js">function(x){
  return a(b(x));
}
</code></pre><p><code>item</code>是<code>2,3,4,5</code>，在下一个例子中是<code>minus、add、multiply</code>。</p><pre><code class="language-js">const compose = (...funcs) =&gt; {
  return funcs.reduce((a, b) =&gt; {
    return function(x){
      return a(b(x));
    }
  })
}
const calc = compose(minus, add, multiply);
console.log(calc(10));
// 100
// 108
</code></pre><p>而<code>Redux.compose(...functions)</code>其实就是这样，只不过中间件是返回双层函数罢了。</p><p>所以返回的是<code>next函数</code>，他们串起来执行了，形成了中间件的洋葱模型。 人们都说一图胜千言。我画了一个相对简单的<code>redux</code>中间件原理图。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-30.png" class="kg-image" alt="image-30" width="1286" height="731" loading="lazy"></figure><p>如果还不是很明白，建议按照我给出的例子，多调试。</p><pre><code class="language-bash">cd redux-analysis &amp;&amp; hs -p 5000
# 上文说过npm i -g http-server
</code></pre><p>打开<code>http://localhost:5000/examples/index.3.html</code>，按<code>F12</code>打开控制台调试。</p><h4 id="5-2-2-compose-">5.2.2 前端框架的 compose 函数的实现</h4><p><strong>lodash</strong>源码中 <code>compose</code>函数的实现，也是类似于数组的<code>reduce</code>，只不过是内部实现的<code>arrayReduce</code></p><p><a href="https://juejin.im/post/5d767e1d6fb9a06b032025ea#heading-20" rel="noopener noreferrer">引用自我的文章：学习lodash源码整体架构(opens new window)</a></p><pre><code class="language-js">// lodash源码
function baseWrapperValue(value, actions) {
	var result = value;
	// 如果是lazyWrapper的实例，则调用LazyWrapper.prototype.value 方法，也就是 lazyValue 方法
	if (result instanceof LazyWrapper) {
		result = result.value();
	}
	// 类似 [].reduce()，把上一个函数返回结果作为参数传递给下一个函数
	return arrayReduce(actions, function(result, action) {
		return action.func.apply(action.thisArg, arrayPush([result], action.args));
	}, result);
}
</code></pre><p><strong>koa-compose</strong>源码也有<code>compose</code>函数的实现。实现是循环加<code>promise</code>。 由于代码比较长我就省略了，具体看链接<a href="https://juejin.im/post/5e69925cf265da571e262fe6#heading-7" rel="noopener noreferrer">若川：学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理 (opens new window)</a>小节 <code>koa-compose 源码</code>（洋葱模型实现）</p><h2 id="6-redux-combinereducers-reducers-">6. Redux.combineReducers(reducers)</h2><p>打开<code>http://localhost:5000/examples/index.4.html</code>，按<code>F12</code>打开控制台，按照给出的例子，调试接下来的<code>Redux.combineReducers(reducers)</code>和<code>Redux.bindActionCreators(actionCreators, dispatch)</code>具体实现。由于文章已经很长了，这两个函数就不那么详细解释了。</p><p><code>combineReducers</code>函数简单来说就是合并多个<code>reducer</code>为一个函数<code>combination</code>。</p><pre><code class="language-js">export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i &lt; reducerKeys.length; i++) {
    const key = reducerKeys[i]

    // 省略一些开发环境判断的代码...

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }

  // 经过一些处理后得到最后的finalReducerKeys
  const finalReducerKeys = Object.keys(finalReducers)

  // 省略一些开发环境判断的代码...

  return function combination(state = {}, action) {
    // ... 省略开发环境的一些判断

   // 用 hasChanged变量 记录前后 state 是否已经修改
    let hasChanged = false
    // 声明对象来存储下一次的state
    const nextState = {}
    //遍历 finalReducerKeys
    for (let i = 0; i &lt; finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      // 执行 reducer
      const nextStateForKey = reducer(previousStateForKey, action)

      // 省略容错代码 ...

      nextState[key] = nextStateForKey
      // 两次 key 对比 不相等则发生改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 最后的 keys 数组对比 不相等则发生改变
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
</code></pre><h2 id="7-redux-bindactioncreators-actioncreators-dispatch-">7. Redux.bindActionCreators(actionCreators, dispatch)</h2><p>如果第一个参数是一个函数，那就直接返回一个函数。如果是一个对象，则遍历赋值，最终生成<code>boundActionCreators</code>对象。</p><pre><code class="language-js">function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // ... 省略一些容错判断

  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
</code></pre><p><code>redux</code>所提供的的<code>API</code> 除了<code>store.replaceReducer(nextReducer)</code>没分析，其他都分析了。</p><h2 id="8-vuex-redux-">8. vuex 和 redux 简单对比</h2><h3 id="8-1-">8.1 源码实现形式</h3><p>从源码实现上来看，<code>vuex</code>源码主要使用了构造函数，而<code>redux</code>则是多用函数式编程、闭包。</p><h3 id="8-2-">8.2 耦合度</h3><p><code>vuex</code> 与 <code>vue</code> 强耦合，脱离了<code>vue</code>则无法使用。而<code>redux</code>跟<code>react</code>没有关系，所以它可以使用于小程序或者<code>jQuery</code>等。如果需要和<code>react</code>使用，还需要结合<code>react-redux</code>库。</p><h3 id="8-3-">8.3 扩展</h3><pre><code class="language-js">// logger 插件，具体实现省略
function logger (store) {
  console.log('store', store);
}
// 作为数组传入
new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  plugins: process.env.NODE_ENV !== 'production'
    ? [logger]
    : []
})
// vuex 源码 插件执行部分
class Store{
  constructor(){
    // 把vuex的实例对象 store整个对象传递给插件使用
    plugins.forEach(plugin =&gt; plugin(this))
  }
}
</code></pre><p><code>vuex</code>实现扩展则是使用插件形式，而<code>redux</code>是中间件的形式。<code>redux</code>的中间件则是AOP（面向切面编程），<code>redux</code>中<code>Redux.applyMiddleware()</code>其实也是一个增强函数，所以也可以用户来实现增强器，所以<a href="https://www.redux.org.cn/docs/introduction/Ecosystem.html" rel="noopener noreferrer"><code>redux</code>生态 (opens new window)</a>比较繁荣。</p><h3 id="8-4-">8.4 上手难易度</h3><p>相对来说，<code>vuex</code>上手相对简单，<code>redux</code>相对难一些，<code>redux</code>涉及到一些函数式编程、高阶函数、纯函数等概念。</p><h2 id="9-">9. 总结</h2><p>文章主要通过一步步调试的方式循序渐进地讲述<code>redux</code>源码的具体实现。旨在教会读者调试源码，不惧怕源码。</p><p>面试官经常喜欢考写一个<code>redux</code>中间件，说说<code>redux</code>中间件的原理。</p><pre><code class="language-js">function logger1({ getState }) {
  return next =&gt; action =&gt; {
      const returnValue = next(action)
      return returnValue
  }
}
</code></pre><pre><code class="language-js">const compose = (...funcs) =&gt; {
  if (funcs.length === 0) {
    return arg =&gt; arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  // 箭头函数
  // return funcs.reduce((a, b) =&gt; (...args) =&gt; a(b(...args)))
  return funcs.reduce((a, b) =&gt; {
    return function(x){
      return a(b(x));
    }
  })
}
</code></pre><pre><code class="language-js">const enhancerStore = Redux.create(reducer, Redux.applyMiddleware(logger1, ...))
enhancerStore.dispatch(action)
</code></pre><p>用户触发<code>enhancerStore.dispatch(action)</code>是增强后的，其实就是第一个中间件函数，中间的<code>next</code>是下一个中间件函数，最后<code>next</code>是没有增强的<code>store.dispatch(action)</code>。</p><p>最后再来看张<code>redux</code>工作流程图</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-31.png" class="kg-image" alt="image-31" width="1440" height="1080" loading="lazy"></figure><p>是不是就更理解些了呢。</p><p>如果读者发现有不妥或可改善之处，再或者哪里没写明白的地方，欢迎评论指出。另外觉得写得不错，对你有些许帮助，可以评论、转发分享，也是对我的一种支持，非常感谢呀。<strong>要是有人说到怎么读源码，正在读文章的你能推荐我的源码系列文章，那真是太好了。</strong></p><p>欢迎阅读我的<a href="https://www.lxchuan12.cn/">更多文章</a>。</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
