阅读视图

发现新文章,点击刷新页面。

React从入门到出门第四章 组件通讯与全局状态管理

G9oezq6XEAECJ26.jpeg 大家好~ 前面我们已经掌握了 React 19 的函数组件、Hooks、虚拟 DOM 等核心基础,今天咱们聚焦 React 应用开发中的核心问题——组件间的通信

在 React 应用中,组件不是孤立的,它们需要通过“传递数据”协同工作:小到父子组件间的简单数据传递,大到跨层级、多组件共享的全局状态管理,都是日常开发中高频遇到的场景。

很多新手会在“该用哪种传参方式”“什么时候需要全局状态”这些问题上困惑。今天这篇文章,我们就从“组件关系”出发,按“简单到复杂”的顺序,拆解 React 19 中的组件传参方案,再深入讲解全局状态管理的核心思路与常用方案,结合代码示例和图例,让大家能根据实际场景灵活选择~

一、先明确:组件关系决定传参方案

在 React 应用中,组件间的关系主要分为 3 类:父子组件、兄弟组件、跨层级组件(祖孙/远亲) 。不同关系对应的传参难度和方案不同,我们先通过一个图例理清组件关系模型:

核心原则:能局部传参就不全局——局部传参(如父子、兄弟)简单直观、性能开销小,全局状态(如 Redux、Context)适合共享数据多、跨层级广的场景,避免过度设计。

二、React 19 组件传参方案全解析(按场景分类)

1. 父子组件传参:最基础的“props 向下+回调向上”

父子组件是最常见的关系,传参核心依赖 props:父组件通过 props 向子组件传递数据(向下传),子组件通过 props 接收父组件的回调函数,将数据传递回父组件(向上传),形成“双向通信”。

场景 1:父传子(数据向下传递)

核心逻辑:父组件在使用子组件时,通过“属性=值”的形式传递数据,子组件通过参数 props 接收(可解构简化)。

// 父组件:传递数据给子组件
function Parent() {
  const parentData = "我是父组件的数据";
  const userInfo = { name: "小明", age: 22 };

  return (
    <div>
      <h3>父组件</h3>
      {/* 通过 props 传递基础类型、对象等数据 */}
      <Child 
        msg={parentData} 
        user={userInfo}
        isShow={true}
      />
    </div>
  );
}

// 子组件:接收并使用父组件传递的数据
// 方式 1:直接通过 props 参数接收
// function Child(props) {
//   return <p>父组件传递的消息:{props.msg}</p>;
// }

// 方式 2:解构 props,更简洁(推荐)
function Child({ msg, user, isShow }) {
  return (
    <div>
      <h4>子组件</h4>
      {isShow && <p>父组件传递的消息:{msg}</p>}
      <p>用户姓名:{user.name},年龄:{user.age}</p>
    </div>
  );
}

注意:props 是只读的!子组件不能直接修改 props 的值(如不能写 user.age = 23),若需修改,需通过“子传父”的方式让父组件更新数据。

场景 2:子传父(数据向上传递)

核心逻辑:父组件传递一个“回调函数”给子组件,子组件触发该函数时,将需要传递的数据作为参数传入,父组件在回调函数中接收并处理数据。

// 父组件:传递回调函数给子组件
function Parent() {
  const [childData, setChildData] = useState("");

  // 回调函数:接收子组件传递的数据
  const handleChildMsg = (data) => {
    console.log("子组件传递的数据:", data);
    setChildData(data); // 更新父组件状态
  };

  return (
    <div>
      <h3>父组件</h3>
      <p>子组件传递的消息:{childData}</p>
      {/* 传递回调函数 */}
      <Child onSendMsg={handleChildMsg} />
    </div>
  );
}

// 子组件:触发回调函数,传递数据给父组件
function Child({ onSendMsg }) {
  const [inputValue, setInputValue] = useState("");

  const handleSubmit = () => {
    // 触发父组件传递的回调函数,传入数据
    onSendMsg(inputValue);
    setInputValue(""); // 清空输入框
  };

  return (
    <div>
      <h4>子组件</h4>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入要传递给父组件的内容"
      />
      <button onClick={handleSubmit} style={{ marginLeft: "10px" }}>
        发送给父组件
      </button>
    </div>
  );
}

场景 3:父子双向绑定(表单常见)

核心逻辑:结合“父传子”和“子传父”,父组件传递数据给子组件(表单默认值),子组件通过回调函数将修改后的值传递回父组件,实现“数据同步”。

// 父组件:管理表单状态
function Parent() {
  const [username, setUsername] = useState("");

  // 接收子组件修改后的值,更新父组件状态
  const handleUsernameChange = (newValue) => {
    setUsername(newValue);
  };

  return (
    <div>
      <h3>父组件:{username}</h3>
      {/* 传递状态(默认值)和回调函数 */}
      <Input 
        value={username} 
        onChange={handleUsernameChange} 
        placeholder="请输入用户名"
      />
    </div>
  );
}

// 子组件:表单输入组件
function Input({ value, onChange, placeholder }) {
  // 输入变化时,触发回调函数传递新值
  const handleInput = (e) => {
    onChange(e.target.value);
  };

  return (
    <input
      type="text"
      value={value}
      onChange={handleInput}
      placeholder={placeholder}
      style={{ width: "300px", height: "30px", padding: "0 8px" }}
    />
  );
}

2. 兄弟组件传参:通过父组件中转

兄弟组件间没有直接的通信通道,需通过“共同的父组件”作为中转:先让“发送数据的兄弟”将数据传递给父组件,再由父组件将数据传递给“接收数据的兄弟”。

用图例展示通信流程:

实战案例:兄弟组件数据同步

// 父组件:作为兄弟组件的中转
function Parent() {
  const [sharedData, setSharedData] = useState("");

  // 接收 Child1 传递的数据
  const handleDataFromChild1 = (data) => {
    setSharedData(data);
  };

  return (
    <div>
      <h3>父组件(中转)</h3>
      {/* 兄弟 1:发送数据 */}
      <Child1 onSendData={handleDataFromChild1} />
      {/* 兄弟 2:接收数据 */}
      <Child2 receivedData={sharedData} />
    </div>
  );
}

