MST

星途 面试题库

面试题:Solid.js createMemo 依赖追踪在复杂场景下的应用

假设你正在开发一个电商购物车功能,购物车中的商品列表有各种属性(如价格、数量、折扣等),并且有一个计算总价的逻辑使用 createMemo 进行缓存。当商品的任何一个属性发生变化时,createMemo 要正确地重新计算总价。请详细说明如何设置依赖追踪以及可能遇到的坑和解决方案。
34.9万 热度难度
前端开发Solid.js

知识考点

AI 面试

面试题答案

一键面试

设置依赖追踪

  1. 确定依赖项
    • 在电商购物车场景中,商品列表中每个商品的价格、数量、折扣等属性都是计算总价的依赖项。例如,如果商品对象数组为cartItems,每个cartItem对象包含price(价格)、quantity(数量)、discount(折扣)等属性。
    • 总价的计算逻辑通常依赖于遍历cartItems数组,并根据每个商品的属性计算其小计(price * quantity * (1 - discount)),然后将所有商品的小计相加得到总价。
  2. 在createMemo中设置依赖
    • 以React为例(假设使用React hooks中的createMemo,即useMemo,这里createMemo类似概念),代码如下:
    import React, { useMemo } from'react';
    
    const Cart = ({ cartItems }) => {
        const totalPrice = useMemo(() => {
            return cartItems.reduce((acc, item) => {
                const subTotal = item.price * item.quantity * (1 - item.discount);
                return acc + subTotal;
            }, 0);
        }, [cartItems]);
    
        return (
            <div>
                {/* 购物车其他内容 */}
                <p>Total Price: {totalPrice}</p>
            </div>
        );
    };
    
    • 在上述代码中,useMemo的第二个参数[cartItems]设置了依赖。这意味着当cartItems数组引用发生变化时,useMemo中的回调函数会重新执行,从而重新计算总价。

可能遇到的坑及解决方案

  1. 浅比较问题
      • 当使用createMemo并将数组(如cartItems)作为依赖时,JavaScript默认进行浅比较。如果只是修改了数组中某个对象的属性,而数组引用没有改变,createMemo不会重新计算。例如:
      import React, { useMemo, useState } from'react';
      
      const Cart = () => {
          const [cartItems, setCartItems] = useState([{ id: 1, price: 10, quantity: 2, discount: 0.1 }]);
          const totalPrice = useMemo(() => {
              return cartItems.reduce((acc, item) => {
                  const subTotal = item.price * item.quantity * (1 - item.discount);
                  return acc + subTotal;
              }, 0);
          }, [cartItems]);
      
          const handlePriceChange = () => {
              const newCartItems = [...cartItems];
              newCartItems[0].price = 15;
              setCartItems(newCartItems);
          };
      
          return (
              <div>
                  <button onClick={handlePriceChange}>Change Price</button>
                  <p>Total Price: {totalPrice}</p>
              </div>
          );
      };
      
      • 在上述代码中,handlePriceChange函数试图修改商品价格,但由于cartItems数组引用在修改后没有改变(只是修改了数组内对象的属性),totalPrice不会重新计算。
    • 解决方案
      • 可以使用immer库来帮助创建新的对象或数组,同时保持数据结构的一致性,确保数组引用发生变化。例如:
      import React, { useMemo, useState } from'react';
      import produce from 'immer';
      
      const Cart = () => {
          const [cartItems, setCartItems] = useState([{ id: 1, price: 10, quantity: 2, discount: 0.1 }]);
          const totalPrice = useMemo(() => {
              return cartItems.reduce((acc, item) => {
                  const subTotal = item.price * item.quantity * (1 - item.discount);
                  return acc + subTotal;
              }, 0);
          }, [cartItems]);
      
          const handlePriceChange = () => {
              setCartItems(produce(cartItems, draft => {
                  draft[0].price = 15;
              }));
          };
      
          return (
              <div>
                  <button onClick={handlePriceChange}>Change Price</button>
                  <p>Total Price: {totalPrice}</p>
              </div>
          );
      };
      
      • produce函数会返回一个新的数组引用,从而触发useMemo重新计算。
  2. 依赖过多或过少问题
      • 依赖过多:如果在createMemo的依赖数组中添加了不必要的依赖,会导致不必要的重新计算。例如,如果有一个与购物车总价无关的组件状态userName,并错误地将其添加到useMemo的依赖数组中:
      import React, { useMemo, useState } from'react';
      
      const Cart = () => {
          const [cartItems, setCartItems] = useState([{ id: 1, price: 10, quantity: 2, discount: 0.1 }]);
          const [userName, setUserName] = useState('John');
          const totalPrice = useMemo(() => {
              return cartItems.reduce((acc, item) => {
                  const subTotal = item.price * item.quantity * (1 - item.discount);
                  return acc + subTotal;
              }, 0);
          }, [cartItems, userName]);
      
          const handleNameChange = () => {
              setUserName('Jane');
          };
      
          return (
              <div>
                  <button onClick={handleNameChange}>Change Name</button>
                  <p>Total Price: {totalPrice}</p>
              </div>
          );
      };
      
      • 此时,每次userName变化都会导致totalPrice重新计算,这是不必要的。
      • 依赖过少:如果遗漏了必要的依赖,会导致createMemo不会在需要时重新计算。例如,如果计算总价还依赖于一个全局的税率taxRate,但没有将其添加到依赖数组中:
      import React, { useMemo, useState } from'react';
      
      const taxRate = 0.05;
      const Cart = () => {
          const [cartItems, setCartItems] = useState([{ id: 1, price: 10, quantity: 2, discount: 0.1 }]);
          const totalPrice = useMemo(() => {
              return cartItems.reduce((acc, item) => {
                  const subTotal = item.price * item.quantity * (1 - item.discount);
                  return acc + subTotal * (1 + taxRate);
              }, 0);
          }, [cartItems]);
      
          const handleTaxRateChange = () => {
              // 假设这里可以改变taxRate,实际场景可能更复杂
              taxRate = 0.08;
          };
      
          return (
              <div>
                  <button onClick={handleTaxRateChange}>Change Tax Rate</button>
                  <p>Total Price: {totalPrice}</p>
              </div>
          );
      };
      
      • taxRate变化时,totalPrice不会重新计算,因为taxRate没有在依赖数组中。
    • 解决方案
      • 仔细分析计算逻辑,确保依赖数组只包含与计算结果直接相关的状态或变量。对于全局变量,如果其值可能变化影响计算结果,考虑将其包装在状态中(如使用useState或其他状态管理工具),并将该状态添加到依赖数组中。对于不必要的依赖,从依赖数组中移除。同时,可以使用ESLint插件(如eslint-plugin-react-hooks)来帮助检测依赖相关的错误,它会提示依赖过多或过少的问题。
  3. 性能问题
      • 如果购物车商品数量非常大,每次重新计算总价可能会带来性能问题。例如,计算逻辑复杂且商品数量达到成百上千时,createMemo的重新计算可能会导致卡顿。
    • 解决方案
      • 可以采用增量计算的方式。例如,记录上一次计算的总价和每个商品的小计,当某个商品属性变化时,只重新计算该商品的小计变化,并相应地更新总价,而不是重新遍历所有商品。另外,可以考虑使用虚拟列表技术来处理大量商品的展示,减少渲染和计算的压力。