惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
Recent Announcements
Recent Announcements
月光博客
月光博客
B
Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
The GitHub Blog
The GitHub Blog
Last Week in AI
Last Week in AI
罗磊的独立博客
NISL@THU
NISL@THU
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Proofpoint News Feed
H
Help Net Security
L
LINUX DO - 最新话题
MongoDB | Blog
MongoDB | Blog
雷峰网
雷峰网
The Hacker News
The Hacker News
Apple Machine Learning Research
Apple Machine Learning Research
I
Intezer
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Know Your Adversary
Know Your Adversary
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Secure Thoughts
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
C
CERT Recently Published Vulnerability Notes
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
F
Full Disclosure
人人都是产品经理
人人都是产品经理
Blog — PlanetScale
Blog — PlanetScale
Recorded Future
Recorded Future
T
Threat Research - Cisco Blogs
博客园 - 三生石上(FineUI控件)
S
Securelist
T
The Exploit Database - CXSecurity.com
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
Security Archives - TechRepublic
Security Archives - TechRepublic
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
V
Visual Studio Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tor Project blog
博客园 - 司徒正美
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org

动物的光合作用

使用 Peer Relay 提高 Tailscale 的速度 内网穿透下 NAS 使用 DNS 轮循作为负载均衡 <众安车保>上当指南 - 动物的光合作用 JS 中的 Shebang/Hashbang - 动物的光合作用 我的自建 NAS - 动物的光合作用 React 组件中复用代码的方式 - 动物的光合作用 负载均衡下的前端资源更新策略 - 动物的光合作用 CSS 基于视区的长度单位 - 动物的光合作用 Excalidraw 支持自定义字体 - 动物的光合作用 我用 Docker 部署的一些服务 - 动物的光合作用 CSS 书写模式和逻辑属性 - 动物的光合作用 大量活动页面持续性维护的解决方案 - 动物的光合作用 Mac mini 搭建外网可访问的简易 NAS - 动物的光合作用 React Fragment 添加事件监听? - 动物的光合作用 如何在 React 解决竞态条件 - 动物的光合作用 使用脚本备份 Github 仓库 - 动物的光合作用 高性能 React 应用的几个小技巧 - 动物的光合作用 复习 DOM 事件 - 动物的光合作用 我的博客架构变迁之路 - 动物的光合作用 浏览器和 JavaScript 的一些新特性 - 动物的光合作用 useCallback 的误区 - 动物的光合作用 如何处理 RESTFUL 数据异常导致的前端错误 - 动物的光合作用 基于 Node.js 的 WebFont 解决方案
在 React 中使用事件分离状态 - 动物的光合作用
mebtte · 2020-01-15 · via 动物的光合作用

最近在 React 项目遇到了一个问题, Web App 有一个登录弹窗, 在没有登录的情况下进行一些需要用户信息的操作, 首先会弹出登录弹窗.

然而这种操作分布在各个组件, 所以在 redux 保留一个变量 loginDialogVisible, 通过 action 控制登录弹窗是否展示.

// reducer
import TYPE from 'path/type';

export const showLoginDialog = () => ({
  type: TYPE.SHOW_LOGIN_DIALOG,
});

export const hideLoginDialog = () => ({
  type: TYPE.HIDE_LOGIN_DIALOG,
});

const initialState = {
  // ...
  loginDialogVisible: false,
};

export default (state = initialState, { type, payload }) => {
  switch (type) {
    // ...
    case TYPE.SHOW_LOGIN_DIALOG:
      return {
        ...state,
        loginDialogVisible: true,
      };
    case TYPE.HIDE_LOGIN_DIALOG:
      return {
        ...state,
        loginDialogVisible: false,
      };
    // ...
  }
};
// 登录弹窗
import React from 'react';
import { useSelector, shallowEqual } from 'react-redux';

const LoginDialog = () => {
  const visible = useSelector(
    (state) => state.loginDialogVisible,
    shallowEqual,
  );
  // ...

  return visible && <div>...</div>;
};