// 兄弟 1:发送数据的组件
function Child1({ onSendData }) {
  const [inputValue, setInputValue] = useState("");

  const handleSend = () => {
    onSendData(inputValue);
    setInputValue("");
  };

  return (
    <div>
      <h4>兄弟组件 1(发送方)</h4>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入要传递给兄弟的数据"
      />
      <button onClick={handleSend} style={{ marginLeft: "10px" }}>
        发送给兄弟
      </button>
    </div>
  );
}

// 兄弟 2:接收数据的组件
function Child2({ receivedData }) {
  return (
    <div>
      <h4>兄弟组件 2(接收方)</h4>
      <p>收到兄弟 1 的数据:{receivedData || "暂无数据"}</p>
    </div>
  );
}

3. 跨层级组件传参:Context API(React 19 原生方案)

当组件层级很深(如“爷爷→爸爸→儿子→孙子”),或者跨多个层级传递数据时,用 props 层层传递(即“props drilling”)会非常繁琐,且代码可维护性差。这时可以用 React 原生的 Context API 解决。

Context API 的核心作用:创建一个“全局数据容器”,让所有后代组件都能直接访问容器中的数据,无需层层传递 props

使用步骤:3 步搞定 Context 传参

  1. 创建 Context:用 createContext 创建一个 Context 对象(可设置默认值);
  2. 提供 Context:用 Context.Provider 包裹需要共享数据的组件树,通过 value 属性传入共享数据;
  3. 消费 Context:后代组件用 useContext Hook 直接获取共享数据。

实战案例:跨层级共享主题状态

import { createContext, useContext, useState } from 'react';

// 步骤 1:创建 Context(默认值仅在无 Provider 时生效)
const ThemeContext = createContext("light");

// 步骤 2:提供 Context 的组件(通常是顶层组件)
function App() {
  const [theme, setTheme] = useState("light");

  // 共享的方法:切换主题
  const toggleTheme = () => {
    setTheme(prev => prev === "light" ? "dark" : "light");
  };

  // 要共享的数据和方法(封装成对象)
  const contextValue = {
    theme,
    toggleTheme
  };

  return (
    // 步骤 2:用 Provider 包裹组件树,传入共享数据
    <ThemeContext.Provider value={contextValue}>
      <div style={{ padding: "20px" }}>
        <h2>顶层组件(提供 Context)</h2>
        <Parent /> {/* 父组件 */}
      </div>
    </ThemeContext.Provider>
  );
}

// 父组件(中间层级,无需传递 theme 相关 props)
function Parent() {
  return (
    <div style={{ border: "1px solid #ccc", padding: "20px", marginTop: "10px" }}>
      <h3>父组件(中间层级)</h3>
      <Child /> {/* 子组件 */}
    </div>
  );
}

// 子组件(后代组件,直接消费 Context)
function Child() {
  // 步骤 3:用 useContext 获取共享数据
  const { theme, toggleTheme } = useContext(ThemeContext);

  // 根据主题设置样式
  const containerStyle = {
    border: "1px solid #ccc",
    padding: "20px",
    marginTop: "10px",
    background: theme === "light" ? "#fff" : "#333",
    color: theme === "light" ? "#333" : "#fff"
  };

  return (
    <div style={containerStyle}>
      <h4>子组件(消费 Context)</h4>
      <p>当前主题:{theme}</p>
      <button onClick={toggleTheme}>切换主题</button>
    </div>
  );
}

Context API 适合场景:共享“变化不频繁”的全局数据(如主题、用户登录状态、语言设置)。如果需要频繁更新数据,且涉及复杂逻辑(如多组件修改同一状态),建议结合 useReducer 或专门的全局状态管理库。

三、全局状态管理:从 Context+useReducer 到专业库

当应用规模扩大,需要共享的数据增多、状态更新逻辑复杂(如购物车、用户中心、多页面共享筛选条件)时,单纯的 Context API 就不够用了(比如多个组件修改 Context 数据时,逻辑分散,难以维护)。这时就需要“全局状态管理”方案。

React 19 中常用的全局状态管理方案有 3 类:Context+useReducer(原生方案)、Redux Toolkit(生态主流)、Zustand/Jotai(轻量方案) 。我们分别讲解它们的核心思路和适用场景。

1. 原生方案:Context+useReducer(适合中小型应用)

useReducer 是 React 内置的 Hooks,用于处理“复杂状态逻辑”——当状态更新依赖于前一个状态、或者有多个子值需要同步更新时,useReducer 比 useState 更清晰。

Context+useReducer 的核心思路:用 useReducer 管理全局状态的更新逻辑,用 Context 提供和共享状态与 dispatch 方法,实现“状态集中管理+全局共享”。

实战案例:全局购物车状态管理

import { createContext, useContext, useReducer } from 'react';

// 步骤 1:创建 Context
const CartContext = createContext();

// 步骤 2:定义 reducer 函数(集中处理状态更新逻辑)
// reducer 接收两个参数:当前状态 state、动作 action(包含 type 和 payload)
function cartReducer(state, action) {
  switch (action.type) {
    // 新增商品
    case "ADD_ITEM":
      // 先判断商品是否已存在
      const existingItem = state.find(item => item.id === action.payload.id);
      if (existingItem) {
        // 已存在:更新数量
        return state.map(item => 
          item.id === action.payload.id 
            ? { ...item, count: item.count + 1 } 
            : item
        );
      } else {
        // 不存在:新增商品
        return [...state, { ...action.payload, count: 1 }];
      }
    // 删除商品
    case "REMOVE_ITEM":
      return state.filter(item => item.id !== action.payload.id);
    // 清空购物车
    case "CLEAR_CART":
      return [];
    default:
      return state;
  }
}

