普通视图

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

面试官:React Diff 算法原理?我:三个假设 + O(n) 复杂度,手撕给你看!

作者 Kincy
2025年7月3日 15:25

🧠 系列前言:

面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 offer!

💬 面试官发问:

"React 的 Virtual DOM diff 算法是怎么实现的?为什么要用 key?三个假设是什么?"

哎呀,这题经典得像老北京炸酱面,每次面试都会遇到。Virtual DOM diff,听起来高大上,其实就是 React 的"比较专家",专门负责找出新旧虚拟 DOM 的差异。

🎯 快答区(初级背诵版)

React 的 diff 算法基于三个假设:同层级比较、不同类型元素会产生不同的树、通过 key 来标识列表元素。算法复杂度从 O(n³) 优化到 O(n),主要通过单层遍历、类型判断和 key 匹配来实现高效更新。

面试官:嗯,还行,但我想听点你背不出来的内容 😏

🧠 深入理解:Diff 算法的江湖传说

📌 1. 为什么需要 Diff 算法?

想象一下,你有一个超大的购物清单,每次修改都要重新写一遍

// 旧清单
const oldList = ['苹果', '香蕉', '橙子', '葡萄'];
// 新清单(只是把香蕉换成了芒果)
const newList = ['苹果', '芒果', '橙子', '葡萄'];

笨方法:撕掉重写 → 浪费纸张 → 环保局找上门
聪明方法:找出差异,只改"香蕉"→"芒果" → 省时省力

React 的 diff 算法就是这个聪明方法,它要在新旧两棵虚拟 DOM 树中找出最小的变化,然后只更新需要变化的部分。

🎯 2. 传统 Diff 的噩梦:O(n³) 复杂度

如果要完美比较两棵树的差异,传统算法需要:

  1. 遍历树 A 的每个节点:O(n)
  2. 遍历树 B 的每个节点:O(n)
  3. 计算编辑距离:O(n)

总复杂度:O(n³)

对于 1000 个节点的应用,就是 10 亿次操作! React 团队一看:这样下去,用户要等到花儿都谢了!

🧠 3. React 的三个"天才假设"

React 团队经过观察发现,实际开发中:

假设 1:同层级比较

不同层级的节点很少会移动

// 这种情况很少发生
<div>
  <span>Hello</span>  // 从这里
</div>
<p>
  <span>Hello</span>  // 移动到这里
</p>

所以 React 只比较同一层级的节点,跨层级直接删除重建

假设 2:不同类型 = 不同树

不同类型的元素会产生不同的树

// 从 div 变成 span
<div>Hello</div>  →  <span>Hello</span>

React 直接删除旧树,创建新树,不做进一步比较!

假设 3:Key 是列表的身份证

开发者可以通过 key 来暗示哪些子元素是稳定的

// 有了 key,React 就知道谁是谁
{items.map(item => 
  <Item key={item.id} data={item} />
)}

基于这三个假设,React 把 O(n³) 优化成了 O(n)

🎢 4. Diff 算法的三大战场

战场1:Tree Diff(树级别)

// 情况1:同类型节点
<div className="old">         <div className="new">
  <span>Hello</span><span>Hello</span>
</div>                        </div>
// 结果:只更新 className

// 情况2:不同类型节点  
<div>Hello</div>  →  <span>Hello</span>
// 结果:删除 div,创建 span

战场2:Component Diff(组件级别)

// 情况1:同类型组件
<MyComponent name="old" />  →  <MyComponent name="new" />
// 结果:更新 props,可能触发重新渲染

// 情况2:不同类型组件
<ComponentA />  →  <ComponentB />
// 结果:卸载 A,挂载 B

战场3:Element Diff(元素级别)

这是最复杂的战场,主要处理列表更新

// 老列表
['A', 'B', 'C', 'D']
// 新列表  
['A', 'C', 'D', 'B']

没有 key 的情况

  • React 按顺序比较
  • B→C,C→D,D→B,最后插入 B
  • 需要 3 次更新 + 1 次插入

有 key 的情况

  • React 知道 B 只是移动了位置
  • 直接移动 B 到末尾
  • 只需要 1 次移动操作

🔍 5. Key 的选择艺术

❌ 错误示范

