React 脱围机制

 一 使用ref引用值

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

我们先来看看函数组件里的普通变量、ref引用值和状态变量

const Child = memo((props) => {
  console.log("组件重新渲染");

  let sum = 0;
  const sumRef = useRef(0);
  const handleClick = () => {
    sum += 1;
    console.log("sum:", sum);
    sumRef.current += 1;
    console.log("sumRef:", sumRef.current);
  };

  const [count, setCount] = useState(0);
  const handleClick2 = () => {
    console.log("改变状态");
    setCount(count + 1);
  };

  return (
    <>
      <div onClick={() => handleClick()}>改变非状态</div>
      <div onClick={() => handleClick2()}>改变状态</div>
      {/* <div>{count}</div> */}
    </>
  );
});

  • 状态变量:每次修改状态会触发函数组件重新调用,组件重新渲染,状态值会被记住。(需要重新触发渲染用)
  • ref引用值:ref引用值每次修改不会引发组件重新渲染,ref引用值会被记住。(中间状态,在需要记住其值且不触发重新渲染用)
  • 普通变量:普通变量修改不会引发组价重新渲染,在组件每次渲染之前,可以任意修改其值,但是组件每次由于props、state变化导致组价重新渲染(函数重新调用),如果普通变量设置了初始值,则会恢复到初始值。(每次重新渲染不需要记住其值用)

二 使用ref操作dom

有时可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个节点获得焦点、滚动到它或测量它的尺寸和位置等。

2.1. 基本使用

import { useRef } from 'react';

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

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

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

2.2. 与map配合使用

借用集合方式实现

const Child = memo((props) => {
  const [list, setList] = useState([
    {
      id: 1,
      name: "zs",
      age: 20,
    },
    {
      id: 2,
      name: "ls",
      age: 20,
    },
  ]);

  const divRef = useRef(null);

  const getMap = () => {
    if (!divRef.current) {
      divRef.current = new Map();
    }
    return divRef.current;
  };

  const handleClick = (id) => {
    const map = getMap();
    const node = map.get(id);
    node.scrollIntoView();
  };
  return (
    <>
      <button onClick={() => handleClick(1)}>zs按钮</button>
      <button onClick={() => handleClick(2)}>ls按钮</button>
      {list.map((item, index) => {
        return (
          <div
            key={item.id}
            style={{
              height: "100vh",
              backgroundColor: "red",
              borderBottom: "2px solid green",
            }}
            ref={(node) => {
              const map = getMap();
              if (node) {
                map.set(item.id, node);
              } else {
                map.delete(item.id);
              }
            }}
          >
            {item.name}
          </div>
        );
      })}
    </>
  );
});

2.3. React 何时添加 refs

  • 在 渲染 阶段, React 调用你的组件来确定屏幕上应该显示什么。 在第一次渲染期间,DOM 节点尚未创建,因此 ref.current 将为 null。在渲染更新的过程中,DOM 节点还没有更新。所以读取它们还为时过早
  • 在 提交 阶段, React 把变更应用于 DOM。更新 DOM 后,React 立即将它们设置到相应的 DOM 节点。

注:通常从事件处理器访问 refs 如果想使用 ref 执行某些操作,但没有特定的事件可以执行此操作,你可能需要一个 effect,在页面渲染完成再执行。

2.4. 使用 refs 操作 DOM 的注意事项

注意,在我们使用refs操作DOM时候,要注意避免和state控制的节点产生冲突

import { useState, useRef } from 'react';

export default function Counter() {
  const [show, setShow] = useState(true);
  const ref = useRef(null);

  return (
    <div>
      <button
        onClick={() => {
          setShow(!show);
        }}>
        通过 setState 切换
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        从 DOM 中删除
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}

三 使用 Effect 同步

Effects 会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步。允许你指定由渲染本身,而不是特定事件引起的副作用-----由渲染引起的副作用。

3.1. 如何编写Effect

  1. 声明 Effect
    import { useEffect } from 'react';
    function MyComponent() {
      useEffect(() => {
        // 每次渲染后都会执行此处的代码
      });
      return <div />;
    }

    wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

    注意:

    1.  effect的使用是在组件渲染完成后需要执行的一些逻辑代码(例如DOM的操作,必须等到组件渲染完成)

    2. Effect 通常应该使组件与 外部 系统保持同步,避免在Effect修改状态,以下代码会陷入死循环(状态改变-》重新渲染-》执行Effect-》状态改变-》...)

      const [count, setCount] = useState(0);
      useEffect(() => {
        setCount(count + 1);
      });
  2. 指定 Effect 依赖
     useEffect(() => {
        
     }, [...]); // ……依赖在此处声明!

    依赖数组如果是个空数组,则代码只会在首次渲染完成后执行。依赖数组可以包含多个依赖项。当指定的所有依赖项在上一次渲染期间的值与当前值完全相同时,React 会跳过重新运行该 Effect。
    注意:不要对依赖项说谎。只要useEffect中的代码涉及到外部变量,都应该考虑添加到依赖项。

    useEffect(() => {
      // 这里的代码会在每次渲染后执行
    });
    
    useEffect(() => {
      // 这里的代码只会在组件挂载后执行
    }, []);
    
    useEffect(() => {
      //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
    }, [a, b]);
    
  3. 必要时添加清理(cleanup)函数
    有时 Effect 需要指定如何停止、撤销,或者清除它的效果。例如,“连接”操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”等。
    例如:链接和断开链接
     useEffect(() => {
        const connection = createConnection();
        connection.connect();
        return () => {
          connection.disconnect();
        };
      }, []);

    每次重新执行 Effect 之前,React 都会调用清理函数;组件被卸载时,也会调用清理函数.
    注意:组件由于严格模式影响Effect调用两次的情况。

3.2. 获取数据(避免竞态)

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false; //避免竟态
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    //清除逻辑的获取数据
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

四 不需要使用effect情形

4.1. 需要根据props或者state变化计算新值的情形

unction Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  const fullName = firstName + ' ' + lastName;
}