// 步骤 3:创建 Provider 组件,提供状态和 dispatch
function CartProvider({ children }) {
  // 用 useReducer 管理状态:初始状态为空数组
  const [cartState, dispatch] = useReducer(cartReducer, []);

  // 共享的数据和方法
  const contextValue = {
    cartState, // 购物车状态
    // 封装 dispatch 方法(让组件更易用,无需直接写 action)
    addItem: (item) => dispatch({ type: "ADD_ITEM", payload: item }),
    removeItem: (id) => dispatch({ type: "REMOVE_ITEM", payload: { id } }),
    clearCart: () => dispatch({ type: "CLEAR_CART" })
  };

  return (
    <CartContext.Provider value={contextValue}>
      {children}
    </CartContext.Provider>
  );
}

// 步骤 4:消费全局状态的组件
// 组件 1:商品列表(添加商品到购物车)
function ProductList() {
  const { addItem } = useContext(CartContext);

  // 模拟商品数据
  const products = [
    { id: 1, name: "React 实战教程", price: 99 },
    { id: 2, name: "Vue 实战教程", price: 89 },
    { id: 3, name: "TypeScript 教程", price: 79 }
  ];

  return (
    <div>
      <h3>商品列表</h3>
      <div style={{ display: "flex", gap: "20px", margin: "10px 0" }}>
        {products.map(product => (
          <div key={product.id} style={{ border: "1px solid #ccc", padding: "10px" }}>
            <p>{product.name}</p>
            <p>价格:{product.price} 元</p>
            <button onClick={() => addItem(product)}>加入购物车</button>
          </div>
        ))}
      </div>
    </div>
  );
}

// 组件 2:购物车(展示/删除/清空商品)
function Cart() {
  const { cartState, removeItem, clearCart } = useContext(CartContext);

  // 计算总价格
  const totalPrice = cartState.reduce((total, item) => {
    return total + item.price * item.count;
  }, 0);

  return (
    <div style={{ marginTop: "20px", border: "1px solid #ccc", padding: "20px" }}>
      <h3>购物车({cartState.length} 种商品)</h3>
      {cartState.length === 0 ? (
        <p>购物车为空</p>
      ) : (
        <>
          {cartState.map(item => (
            <div key={item.id} style={{ display: "flex", gap: "10px", margin: "10px 0" }}>
              <p>{item.name} × {item.count}</p>
              <p>{item.price * item.count} 元</p>
              <button onClick={() => removeItem(item.id)}>删除</button>
            </div>
          ))}
          <p>总价:{totalPrice} 元</p>
          <button onClick={clearCart} style={{ marginTop: "10px" }}>清空购物车</button>
        </>
      )}
    </div>
  );
}

// 根组件
function App() {
  return (
    <CartProvider>
      <div style={{ padding: "20px" }}>
        <h2>全局购物车管理(Context+useReducer)</h2>
        <ProductList />
        <Cart />
      </div>
    </CartProvider>
  );
}

2. 生态主流:Redux Toolkit(适合大型复杂应用)

Redux 是 React 生态中最成熟的全局状态管理库,而 Redux Toolkit(RTK)是官方推荐的 Redux 简化方案(解决了原生 Redux 代码繁琐、配置复杂的问题)。

核心优势:状态集中管理、可预测性强、支持中间件(如异步请求)、调试工具完善,适合大型应用中多团队协作、复杂状态逻辑的场景。

核心概念与使用步骤(简化)

  1. 安装依赖:npm install @reduxjs/toolkit react-redux
  2. 创建切片(Slice):用 createSlice 定义状态初始值、reducer 函数(同步/异步);
  3. 创建 Store:用 configureStore 整合所有切片;
  4. 提供 Store:用 Provider(来自 react-redux)包裹根组件;
  5. 消费 Store:用 useSelector 获取状态,用 useDispatch 触发状态更新。

实战案例:Redux Toolkit 实现购物车

// 1. 安装依赖后,创建切片(src/features/cart/cartSlice.js)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 模拟异步请求:从接口获取商品数据(异步 action)
export const fetchProducts = createAsyncThunk(
  'cart/fetchProducts',
  async () => {
    const res = await fetch('https://api.example.com/products');
    return res.json();
  }
);

// 创建切片
const cartSlice = createSlice({
  name: 'cart', // 切片名称(唯一)
  initialState: {
    products: [], // 商品列表
    cartItems: [], // 购物车商品
    loading: false, // 加载状态
    error: null // 错误信息
  },
  reducers: {
    // 同步 action:添加商品到购物车
    addToCart: (state, action) => {
      const existingItem = state.cartItems.find(item => item.id === action.payload.id);
      if (existingItem) {
        existingItem.count += 1;
      } else {
        state.cartItems.push({ ...action.payload, count: 1 });
      }
    },
    // 同步 action:从购物车删除商品
    removeFromCart: (state, action) => {
      state.cartItems = state.cartItems.filter(item => item.id !== action.payload);
    },
    // 同步 action:清空购物车
    clearCart: (state) => {
      state.cartItems = [];
    }
  },
  // 处理异步 action 的状态(pending/fulfilled/rejected)
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.loading = false;
        state.products = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  }
});

// 导出同步 action
export const { addToCart, removeFromCart, clearCart } = cartSlice.actions;

// 导出 reducer
export default cartSlice.reducer;

// 2. 创建 Store(src/app/store.js)
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from '../features/cart/cartSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer // 整合 cart 切片
  }
});

// 3. 根组件提供 Store(src/App.js)
import { Provider } from 'react-redux';
import { store } from './app/store';
import ProductList from './features/cart/ProductList';
import Cart from './features/cart/Cart';

function App() {
  return (
    <Provider store={store}> {/* 提供 Store */}
      <div style={{ padding: "20px" }}>
        <h2>Redux Toolkit 购物车</h2>
        <ProductList />
        <Cart />
      </div>
    </Provider>
  );
}

// 4. 消费 Store:商品列表组件(src/features/cart/ProductList.js)
import { useDispatch, useSelector } from 'react-redux';
import { fetchProducts, addToCart } from './cartSlice';
import { useEffect } from 'react';