// 用 index 当 key(面试官最爱问的反面教材)
{items.map((item, index) => 
  <Item key={index} data={item} />
)}

问题:当列表顺序改变时,key 和内容的对应关系就乱了!

✅ 正确示范

// 用稳定的唯一标识当 key
{items.map(item => 
  <Item key={item.id} data={item} />
)}

🎨 6. Diff 算法的核心实现逻辑

function diff(oldVNode, newVNode) {
  // 1. 节点类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return { type: 'REPLACE', newVNode };
  }
  
  // 2. 文本节点,比较内容
  if (typeof newVNode === 'string') {
    if (oldVNode !== newVNode) {
      return { type: 'TEXT', newVNode };
    }
    return null;
  }
  
  // 3. 同类型元素,比较属性和子节点
  const propsPatches = diffProps(oldVNode.props, newVNode.props);
  const childrenPatches = diffChildren(oldVNode.children, newVNode.children);
  
  if (propsPatches.length || childrenPatches.length) {
    return { type: 'UPDATE', propsPatches, childrenPatches };
  }
  
  return null;
}

🎯 7. 列表 Diff 的核心算法

React 使用了一个聪明的双指针算法

function diffChildren(oldChildren, newChildren) {
  let oldStartIdx = 0, newStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newEndIdx = newChildren.length - 1;
  
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 头头比较
    if (sameVnode(oldChildren[oldStartIdx], newChildren[newStartIdx])) {
      // 相同节点,继续比较
      diff(oldChildren[oldStartIdx], newChildren[newStartIdx]);
      oldStartIdx++;
      newStartIdx++;
    }
    // 尾尾比较
    else if (sameVnode(oldChildren[oldEndIdx], newChildren[newEndIdx])) {
      diff(oldChildren[oldEndIdx], newChildren[newEndIdx]);
      oldEndIdx--;
      newEndIdx--;
    }
    // 头尾比较
    else if (sameVnode(oldChildren[oldStartIdx], newChildren[newEndIdx])) {
      // 需要移动节点
      moveNode(oldChildren[oldStartIdx], 'after', oldChildren[oldEndIdx]);
      oldStartIdx++;
      newEndIdx--;
    }
    // 尾头比较
    else if (sameVnode(oldChildren[oldEndIdx], newChildren[newStartIdx])) {
      moveNode(oldChildren[oldEndIdx], 'before', oldChildren[oldStartIdx]);
      oldEndIdx--;
      newStartIdx++;
    }
    // 都不匹配,查找表
    else {
      findAndMove();
    }
  }
  
  // 处理剩余节点
  handleRemainingNodes();
}

💬 面试中可以抛出的装 X 语录

  • "React 的 diff 算法是一种启发式算法,通过合理的假设将复杂度从 O(n³) 降到 O(n)。"
  • "Key 不仅是性能优化的手段,更是帮助 React 理解组件身份的重要线索。"
  • "Same level comparison 是 React diff 的核心思想,体现了工程化中的权衡艺术。"
  • "双指针算法在列表 diff 中的应用,让我看到了算法在实际场景中的优雅实现。"

(说完记得停顿两秒,喝一口水,看面试官点头)

✅ 总结一句话

React Diff 算法 = 基于三个假设的启发式算法,通过同层比较、类型判断和 key 匹配,实现了 O(n) 复杂度的高效 DOM 更新,它是 React 性能优化的核心基石。

🔚 系列结尾:明日继续爆料!

明天继续来一道面试题,咱们聊聊 React Hooks 的原理和闭包陷阱(别看 Hooks 用得溜,原理可能比你想象的更有趣🪝)。

📌 点赞 + 收藏 + 关注系列,让你成为面霸不是梦!

昨天以前首页

面试官:你知道 React Fiber 吗?我:你是说 调度届的王者?

作者 Kincy
2025年7月2日 16:09

🧠 系列前言:
面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 offer!

💬 面试官发问:

“你了解 React Fiber 吗?能说说它的核心原理和解决了什么问题吗?”

哎呀,这题我熟。React Fiber,听起来像个健身品牌,其实是 React 自己下的一盘“大棋”。

🎯 快答区(初级背诵版)

React Fiber 是 React 16 开始引入的新架构,用来优化之前递归更新的流程,解决了无法中断渲染、更新过程不够灵活的问题。Fiber 支持任务拆分、优先级调度和异步渲染,使得用户交互更流畅,页面不卡顿。