export default LoginDialog;
// 需要弹出登录弹窗的组件
import React from 'react';
import { useDispatch } from 'react-redux';

import { showLoginDialog } from 'path/reducer';

const Component = () => {
  const dispatch = useDispatch();
  const onShowLoginDialog = () => dispatch(showLoginDialog());

  return (
    <button type="button" onClick={onShowLoginDialog}>
      show login dialog
    </button>
  );
};

export default Component;

随着功能的迭代, 需要弹出登录弹窗的组件越来越多, 所以下面的代码会重复出现.

import { useDispatch } from 'react-redux';

import { showLoginDialog } from 'path/reducer';

// ...
const dispatch = useDispatch();
const onShowLoginDialog = () => dispatch(showLoginDialog());
// ...

为了减少重复代码, 可以提取成一个自定义 hooks.

// useShowLoginDialog.js
import { useDispatch, useCallback } from 'react-redux';

import { showLoginDialog } from 'path/reducer';

export default () => {
  const dispatch = useDispatch();
  const onShowLoginDialog = useCallback(() => dispatch(showLoginDialog()), [
    dispatch,
  ]);
  return onShowLoginDialog;
};

在需要弹出弹窗的组件移除重复的代码.

import React from 'react';

import useShowLoginDialog from 'path/useShowLoginDialog';

const Component = () => {
  const onShowLoginDialog = useShowLoginDialog();

  return (
    <button type="button" onClick={onShowLoginDialog}>
      show login dialog
    </button>
  );
};

export default Component;

虽然上面的解决方案已经挺好, 但有一个问题, loginDialogVisible 这个状态有且只有 LoginDialog 使用, 应属于 LoginDialog局部状态, 没有必要放在 redux 影响全局状态, 放在 redux 的目的只是为了其他组件能够控制这个状态.

怎么使 loginDialogVisible 成为 LoginDialog 的局部状态, 并且能在其他组件控制这个状态?

答案就是通过 事件.

首先实现一个全局事件触发器, 当然, 也可以选用一些非常受欢迎的库, 比如 eventemitter3.

为了重复造轮子(其实是为了减小包体积), 下面以自定义全局事件触发器为例, 原理是利用浏览器自身的 CustomEvent 接口.

// eventemitter.js

// 自定义事件类型
export const TYPE = {
  SHOW_LOGIN_DIALOG: 'show_login_dialog',
};

export default {
  on: (type, listener) => document.addEventListener(type, listener),
  off: (type, listener) => document.removeEventListener(type, listener),
  /**
   * payload就是在触发事件的同时携带数据
   * 但是在登录弹窗这个例子里没有用到
   * 通过event.detail可以获取到payload
   */
  emit: (type, payload) =>
    document.dispatchEvent(
      new CustomEvent(type, {
        detail: payload,
        bubbles: false,
        cancelable: false,
      }),
    ),
};
// 登录弹窗
import React, { useState, useEffect } from 'react';

import eventemitter, { TYPE } from 'path/eventemitter';

const LoginDialog = () => {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const listener = () => setVisible(true);
    eventemitter.on(TYPE.SHOW_LOGIN_DIALOG, listener);
    return () => eventemitter.off(TYPE.SHOW_LOGIN_DIALOG, listener);
  }, []);

  return visible && <div>...</div>;
};

export default LoginDialog;
// 触发弹窗的组件
import React, { useCallback } from 'react';

import eventemitter, { TYPE } from 'path/eventemitter';

const Component = () => {
  const onShowLoginDialog = useCallback(
    () => eventemitter.emit(TYPE.SHOW_LOGIN_DIALOG),
    [],
  );

  return (
    <button type="button" onClick={onShowLoginDialog}>
      show login dialog
    </button>
  );
};

export default Component;

这样, 使 loginDialogVisible 成为 LoginDialog 的局部状态, 其他组件依然能够控制 loginDialogVisible, 并且完全脱离了 redux 的依赖.