function ProductList() {
  const dispatch = useDispatch();
  const { products, loading, error } = useSelector(state => state.cart);

  // 组件挂载时获取商品数据
  useEffect(() => {
    dispatch(fetchProducts());
  }, [dispatch]);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误:{error}</p>;

  return (
    <div>
      <h3>商品列表</h3>
      <div style={{ display: "flex", gap: "20px", margin: "10px 0" }}>
        {products.map(product => (
          <div key={product.id} style={{ border: "1px solid #ccc", padding: "10px" }}>
            <p>{product.name}</p>
            <p>价格:{product.price} 元</p>
            <button onClick={() => dispatch(addToCart(product))}>加入购物车</button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default ProductList;

// 5. 消费 Store:购物车组件(src/features/cart/Cart.js)
import { useDispatch, useSelector } from 'react-redux';
import { removeFromCart, clearCart } from './cartSlice';

function Cart() {
  const dispatch = useDispatch();
  const { cartItems } = useSelector(state => state.cart);

  const totalPrice = cartItems.reduce((total, item) => {
    return total + item.price * item.count;
  }, 0);

  return (
    <div style={{ marginTop: "20px", border: "1px solid #ccc", padding: "20px" }}>
      <h3>购物车({cartItems.length} 种商品)</h3>
      {cartItems.length === 0 ? (
        <p>购物车为空</p>
      ) : (
        <>
          {cartItems.map(item => (
            <div key={item.id} style={{ display: "flex", gap: "10px", margin: "10px 0" }}>
              <p>{item.name} × {item.count}</p>
              <p>{item.price * item.count} 元</p>
              <button onClick={() => dispatch(removeFromCart(item.id))}>删除</button>
            </div>
          ))}
          <p>总价:{totalPrice} 元</p>
          <button onClick={() => dispatch(clearCart())} style={{ marginTop: "10px" }}>清空购物车</button>
        </>
      )}
    </div>
  );
}

export default Cart;

3. 轻量方案:Zustand(适合中小型应用,简洁高效)

如果觉得 Redux Toolkit 配置还是繁琐,而 Context+useReducer 在复杂场景下不够灵活,可以选择 Zustand——一个轻量级的全局状态管理库,API 简洁,无需过多配置,深受 React 开发者喜爱。

核心优势:代码简洁、学习成本低、无需 Provider 包裹、支持中间件(异步请求、持久化等) ,适合中小型应用或对开发效率有要求的场景。

实战案例:Zustand 实现购物车

// 1. 安装依赖:npm install zustand
import { create } from 'zustand';
import { useEffect } from 'react';

// 2. 创建 Store
const useCartStore = create((set) => ({
  // 状态
  products: [],
  cartItems: [],
  loading: false,
  error: null,

  // 异步 action:获取商品数据
  fetchProducts: async () => {
    set({ loading: true, error: null });
    try {
      const res = await fetch('https://api.example.com/products');
      const data = await res.json();
      set({ products: data, loading: false });
    } catch (err) {
      set({ error: err.message, loading: false });
    }
  },

  // 同步 action:添加商品到购物车
  addToCart: (product) => {
    set((state) => {
      const existingItem = state.cartItems.find(item => item.id === product.id);
      if (existingItem) {
        return {
          cartItems: state.cartItems.map(item => 
            item.id === product.id ? { ...item, count: item.count + 1 } : item
          )
        };
      } else {
        return { cartItems: [...state.cartItems, { ...product, count: 1 }] };
      }
    });
  },

  // 同步 action:删除商品
  removeFromCart: (id) => {
    set((state) => ({
      cartItems: state.cartItems.filter(item => item.id !== id)
    }));
  },

  // 同步 action:清空购物车
  clearCart: () => {
    set({ cartItems: [] });
  }
}));

// 3. 商品列表组件
function ProductList() {
  // 从 Store 获取状态和方法
  const { products, loading, error, fetchProducts, addToCart } = useCartStore();

  useEffect(() => {
    fetchProducts();
  }, [fetchProducts]);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误:{error}</p>;

  return (
    <div>
      <h3>商品列表(Zustand)</h3>
      <div style={{ display: "flex", gap: "20px", margin: "10px 0" }}>
        {products.map(product => (
          <div key={product.id} style={{ border: "1px solid #ccc", padding: "10px" }}>
            <p>{product.name}</p>
            <p>价格:{product.price} 元</p>
            <button onClick={() => addToCart(product)}>加入购物车</button>
          </div>
        ))}
      </div>
    </div>
  );
}

// 4. 购物车组件
function Cart() {
  // 从 Store 获取状态和方法
  const { cartItems, removeFromCart, clearCart } = useCartStore();

  const totalPrice = cartItems.reduce((total, item) => {
    return total + item.price * item.count;
  }, 0);

  return (
    <div style={{ marginTop: "20px", border: "1px solid #ccc", padding: "20px" }}>
      <h3>购物车({cartItems.length} 种商品)</h3>
      {cartItems.length === 0 ? (
        <p>购物车为空</p>
      ) : (
        <>
          {cartItems.map(item => (
            <div key={item.id} style={{ display: "flex", gap: "10px", margin: "10px 0" }}>
              <p>{item.name} × {item.count}</p>
              <p>{item.price * item.count} 元</p>
              <button onClick={() => removeFromCart(item.id)}>删除</button>
            </div>
          ))}
          <p>总价:{totalPrice} 元</p>
          <button onClick={clearCart} style={{ marginTop: "10px" }}>清空购物车</button>
        </>
      )}
    </div>
  );
}

// 5. 根组件
function App() {
  return (
    <div style={{ padding: "20px" }}>
      <h2>Zustand 购物车</h2>
      <ProductList />
      <Cart />
    </div>
  );
}

四、全局状态管理方案对比与选择建议

为了让大家能根据项目规模和需求选择合适的方案,我们用表格对比常用的 3 种全局状态管理方案:

方案 核心优势 劣势 适用场景
Context+useReducer 1. 原生方案,无需额外安装依赖;2. 实现简单,学习成本低;3. 轻量无冗余 1. 不支持中间件,处理异步逻辑繁琐;2. 状态更新会触发所有消费组件重渲染(需配合 memo 优化);3. 不适合复杂状态逻辑 中小型应用、简单全局状态(如主题、登录状态)
Redux Toolkit 1. 状态集中管理,可预测性强;2. 支持中间件(异步、日志等);3. 调试工具完善;4. 适合多团队协作 1. 配置相对繁琐,学习成本高;2. 代码量较多;3. 轻量应用可能显得过重 大型复杂应用、多团队协作、复杂状态逻辑(如电商、后台管理系统)
Zustand 1. API 简洁,学习成本低;2. 无需 Provider 包裹;3. 支持中间件,处理异步简单;4. 性能优秀(精准更新) 1. 生态不如 Redux 完善;2. 大型复杂应用的协作规范不如 Redux 成熟 中小型应用、对开发效率有要求的场景、需要轻量方案替代 Context+useReducer

五、核心总结与避坑指南

核心总结

  1. 组件传参遵循“就近原则” :父子/兄弟组件用 props+回调,跨层级用 Context API,全局共享用专门的状态管理方案;
  2. 全局状态管理“按需选择” :小型应用用 Context+useReducer,中型用 Zustand,大型复杂应用用 Redux Toolkit;
  3. props 是只读的:子组件不能直接修改 props,需通过回调让父组件更新,避免破坏单向数据流;
  4. 避免过度设计:不要一开始就用全局状态,先尝试局部传参,当局部传参无法满足需求时再引入全局状态。

避坑指南

  • 坑 1:滥用 Context API:Context 会导致所有消费组件在状态更新时重渲染,若状态更新频繁,需配合 memo、useMemo 优化;
  • 坑 2:Redux 过度使用:不是所有状态都需要放入 Redux,局部状态(如组件内部的表单输入)用 useState 即可;
  • 坑 3:列表渲染忘记加 key:传参时若涉及列表渲染,务必给列表项加唯一 key,避免 React 误判节点导致性能问题;
  • 坑 4:直接修改状态:无论是局部状态还是全局状态,都要遵循“不可变更新”原则(如用扩展运算符、map 等方法创建新状态),避免直接修改原状态。

六、下一步学习方向

今天我们掌握了 React 19 组件传参与全局状态管理的核心方案,下一步可以重点学习:

  • 状态管理性能优化:如 memo、useMemo、useCallback 与状态管理的配合使用;
  • 其他轻量状态管理库:如 Jotai、Recoil(原子化状态管理,适合细粒度状态共享);
  • Redux 高级特性:如中间件(redux-thunk、redux-saga)、状态持久化(redux-persist);
  • React 19 新增状态相关特性:如 useOptimistic(乐观更新)、useActionState(表单状态管理)。

如果这篇文章对你有帮助,欢迎点赞、收藏、转发~ 有任何问题也可以在评论区留言交流~ 我们下期再见!

手写 Vue 模板编译(生成篇)

前言

写本文的背景 《鸽了六年的某大厂面试题:你会手写一个模板引擎吗?》

阅读本文前请务必先阅读解析篇 《手写 Vue 模板编译(解析篇)》

复习

在前文 《手写 Vue 模板编译(解析篇)》 中,我们已经知道,模板编译的过程分为三步:解析、优化、生成。

  1. 解析 parse:在这一步,Vue 会解析模板字符串,并生成对应的 AST
  2. 优化 optimize:这个阶段主要是通过标记静态节点来进行语法树优化。
  3. 生成 generate:利用前面生成 AST(抽象语法树)转换成渲染函数(render function)。

在前文中,我们已经学习了如何生成 AST 接下来我们需要学习如何 optimize 和 generate。

optimize:优化 AST

第一步:标记静态节点

如上文所说,这个阶段主要是通过标记静态节点来进行语法树优化,在进行优化后,Vue 会在 AST 中标记某个节点是否为静态节点。

在上一节生成 AST 中,我们定义了节点类型:

  • 元素节点 type=1
  • 表达式节点 type=2
  • 文本节点 type=3

在不存在子节点时:

  • 首先文本节点必为静态节点,因为文本内容固定不变。
  • 其次表达式则必不为静态节点,因为它的值依赖于引用的表达式。
  • 最后普通节点,如果有 v-ifv-for 指令就是动态节点,否则是静态节点

    注:实际 Vue 中还有 v-bind 等指令也会让节点变为动态,这里简化处理

/**
 * 判断一个节点是否为静态节点
 */
function isStatic(node) {
  // 如果节点是表达式节点,则不是静态节点
  if (node.type === 2) {
    return false
  }

  // 如果节点是文本节点,则是静态节点
  if (node.type === 3) {
    return true
  }

  // 如果节点没有 v-if 和 v-for 指令,则是静态节点
  return !node.if && !node.for
}

对于有子节点的情况:父节点必须满足以下两个条件才能成为静态节点:

  1. 自身是静态节点(没有 v-ifv-for 等动态指令)
  2. 所有子节点都是静态节点

接下来我们处理节点树

/**
 * 标记一个节点是否为静态节点
 */
function markStatic(node) {
  // 先用 isStatic 判断当前节点自身是否为静态
  node.static = isStatic(node)
  // 如果是元素节点,需要检查子节点
  if (node.type === 1) {
    // 遍历所有的子节点
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      // 先递归处理子节点
      markStatic(child)
      // 只要有一个子节点是动态的,父节点也必须是动态的
      if (!child.static) {
        node.static = false
      }
    }
  }
}

第二步:标记静态根节点

接下来是 Vue 优化系统的另一个关键部分 markStaticRoots

markStaticRoots 函数用于标记静态根节点,被标记为静态根节点的元素及其子树会在代码生成阶段被特殊处理:

  1. 提升为常量:将其渲染代码提升到 staticRenderFns 数组中,只生成一次
  2. 跳过 patch:更新时直接复用,不需要重新创建和对比 VNode
  3. 性能提升:减少运行时的计算开销
/**
 * 标记静态根节点
 * @param {Object} node - AST 节点
 */
function markStaticRoots(node) {
  if (node.type === 1) {
    // 只有元素节点才处理

    // 判断是否为静态根节点:必须是静态节点 + 有子节点
    if (node.static && node.children.length) {
      node.staticRoot = true
      // 找到静态根后直接返回,子节点会被整体提升,无需继续遍历
      return
    } else {
      node.staticRoot = false
    }

    // 递归处理所有子节点
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i])
      }
    }

    // 处理 v-if 的其他分支(v-else-if、v-else)
    // 注意:从 i=1 开始,因为 ifConditions[0] 就是当前节点
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block)
      }
    }
  }
}

