阅读视图

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

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(表单状态管理)。

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

科技爱好者周刊(第 380 期):为什么人们拥抱"不对称收益"

这里记录每周值得分享的科技内容,周五发布。

本杂志开源,欢迎投稿。另有《谁在招人》服务,发布程序员招聘信息。合作请邮件联系(yifeng.ruan@gmail.com)。

封面图

中法合作的一个艺术项目《挑战第841次》,让路过的行人在黄浦江边的一个玻璃亭子里,弹奏法国作曲家的一个钢琴作品。(via

为什么人们拥抱"不对称收益"

前两周,我跟大家说,美国现在最流行"预测市场"。我当时没有统计数字,现在有了。

2025年11月,美国前两大预测市场---- PolymarketKalshi ---- 一共成交了超过100亿美元。

看这个数字,大家可能没感觉。作为对比,美国全国的体育彩票,2024年的销售额是137亿美元。

这就是说,预测市场一个月的交易量,接近了体育彩票全年的销售额。要知道,这两个网站6年前都还不存在!

这么恐怖的增长速度,难怪美国各大公司现在都想挤入这个市场,分一杯羹。

预测市场就是变相的网络彩票,它的火爆只能说明一件事情,美国正出现疯狂的"彩票热"。

本周,我看到一篇文章(上图),一位风险投资家分析这个现象。我想分享他的观点,他认为,预测市场火爆的根本原因,是社会心态的焦虑和绝望

(1)财富转移机制失效了,通过正常工作致富,越来越不可能。工资的增长速度,低于消费的增长速度,个人债务正在变多。

虽然资产的价格(比如股票、黄金、房产)也在上涨,但只是让那些拥有资产的人受益,对于没有这些资产的穷人,只是变得更贫穷。

(2)传统的人生模式也失效了。以前的模式是,找一家大公司,每天按时上班,努力工作,对公司忠心耿耿,坚持多年就会得到回报。你会收到公司的奖励,退休后还有养老金。

这种模式现在行不通了。公司的经营短期化,能存活20年的公司并不多,更不要说你的岗位了。一旦失去现在的工作,再次就业非常困难,以前的工作经验很可能用处不大。

(3)AI 的出现,加剧了前两种情况的发展速度。AI 让一切加速了,压缩了时间。以前,你有五年的时间奋斗,AI 让你感到必须在一年里拿到结果,否则就可能为时已晚。

(4)社交媒体则使得人们永远不会对现状满意。

以前,你的参照群体只是周围人群,现在的参照群体是全世界。你每天看到的都是收入高、赚钱容易、生活优渥的人群,永远会让你感到自己的生活不够好,而无论你已经取得了怎样的成就。

(5)结果就是,越来越多的人失去了耐心,不再相信长期投入,不再幻想长期的劳动积累会通往圆满的人生,社会也不奖励耐心。

为什么要苦苦奋斗20年,去争取10年后可能根本不存在的晋升机会?我要的是一条快速的道路,摆脱日常生活的困境,而且越快越好。

(6)这种心态下,人们的风险偏好发生了变化。为了快速摆脱困境,在风险更大的选项上放手一搏,成了合理的选择

即使只有5%的希望,也比100%的停滞不前更有吸引力。这就是彩票在贫困社区更畅销的原因。

这在经济学上称为"不对称收益"(asymmetric returns),就是风险和收益不对称。失败的可能性很大,但只会损失一小笔钱,成功的可能性很小,但是一旦成功,就会获得巨大收益,简单说就是"小亏大赚"。

追求不对称收益,已经成了一种普遍的心态。它推动了前几年的加密货币和 NFT 的热潮,现在又推动了预测市场。

可以确定,凡是能够产生"不对称收益"的事情,今后都会迅速成为热点。

新人上手 Claude Code 的简单方案

AI 编程工具,我用的是 Claude Code。以前推荐过,非常好用,功能很强。

我现在依然这样认为,但是必须说,Claude Code 不适合所有人,有使用门槛

它要求用户熟悉命令行,而且 Windows 安装不方便,需要启用 Linux 子系统 WSL。另外,如果在外面,没有自己的计算机,临时想用一下,也很麻烦。

元旦的时候,我在广东听说,有人做了"云端 Claude Code 客户端",解决了这些痛点,就很感兴趣。

他们团队叫做 302.AI,我以前就有接触。他们做云端服务很多年了,现在专注于 AI 模型接入。大家可以去官网看一下,用他们的 API 能够接入几乎所有主流模型,数量有几百个。

他们跟我一样,也感到 Claude Code 的诸多不便,就想能不能再开发一个它的客户端,封装所有复杂性,提供最好用的 AI 编程体验。

(1)跨平台桌面应用。他们提供 Win/Mac/Linux 安装程序,通过桌面窗口去使用云端的 Claude Code。

(2)零配置的云端沙盒。云端的 Claude Code 预装在一个沙盒里,集成了 Node.js、Python、Git、CMake、build-essential 等开发工具,不需要任何本地环境配置,开箱即用。

同时,沙盒也保障了安全,跟本地电脑是隔离的,AI 就不会误删本地文件。

(3)对话界面。对于不习惯命令行的用户,他们提供对话式交互界面(Chat UI),以聊天方式完成编程。

(4)随意更换模型。Claude Code 更换底层模型,需要配置环境变量,他们的客户端不需要这么麻烦,只需要鼠标选中即可。

你可以直接用他们的 API,也可以配置自己的 API Key。

(5)一键部署。他们还提供了部署功能,AI 生成的结果可以一键发布到公网,直接访问,无需购买服务器或配置域名。

可以说,这个方案完全针对 Claude Code 的各种痛点,目标是打造新手最容易上手的 Vibe Coding 工具。

感兴趣的朋友可以去 studio.302.ai 下载,体验一下。(提醒:使用前需要注册/登录 302.AI 账号。)

科技动态

1、乔布斯写的程序

乔布斯创立苹果公司之前,当过短时间的程序员。1975年,他20岁,从大学退学后,进入雅达利公司写电子游戏。

人们一直不知道,他的编程水平如何,现在终于曝光了。

本周,乔布斯的一些个人档案公开拍卖,其中就有当年他写的程序,打印纸上还有他的亲笔注释。

有人把这个程序还原出来,放到虚拟机上跑,终于让我们看到了乔布斯的软件作品。

这个程序叫做 AstroChart,跟星座有关。用户提供出生的时间地点,它会显示太阳系主要天体的位置。

从代码来看,乔布斯的编程水平可以,他使用三角函数计算行星位置,并且绕过当年硬件没有双精度浮点数的限制,用整数除法代替。

2、世界最大电动船

澳大利亚建造了世界最大的电力轮船,长度130米,里面的电池重达250吨。

这艘船将用作阿根廷与乌拉圭之间的轮渡,可以搭载多达2100名乘客和225辆汽车。

这艘船不仅是史上最大的电动船,可能也是史上最大的电动装置,一次可以携带超过4万度电。

3、最高过山车

2025年的最后一天,沙特阿拉伯在距离首都利雅得40分钟车程的地方,开张了一个乐园。

这个乐园有27个游乐设施,很多都是世界之最,其中就有目前世界最高的过山车。

这个过山车高达195米,相当于60层楼,比先前的世界纪录高出了55米。

整个过山车的长度是4.2公里,最高速度可以达到240公里/小时,全程只有3分多钟。

网上有很多这个过山车的视频,不要说坐在车上,就是看视频都觉得惊心动魄。

文章

1、2025年大模型回顾(英文)

西蒙·威利森(Simon Willison)的 AI 年度回顾,过去一年的大事件基本都提及了,总结和评点得非常好,推荐阅读。

2、华为的 5nm 制程怎么样?(英文)

这是一家美国技术媒体对华为麒麟9030芯片(搭载于最新的 Mate 80 手机)的分析文章。

该文认为,该芯片比早先的 7nm 制程有提升,是大陆制造的最先进芯片,但从跑分看,还没达到台积电的 5nm 水平。文章有中文版

3、Opus 4.5 将会改变一切(英文)

作者不相信 AI 会取代程序员,直到遇到 Anthropic 公司的 Opus 4.5 模型。本文是他的4个项目的编程体会,他现在确信程序员会被替代。

4、HTTP caching, a refresher(英文)

对于 HTTP 缓存机制的一个总体介绍,梳理浏览器缓存的处理逻辑。

5、Vitest 的浏览器模式介绍(英文)

JS 测试框架 Vitest 4.0 引入了浏览器模式,可以进行浏览器自动化,类似于 Playwright,进行 UI 测试,本文是一个简单介绍。

6、如何提高 JS 数组的读写速度(英文)

一篇 JavaScript 中级教程,介绍通过为 JS 数组分配连续内存,提高数组的读写速度。

工具

1、ZenOps

一个命令行工具,在本地终端里查询阿里云/腾讯云等云平台的运行数据,并提供钉钉、飞书、企微机器人,进行自然语言查询。(@eryajf 投稿)

2、白虎面板

轻量级的服务器定时任务管理系统,适合低配置的服务器。(@engigu 投稿)

3、OnlinePlayer

一个网页播放器,可以播放本地视频和云盘视频。(@13068240601 投稿)

4、gitstats

命令行工具,生成 Git 仓库的统计数据。(@shenxianpeng 投稿)

5、云图

一个极简风格的图床,可以搭建到自己的 NAS,提供灵活的 API。(@qazzxxx 投稿)

6、KeyStats

开源的 macOS 小工具,对按键行为进行统计。(@debugtheworldbot 投稿)

7、py2dist

这个工具可以将 Python 脚本编译成二进制模块,方便隐藏源码。(@xxnuo 投稿)

8、Stream Panel

Chrome 浏览器开发者工具的一个扩展,用来调试服务器发送事件 (SSE) 和 Fetch 的流式连接。(@bywwcnll 投稿)

9、Zedis

Redis 的图形客户端,跨平台的桌面应用,不使用 Electron,而是使用 Rust + GPUI,性能更好。(@vicanso 投稿)

10、QDav

这个网站可以为夸克网盘加入 WebDAV 协议,从而挂载到网盘播放器来播放夸克网盘的视频。(@ZhouCai-bo 投稿)

11、XApi

开源的 Chrome 浏览器插件,自动捕获当前网页的 Fetch 与 XHR 网络请求,支持改写 Cookie、Origin、Referer 字段,方便开发调试。(@lustan 投稿)

12、PDFCraft

纯浏览器的 PDF 开源工具集,目前有80多个工具。(@pccprint 投稿)

AI 相关

1、Open-AutoGLM

智源公司的开源安卓应用,使用自然语言,让 AI 操作手机,进行手机自动化,可以接入各种模型,无需电脑端。(@Luokavin 投稿)

2、Claude-Ally-Health

一个基于 Claude Code 的个人医疗数据中心,定义了一组自己的命令和技能,用 AI 分析个人医疗数据(体检报告、影像片子、处方单、出院小结)。(@huifer 投稿)

3、灵猫

免费的 AI 图片去水印网站,但只是去除视觉水印,嵌入的数字水印还在。(@pangxiaobin 投稿)

4、DeepDiagram AI

开源的 AI 应用,用自然语言驱动内置的 mermaid、echarts、mindmap、Draw.io 等绘图工具生成图表。(@twwch 投稿)

资源

1、100万首页截图

这个网站收集了100万个热门网站的首页截图,将它们做在一个页面,可以放大查看。

2、Emulator Gamer

各种老游戏机的经典游戏,通过模拟器免费在线游玩。(@SinanWang 投稿)

图片

1、如今的 Mozilla

Mozilla 浏览器的新任 CEO 宣称,公司的发展方向是 AI 浏览器

这让 Mozilla 社区感到担忧,因为没人是为了 AI 而使用它。一位使用者就画了下面这张图。

Mozilla 的吉祥物----一只小狐狸拿着锯子,把自己正坐着的树枝锯断,旁边还有一只鸟,为它递上更锋利的电动锯子,上面写着"AI"。

这张图比喻 Mozilla 一直在自寻死路,全力转向 AI 只会死得更快。

文摘

1、外卖应用的秘密

我是一个大型外卖应用的开发者,受一项严格的保密协议约束。但是,我已经不在乎了,我昨天向公司递交了离职报告。

说实话,我希望公司能起诉我,这样一来,这些事情就会曝光。

我已经消极工作大约八个月了,只是看着代码被推送到生产环境。一想到自己参与了这台机器,我夜里都睡不着。

人们总怀疑算法对用户不利,现实比这更糟。我是一名后端工程师,每周参加产品会议,产品经理(PM)讨论如何才能挤出额外0.4%的利润,他们把用户当成有待开发的资源。

公司有一个"优先配送"服务,你多付2.99美元,就可以更快拿到外卖。这完全是个骗局,根本没有加快派送的速度,而是人为把非优先订单延迟5到10分钟,让你感觉优先订单更快。我们仅仅通过让标准服务变差,就赚取了数百万美元的纯利润,而不是真正改善服务。

最让我恶心的是"绝望分数",这是一个隐藏的外送员指标,根据外送员的行为判断他们多想赚钱。

如果外送员在晚上10点登录系统,毫不犹豫地立即接下每一个3美元的垃圾订单,算法会将他们标记为"高度绝望"。一旦被标记,系统就会停止向他们显示高价订单,理由是"既然我们知道他绝望到愿意接受3美元,为什么还要让他看到15美元的订单呢?"。系统把高价订单留给"休闲"外送员,即那些不愿接低价单的外送员,吸引他们接单,而全职外送员则被碾压成尘埃。

公司还会从用户的账单扣除一笔1.50美元的"外送员福利费",这个名字让用户感觉在帮助外送员。实际上,这笔钱流入了游说反对外送员成立工会的基金,这是公司用于"政策防御"的费用。用户实际上是在为那些高端律师付费,那些律师为削弱外送员的权益而工作。

最后,虽然公司不再从外送员的小费里面提成,因为被起诉过,但是使用其他方法窃取小费。

如果算法预测你是"可能支付小费的用户",而且你很可能会给10美元小费,那么公司只会给外送员可怜的2美元基本派送费。如果你给了0美元小费,公司会给外送员8美元的基本派送费。结果是用户的小费并没有奖励外送员,而是在补贴公司。用户给外送员付工资,这样我们就不用付了。

言论

1、

在美国东海岸(纽约和华盛顿),人们会问:"中国是否就要失败了",而在西海岸(洛杉矶和旧金山),人们更倾向于问:"万一中国成功了会怎样?"

这一定程度上反映了硅谷的特点:更注重收益最大化,而非风险最小化。东海岸的问题也值得认真对待,但过分关注中国是否失败,会助长一种美国无需做出任何改变就能击败对手的论调,从而削弱美国改革的紧迫性。

-- Dan Wang《2025年度信件》

2、

如果美国或中国在某个方面落后太多,落后者就会奋起直追。这将是未来数年甚至数十年世界变化的动力。

-- Dan Wang《2025年度信件》

3、

程序员对待 AI 有两种态度:一种以结果为导向,渴望通过 AI 更快拿到结果;另一种以过程为导向,他们从工程本身获得意义,对于被剥夺这种体验感到不满。

-- Ben Werdmuller

4、

AI 数据中心的建设热潮,导致内存价格暴涨,进而产生一系列连锁反应。

手机和电脑厂商别无选择,只能提价。我们估计,2026年全球的手机市场和电脑市场都会萎缩。手机萎缩2.9%到5.2%,电脑萎缩4.9%到8.9%。

-- IDC 公司的预测

5、

eSIM 手机卡一旦更换就可能失效,相比之下,实体 SIM 卡可以随意插上插下,几乎不会出现故障。推广 eSIM 的后果就是,手机号丢失的事件会大大增多。

-- 《我后悔使用 eSIM》

往年回顾

一切都要支付两次(#333)

没有目的地,向前走(#283)

生活就像一个鱼缸(#233)

腾讯的员工退休福利(#183)

(完)

文档信息

  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证
  • 发表日期: 2026年1月 9日

手写 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_晨曦时梦见兮的文章,特别喜欢他那种把问题深入浅出讲明白的文章。可是当我自己开始写的时候,才发现想写好真的好难。

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

每日一题-具有所有最深节点的最小子树🟡

给定一个根为 root 的二叉树,每个节点的深度是 该节点到根的最短距离

返回包含原始树中所有 最深节点最小子树

如果一个节点在 整个树 的任意节点之间具有最大的深度,则该节点是 最深的

一个节点的 子树 是该节点加上它的所有后代的集合。

 

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4]
输出:[2,7,4]
解释:
我们返回值为 2 的节点,在图中用黄色标记。
在图中用蓝色标记的是树的最深的节点。
注意,节点 5、3 和 2 包含树中最深的节点,但节点 2 的子树最小,因此我们返回它。

示例 2:

输入:root = [1]
输出:[1]
解释:根节点是树中最深的节点。

示例 3:

输入:root = [0,1,3,null,2]
输出:[2]
解释:树中最深的节点为 2 ,有效子树为节点 2、1 和 0 的子树,但节点 2 的子树最小。

 

提示:

  • 树中节点的数量在 [1, 500] 范围内。
  • 0 <= Node.val <= 500
  • 每个节点的值都是 独一无二 的。

 

注意:本题与力扣 1123 重复:https://leetcode.cn/problems/lowest-common-ancestor-of-deepest-leaves

两种 O(n) 递归写法(Python/Java/C++/Go/JS)

前置题目236. 二叉树的最近公共祖先

视频讲解二叉树的最近公共祖先【基础算法精讲 12】

方法一:递归递归,有递有归

{:width=360px}

看上图(示例 1),这棵树的节点 $3,5,2$ 都是最深叶节点 $7,4$ 的公共祖先,但只有节点 $2$ 是最近的公共祖先。

上面视频中提到,如果我们要找的节点只在左子树中,那么最近公共祖先也必然只在左子树中。对于本题,如果左子树的最大深度比右子树的大,那么最深叶结点就只在左子树中,所以最近公共祖先也只在左子树中。

如果左右子树的最大深度一样呢?当前节点一定是最近公共祖先吗?

不一定。比如节点 $1$ 的左右子树最深叶节点 $0,8$ 的深度都是 $2$,但该深度并不是全局最大深度,所以节点 $1$ 并不能是答案。

根据以上讨论,正确做法如下:

  1. 递归这棵二叉树,同时维护全局最大深度 $\textit{maxDepth}$。
  2. 在「递」的时候往下传 $\textit{depth}$,用来表示当前节点的深度。
  3. 在「归」的时候往上传当前子树最深叶节点的深度。
  4. 设左子树最深叶节点的深度为 $\textit{leftMaxDepth}$,右子树最深叶节点的深度为 $\textit{rightMaxDepth}$。如果 $\textit{leftMaxDepth}=\textit{rightMaxDepth}=\textit{maxDepth}$,那么更新答案为当前节点。⚠注意:这并不代表我们立刻找到了答案,如果后面发现了更深的叶节点,答案还会更新。
class Solution:
    def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        max_depth = -1  # 全局最大深度
        ans = None

        def dfs(node: Optional[TreeNode], depth: int) -> int:
            nonlocal ans, max_depth
            if node is None:
                max_depth = max(max_depth, depth)  # 维护全局最大深度
                return depth

            left_max_depth = dfs(node.left, depth + 1)  # 获取左子树最深叶节点的深度
            right_max_depth = dfs(node.right, depth + 1)  # 获取右子树最深叶节点的深度

            if left_max_depth == right_max_depth == max_depth:
                ans = node  # node 可能是答案

            return max(left_max_depth, right_max_depth)  # 当前子树最深叶节点的深度

        dfs(root, 0)
        return ans
class Solution {
    private int maxDepth = -1; // 全局最大深度
    private TreeNode ans;

    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        dfs(root, 0);
        return ans;
    }

    private int dfs(TreeNode node, int depth) {
        if (node == null) {
            maxDepth = Math.max(maxDepth, depth); // 维护全局最大深度
            return depth;
        }

        int leftMaxDepth = dfs(node.left, depth + 1); // 获取左子树最深叶节点的深度
        int rightMaxDepth = dfs(node.right, depth + 1); // 获取右子树最深叶节点的深度

        if (leftMaxDepth == rightMaxDepth && leftMaxDepth == maxDepth) {
            ans = node; // node 可能是答案
        }

        return Math.max(leftMaxDepth, rightMaxDepth); // 当前子树最深叶节点的深度
    }
}
class Solution {
public:
    TreeNode* subtreeWithAllDeepest(TreeNode* root) {
        int max_depth = -1; // 全局最大深度
        TreeNode* ans;

        auto dfs = [&](this auto&& dfs, TreeNode* node, int depth) {
            if (node == nullptr) {
                max_depth = max(max_depth, depth); // 维护全局最大深度
                return depth;
            }

            int left_max_depth = dfs(node->left, depth + 1); // 获取左子树最深叶节点的深度
            int right_max_depth = dfs(node->right, depth + 1); // 获取右子树最深叶节点的深度

            if (left_max_depth == right_max_depth && left_max_depth == max_depth) {
                ans = node; // node 可能是答案
            }

            return max(left_max_depth, right_max_depth); // 当前子树最深叶节点的深度
        };

        dfs(root, 0);
        return ans;
    }
};
func subtreeWithAllDeepest(root *TreeNode) (ans *TreeNode) {
    maxDepth := -1 // 全局最大深度

    var dfs func(*TreeNode, int) int
    dfs = func(node *TreeNode, depth int) int {
        if node == nil {
            maxDepth = max(maxDepth, depth) // 维护全局最大深度
            return depth
        }

        leftMaxDepth := dfs(node.Left, depth+1) // 获取左子树最深叶节点的深度
        rightMaxDepth := dfs(node.Right, depth+1) // 获取右子树最深叶节点的深度

        if leftMaxDepth == rightMaxDepth && leftMaxDepth == maxDepth {
            ans = node // node 可能是答案
        }

        return max(leftMaxDepth, rightMaxDepth) // 当前子树最深叶节点的深度
    }

    dfs(root, 0)
    return
}
var subtreeWithAllDeepest = function(root) {
    let maxDepth = -1; // 全局最大深度
    let ans = null;

    function dfs(node, depth) {
        if (node === null) {
            maxDepth = Math.max(maxDepth, depth); // 维护全局最大深度
            return depth;
        }

        const leftMaxDepth = dfs(node.left, depth + 1); // 获取左子树最深叶节点的深度
        const rightMaxDepth = dfs(node.right, depth + 1); // 获取右子树最深叶节点的深度

        if (leftMaxDepth === rightMaxDepth && leftMaxDepth === maxDepth) {
            ans = node; // node 可能是答案
        }

        return Math.max(leftMaxDepth, rightMaxDepth); // 当前子树最深叶节点的深度
    }

    dfs(root, 0);
    return ans;
};

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。每个节点都会恰好访问一次。
  • 空间复杂度:$\mathcal{O}(n)$。最坏情况下,二叉树是一条链,递归需要 $\mathcal{O}(n)$ 的栈空间。

方法二:自底向上

也可以不用全局变量,而是把每棵子树都看成是一个「子问题」,即对于每棵子树,我们需要知道:

  • 这棵子树最深叶结点的深度。这里是指叶子在这棵子树内的深度,而不是在整棵二叉树的视角下的深度。相当于这棵子树的高度
  • 这棵子树的最深叶结点的最近公共祖先 $\textit{lca}$。

分类讨论:

  • 设子树的根节点为 $\textit{node}$,$\textit{node}$ 的左子树的高度为 $\textit{leftHeight}$,$\textit{node}$ 的右子树的高度为 $\textit{rightHeight}$。
  • 如果 $\textit{leftHeight} > \textit{rightHeight}$,那么子树的高度为 $\textit{leftHeight} + 1$,$\textit{lca}$ 是左子树的 $\textit{lca}$。
  • 如果 $\textit{leftHeight} < \textit{rightHeight}$,那么子树的高度为 $\textit{rightHeight} + 1$,$\textit{lca}$ 是右子树的 $\textit{lca}$。
  • 如果 $\textit{leftHeight} = \textit{rightHeight}$,那么子树的高度为 $\textit{leftHeight} + 1$,$\textit{lca}$ 就是 $\textit{node}$。反证法:如果 $\textit{lca}$ 在左子树中,那么 $\textit{lca}$ 不是右子树的最深叶结点的祖先,这不对;如果 $\textit{lca}$ 在右子树中,那么 $\textit{lca}$ 不是左子树的最深叶结点的祖先,这也不对;如果 $\textit{lca}$ 在 $\textit{node}$ 的上面,那就不符合「最近」的要求。所以 $\textit{lca}$ 只能是 $\textit{node}$。
class Solution:
    def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        def dfs(node: Optional[TreeNode]) -> Tuple[int, Optional[TreeNode]]:
            if node is None:
                return 0, None

            left_height, left_lca = dfs(node.left)
            right_height, right_lca = dfs(node.right)

            if left_height > right_height:  # 左子树更高
                return left_height + 1, left_lca
            if left_height < right_height:  # 右子树更高
                return right_height + 1, right_lca
            return left_height + 1, node  # 一样高

        return dfs(root)[1]
class Solution {
    private record Pair(int height, TreeNode lca) {}

    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        return dfs(root).lca;
    }

    private Pair dfs(TreeNode node) {
        if (node == null) {
            return new Pair(0, null);
        }

        Pair left = dfs(node.left);
        Pair right = dfs(node.right);

        if (left.height > right.height) { // 左子树更高
            return new Pair(left.height + 1, left.lca);
        }
        if (left.height < right.height) { // 右子树更高
            return new Pair(right.height + 1, right.lca);
        }
        return new Pair(left.height + 1, node); // 一样高
    }
}
class Solution {
    pair<int, TreeNode*> dfs(TreeNode* node) {
        if (node == nullptr) {
            return {0, nullptr};
        }

        auto [left_height, left_lca] = dfs(node->left);
        auto [right_height, right_lca] = dfs(node->right);

        if (left_height > right_height) { // 左子树更高
            return {left_height + 1, left_lca};
        }
        if (left_height < right_height) { // 右子树更高
            return {right_height + 1, right_lca};
        }
        return {left_height + 1, node}; // 一样高
    }

public:
    TreeNode* subtreeWithAllDeepest(TreeNode* root) {
        return dfs(root).second;
    }
};
func dfs(node *TreeNode) (int, *TreeNode) {
    if node == nil {
        return 0, nil
    }

    leftHeight, leftLCA := dfs(node.Left)
    rightHeight, rightLCA := dfs(node.Right)

    if leftHeight > rightHeight { // 左子树更高
        return leftHeight + 1, leftLCA
    }
    if leftHeight < rightHeight { // 右子树更高
        return rightHeight + 1, rightLCA
    }
    return leftHeight + 1, node // 一样高
}

func subtreeWithAllDeepest(root *TreeNode) *TreeNode {
    _, lca := dfs(root)
    return lca
}
function dfs(node) {
    if (node === null) {
        return [0, null];
    }

    const [leftHeight, leftLca] = dfs(node.left);
    const [rightHeight, rightLca] = dfs(node.right);

    if (leftHeight > rightHeight) { // 左子树更高
        return [leftHeight + 1, leftLca];
    }
    if (leftHeight < rightHeight) { // 右子树更高
        return [rightHeight + 1, rightLca];
    }
    return [leftHeight + 1, node]; // 一样高
}

var subtreeWithAllDeepest = function(root) {
    return dfs(root, 0)[1];
};

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。每个节点都会恰好访问一次。
  • 空间复杂度:$\mathcal{O}(n)$。最坏情况下,二叉树是一条链,递归需要 $\mathcal{O}(n)$ 的栈空间。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Java递归, O(n)一次遍历

image.png

看了下下面的题解,除了官方题解用多个返回值只遍历了一次,其他题解大都反复计算深度,其实复杂度是O(nlogn)
本题解进行一次遍历得到结果

主要思想:

在一次遍历深度的过程中,找到左右子树深度都为最大值的节点记录下来

class Solution {
  private int maxDeep = Integer.MIN_VALUE;
  private TreeNode result;
  public TreeNode subtreeWithAllDeepest(TreeNode root) {
    maxDeep(root, 0);
    return result;
  }
  private int maxDeep(TreeNode node, int deep) {
    if (node == null) {
      return deep;
    }
    int left = maxDeep(node.left, deep+1);
    int right = maxDeep(node.right, deep+1);
    int currentMax = Math.max(left, right);
    maxDeep = Math.max(maxDeep, currentMax);
    if (left == maxDeep && right == maxDeep) {
      result = node;
    }
    return currentMax;
  }
}

粗俗易懂:直接看代码和注解,简单

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

class Solution {
    // 思路:从每个树开始,获得当前节点的左右子树的最大深度
    // 深度相同,说明最深的节点在这个节点两边,那这个节点就是结果
    // 如果深度不相同,则去深度大的子树继续判断,最终就能得到结果
    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        if (root == null) return root;

        // 获取当前节点的左右子树的最大深度
        int leftMaxDepth = getMaxDepth(root.left);
        int rightMaxDepth = getMaxDepth(root.right);

        // 如果两边最大深度相同,则这个节点就是结果
        if (leftMaxDepth == rightMaxDepth) return root;

        // 不相等,那就去深度大的子树那边继续找
        if (leftMaxDepth > rightMaxDepth){
            return subtreeWithAllDeepest(root.left);
        }

        return subtreeWithAllDeepest(root.right);
    }

    public int getMaxDepth(TreeNode root){
        if (root == null) return 0;

        return Math.max(getMaxDepth(root.left), getMaxDepth(root.right)) + 1;
    }
}

GDAL 空间关系解析

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

分享一下思考过程 C++

  • 知识点:动态规划
  • 时间复杂度:O(n*m);n,m 分别为两个序列的长度。

动态规划题目一般要先想好两个问题:

  • 状态如何定义。
  • 状态之间如何转移。

对于该题,最终目标是在分别两个序列中选取等长且非空的子序列,使得两个子序列的点积最大。
即,在 nums1 的前 n 个数中,在 nums2 的前 m 个数字中分别选取等长且非空的子序列, 使其点积最大。
推而广之,我们可以将问题表示为,在nums1的前 x (x <= n)个数字中,在nums2的前y(y <= m)个数字中,分别选取等长且非空的子序列, 使其点积最大。
为了方便,我们用 f(x, y) 表示子问题的最优方案的点积。
当(x, y) = (n,m) 时,f(x,y) 就是最终答案。

状态转移主要是分析状态之间的关联或差异,利用小问题的解高效的构造大问题的解。
来思考下该题状态如何转移,该题的特点是小问题总是为大问题的前缀,总是可以向小问题中追加数字得到一个大问题。
设 nx = nums1[x],ny = nums2[y]。
f(x,y) 可能由以下几个状态转移得到:

  • 向 f(x-1, y-1) 追加 nx,ny 获得 f(x, y)。
  • 向 f(x, y-1) 追加 ny 获得 f(x, y)。
  • 向 f(x-1, y) 追加 nx 获得 f(x,y)。

当然,也可以同时追加多个数字,由更小的问题获得 f(x, y),但这本质上还是通过上述三种子问题间接转移过来的。
那么,为何f(x-1,y-1) 不能用 f(x-1, y) 或者 f(x, y-1) 间接转移过来呢?因为在求解过程中要考虑nx 和 ny 在对应位置的情况。

总结一下,该题的状态方程如下:
$$
f(x,y) = max \left{ \begin{array}{c}
nxny&, 有且只有 nx,ny\
nx
ny + f(x-1, y-1)&, 包含 nx,ny \
f(x, y-1)&, 不包含 nx \
f(x-1, y)&, 不包含 ny \
f(x-1, y-1)&, 不包含 nx,ny\
\end{array}\right.
$$

###cpp

class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int dp[501][501];
        for(int i = 0; i <= nums1.size(); i++) {
            for(int j = 0; j <= nums2.size(); j++) {
                dp[i][j] = -1000*1000*500;
            }
        }
        for(int i = 1; i <= nums1.size(); i++) {
            for(int j = 1; j <= nums2.size(); j++) {
                int a = nums1[i-1];
                int b = nums2[j-1];
                dp[i][j] = max(dp[i][j], a*b);
                dp[i][j] = max(dp[i][j], dp[i-1][j-1] + a*b);
                dp[i][j] = max(dp[i][j], dp[i-1][j-1]);
                dp[i][j] = max(dp[i][j], dp[i-1][j]);
                dp[i][j] = max(dp[i][j], dp[i][j-1]);
            }
        }
        return dp[nums1.size()][nums2.size()];
    }
};

如果感觉有点意思,可以关注👏HelloNebula👏

  • 分享周赛题解
  • 分享计算机专业课知识
  • 分享C++相关岗位面试题
  • 分享专业书籍PDF

在手机端做个滚动效果

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