普通视图

发现新文章,点击刷新页面。
昨天 — 2025年6月29日首页

useReducer+useContext +扁平化

作者 Mr_李先生
2025年6月29日 17:13

背景

作者之前是在百度实习,从事的是小程序和h5的开发,虽然H5用的hooks,但是对于某些hooks的用法不是很熟悉,对于大型Saas系统状态管理不是很熟悉,因此写下这篇来回顾总结上次遇到知识点不足的问题

useReducer

useReducer接收三个参数

  • reducer 函数:指定如何更新状态的还原函数,它必须是纯函数,以 state 和 dispatch 为参数,并返回下一个状态。
  • 初始状态:初始状态的计算值。
  • (可选的)初始化参数:用于返回初始状态。如果未指定,初始状态将设置为 initialArg;如果有指定,初始状态将被设置为调用init(initialArg)的结果。

useReducer返回两个参数

  • 当前的状态:当前状态。在第一次渲染时,它会被设置为init(initialArg)或 initialArg(如果没有 init 的情况下)。
  • dispatch:调度函数,用于调用 reducer 函数,以更新状态并触发重新渲染。

useContext

使用createContext创建上下文

export const ContextDemo = createContext(initDefault);

并且在外层使用Context.Provider来包裹子组件,使用的context 则是

    const {state, setSku} = useContext(ContextDemo);

扁平化

我们扁平化的目的就是在于,我们写代码更改某个值,希望使用的函数是类似于setXXX, 然而对于reducer来讲就是需要dispatch,但是这样在页面写dipspatch,有可能会充斥大量dispatch xxx类似这样的代码,影响阅读,同时调用多个相同的dispatch会增加代码行数,因此需要扁平化。使得我们context里面有state以及更改state中相应某个值。
这里扁平化逻辑实则对reducerdispatch进行二次封装。

//useContext.tsx
import React, {createContext, FC, ReactNode, useReducer} from 'react';
import {dispatcher, initState, reducer as useReducers} from './useReducer';
import {Action} from './interface';
const reducer = (State, action: Action) => {
 // 这里更改数据对数据进行二次封装和更改,应该可以算作策略设计模式吗?
 const fn = useReducers[action.type];
 return fn(State, action.value);
};
export const ContextDemo = createContext({});

export const Provider: FC<{children: ReactNode}> = ({children}) => {
 const [contextValue, dispatch] = useReducer(reducer, initState);
 // 这里传入dispatch,对dispatch进行二次封装
 const setter = dispatcher(contextValue, dispatch);

 return (
     <ContextDemo.Provider
         value={{
             state: contextValue,
             ...setter
         }}
     >
         {children}
     </ContextDemo.Provider>
 );
};

在上面代码中,我们关注到在useReducer的第一个参数中对数据进行二次封装抽象,这样避免了很多switch case等等 同时我们在Provider(14行)对dispatch进行二次封装 这两个封装是针对同一个useReducer的,因此我将他们写在useReducer.tsx文件里面

import {State, Action} from './interface';

export const initState: State = {
   sku: '',
   image: '',
   dockerUrl: ''
};

const setter = (s: any, props: string, value: any) => {
   return {
       ...s,
       [props]: value
   };
};
type action = 'setSku' | 'setImage' | 'setImageUrl';
type Reducer = (s: State, value?: any) => State;
export const reducer: {[key in action]: Reducer} = {
   setSku: (state: any, val: any) => setter(state, 'sku', val),
   setImage: (state: any, val: any) => setter(state, 'image', val),
   setImageUrl: (state: any, val: any) => setter(state, 'imageUrl', val)
};

// 外部传入 dispatch 和 state ,在这里二次封装
export const dispatcher = (state: State, dispatch: (val: {type: action; value?: string}) => void) => {
   return {
       setSku(value: string) {
           dispatch({
               type: 'setSku',
               value: value
           });
           // 这里可以return 做些其他逻辑
       },
       setImage(value: string) {
           dispatch({
               type: 'setImage',
               value: value
           });
       },
       setImageUrl(value: string) {
           dispatch({
               type: 'setImageUrl',
               value: value
           });
       }
   };
};

  1. 先说对dispatch进行二次封装,useContext.tsx里面 我们通过将dispatch传入来派发数据 同时返回一个对象,然后在Provider将对象解构,也就是useContext.tsx文件里面的15 、21行这样我们在全局就注入了setXXX函数
  2. 对于State的修改我们要记住,reducer必须返回新的state 不然不会更新数据,既然扁平化,我在useReducer里面 写了reducer对象(不是函数目的在于映射),代码useContext.tsx使用的reudcer是第行的reducer函数,在reducer函数里面接收到action和state(注意reducer as useReducers} from './useReducer';作者能力有限,命名不是很规范)通过action的type,映射到useReducer.tsx的reducer对象的更改某个值的函数,这样封装完成了对数据的修改和返回

总结

这样扁平化之后我们context上下文就是state以及各种setXXX的函数,useContext与useReducer的结合用法在掘金有很多文章,作者只不过进一步扁平化罢了, 实际场景中函数有副作用,比如获取数据,结合hooks的方案,常用的就是使用react-query这个库,还有很多状态管理库 比如zustand redux等,道阻且长。点个赞吧(作者25届自己菜,感觉不适合这一行,但又不知道干啥,就只能这样了)