现在我们来实现 optimize

function optimize(root) {
  if (!root) return
  // 第一步:标记静态节点
  markStatic(root)
  // 第二步:标记静态根
  markStaticRoots(root)
}

举个例子来看看效果:

// 要处理的模板字符串
const str = `<div><h1 v-if="true">hello</h1><h2>cookie</h2></div>`

// 解析为 ast
const ast = parse(str, {
  // ...
})
// 优化 ast
optimize(ast)

console.dir(ast, {
  depth: null,
})

打印结果:

<ref *2> {
  type: 1,
  tag: 'div',
  attrsList: [],
  attrsMap: {},
  children: [
    <ref *1> {
      type: 1,
      tag: 'h1',
      attrsList: [],
      attrsMap: { 'v-if': 'true' },
      children: [ { type: 3, text: 'hello', static: true } ],
      if: 'true',
      ifConditions: [ { exp: 'true', block: [Circular *1] } ],
      parent: [Circular *2],
      static: false,
      staticRoot: false
    },
    {
      type: 1,
      tag: 'h2',
      attrsList: [],
      attrsMap: {},
      children: [ { type: 3, text: 'cookie', static: true } ],
      parent: [Circular *2],
      static: true,
      staticRoot: true
    }
  ],
  static: false,
  staticRoot: false
}