面试官:嗯,还行,但我想听点你背不出来的内容 😏

🧠 深入理解:Fiber 究竟是谁?

📌 1. React 旧架构的问题

React 15 及以前的版本在更新组件时,采用的是同步递归的方式:

<MyHugeComponent />

页面越大,递归越深,React 就越像一位卷王:咬牙更新完才休息。

问题是:

  • 你点个按钮,页面卡了 300ms;
  • 你输入表单,光标卡了一下;
  • 用户体验直接下滑成海底捞。

React 团队一拍桌子:我们要让更新能暂停、能恢复、能控制优先级!
于是 Fiber 诞生。

🧱 2. 什么是 Fiber 架构?

通俗点说,Fiber 是 React 的 任务调度中心 + 渲染流水线,是对 Virtual DOM 的一次架构升级。

它的目标是实现可中断、可恢复、可控制优先级的更新流程。

它怎么实现的?

  • 把组件更新任务拆成小块(Unit of Work)
  • 每一块都可以:
    • 执行一点点
    • 暂停
    • 恢复
    • 放到空闲时执行

React:我不再一口气更新到底了,我要 边干活边喘气

🧩 3. Fiber 是什么数据结构?

别怕,这不难!

Fiber 本质上是个双缓冲链表树结构(听起来吓人,画出来就很简单):

每个组件都会生成一个 FiberNode,类似这样:

type FiberNode = {
  type: string | Function;
  child: FiberNode | null;
  sibling: FiberNode | null;
  return: FiberNode | null;
  stateNode: any;
  alternate: FiberNode | null; // 旧节点,用于比较
  effectTag: string;
  // ...还有一些调度相关字段
}

每个节点都是可控的、可遍历的、可“后悔”的!

⏱️ 4. Fiber 的两大阶段

React Fiber 的更新流程分成两个阶段:

阶段名 作用 是否可中断
Reconciliation 构建 Fiber 树(diff) ✅ 可以
Commit 真实操作 DOM ❌ 不行

🚦在第一阶段,React 可以使用浏览器的空闲时间 requestIdleCallback 做点“碎片工作”。

🧠 如果任务来不及做完,React 可以暂停,先处理用户交互(比如你点个按钮),然后回来接着干

🎢 5. 支持优先级调度是怎么回事?

React Fiber 的另一个大招是:给不同的更新任务分等级!

任务类型 优先级
用户输入(点击、键盘)
动画
网络请求后的渲染
页面初始化加载

React 使用了自己的调度器(scheduler),确保重要的任务插队处理,低优先级的任务以后再说

💬 面试中可以抛出的装 X 语录

  • “Fiber 是 React 从同步递归向异步可控调度的一次范式转变。”
  • “Fiber 把 Virtual DOM 升级成任务流,赋予了调度权。”
  • “它让我重新相信:写前端也能讲并发。”

(说完记得停顿两秒,喝一口水,看面试官点头)

✅ 总结一句话

React Fiber = 可中断、可恢复、可优先级调度的 Virtual DOM 架构升级版,它让 React 更聪明、更流畅,也为后续的 Concurrent Mode 奠定了基础。

🔚 系列结尾:明日继续爆料!

明天继续来一道面试题,咱们聊聊 React 是怎么做 Virtual DOM diff 的(别看是老生常谈,其实里面猫腻不少🐱)。

📌 点赞 + 收藏 + 关注系列,让你成为面霸不是梦!

状态三国演义:Redux、Zustand、Jotai 源码趣谈

作者 Kincy
2025年7月1日 14:49

你可能用过 Redux,也可能听说过 Zustand 和 Jotai。它们风格各异,但你有没有想过,它们的“性格”其实就藏在源码里?

本文将用一种轻松趣味的方式,带你深入三大主流 React 状态库的源码核心,从 Redux 的严谨,到 Zustand 的野性,再到 Jotai 的哲学,我们一起探究状态管理的“性格密码”。

没有枯燥的源码大段堆砌,有的是清晰逻辑、趣味比喻和可视化解析。

👉 准备好了吗?来一场状态管理的三国探秘之旅吧!

