美文网首页
第三五章 逃生舱口-序

第三五章 逃生舱口-序

作者: 深圳都这么冷 | 来源:发表于2023-02-22 10:26 被阅读0次

逃生舱口

你的一些组件可能需要控制和同步 React 之外的系统。 例如,您可能需要使用浏览器 API 聚焦输入,播放和暂停未使用 React 实现的视频播放器,或者连接并收听来自远程服务器的消息。 在本章中,您将学习让您“走出去”React 并连接到外部系统的逃生舱口。 大多数应用程序逻辑和数据流不应依赖这些功能。

本章内容预告

  • 如何在不重新渲染的情况下“记住”信息
  • 如何访问 React 管理的 DOM 元素
  • 如何将组件与外部系统同步
  • 如何从组件中删除不必要的效果
  • Effect 的生命周期与组件的生命周期有何不同
  • 如何防止某些值重新触发 Effects
  • 如何减少 Effect 重新运行的频率
  • 如何在组件之间共享逻辑

使用 ref 引用值

当你想让一个组件“记住”一些信息,但又不想让这些信息触发新的渲染时,你可以使用 ref:

const ref = useRef(0);

与状态一样,refs 在重新渲染之间由 React 保留。 但是,设置状态会重新渲染组件。 更改 ref 不会! 您可以通过 ref.current 属性访问该 ref 的当前值。

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

ref 就像 React 不跟踪的组件的秘密口袋。 例如,您可以使用 refs 来存储超时 ID、DOM 元素和其他不影响组件渲染输出的对象。

准备好学习这个主题了吗?

阅读使用 ref 引用值了解如何使用 refs 来记住信息。

使用 ref 操作 DOM

React 会自动更新 DOM 以匹配您的渲染输出,因此您的组件不需要经常操作它。 然而,有时您可能需要访问由 React 管理的 DOM 元素——例如,聚焦一个节点,滚动到它,或者测量它的大小和位置。 在 React 中没有内置的方法来做这些事情,所以你需要一个 DOM 节点的引用。 例如,单击按钮将使用 ref 聚焦输入:

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

准备好学习这个主题了吗?

阅读使用 Refs 操作 DOM 以了解如何访问由 React 管理的 DOM 元素。

使用Effect同步

一些组件需要与外部系统同步。 例如,您可能希望根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。 与让您处理特定事件的事件处理程序不同,Effects 让您在渲染后运行一些代码。 使用它们将你的组件与 React 之外的一些系统同步。

多次按下播放/暂停键,看看视频播放器如何与 isPlaying 属性值保持同步:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

许多 Effects 也需要自己“清理”。 例如,如果你的 Effect 建立了一个到聊天服务器的连接,它应该返回一个清理函数来告诉 React 如何断开你的组件与该服务器的连接:
App.js

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}

chat.js

export function createConnection() {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting...');
    },
    disconnect() {
      console.log('❌ Disconnected.');
    }
  };
}

在开发中,React 将立即运行并额外清理一次 Effect。 这就是为什么您会看到两次打印“✅ Connecting...”的原因。 这确保您不会忘记实现清理功能。

准备好学习这个主题了吗?

阅读使用Effect同步以了解如何将组件与外部系统同步。

你可能不需要Effect

Effects 是 React 范式的逃生通道。 它们让你“走出”React 并将你的组件与一些外部系统同步。 如果不涉及外部系统(例如,如果您想在某些属性或状态更改时更新组件的状态),则不需要 Effect。 删除不必要的 Effects 将使您的代码更易于理解、运行速度更快并且更不容易出错。

有两种常见情况不需要 Effects:

  • 您不需要 Effects 来转换数据以进行渲染。
  • 您不需要 Effects 来处理用户事件。

例如,您不需要 Effect 来根据其他状态调整某些状态:

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

相反,在渲染时尽可能多地计算:

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

但是,您确实需要 Effects 才能与外部系统同步。

准备好学习这个主题了吗?

阅读您可能不需要效果以了解如何删除不必要的效果。

React Effect的生命周期

Effect 与组件有不同的生命周期。 组件可以挂载、更新或卸载。 Effect 只能做两件事:开始同步某些东西,然后停止同步它。 如果你的 Effect 依赖于随时间变化的props和状态,这个循环可能会发生多次。