可以观察到 <h2> 节点被标记了静态根节点。

生成代码前置知识

1、new Function

我们知道创建函数除了常用的函数声明、函数表达式、箭头函数以外,还有一个不常用的:构造函数。语法如下:

new Function(corpsFonction)
new Function(arg1, corpsFonction)
new Function(arg1, ...argN, corpsFonction)
// 例:
const addFn = new Function('a', 'b', 'return a + b')
const sum = addFn(1, 2) // 3
  • argN:可选,零个或多个,函数形参的名称,每个名称都必须是字符串
  • corpsFonction:一个包含构成函数定义的 JavaScript 语句的字符串。

2、with 语句

with 可以将一个对象的属性添加到作用域链的顶部,让我们在代码块内直接访问对象的属性。

with (对象) {
  // 在这里可以直接访问对象的属性
}

在 Vue 模板中我们写 {{message}},实际上访问的是 this.message。使用 with(this) 可以省略 this. 前缀,让生成的代码更简洁。

示例:

function foo() {
  let name = 'other' // 局部变量
  let obj = {
    name: 'cookie', // 对象属性
  }
  with (obj) {
    console.log(name) // 优先从 obj 中查找,输出 'cookie'
  }
}
foo() // 输出: cookie

generate 生成代码

终于到了模板编译的最后一步,生成代码!在这一步,我们将根据前面得到的 AST 生成 render 函数。

1、整体入口:generate

generate 是代码生成的入口函数,负责将 AST 转换为可执行的渲染代码:

  1. 递归生成代码:调用 genElement 遍历 AST,生成类似 _c('div', [...]) 的代码字符串
  2. 包装 with 作用域:用 with(this){} 包裹,让代码能访问 Vue 实例的属性
  3. 收集静态渲染函数:将静态根节点单独提取到 staticRenderFns 数组中
/**
 * 代码生成器入口
 * @param {Object} ast - 经过 parse 和 optimize 处理后的抽象语法树
 * @returns {Object} 返回包含 render 函数和 staticRenderFns 数组的对象
 */
function generate(ast) {
  // state 用于存储编译过程中的状态
  // staticRenderFns: 用于收集静态根节点的渲染函数,这些函数只需要生成一次,后续渲染可以直接复用,提升性能
  const state = { staticRenderFns: [] }

  // 递归生成核心渲染代码字符串
  // 例如:_c('div',[_v(_s(message))])
  const code = genElement(ast, state)

  return {
    // render: 主渲染函数的代码字符串
    // 使用 with(this) 包装后,模板中的变量(如 {{message}})能直接从 Vue 实例上获取
    // with(this) 会将 Vue 实例添加到作用域链顶部
    // 这样 message 会自动从 this.message 获取,而不需要在模板里写 this.message
    render: `with(this){return ${code}}`,
    // staticRenderFns: 静态根节点渲染函数的数组
    staticRenderFns: state.staticRenderFns,
  }
}

2、核心函数:genElement

genElement 是代码生成的调度中心,它根据节点类型来使用不同的处理函数。

/**
 * 生成元素的渲染代码
 * @param {Object} el - AST 元素节点
 * @param {Object} state - 渲染状态,包含 staticRenderFns 数组
 * @returns {string} 渲染代码字符串,如:_c('div', [...])
 */
