记 Vue 3 的几个坑

梨子2022-10-19
本文全长 1160 字,全部读完大约需要 4 分钟。

作为一个从写 Mixin 到 HOC 再到 Hooks 过来的人,一直以来最推崇的是 React,尤其是学会用 Hooks 以后,更是只喜欢写 Hooks,看见 Class 就烦,逢人必安利 Hooks。但是现在在用了 Vue 3 几个月以后,我变得再也不想用 React 了。所以,请允许我简要安利一下 Vue 3。

Composition API + Setup Script 可以让我们使用像 Hooks 一样的函数组合写法来书写组件逻辑,和 Hooks 一样具有任意的抽象灵活度。同时对象深响应的能力又摆脱了 Vue 2 必须用 data 浅对象的尴尬问题,可以说是直接摆脱了 Redux、MobX 等等一众工具和它们的捆绑的范式,想怎么写怎么写,又没有 React 的性能问题。

引用官方文档描写 React Hooks 的一些话,觉得写得非常真实,很多用 React 的人不愿意用 Hooks,说 Hooks 难学,基本上问题都总结在这了:

React Hooks are invoked repeatedly every time a component updates. This creates a number of caveats that can confuse even seasoned React developers. It also leads to performance optimization issues that can severely affect development experience. Here are some examples:

  • Hooks are call-order sensitive and cannot be conditional.
  • Variables declared in a React component can be captured by a hook closure and become "stale" if the developer fails to pass in the correct dependencies array. This leads to React developers relying on ESLint rules to ensure correct dependencies are passed. However, the rule is often not smart enough and over-compensates for correctness, which leads to unnecessary invalidation and headaches when edge cases are encountered.
  • Expensive computations require the use of useMemo, which again requires manually passing in the correct dependencies array.
  • Event handlers passed to child components cause unnecessary child updates by default, and require explicit useCallback as an optimization. This is almost always needed, and again requires a correct dependencies array. Neglecting this leads to over-rendering apps by default and can cause performance issues without realizing it.
  • The stale closure problem, combined with Concurrent features, makes it difficult to reason about when a piece of hooks code is run, and makes working with mutable state that should persist across renders (via useRef) cumbersome.

所以,如果这个年头有人想学前端或给项目技术选型的话,我会回答大中小项目都建议直接用 Vue 3,不需要再权衡任何东西。

不过这套东西虽然精妙,还是遇到了几个坑,

toRef、toRefs 对源对象赋值导致解绑问题

Vue SFC Playground

打开上面的网页,先点击【set b】,会发现两个值都联动地被修改了,很好。再点击【set a】,会发现只有「a.b」更新了,而「b」没有更新,并且从此以后再【set b】都不好使了。

这问题还挺容易触发的,比如在某 hook 中返回了一个对象 a,然后再在某处组件中自然而然地(文档也这么教的)使用 toRefs 解构出了 b,结果用着用着,在 hook 内部对 a 进行了赋值(hook 的作者是考虑不到在外面被 toRef 这回事的),就导致 b 和 a 解绑了,然后就会发生很诡异的问题。

解决方法就是不要用 toRefs,改用 computed 代替。就是这样每个写起来巨长,如果是解构一堆对象那更是要命地长。

// BAD
const { b } = toRefs(a.value)
// GOOD
const b = computed({
  get () {
    return a.value.b
  },
  set (x) {
    a.value.b = x
  }
})

未激活的 keep-alive 组件中子组件生命周期钩子正常触发

Vue SFC Playground

打开上面的网页,依次点击【1】、【2】、【3】、【4】,会发现越往后,挂载的组件越多。第一次遇到时会觉得非常诡异,其实是因为组件虽然在 keep-alive 中没激活,但是仍然是会正常渲染的,因此子组件树中的生命周期钩子会被正常调用。此处如果不注意的话,容易造成内存泄漏或大量未退订的事件导致卡顿。

尴尬的是,onActivated 和 onDeactivated 这两个钩子子组件是用不了的,只有 keep-alive 下面直接一级的组件可以用。所以后代组件要知道自己是不是被激活,需要一级组件想办法向下透传,这就很麻烦。

除特殊说明以外,本网站文章采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。