理解 Tree Shaking:原理与那些“甩不掉”的代码陷阱

作者 CAD老兵
2025年6月28日 21:54

在现代前端构建中,“Tree Shaking”是一项至关重要的优化技术。它的目标是在打包过程中移除那些未被使用的“死代码”,从而减小最终 bundle 的体积,提升加载性能。

本文将深入剖析 Tree Shaking 的原理,并结合实际代码示例讲解一些常见的“甩不掉”的坑。


一、什么是 Tree Shaking?

Tree Shaking 是一种 静态分析(Static Analysis) 技术,用于在构建过程中移除未被引用的 ES 模块导出内容。

前提条件:

  • 必须使用 ES Module(即 ES6 的 import / export 语法)
  • 必须启用 生产模式构建(如 webpack 的 mode: 'production'
  • 构建工具支持 Tree Shaking(如 Webpack、Rollup、esbuild、Vite 等)

工作原理简述:

  1. 构建工具解析模块依赖关系;
  2. 标记哪些导出被实际引用;
  3. 移除未被引用的导出代码(Dead Code Elimination);
  4. 压缩器(如 Terser)进一步移除无效语句。

二、一个简单示例

// math.js
export function add(a, b) {
  return a + b;
}

export function sub(a, b) {
  return a - b;
}
// main.js
import { add } from './math.js';

console.log(add(2, 3));

构建后,如果使用了 Tree Shaking,sub() 方法将不会被打包进最终 bundle。


三、Tree Shaking 常见失效案例

1. 使用 CommonJS(require

// bad-math.js (CommonJS)
exports.add = (a, b) => a + b;
exports.sub = (a, b) => a - b;
// main.js
const { add } = require('./bad-math');
console.log(add(1, 2));

问题:Tree Shaking 无法分析 CommonJS 模块导出内容(因其导出是动态对象,静态分析无法进行)。

建议:统一使用 import/export 的 ES Module 语法。


2. 导出对象或数组,统一挂载多个功能

// utils.js
export const utils = {
  a: () => console.log('A'),
  b: () => console.log('B'),
};
// main.js
import { utils } from './utils.js';
utils.a();

问题:尽管只用了 a(),但整个 utils 对象都会被打包进去。

建议:尽可能按功能粒度导出:

export const a = () => console.log('A');
export const b = () => console.log('B');

3. 间接引用导致 Tree Shaking 失效(以及如何正确写)

Tree Shaking 对代码的分析是基于静态导入路径的。当你通过中间模块“转发”某个模块的内容时,特别是使用聚合对象导出时,可能会导致 Tree Shaking 无法判断你真正使用了什么。

// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
// index.js
import * as math from './math.js';
export default math;
// main.js
import math from './index.js';
math.add(1, 2);

问题

你可能只用了 add,但由于 math 是整个对象,打包工具无法静态分析 math.sub 是否会在运行时被用到。因此它会保留整个模块的内容(包括 sub)。

建议:在写模块聚合的时候

  • 避免 default export 聚合对象
  • 使用 export { xxx } from 的形式将原始模块静态 re-export
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
// index.js
export { add, sub } from './math.js'; // 直接 re-export 静态绑定
// main.js
import { add } from './index.js';
add(1, 2);

这样,构建工具可以静态分析 add 的引用。sub 没被引用,会被安全地移除。


4. 使用副作用模块(Side Effects)

js
// side.js
console.log('I will always run');
export const x = 1;

问题:即使你不使用 x,这个模块仍然会被打包进来,因为它有副作用(console.log())。

解决方案

  • package.json 中配置 "sideEffects": false,告诉构建工具默认模块无副作用;
  • 对于有副作用的模块明确标注 "sideEffects": ["./side.js"]

5. 动态语法阻止静态分析

export const tools = {};

tools['run'] = () => console.log('run');
tools['build'] = () => console.log('build');

问题:动态属性访问使构建工具无法判断哪个属性被使用,只能保留整个对象。

建议:使用静态结构进行导出。


四、如何判断 Tree Shaking 是否生效?

方法一:查看构建产物体积

  • 使用 webpack-bundle-analyzersource-map-explorer 等工具分析 bundle;
  • 比较仅引入部分函数和引入整包的体积差异。

方法二:查看产物代码

  • 打开打包后文件;
  • 搜索你不希望打包进去的函数名,看是否仍然存在。

五、总结

错误用法 原因 建议修复方式
使用 require() 动态导入,静态分析失败 改为 ES Module 的 import
聚合导出为对象 无法移除未用属性 单独导出每个函数/变量
使用 export * + 再导出 无法精准分析具体依赖 直接从源文件 import 所需内容
动态属性赋值 静态分析工具无法识别 改为静态结构导出
模块含副作用 构建工具不敢随意删除 使用 sideEffects 明确标注

六、结语

Tree Shaking 并不是魔法,而是一种依赖静态语法分析的工具优化手段。理解它的工作机制并遵循相关规则,才能让你的项目瘦身成功,跑得更快!

如果你还在为构建体积太大而苦恼,或许是时候检查一下你的代码结构,是不是也存在一些“甩不掉”的“树枝”呢?

❌
❌