function genElement(el, state) {
  // 1:处理静态根节点
  // el.staticRoot: 在 optimize 阶段标记的静态根节点
  // el.staticProcessed: 防止重复处理的标记
  if (el.staticRoot && !el.staticProcessed) {
    // 静态根节点会被提升为单独的函数存储在 staticRenderFns 中
    // 返回类似 _m(0) 的代码,0 是在 staticRenderFns 数组中的索引
    return genStatic(el, state)
  }
  // 2:处理 v-for 指令
  // el.for: 在 parse 阶段解析的 v-for 属性
  // el.forProcessed: 防止递归时重复处理
  else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  }
  // 3:处理 v-if 指令
  // el.if: 在 parse 阶段解析的 v-if 条件表达式
  // el.ifProcessed: 防止递归时重复处理
  else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  }
  // 4:处理普通元素
  else {
    // 生成标签名字符串,如 'div'
    const tag = `'${el.tag}'`

    // 递归生成所有子节点的代码
    // 返回类似 [_v("hello"), _c('span')] 的数组代码
    const children = genChildren(el, state)

    // 最终调用 _c (createElement) 创建 VNode
    // 生成代码示例:_c('div', [_v("hello")])
    // 如果没有子节点,生成:_c('div')
    return `_c(${tag}${children ? `,${children}` : ''})`
  }
}

处理子节点:genChildren 和 genNode

genElement 中调用了 genChildren 来遍历子节点,genChildren 内部又使用 genNode 来处理每一个子节点,

/**
 * 生成子节点数组的渲染代码
 * @param {Object} el - 父元素节点
 * @param {Object} state - 渲染状态
 * @returns {string} 子节点数组代码,如:[_v("text"), _c('span')]
 */