通过事件机制, 可以很大程度上分离 react 中的状态, 不仅可以分离全局状态, 而且可以提升 React 性能.

import React, { useState } from 'react';

import Increase from 'path/increase';
import Decrease from 'path/decrease';
import Display from 'path/display';

const App = () => {
  const [count, setCount] = useState(0);
  const onIncrease = () => setCount((c) => c + 1);
  const onDecrease = () => setCount((c) => c - 1);

  return (
    <div>
      <Display count={count} />
      <Increase onIncrease={onIncrease} />
      <Decrease onDecrease={onDecrease} />
    </div>
  );
};

上面点击 Increase 组件使 count + 1, 点击 Decrease 组件使 count - 1, Display 组件负责展示 count.

因为状态 count 属于根组件, 所以无论点击 Increase 或者 Decrease 组件导致的变更, 三个子组件都会重新执行各自的 render 方法. 然而实际上无论 count 怎么变, 我们都知道 IncreaseDecreaserender 结果都不会变, 所以重新执行 render 是没有必要的, 唯一需要更新的只有 Display 组件.

当然, 通过 React 自带的一些工具方法可以达到性能优化的目的.

import React, { useCallback, useMemo } from 'react';

// ...
const onIncrease = useCallback(() => setCount((c) => c + 1), []);
const onDecrease = useCallback(() => setCount((c) => c - 1), []);
return (
  <div>
    <Display count={count} />
    {useMemo(
      () => (
        <Increase onIncrease={onIncrease} />
      ),
      [onIncrease],
    )}
    {useMemo(
      () => (
        <Decrease onIncrease={onIncrease} />
      ),
      [onDecrease],
    )}
  </div>
);

通过 useCallback 能够保证 onIncreaseonDecrease 在组件存活周期内不变, 而通过 useMemo 可以缓存 IncreaseDecrease 的渲染结果, 但是 useCallbackuseMemo 依然要消耗性能.

通过事件机制, 可以更简单地实现性能优化.

IncreaseDecrease 组件保持不变, 将状态 count 放到 Display 中, 通过事件传递 count 的变更.

// app.js
import React, { useState } from 'react';

import eventemitter from 'path/eventemitter';

import Increase from 'path/increase';
import Decrease from 'path/decrease';
import Display from 'path/display';

const App = () => {
  const onIncrease = () => eventemitter.emit(TYPE.INCREASE);
  const onDecrease = () => eventemitter.emit(TYPE.DECREASE);

  return (
    <div>
      <Display />
      <Increase onIncrease={onIncrease} />
      <Decrease onDecrease={onDecrease} />
    </div>
  );
};
// display.js
import React, { useState, useEffect } from 'react';

import eventemitter from 'path/eventemitter';

const Display = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const increaseListener = () => setCount((c) => c + 1);
    const decreaseListener = () => setCount((c) => c - 1);
    eventemitter.on(TYPE.INCREASE, increaseListener);
    eventemitter.on(TYPE.DECREASE, decreaseListener);
    return () => {
      eventemitter.off(TYPE.INCREASE, increaseListener);
      eventemitter.off(TYPE.DECREASE, decreaseListener);
    };
  }, []);

  return <div>{count}</div>;
};

export default Display;

这样, 无论 count 怎么发生变更, 只会 rerender Display 组件. 当然, 为了节省内存, 可以合并 INCREASEDECREASE, 通过 eventemitterpayload 表示增加还是减少.

// app.js
const onIncrease = () => eventemitter.emit(TYPE.COUNT_CHANGE, 1);
const onDecrease = () => eventemitter.emit(TYPE.COUNT_CHANGE, -1);

// display.js
useEffect(() => {
  const listener = (event) => setCount((c) => c + event.detail);
  eventemitter.on(TYPE.COUNT_CHANGE, listener);
  return () => eventemitter.off(TYPE.COUNT_CHANGE, listener);
}, []);

最后, 要记得移除事件监听器.