其在渲染期间就可以将需要的结果计算出来渲染,避免使用effect产生级联渲染。

4.2. 缓存昂贵的计算

可以使用 useMemp Hook 缓存,传入 useMemp 的函数会在渲染期间执行,所以它仅适用于 纯函数 场景。

import { useMemo } from 'react';

function TodoList({ todos, filter }) {
  const visibleTodos = useMemo(() => {
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

4.3. 当 props key属性值变化时重置所有 state

传统的处理方式(会导致级联更新渲染)

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

高效的处理方式

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // 当 key 变化时,该组件内的 comment 或其他 state 会自动被重置
  const [comment, setComment] = useState('');
  // ...
}

4.4. 避免将事件逻辑函数移到effect

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

  // ✅ 非常好:这个逻辑应该在组件显示时执行
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  // 🔴 避免:在 Effect 中处理属于事件特定的逻辑
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);

  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
  // ...
}

注:

  • effect 使用场景:组件渲染完成后需要执行的逻辑
  • 事件执行场景:当发起特定的交互操作需要执行的逻辑

4.5. 避免链式渲染

useEffect(() => {
    ...
    setB('')
}, [a]);

useEffect(() => {
    ...
    setC('')
}, [b]);

useEffect(() => {
    ...
    setD('')
}, [c]);
...

这种方式非常低效,尽可能在渲染期间进行计算,以及在事件处理函数中调整 state。

  4.6. 初始化应用时

某些逻辑应该只在应用程序启动时运行一次。比如,验证登陆状态和加载本地程序数据。可以将其放在组件之外

if (typeof window !== 'undefined') { // 检查是否在浏览器中运行
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ……
}

这保证了这种逻辑在浏览器加载页面后只运行一次。

五 effect 生命周期

Effect 描述了如何将外部系统与当前的 props 和 state 同步。随着代码的变化,同步的频率可能会增加或减少。

5.1 Effect 的生命周期与组件的生命周期区别

组件生命周期:

  • 当组件被添加到屏幕上时,它会进行组件的 挂载
  • 当组件接收到新的 props 或 state 时,通常是作为对交互的响应,它会进行组件的 更新
  • 当组件从屏幕上移除时,它会进行组件的 卸载

Effect生命周期:

  • 主体部分指定了如何 开始同步
  • 返回的清理函数指定了如何 停止同步
useEffect(()=>{
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
},[roomId])

每次在组件重新渲染后,React 都会查看传递的依赖项数组。如果数组中的任何值与上一次渲染时在相同位置传递的值不同,React 将重新同步 Effect。

注:effect同步代码中涉及的响应式值必须包含在依赖项中

5.2 每个 Effect 表示一个独立的同步过程

避免将与 Effect 无关的逻辑添加到已经编写的 Effect 中,仅仅因为这些逻辑需要与 Effect 同时运行。

function ChatRoom({ roomId }) {
  useEffect(() => {
    logVisit(roomId);
  }, [roomId]);

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    // ...
  }, [roomId]);
  // ...
}

代码中的每个 Effect 应该代表一个独立的同步过程。

5.3 在组件主体中声明的所有变量都是响应式的 

Props 和 state 并不是唯一的响应式值。从它们计算出的值也是响应式的。如果 props 或 state 发生变化,组件将重新渲染,从中计算出的值也会随之改变。这就是为什么 Effect 使用的组件主体中的所有变量都应该在依赖列表中。

5.4 不想进行重新同步时该怎么办 

如果 effect主体代码设计的变量不依赖于渲染并且始终具有相同的值,可以将它们移到组件外部或者移动到 Effect 内部。

//外部
const serverUrl = 'https://localhost:1234'; // serverUrl 不是响应式的
const roomId = 'general'; // roomId 不是响应式的

function ChatRoom() {
  useEffect(() => {
    //内部
    const serverUrl = 'https://localhost:1234'; // serverUrl 不是响应式的
    const roomId = 'general'; // roomId 不是响应式的

    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []); // ✅ 声明的所有依赖
  // ...
}

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值