function genChildren(el, state) {
  const children = el.children
  if (children.length) {
    // 获取子节点数组需要的规范化类型
    const normalizationType = getNormalizationType(children)
    // 遍历所有子节点,为每个子节点生成代码
    return `[${children.map((c) => genNode(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

/**
 * 根据节点类型调用对应的生成函数
 * @param {Object} node - AST 节点
 * @param {Object} state - 渲染状态
 * @returns {string} 节点渲染代码
 */
function genNode(node, state) {
  if (node.type === 1) {
    // type === 1: 元素节点,递归调用 genElement
    return genElement(node, state)
  } else {
    // type === 2/3: 文本节点或表达式节点,调用 genText
    return genText(node)
  }
}

为什么需要 getNormalizationType

因为 v-for 会生成数组,导致 children 出现嵌套数组的情况:

<div>
  <span>first</span>
  <span v-for="item in [1,2,3]">{{item}}</span>
  <span>last</span>
</div>

生成的子节点结构会是:

[span_first, [span_1, span_1, span_1], span_last]

Vue 的 createElement 需要知道如何处理这种嵌套,所以要告诉它规范化类型:

  • 不需要规范化(没有嵌套)- type = 0
  • 简单规范化(一层嵌套,如组件)- type = 1
  • 完全规范化(多层嵌套,如 v-for)- type = 2
/**
 * 确定子节点数组需要的规范化(normalization)类型
 * @param {Array} children - 子节点数组
 * @returns {number} 0 | 1 | 2 - 规范化类型
 */
function getNormalizationType(children) {
  let res = 0 // 默认不需要规范化

  // 遍历所有子节点,检测是否需要规范化
  for (let i = 0; i < children.length; i++) {
    const el = children[i]

    // 跳过非元素节点(type=2 表达式节点、type=3 文本节点)
    // 只有元素节点(type=1)才可能需要规范化处理
    if (el.type !== 1) continue

    // 检查是否需要完全规范化(优先级最高,返回 2)
    if (
      el.for !== undefined || // 当前节点有 v-for
      (el.ifConditions && // 或者 v-if 条件分支中有v-for
        el.ifConditions.some((c) => c.block.for !== undefined))
    ) {
      // 需要完全规范化:因为 v-for 会返回数组,需要递归扁平化
      // 例如:[_v(" "), _l(arr, ...), _v(" ")] 其中 _l 返回 [span1, span2, span3]
      // 实际结构是:[_v(" "), [span1, span2, span3], _v(" ")] 需要扁平化为一维数组
      res = 2
      break // 找到一个需要完全规范化的就可以退出,不需要继续检查
    }

    // 检查是否需要简单规范化 返回 1
    // 当前节点可能是组件时,组件可能返回多个根节点(数组)
    // 但组件的 render 函数已经返回规范化的 VNode,所以只需要一层扁平化
    // 我们的模板里不处理组件,这里也就不用考虑这种情况
    // res = 1
  }

  return res // 返回 0、1 或 2
}

3、分类处理函数

Vue 内部函数

首先在 Vue 内部,有一些简写函数,在后续生成代码的时候会用到,列表如下:

缩写 全称 作用 示例
_c createElement 创建元素 VNode _c('div', [...])
_v createTextVNode 创建文本 VNode _v("hello")
_s toString 将变量转为字符串 _v(_s(message))
_l renderList 渲染列表(v-for) _l(items, fn)
_m renderStatic 渲染静态内容 _m(0)
_e createEmptyVNode 创建空节点(v-if 失败时) _e()

静态节点 (genStatic)

静态根节点会被提升为单独的函数,提升性能:

/**
 * 生成静态根节点的渲染代码
 * @param {Object} el - 静态根节点
 * @param {Object} state - 渲染状态
 * @returns {string} 静态节点引用代码,如:_m(0)
 */
function genStatic(el, state) {
  // 标记已处理,防止重复处理
  el.staticProcessed = true

  // 递归生成静态节点的完整代码
  const code = genElement(el, state)

  // 将静态节点函数存储到 staticRenderFns 数组中
  state.staticRenderFns.push(`with(this){return ${code}}`)

  // 返回对静态函数的引用,_m(index) 表示调用 staticRenderFns[index]
  // 索引是当前数组长度减 1
  return `_m(${state.staticRenderFns.length - 1})`
}

静态渲染函数和主 render 函数(vm._render)独立,需要单独设置 with 作用域。

文本处理 (genText)

/**
 * 生成文本节点的渲染代码
 * @param {Object} text - 文本 AST 节点
 * @returns {string} _v('...') 或 _v(_s(variable))
 */
function genText(text) {
  // type 2 是带 {{}} 的表达式,type 3 是普通纯文本
  // 表达式直接使用解析好的 expression 纯文本需要用 JSON.stringify 转义
  // 比如 text 为 "hello":生成代码 `v(hello)` 此时 JS 会去找名为 hello 的变量,这会导致报错(ReferenceError)
  // JSON.stringify("hello") 会返回 "\"hello\""(即带双引号的字符串)。生成的代码会变成:_v("hello")
  const value = text.type === 2 ? text.expression : JSON.stringify(text.text)
  return `_v(${value})`
}

条件渲染 (v-if)

在 Vue 中我们可以使用 v-if/else-if/else 指令,比如下面的模板:

<div v-if="a">A</div>
<div v-else-if="b">B</div>
<div v-else>C</div>

我们生成的 AST 为:

conditions = [
  { exp: 'a', block: { AST节点A } }, // v-if
  { exp: 'b', block: { AST节点B } }, // v-else-if
  { exp: undefined, block: { AST节点C } }, // v-else (没有exp)
]

v-if/else-if/else 的本质就是多重条件判断,在 JavaScript 中最适合用嵌套三元表达式来表达:

// 目标:生成这样的代码
// (a) ? A节点 : (b) ? B节点 : C节点
a ? _c('div', 'A') : b ? _c('div', 'B') : _c('div', 'C')

接下来是代码实现,我们处理 conditions 时,每次弹出第一个条件块,如果为真,则展示当前模块,如果为假,则递归处理剩余的 conditions,如果没有条件(v-else),则直接展示该模块。

/**
 * 生成 v-if 指令的入口函数
 * @param {Object} el - 带有 v-if 的 AST 节点
 * @param {Object} state - 渲染状态
 * @returns {string} 三元表达式形式的渲染代码
 */
function genIf(el, state) {
  // 标记已处理,防止递归时重复处理
  el.ifProcessed = true
  // 使用 slice() 复制数组,避免 shift() 修改原始的 ifConditions 数组
  return genIfConditions(el.ifConditions.slice(), state)
}

/**
 * 生成 v-if/else-if/else 指令的渲染代码
 * @param {Array} conditions - 条件数组
 * @param {Object} state - 渲染状态
 * @returns {string} 三元表达式形式的代码
 */
function genIfConditions(conditions, state) {
  // 递归终止条件:所有分支都处理完了 返回空节点
  if (!conditions.length) return '_e()'

  // 取出第一个条件
  const condition = conditions.shift()
  if (condition.exp) {
    // 有条件表达式:v-if 或 v-else-if
    // 生成:(条件) ? 满足时的内容 : 递归处理剩余条件
    return `(${condition.exp})?${genElement(
      condition.block,
      state
    )}:${genIfConditions(conditions, state)}`
  } else {
    // 没有条件表达式:v-else
    // 兜底分支,直接生成节点
    return genElement(condition.block, state)
  }
}

列表渲染 (v-for)

最后我们处理 v-for 指令,考虑下面的例子:

<div v-for="(item, index) in items">{{ item.name }}</div>

在 parse 阶段,这个指令会被解析为 AST 节点的属性:

{
  for: "items",      // 要遍历的数据源
  alias: "item",     // 当前项的别名
  iterator1: "index" // 索引的别名(可选)
}

我们通过_l 来进行遍历。_l 是 Vue 的内部函数 renderList,它的作用是遍历数组/对象,为每一项调用渲染函数。我们要生成的代码格式是:

_l(数据源, function (item, index) {
  return 每一项的渲染代码
})

代码实现:

/**
 * 生成 v-for 指令的渲染代码
 * @param {Object} el - 带有 v-for 的 AST 节点
 * @param {Object} state - 渲染状态
 * @returns {string} _l() 函数调用代码
 */
function genFor(el, state) {
  // 1. 提取遍历的相关信息
  const exp = el.for // 遍历的对象
  const alias = el.alias // 别名 item

  // 2. 处理索引参数(可选)
  // 如果有索引,格式为 ",index";没有则为空字符串
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  // 3. 标记已处理,防止递归时重复处理
  el.forProcessed = true

  // 4. 生成 _l() 函数调用
  return `_l((${exp}),function(${alias}${iterator1}${iterator2}){return ${genElement(
    el,
    state
  )}})`
}

最后会转化为代码:

_l(items, function (item, index) {
  return _c('div', [_v(_s(item.name))])
})

运行实例

我们已经写完了模板编译代码,完整的使用流程如下:

<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/vue@2.7.16/dist/vue.min.js"></script>
</head>
<body>
  <div id="app"></div>

  <script>
    // 1. 模板字符串
    const template = `
      <div>
        <h1>恭喜!编译成功!</h1>
        <h1 v-if="true">随机数{{ Math.random() }}</h1>
        <h1 v-if="false">这里不会被展示</h1>
        <h2>
          <span>{{message}}</span>
          <span v-for="item in list">{{item}}</span>
        </h2>
      </div>
    `

    // 2. 三步编译:parse -> optimize -> generate
    // (函数实现见上文,这里省略)
    const ast = parse(template, {...})
    optimize(ast)
    const { render, staticRenderFns } = generate(ast)

    // 3. 使用生成的 render 函数创建 Vue 实例
    const vm = new Vue({
      el: '#app',
      data: {
        message: 'Hello',
        list: ['我不吃', '饼干', '🍪'],
      },
      render: new Function(render),
      staticRenderFns: staticRenderFns.map(fn => new Function(fn)),
    })
  </script>
</body>
</html>

最终页面展示效果如下:

image.png

后记

以前看大佬ssh_晨曦时梦见兮的文章,特别喜欢他那种把问题深入浅出讲明白的文章。可是当我自己开始写的时候,才发现想写好真的好难。

有任何问题都可以在评论区提出,感谢大家看到这里。

GDAL 空间关系解析

^ 关注我,带你一起学GIS ^ 前言 在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解如何使用GDAL 空间关

在手机端做个滚动效果

用react开发一个antd mobile组件,页面上有多个高度不等的div元素,每个div底部都有一个「核对」按钮。当div的高度大于屏幕高度的时候,就要将这个div底部的「核对」按钮锁定在页面底部
❌