此效果取决于 roomId 属性的值。 属性是反应值,这意味着它们可以在重新渲染时改变。 请注意,在您更新 roomId 后,Effect 会重新同步(并重新连接到服务器):

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

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

  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

React 提供了一个 linter 规则来检查您是否正确指定了 Effect 的依赖项。 如果您忘记在上述示例的依赖项列表中指定 roomId,linter 会自动找到该错误。

准备好学习这个主题了吗?

阅读反应事件的生命周期,了解 Effect 的生命周期与组件的生命周期有何不同。

将事件与Effect分开

建设中...
本节描述了一个尚未添加到 React 中的实验性 API,因此您还不能使用它。

事件处理程序仅在您再次执行相同的交互时重新运行。 与事件处理程序不同,如果 Effects 读取的某些值(如 prop 或状态变量)与上次渲染时的值不同,则 Effects 会重新同步。 有时,您需要两种行为的混合:一个 Effect 重新运行以响应某些值而不是其他值。

Effects 中的所有代码都是反应式的。 如果它读取的某些反应值由于重新渲染而发生变化,它将再次运行。 例如,如果 roomId 或主题在交互后发生更改,此 Effect 将重新连接到聊天:
App.js

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}

chat.js

export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  let connectedCallback;
  let timeout;
  return {
    connect() {
      timeout = setTimeout(() => {
        if (connectedCallback) {
          connectedCallback();
        }
      }, 100);
    },
    on(event, callback) {
      if (connectedCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'connected') {
        throw Error('Only "connected" event is supported.');
      }
      connectedCallback = callback;
    },
    disconnect() {
      clearTimeout(timeout);
    }
  };
}

notifications.js

import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';

export function showNotification(message, theme) {
  Toastify({
    text: message,
    duration: 2000,
    gravity: 'top',
    position: 'right',
    style: {
      background: theme === 'dark' ? 'black' : 'white',
      color: theme === 'dark' ? 'white' : 'black',
    },
  }).showToast();
}

这并不理想。 仅当 roomId 已更改时,您才想重新连接到聊天。 切换主题不应该重新连接到聊天! 将代码阅读主题从 Effect 移到 Event 函数中:
App.js

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}

chat.js

export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  let connectedCallback;
  let timeout;
  return {
    connect() {
      timeout = setTimeout(() => {
        if (connectedCallback) {
          connectedCallback();
        }
      }, 100);
    },
    on(event, callback) {
      if (connectedCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'connected') {
        throw Error('Only "connected" event is supported.');
      }
      connectedCallback = callback;
    },
    disconnect() {
      clearTimeout(timeout);
    }
  };
}

事件函数中的代码不是反应性的,因此更改主题不再使您的效果重新连接。

准备好学习这个主题了吗?

阅读将事件与Effect分开了解如何防止某些值重新触发 Effects。

删除 Effect 依赖项

当您编写 Effect 时,linter 将验证您是否已将 Effect 读取的每个反应值(如 props 和 state)包含在 Effect 的依赖项列表中。 这确保您的 Effect 与组件的最新道具和状态保持同步。 不必要的依赖项可能会导致您的 Effect 运行过于频繁,甚至会造成无限循环。 删除它们的方式取决于具体情况。

例如,此 Effect 取决于每次编辑输入时都会重新创建的选项对象:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

您不希望每次开始在聊天中输入消息时聊天都重新连接。 要解决此问题,请在 Effect 中移动选项对象的创建,以便 Effect 仅依赖于 roomId 字符串:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

请注意,您并没有通过编辑依赖项列表来删除选项依赖项。 那是错误的。 相反,您更改了周围的代码,这样依赖性就变得不必要了。 您可以将依赖项列表视为您的 Effect 代码使用的所有反应值的列表。 您不会有意选择要放在该列表中的内容。 该列表描述了您的代码。 要更改依赖项列表,请更改代码。

准备好学习这个主题了吗?

阅读删除 Effect 依赖项了解如何减少 Effect 重新运行的频率。

通过自定义 Hooks 重用逻辑

React 带有内置的 Hook,例如 useState、useContext 和 useEffect。 有时,您会希望有一个 Hook 用于某些更具体的目的:例如,获取数据、跟踪用户是否在线或连接到聊天室。 为此,您可以根据应用程序的需要创建自己的 Hook。

在此示例中,usePointerPosition 自定义 Hook 跟踪光标位置,而 useDelayedValue 自定义 Hook 返回一个“滞后”您传递的值一定毫秒数的值。 将光标移到沙盒预览区域上以查看跟随光标移动的点轨迹:
App.js

