美文网首页
【转】一个新的React概念:Effect Event

【转】一个新的React概念:Effect Event

作者: 涅槃快乐是金 | 来源:发表于2023-07-16 00:56 被阅读0次

    大家好,我卡颂。

    每个框架由于实现原理的区别,都会有些独特的概念。比如:

    • Vue3由于其响应式的实现原理,衍生出refreactive等概念
    • Svelte重度依赖自身的编译器,所以衍生出与编译相关的概念(比如其对label标签的创新性使用)

    React中,有一个非常容易被误用的API —— useEffect,今天要介绍的Effect Event就属于由useEffect衍生出的概念。

    欢迎围观朋友圈、加入人类高质量前端交流群,带飞

    被误用的useEffect

    本文一共会涉及三个概念:

    • Event(事件)
    • Effect(副作用)
    • Effect Event(副作用事件)

    首先来聊聊EventEffectuseEffect容易被误用也是因为这两个概念很容易混淆。

    Event的概念

    在下面的代码中,点击div会触发点击事件,onClick是点击回调。其中onClick就属于Event

    function App() {
      const [num , update] = useState(0);
    
      function onClick() {
        update(num + 1);
      }
    
      return (
        <div onClick={onClick}>{num}</div>
      )
    }
    

    Event的特点是:是由某些行为触发,而不是状态变化触发的逻辑

    比如,在上述代码中,onClick是由点击事件这一行为触发的逻辑,num状态变化不会触发onClick

    Effect的概念

    Effect则与Event相反,他是由某些状态变化触发的,而不是某些行为触发的逻辑

    比如,在下述代码中,当title变化后document.title会更新为title的值:

    function Title({title}) {
      useEffect(() => {
        document.title = title;
      }, [title])
    
      // ...
    }
    

    上述代码中useEffect的逻辑就属于Effect,他是由title变化触发的。除了useEffect外,下面两个Hook也属于Effect

    • useLayoutEffect(不常用)
    • useInsertionEffect(很不常用)

    为什么容易误用?

    现在问题来了:EventEffect的概念完全不同,为什么会被误用?

    举个例子,在项目的第一个版本中,我们在useEffect中有个初始化数据的逻辑:

    function App() {
      const [data, updateData] = useState(null);
    
      useEffect(() => {
        fetchData().then(data => {
          // ...一些业务逻辑
          // 更新data
          updateData(data);
        })
      }, []);
    
      // ...
    }
    

    随着项目发展,你又接到一个需求:提交表单后更新数据。

    为了复用之前的逻辑,你新增了options状态(保存表单数据),并将他作为useEffect的依赖:

    function App() {
      const [data, updateData] = useState(null);
      const [options, updateOptions] = useState(null);
    
      useEffect(() => {
        fetchData(options).then(data => {
          // ...一些业务逻辑
          // 更新data
          updateData(data);
        })
      }, [options]);
    
      function onSubmit(opt) {
        updateOptions(opt);
      }
    
      // ...
    }
    

    现在,提交表单后(触发onSubmit回调)就能复用之前的数据初始化逻辑。

    这么做实在是方便,以至于很多同学认为这就是useEffect的用法。但其实这是典型的useEffect误用

    仔细分析我们会发现:提交表单显然是个Event(由提交的行为触发),Event的逻辑应该写在事件回调中,而不是useEffect中。正确的写法应该是这样:

    function App() {
      const [data, updateData] = useState(null);
    
      useEffect(() => {
        fetchData().then(data => {
          // ...一些业务逻辑
          // 更新data
          updateData(data);
        })
      }, []);
    
      function onSubmit(opt) {
        fetchData(opt).then(data => {
          // ...一些业务逻辑
          // 更新data
          updateData(data);
        })
      }
    
      // ...
    }
    

    上述例子逻辑比较简单,两种写法的区别不大。但在实际项目中,随着项目不断迭代,可能出现如下代码:

    useEffect(() => {
      fetchData(options).then(data => {
        // ...一些业务逻辑
        // 更新data
        updateData(data);
      })
    }, [options, xxx, yyy, zzz]);
    

    届时,很难清楚fetchData方法会在什么情况下执行,因为:

    1. useEffect的依赖项太多了
    2. 很难完全掌握每个依赖项变化的时机

    所以,在React中,我们需要清楚的区分EventEffect,也就是清楚的区分一段逻辑是由行为触发的,还是状态变化触发的?

    useEffect的依赖问题

    现在,我们已经能清楚的区分EventEffect,按理说写项目不会有问题了。但是,由于Effect的机制问题,我们还面临一个新问题。

    假设我们有段聊天室代码,当roomId变化后,要重新连接到新聊天室。在这个场景下,聊天室的断开/重新连接依赖于roomId状态的变化,显然属于Effect,代码如下:

    function ChatRoom({roomId}) {
      useEffect(() => {
        const connection = createConnection(roomId);
        connection.connect();
    
        return () => {
          connection.disconnect()
        };
      }, [roomId]);
    
      // ...
    }
    

    接下来你接到了新需求 —— 当连接成功后,弹出全局提醒

    全局提醒是否是黑暗模式,受到theme props影响。useEffect修改后的代码如下:

    useEffect(() => {
      const connection = createConnection(roomId);
      connection.connect();
    
      connection.on('connected', () => {
        showNotification('连接成功!', theme);
      });
    
      return () => connection.disconnect();
    }, [roomId, theme]);
    

    但这段代码有个严重问题 —— 任何导致theme变化的情况都会导致聊天室断开/重新连接。毕竟,theme也是useEffect的依赖项。

    在这个例子中,虽然Effect依赖theme,但Effect并不是由theme变化而触发的(他是由roomId变化触发的)。

    为了应对这种场景,React提出了一个新概念 —— Effect Event。他指那些在Effect内执行,但Effect并不依赖其中状态的逻辑,比如上例中的:

    () => {
      showNotification('连接成功!', theme);
    }
    

    我们可以使用useEffectEvent(这是个试验性Hook)定义Effect Event

    function ChatRoom({roomId, theme}) {
      const onConnected = useEffectEvent(() => {
        showNotification('连接成功!', theme);
      });
    
      useEffect(() => {
        const connection = createConnection(roomId);
        connection.connect();
    
        connection.on('connected', () => {
          onConnected();
        });
    
        return () => {
          connection.disconnect()
        };
      }, [roomId]);
    
      // ...
    }
    

    在上面代码中,theme被移到onConnected(他是个Effect Event)中,useEffect虽然使用了theme的最新值,但并不需要将他作为依赖。

    useEffectEvent源码解析

    useEffectEvent的实现并不复杂,核心代码如下:

    function updateEvent(callback) {
      const hook = updateWorkInProgressHook();
      // 保存callback的引用
      const ref = hook.memoizedState;
      // 在useEffect执行前更新callback的引用
      useEffectEventImpl({ref, nextImpl: callback});
    
      return function eventFn() {
        if (isInvalidExecutionContextForEventFunction()) {
          throw new Error(
            "A function wrapped in useEffectEvent can't be called during rendering.",
          );
        }
        return ref.impl.apply(undefined, arguments);
      };
    }
    

    其中ref变量保存callback的引用。对于上述例子中:

    const onConnected = useEffectEvent(() => {
      showNotification('连接成功!', theme);
    });
    

    ref保存对如下函数的引用:

    () => {
      showNotification('连接成功!', theme);
    }
    

    useEffectEventImpl方法接受refcallback的最新值为参数,在useEffect执行前会将ref中保存的callback引用更新为callback的最新值

    所以,当在useEffect中执行onConnected,获取的就是ref中保存的下述闭包的最新值:

    () => {
      showNotification('连接成功!', theme);
    }
    

    闭包中的theme自然也是最新值。

    useEffectEvent与useEvent

    仔细观察下useEffectEvent的返回值,他包含了两个限制:

    return function eventFn() {
        if (isInvalidExecutionContextForEventFunction()) {
          throw new Error(
            "A function wrapped in useEffectEvent can't be called during rendering.",
          );
        }
        return ref.impl.apply(undefined, arguments);
    };
    

    第一个限制比较明显 —— 下面这行代码限制useEffectEvent的返回值只能在useEffect回调中执行(否则会报错):

    if (isInvalidExecutionContextForEventFunction()) {
      // ... 
    }
    

    另一个限制则比较隐晦 —— 返回值是个全新的引用:

    return function eventFn() {
      // ...
    };
    

    如果你不太明白全新的引用为什么是个限制,考虑下返回一个useCallback返回值:

    return useCallback((...args) => {
        const fn = ref.impl;
        return fn(...args);
    }, []);
    

    这将会让useEffectEvent的返回值成为不变的引用,如果再去掉只能在useEffect回调中执行的限制,那么useEffectEvent将是加强版的useCallback

    举个例子,如果破除上述限制,那么对于下面的代码:

    function App({a, b}) {
      const [c, updateC] = useState(0);
      const fn = useCallback(() => a + b + c, [a, b, c])
    
      // ...
    }
    

    useEffectEvent替代useCallback,代码如下:

    const fn = useEffectEvent(() => a + b + c)
    

    相比于useCallback,他有2个优点:

    1. 不用显式声明依赖
    2. 即使依赖变了,fn的引用也不会变,简直是性能优化的最佳选择

    那么React为什么要为useEffectEvent加上限制呢?

    实际上,useEffectEvent的前身useEvent就是遵循上述实现,但是由于:

    1. useEvent的定位应该是Effect Event,但实际用途更广(可以替代useCallback),这不符合他的定位
    2. 当前React Forget(能生成等效于useMemouseCallback代码的官方编译器)并未考虑useEvent,如果增加这个hook,会提高React Forget实现的难度

    所以,useEvent并没有正式进入标准。相反,拥有更多限制的useEffectEvent反而进入了React文档

    总结

    今天我们学到三个概念:

    • Event:由某些行为触发,而不是状态变化触发的逻辑
    • Effect:由某些状态变化触发的,而不是某些行为触发的逻辑
    • Effect Event:在Effect内执行,但Effect并不依赖其中状态的逻辑

    其中Effect EventReact中的具体实现是useEffectEvent。相比于他的前身useEvent,他附加了2条限制:

    1. 只能在Effect内执行
    2. 始终返回不同的引用

    在我看来,Effect Event的出现完全是由于Hooks实现机制上的复杂性(必须显式指明依赖)导致的心智负担。

    毕竟,同样遵循Hooks理念的Vue Composition API就没有这方面问题。

    相关文章

      网友评论

          本文标题:【转】一个新的React概念:Effect Event

          本文链接:https://www.haomeiwen.com/subject/hfviudtx.html