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

大家好,我卡颂。

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

  • 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就没有这方面问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容