状态管理江湖,群雄并起。Redux 持剑而立,Zustand 嘶吼奔腾,Jotai 端坐冥思。今日我潜入三大门派,直探源码秘笈,与你共赏这场状态三国的传奇。

一、引言:状态的烦恼

曾几何时,React 的 useState 用得正香,组件树一复杂,状态就满天飞。

  • 数据共享:prop drilling 痛苦无比
  • 组件通信:context 一层层包
  • 异步状态:还得加 middleware?

于是江湖上诞生了三大门派:

  • Redux:长者风范,纪律严明,招式繁多
  • Zustand:野性十足,灵活简单,喜欢钩子
  • Jotai:哲学派别,万物皆原子,依赖追踪

它们不仅风格各异,源码中更藏着各自的“性格密码” ,接下来我们就一探究竟!

二、Redux:老派骑士的战斗美学

Redux 是 React 状态管理领域的“武当派”。讲究纪律、数据单向流、不可变,是许多项目的启蒙之选。

🌟 核心机制回顾

  • createStore:建功立业的入口函数
  • dispatch(action):发起一次战斗
  • reducer(state, action):决定如何改变世界
  • subscribe(listener):监听战况

🔍 源码核心拆解:createStore

function createStore(reducer, preloadedState, enhancer) {
  let currentState = preloadedState
  let currentListeners = []

  function getState() {
    return currentState
  }

  function dispatch(action) {
    currentState = reducer(currentState, action)
    currentListeners.forEach(listener => listener())
    return action
  }

  function subscribe(listener) {
    currentListeners.push(listener)
    return () => {
      const index = currentListeners.indexOf(listener)
      currentListeners.splice(index, 1)
    }
  }

  // 派发一次初始化 action
  dispatch({ type: '@@redux/INIT' })

  return { getState, dispatch, subscribe }
}

🔧 重点解析

  • reducer 是“状态的唯一改造者”
  • dispatch 就是发号施令,触发 state 更新
  • subscribe 是监听器列表,纯数组,简单高效

🧠 源码的美感

Redux 核心逻辑不到 100 行,却构建出一个完整的状态系统。这种结构就像一个“微型操作系统”,你能感受到作者那种“我不搞花活,代码要讲道理”的精神。

🍄 applyMiddleware:洋葱中传秘籍

Redux 支持中间件,通过 applyMiddleware(...middlewares) 实现异步、日志等功能。来看它的实现核心:

function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState)
    let dispatch = store.dispatch

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action),
    }

    const chain = middlewares.map((middleware) => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return { ...store, dispatch }
  }
}

关键词:compose、闭包、递归调度

🌰 举个栗子:

const logger = ({ getState }) => next => action => {
  console.log('before:', getState())
  const result = next(action)
  console.log('after:', getState())
  return result
}

是不是像一颗洋葱?一层层包裹住 dispatch,每层都可以动手脚,堪称函数式编程的艺术品!

🧝 Redux 小结:老骑士的优雅与疲惫

Redux 就像一位着甲持剑的老骑士:

  • 🏛️ 优点:稳定、可预测、开发工具强大
  • 🐌 缺点:样板多、入门曲线陡、写起来有点“重”

虽然它已经不再风靡,但它的“源码结构之美”依然值得每一位工程师学习。

三、Zustand:状态管理的灵兽馆

Zustand(德语中的“状态”)是一个由 Poimandres 团队打造的轻量状态库,特点是:

  • 不用 Provider,不用 Context
  • 核心 API 一个:create
  • 写起来像“驯一只动物”:你设定它,它忠实响应

🧠 核心机制回顾

Zustand 的使用方式大概长这样:

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

你在组件里只要这样用:

const count = useStore((state) => state.count)

没有 context、没有 reducer、甚至没有 action 类型 —— 是不是有点“反 Redux”的味道?

🔍 源码核心拆解:createStore

Zustand 本质是封装了一个状态容器,代码核心如下:

export const createStore = (initializer) => {
  let state
  const listeners = new Set()

  const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state = replace ? nextState : { ...state, ...nextState }
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  const getState = () => state

  const subscribe = (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }

  const api = { setState, getState, subscribe }
  state = initializer(setState, getState, api)

  return api
}

🧪 设计亮点

  • 使用闭包保存 state 与 listeners,不用 React context
  • 通过 subscribe 精准监听,性能比 context 更稳
  • setState 支持函数式更新,支持合并与替换

