Vue 中没有闭包陷阱,但为此付出了什么

在社区里,部分 Vue 使用者,会常常因为 React 中存在闭包陷阱,而认为 Vue 是一个更加优秀的框架。个别极端的 Vue 使用者,还会因此而贬低 React,认为闭包陷阱是 React 的一个设计缺陷。

今天聊一个非常有争议的话题。

在社区里,部分 Vue 使用者,会常常因为 React 中存在闭包陷阱,而认为 Vue 是一个更加优秀的框架。个别极端的 Vue 使用者,还会因此而贬低 React,认为闭包陷阱是 React 的一个设计缺陷。

那么,这真的是 React 的设计缺陷吗?

Vue 中没有闭包陷阱,那它是否也为此付出了什么代价呢?

我们一点点来分析一下

1.前置知识

我们来思考一个场景。在一个单独的模块 A.js 中定义一个变量。

// A.js
let a = 20

然后在模块 B.js 中,我们想要访问这个变量 a,并且能够修改这个变量 a 的值,应该怎么办呢?

// B.js
import A from './A.js'

// 如何访问模块 A 中的变量 a

我们发现无法直接访问,因此,我们通常的做法是在模块 A 中,导出一个专门用于访问 变量 a 的函数,和一个专门用于修改 变量 a 的函数。

// A.js
let a = 20
export function getA() {
  return a
}
export function setA(value) {
  a = value
}

然后我们在模块 B 中,就可以调用 getA 函数来访问变量 a,也可以调用 setA 函数来修改变量 a 的值。

// B.js
import { getA, setA } from './A.js'

const value = getA()
console.log(value)
setA(30)
// 此时 value 的值会变成 30 吗?

此时,我们就遇到一个经典问题:当我调用了 setA 修改了 A 的值之后,上面代码中的 value 的值会发生变化吗?

正确答案是:不会。

这就有意思了,为什么 value 的值不会发生变化呢?

这是因为我们通过 getA 函数访问的是变量 a 的值,而不是变量 a 的引用。因此,如果想要得到新的值,我们还需要重新调用一次 getA 函数。

// B.js
import { getA, setA } from './A.js'

const value = getA()
console.log(value)
setA(30)
// 此时得到最新值
const value2 = getA()
console.log(value2) // 30

那我们能不能不通过调用 getA 函数,就能够直接访问到变量 a 的值呢?

答案是不行。

现在,我们对这种传统的方式进行两种思路的调整。

第一种是稍作修改,模仿成 React 语法的样子。

// A.js
let a = 20

// 充当了 get 的角色
function useState() {
  return [a, setA]
}