import { usePointerPosition } from './usePointerPosition.js';
import { useDelayedValue } from './useDelayedValue.js';

export default function Canvas() {
  const pos1 = usePointerPosition();
  const pos2 = useDelayedValue(pos1, 100);
  const pos3 = useDelayedValue(pos2, 200);
  const pos4 = useDelayedValue(pos3, 100);
  const pos5 = useDelayedValue(pos3, 50);
  return (
    <>
      <Dot position={pos1} opacity={1} />
      <Dot position={pos2} opacity={0.8} />
      <Dot position={pos3} opacity={0.6} />
      <Dot position={pos4} opacity={0.4} />
      <Dot position={pos5} opacity={0.2} />
    </>
  );
}

function Dot({ position, opacity }) {
  return (
    <div style={{
      position: 'absolute',
      backgroundColor: 'pink',
      borderRadius: '50%',
      opacity,
      transform: `translate(${position.x}px, ${position.y}px)`,
      pointerEvents: 'none',
      left: -20,
      top: -20,
      width: 40,
      height: 40,
    }} />
  );
}

usePointerPosition.js

import { useState, useEffect } from 'react';

export function usePointerPosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
  }, []);
  return position;
}

useDelayedValue.js

import { useState, useEffect } from 'react';

export function useDelayedValue(value, delay) {
  const [delayedValue, setDelayedValue] = useState(value);

  useEffect(() => {
    setTimeout(() => {
      setDelayedValue(value);
    }, delay);
  }, [value, delay]);

  return delayedValue;
}

您可以创建自定义 Hooks,将它们组合在一起,在它们之间传递数据,并在组件之间重用它们。 随着您的应用程序的增长,您将减少手动编写的 Effects,因为您将能够重用您已经编写的自定义 Hooks。 React 社区也维护了很多优秀的自定义 Hooks。

准备好学习这个主题了吗?

阅读通过自定义 Hooks 重用逻辑了解如何在组件之间共享逻辑。

下一步是什么?

前往使用 Refs 引用值开始逐页阅读本章!

相关文章

  • 创业政策|高职创业教育“温州模式”

    在一台半人高外形如冰柜的白色机器前,用户按下启动按钮,通过扫描二维码打开机器的入纸舱口,将废纸从舱口中投入,然后这...

  • 创业政策|高职创业教育“温州模式”

    在一台半人高外形如冰柜的白色机器前,用户按下启动按钮,通过扫描二维码打开机器的入纸舱口,将废纸从舱口中投入,然后这...

  • 隧道安全逃生管道最新成果

    新型高分子隧道安全逃生管道分别有高分子逃生管道、新型隧道逃生管道、轻型逃生管道、悬挂式隧道逃生管道、防腐逃生管道、...

  • Spark快速大数据分析(1)

    推荐序译者序序前言第1章 Spark数据分析导论第2章 Spark下载与入门第3章 RDD基础第4章 键值对操作第...

  • 《逃生缘之前传 》 目录

    此为逃生缘第一部,敬请赏读逃生缘第二部《逃生缘之缘起与神探肖》 逃生缘之前传 (1) 灭门之灾逃生缘之前传...

  • 这有一套虚拟家庭消防隐患排查VR系统给你看看

    消防教育基地集防火防灾知识、消防互动体验、学习逃生自救技能于一体,划分四个展区即序厅、探索区、体验区、消防大课堂。...

  • 2021.1.13《搅动激活》

    2021.1.13【视频号滋养我的上班路(16)】 2021.1.13 07:21 《速率就是逃生的速度》第647...

  • 在逃生游戏里当最6主播

    全球人民都被投放到一款无限逃生游戏之中,每个逃生者都有一个专属编号的直播间,逃生失败的人可以在现实世界收看直播逃生...

  • 第142章 死里逃生

    再醒过来,我已经退烧了,肚子上的伤口发炎了。只能用消毒水简单清理一下。 腹部在痛,我勉强坐了起来,看了一下沈清悦。...

  • 第22章 火场逃生

    这个信号与发现那些奇怪的家伙不一样,因为这些信号里跳出一些数据,我猛地回头看向卢燕,她也正看向我,虽然我们都戴着头...

网友评论

      本文标题:第三五章 逃生舱口-序

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