🐻 Zustand 像什么?

它像一只你亲手养大的灵兽:

  • 你教它:“当我点按钮就 count++”
  • 它记住后,每次都忠诚响应
  • 而且不吵不闹,不加 Provider,不抱怨 Props

一句话总结:

Redux 是写代码,Zustand 是养宠物。

🔍 Selector 精准监听的实现

Zustand 的 useStore(selector) 并不会导致全组件更新,因为它用了 shallow equality + 订阅机制:

useSyncExternalStore(
  store.subscribe,
  () => selector(store.getState()),
)

React 的 useSyncExternalStore 是为了这种外部状态订阅而生,Zustand 在这里用得恰到好处。

四、Jotai:状态即原子,原子即宇宙

Jotai(日语中意为“原子”)是 Recoil 的轻量替代者,由著名的 Zustand 团队出品。它提出了一个哲学式概念:

组件 = 原子状态的观察者

每个状态都是独立原子,互不干扰,天然响应式。

🧠 核心机制回顾

Jotai 的使用方式像这样:

const countAtom = atom(0)

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}

就像 Vue 的 ref,一切都是“声明原子 + 使用原子”。

🔍 源码核心拆解:createAtom + read/write

Jotai 的原子其实是一个描述对象(不是值本身):

const countAtom = {
  read: () => 0,
  write: (get, set, update) => ...
}

再结合 useAtom 和 Provider 构建状态依赖图。核心代码(简化后):

const atomState = new WeakMap()

function readAtom(atom) {
  const value = atom.read(getter)
  atomState.set(atom, { value })
  return value
}

function writeAtom(atom, update) {
  if (atom.write) {
    atom.write(getter, setter, update)
  }
}

Jotai 是一个运行时构建依赖关系图的库,当你读取一个 atom 时,它就追踪了谁依赖谁

🧮 Suspense & 异步 atom

Jotai 支持 Suspense,异步 atom 只需:

const userAtom = atom(async () => {
  const res = await fetch('/api/user')
  return res.json()
})

一旦调用 useAtom(userAtom),Suspense 自动生效。

这要归功于 Jotai 在内部构建了一张“Promise 状态图”,并且用异步缓存做了懒加载处理。

🧘 Jotai 像什么?

Jotai 更像一位禅宗大师:

  • 不喧哗,不绑定 UI 框架
  • 所有状态皆可组合,读写可分离
  • 当你需要异步,甚至 Suspense,它也心如止水

一句话总结:

Jotai 是 “响应式哲学” 的代言人。

五、源码哲学对比

特性 Redux Zustand Jotai
核心结构 Reducer + Store + Action createStore + Hook 原子 Atom + Provider
Context ✅ 必须 ❌ 不用 ✅ 可选(用于依赖图)
懒加载支持 ❌ 无 ✅ 内建 ✅ 优雅且兼容 Suspense
写法风格 函数式 + 模块化 Hook 风格 + 自由 哲学式声明 + 响应式
学习曲线 📈 陡峭 🟦 平稳 📉 初学友好,高级复杂
适用场景 企业级系统 中小项目 + 快速原型 响应式编程、组件粒度状态

六、结语:源码之下,皆有趣味

三大状态库,各有性格,各有光辉。

  • Redux 是架构控的浪漫
  • Zustand 是自由派的狂欢
  • Jotai 是响应式哲学的轻吟

源码不仅是工具的“内部运作”,更是设计哲学与取舍艺术的结晶

📚 阅读源码的姿势:

  • 带着问题去看
  • 从用户代码出发,逆推到内部实现
  • 找到每一个 “aha!” 的瞬间

状态管理没有银弹,但有哲学。

Redux 教我们组织和规范,Zustand 给我们灵活与自由,而 Jotai 提醒我们“原子化”思维的美妙。

如果你想写一个状态库,不妨多看看他们的源码。你会发现,每一行“简单”的代码背后,其实都藏着深思熟虑的设计决策。

💬 最后:你更喜欢哪一个状态库?或者你有没有看过哪个库的源码印象最深?欢迎留言讨论!

如果这篇文章对你有启发,一键三连 + 收藏 是我继续产出源码趣读内容的最大动力❤️

❌
❌