function setA(value) {
  a = value
}
    // B.js
    
    import { useState } from './A.js'
    
    const [a, setA] = useState()
    console.log(a)
    setA(30)
    console.log(a) // 20

    我们会发现,这个情况,就跟 React 中,我们修改了 state 值之后,无法直接访问到最新的 state 值一样了。

    所示我经常说,无法获取到最新值,不是 React 的设计缺陷,而是 JS 语言特性他就是如此。

    第二种,我们可以通过重新定义 a 的类型,来避免使用 getA 才能访问新值。

    重新修改 A.js 模块,代码如下所示:

    // A.js
    
    let a = {
      value: 20
    }
    
    // 充当 get 的角色
    export function ref() {
      return a
    }
      // B.js
      
      import { ref } from './A.js'
      
      const a = ref()
      console.log(a.value)
      // 充当 set 的角色
      a.value = 30
      // 此时通过 .value 访问到最新值
      console.log(a.value) // 30

      此时,由于我们拿到的直接是一个引用类型,因此,我们可以通过 .value 的方式,做了一个访问的动作,从而得到最新的值。

      此时,我们就可以发现,虽然上面的代码演变,一直都是框架无关的,但是,我们只需要稍作调整,就可以几乎完全一致的分别还原 React 与 Vue 的语法。

      2.Vue 付出的代价是什么?

      接下来,我们要思考的是,当我们通过调整变量的类型结构,把基础类型包装成引用类型之后,Vue 为此付出了什么代价?

      首先一个很明显的代价就是:语义不一致。

      在 Vue 中,当我们使用 ref 定义一个响应式状态时,认为这个状态应该是一个基础类型。但是实际上,我们拿到的是一个引用类型。

      通过 ref 传入的基础类型必须包裹到一个引用类型中,才能让能力变得正常。

      因此,我们必须使用 .value 的方式来访问最新值。

      不少开发者会觉得这种方式不够优雅。

      所以,在某个阶段,Vue 团队也曾经试图解决这个问题,并提出了如下这种方案。

      let count = $ref(0)
      
      // 直接访问,无需 .value
      console.log(count)
      
      function increment() {
        count++
      }

      这种方式是通过在编译时,自动添加 .value 的方式来访问最新值。但是最终由于要解决的问题更多,还是放弃掉了这种方案,ref 也被扩展到可以传入对象,并被官方团队作为推荐使用。

      其次,由于语义的不一致,.value 的使用,在 templatewatch、深层监听、 组件传参 等问题中,用法也不一样,比较混乱。

      如下所示:

      const x = ref(0)
      const y = ref(0)
      
      // 不用 .value
      watch(x, (newX) => {
        console.log(`x is ${newX}`)
      })
      
      // 使用 .value
      watch(
        () => x.value + y.value,
        (sum) => {
          console.log(`sum of x + y is: ${sum}`)
        }
      )
      
      // 不用 .value 与 使用 .value 混用
      watch([x, () => y.value], ([newX, newY]) => {
        console.log(`x is ${newX} and y is ${newY}`)
      })

      假如你是一名 Vue 新玩家,看到这样的使用场景,你会不会感觉有点懵?

      于是,在使用 Vue 的时候,有的同学老有一种我使用的这个值,到底有没有被监听到、还有没有响应性的心理负担存在。

      事实上,为了与 React 在底层的实现保持差异,Solidjs 在语法上也付出了与 Vue 类似的代价。

      如下所示,是一个 Solidjs 的案例。

      const CountingComponent = () => {
        const [count, setCount] = createSignal(0);
        const interval = setInterval(
          () => setCount(count => count + 1),
          1000
        );
        onCleanup(() => clearInterval(interval));
        return <div>Count value is {count()}</div>;
      };

      这里我们要非常关注的是,状态 count 返回的不是一个值,而是一个函数。他虽然不需要通过 .value 去获取最新值,但是他需要通过返回一个函数,并通过调用该函数的方式,才能得到最新值。

      所以他使用的时候就变成这个样子了。

      <div>Count value is {count()}</div>

        但是与此同时,他的 set 方法中回调函数的参数,又不是一个函数,而是一个状态值,所以写法就与外面的 count() 不一致。

        setCount(count => count + 1)
        
        // or
        setCount(count() + 1)

        也正因为如此,Vue 语法不一致、状态易丢失响应性的坑,Solidjs 一个也避免不了。特别是在组件传 props 时,迷惑性很强。也是采用了一堆语法糖来修修补补。

        这就是代价。

        所以当你觉得 React 的闭包陷阱,是一个设计缺陷的时候,不妨也想想 Vue 和 Solidjs 为了不出现闭包陷阱,都付出了什么样的代价,也许你会有不一样的答案。

        我的观点是,并不存在谁的设计理念更先进,这只是在没有完美方案之下的权衡而已。

        3.React 是如何思考的?

        实际上,在 React 中,也有通过访问引用类型的方式,直接获取值的语法,这就是 useRef()

        const count = useRef(0)
        
        // 通过 .current 访问最新值
        count.current

        但是区别就是,我们使用 useRef 定义的值,不具备响应性,他只是一个普通的 JS 变量,不与组件状态绑定。那为什么 React 要这样做呢?

        React 基于一个很重要的原则:使用 useState 定义的值,只与组件状态绑定,而使用 useRef 定义的值,则仅参与逻辑运算,不与组件状态绑定,更新时也不影响组件的重新渲染。

        而状态值的更新,会引发组件重新执行,此时 useState 就会自然得到一次执行机会,从而获取到最新值。

        因此,在理想情况下,如果使用者能够正确分清楚:哪些是状态值,哪些是逻辑值,就能极大的避免需要获取最新值的场景出现。

        但是麻烦的地方就在于,一部分 React 开发者由于自学的缘故,所以并没有意识到应该去区分状态的属性问题。于是就有一种情况出现,他们在项目中,会疯狂滥用 useState,定义任何变量都是 useState

        这种情况之下,闭包陷阱就非常容易出现。

        4.React 中更麻烦的情况

        前面我们提到了要区分状态值和逻辑值,但是这个时候,会存在一个更麻烦的情况,那就是,在少部分情况下,有一个状态,他他既是状态值,又是逻辑值,事情就麻烦了。

        这就会非常容易导致闭包陷阱的产生。就如这个案例的 increment 变量,他既是状态值,又是逻辑值。

        面对这种情况,我们通过将该状态值一分为二的方式来解决,分别定义一个状态值,一个逻辑值。如下所示:

        import { useState, useEffect, useRef } from 'react';
        import Button from 'components/ui/button';
        
        export default function Timer() {
          const [count, setCount] = useState(0);
          const [increment, setIncrement] = useState(1);
          const incrementRef = useRef(1);
        
          useEffect(() => {
            const id = setInterval(() => {
              setCount(c => c + incrementRef.current);
            }, 1000);
            return () => {
              clearInterval(id);
            };
          }, []);
        
          function incrementHandler() {
            setIncrement(i => i + 1);
            incrementRef.current += 1;
          }
        
          function decrementHandler() {
            setIncrement(i => i - 1);
            incrementRef.current -= 1;
          }
        
          function resetHandler() {
            setCount(0);
          }
        
          return (
            <div className='p-4'>
              <div className='flex items-center justify-between'>
                <div className='text-2xl font-bold font-din'>
                  Counter: {count}
                </div>
                
                <Button onClick={resetHandler}>Reset</Button>
              </div>
              <hr />
              <div className='flex items-center gap-2'>
                Every second, increment by:
                <Button disabled={increment === 0} onClick={decrementHandler}>–</Button>
                <span className='text-lg font-din'>{increment}</span>
                <Button onClick={incrementHandler}>+</Button>
              </div>
            </div>
          );
        }

        我们希望他以逻辑值的身份参与到 useEffect 的回调函数中,而不是以状态值的身份去添加到依赖项中。

        因此,在过往的解决方案中,我们为了绕开闭包陷阱,但是又不想把 increment 作为依赖项,我们就会把这个变量一分为二,分别定义一个状态值,一个逻辑值。

        // 状态值驱动 UI 变化
        const [increment, setIncrement] = useState(1);
        
        // 逻辑值参与 useEffect 的回调函数逻辑运算
        const incrementRef = useRef(1);

        然后在更新时,保证状态值与逻辑值的同步更新。

        setIncrement(i => i + 1);
        incrementRef.current += 1;

        这样,我们就可以保证在 useEffect 的回调函数中,使用的 increment 值始终是最新的值,又不用把 increment 作为依赖项。

        5.总结

        很显然,在如何访问到最新值上面,Vue 和 React 做了不一样的选择。但是,也并不是没有付出任何代价。Vue 在语法上付出了语义不一致的代价,React 在逻辑上付出了需要区分状态值和逻辑值的代价。

        两种方案都不完美,这只是一种根据实际情况做出的选择,而不存在谁一定比谁更好,谁一定就是最优解的说法。

        对于我个人而言,我更倾向于 React 的选择。这是因为,随着我们对 React 的理解越来越深,我可以通过提高自己个人开发能力的方式合理的区分状态值与逻辑值,从而避免闭包陷阱的产生。

        但是 Vue/solidjs 语义不一致的问题,却永远都会存在。

        如果是你,你会更倾向于哪种方案呢?

        AI大模型学习福利

        作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

        一、全套AGI大模型学习路线

        AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

        二、640套AI大模型报告合集

        这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

        三、AI大模型经典PDF籍

        随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。


        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

        四、AI大模型商业化落地方案

        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

        作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

        评论
        成就一亿技术人!
        拼手气红包6.0元
        还能输入1000个字符
         
        红包 添加红包
        表情包 插入表情
         条评论被折叠 查看
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

        当前余额3.43前往充值 >
        需支付:10.00
        成就一亿技术人!
        领取后你会自动成为博主和红包主的粉丝 规则
        hope_wisdom
        发出的红包
        实付
        使用余额支付
        点击重新获取
        扫码支付
        钱包余额 0

        抵扣说明:

        1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
        2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

        余额充值