React学习记录(一)

描述UI

组件导入与导出

语法导出语句导入语句
默认export default function Button() {}import Button from ‘./Button.js’;
具名export function Button() {}import { Button } from ‘./Button.js’;

props传递组件

  1. 在 React 组件中,children 是一个特殊的 prop(属性),用来表示那些被传递到组件的子元素。当一个组件在使用时包裹了其他的内容(无论是文本、HTML、或其他组件),这些被包裹的内容就会作为 children prop 传递给该组件。

  2. 例如,对于下面的 AlertButton 组件,使用 children 允许你自定义按钮内显示的内容。这里的 children 使得 AlertButton 组件非常灵活,因为你可以在其中嵌入任何 React 元素或组件,而这些嵌入的内容将显示在按钮内部。看一个例子:

    function AlertButton({ message, children }) {
      return (
        <button onClick={() => alert(message)}>
          {children}
        </button>
      );
    }
    <AlertButton message="Hello World!">
    	Click me!
    </AlertButton>
    

渲染列表的小技巧

  1. 使用map()函数
  2. 使用filter()函数

保证组件的纯粹性

  1. 一个组件必须是纯粹的,就意味着:
    • 只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
    • 输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的 JSX。
  2. 渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
  3. 你不应该改变任何用于组件渲染的输入。这包括 props、state 和 context。通过 “设置” state 来更新界面,而不要改变预先存在的对象。
  4. 努力在你返回的 JSX 中表达你的组件逻辑。当你需要“改变事物”时,你通常希望在事件处理程序中进行。作为最后的手段,你可以使用 useEffect。

添加交互

state与渲染

  1. Hook 是特殊的函数,只在 React 渲染时有效。它们能让你 “hook” 到不同的 React 特性中去。

    Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。 Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块

  2. 理解react的触发、渲染、提交流程

渲染提交流程

  1. 触发第一次渲染:
    • 组件的第一次渲染
    • 组件状态发生改变时
  2. React渲染你的组件:在你触发渲染后,React 会调用你的组件来确定要在屏幕上显示的内容。“渲染中” 即 React 在调用你的组件。
  3. React 把更改提交到 DOM 上

对于State的理解

  1. 一张快照,注意下面这个例子更能说明一切,下面点击按钮之后,虽然看起来调用了三次set,但是实际上,只会加1
    export default function Counter() {
      const [number, setNumber] = useState(0);
      return (
        <>
          <h1>{number}</h1>
          <button onClick={() => {
            setNumber(number + 1);
            setNumber(number + 1);
            setNumber(number + 1);
          }}>+3</button>
        </>
      )
    }
    
    setNumber(number + 1):number 是 0 所以 setNumber(0 + 1)。尽管你调用了三次 setNumber(number + 1),但在 这次渲染的 事件处理函数中 number 会一直是 0,所以你会三次将 state 设置成 1。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的 number 等于 1 而不是 3。
  2. **一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。**它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了。

如何对state进行多次操作

  1. 这里的比喻是这样的:餐厅里帮你点菜的服务员。服务员不会在你说第一道菜的时候就跑到厨房!相反,他们会让你把菜点完,让你修改菜品,甚至会帮桌上的其他人点菜。

  2. 当然也可以同时更新同一个State:

    import { useState } from 'react';
    
    export default function Counter() {
      const [number, setNumber] = useState(0);
    
      return (
        <>
          <h1>{number}</h1>
          <button onClick={() => {
            setNumber(n => n + 1);
            setNumber(n => n + 1);
            setNumber(n => n + 1);
          }}>+3</button>
        </>
      )
    }
    

    在这里n => n + 1 被称为 更新函数。

    • React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理
    • 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。

    下面是 React 在执行事件处理函数时处理这几行代码的过程:

    setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。 这一操作完成了三次

    当你在下次渲染期间调用 useState 时,React 会遍历队列。之前的 number 的 state 的值是 0,所以这就是 React 作为参数 n 传递给第一个更新函数的值。然后 React 会获取你上一个更新函数的返回值,并将其作为 n 传递给下一个更新函数,以此类推:

    更新队列n返回值
    n => n + 100+1=1
    n => n + 111+1=2
    n => n + 122+1=3

    因此最终返回值为n=3

  3. 如果是下面的处理逻辑呢?

    <button onClick={() => {
      setNumber(number + 5);
      setNumber(n => n + 1);
    }}>
    

    会增加到6,因为setNumber(number + 5)number 为 0,所以 setNumber(0 + 5)。React 将 “替换为 5” 添加到其队列中。setNumber(n => n + 1)n => n + 1 是一个更新函数。 React 将 该函数 添加到其队列中。
    或者你可以把setState(x) 理解为setState(n => x)一样运行,只是没有使用 n!

更新State中的对象

  1. 对于所有state的值应该都被视为是只读的
  2. 比如更新鼠标位置
    export default function MovingDot() {
      const [position, setPosition] = useState({
        x: 0,
        y: 0
      });
      ...
      <div
      	onPointerMove={e => {
            setPosition({
              x: e.clientX,
              y: e.clientY
            });
          }}
       >
     }
    
    当然使用...对象展开语法可以只改变对象中的一个值
  3. 更新嵌套对象:
    const [person, setPerson] = useState({
      name: 'Niki de Saint Phalle',
      artwork: {
        title: 'Blue Nana',
        city: 'Hamburg',
        image: 'https://i.imgur.com/Sd1AgUOm.jpg',
      }
    });
    
    如果使用mutation的方式直接,person.artwork.city = 'New Delhi';即可,但是在react中为了修改 city 的值,你首先需要创建一个新的 artwork 对象(其中预先填充了上一个 artwork 对象中的数据),然后创建一个新的 person 对象,并使得其中的 artwork 属性指向新创建的 artwork 对象:
    const nextArtwork = { ...person.artwork, city: 'New Delhi' };
    const nextPerson = { ...person, artwork: nextArtwork };
    setPerson(nextPerson);
    

更新State中的数组

  1. 当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
  2. 这意味着你不应该使用类似于arr[0] = 'bird' 这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如push()pop()
    避免使用 (会改变原始数组) 推荐使用 (会返回一个新数组)
    避免使用推荐使用
    添加元素push,unshiftconcat,[…arr] 展开语法
    删除元素pop,shift,splicefilter,slice
    替换元素splice,arr[i] = … 赋值map
    排序reverse,sort先将数组复制一份

状态管理

用State响应输入

  1. 命令式控制UI与声明式控制UI,学会用状态机的方式控制

在组件之间共享状态

  1. 主要是通过Props进行传递

对State进行保留与重置

相同位置的相同组件的state会被保留

  1. 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置! 这个组件在 if 内外有两个return 语句,它们带有不同的 JSX 标签:
    import { useState } from 'react';
    
    export default function App() {
      const [isFancy, setIsFancy] = useState(false);
      if (isFancy) {
        return (
          <div>
            <Counter isFancy={true} />
            <label>
              <input type="checkbox" checked={isFancy} onChange={e => {setIsFancy(e.target.checked) }}/>
              使用好看的样式
            </label>
          </div>
        );
      }
      return (
        <div>
          <Counter isFancy={false} />
          <label>
            <input type="checkbox" checked={isFancy} onChange={e => {setIsFancy(e.target.checked)}}/>
            使用好看的样式
          </label>
        </div>
      );
    }
    
    function Counter({ isFancy }) {
      const [score, setScore] = useState(0);
      const [hover, setHover] = useState(false);
      let className = 'counter';
      if (hover) {className += ' hover'; }
      if (isFancy) { className += ' fancy';}
      return (
        <div
          className={className}
          onPointerEnter={() => setHover(true)}
          onPointerLeave={() => setHover(false)}
        >
          <h1>{score}</h1>
          <button onClick={() => setScore(score + 1)}>
            加一
          </button>
        </div>
      );
    }
    
    你可能以为当你勾选复选框的时候 state 会被重置,但它并没有!这是因为 两个 标签被渲染在了相同的位置。 React 不知道你的函数里是如何进行条件判断的,它只会“看到”你返回的树。在这两种情况下,App 组件都会返回一个包裹着 作为第一个子组件的 div。这就是 React 认为它们是 同一个 的原因。
    同样这里如果换成,也可以成立
    <div>
          {isFancy ? (
            <Counter isFancy={true} /> 
          ) : (
            <Counter isFancy={false} /> 
          )}
          <label>
            <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} />
            使用好看的样式
          </label>
    </div>
    
    但是如果换成,下述两种就没办法保留state
    {isPaused ? (
       <p>待会见!</p> 
      ) : (
      <Counter /> 
    )}
    ----------------------
    {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
    

如何重置相同位置的State

  1. 将组件渲染在不同的位置 ,呃
  2. 使用 key 来重置 state ,key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部的顺序(“第一个计数器”、“第二个计数器”)来区分组件。

脱围机制

使用ref引用值

  1. ref本质上就是个对象,当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref 。

    refstate
    useRef(initialValue)返回 { current: initialValue }useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])
    更改时不会触发重新渲染更改时触发重新渲染。
    可变 —— 你可以在渲染过程之外修改和更新 current 的值。“不可变” —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。
    你不应在渲染期间读取(或写入) current 值。你可以随时读取 state。但是,每次渲染都有自己不变的 state。
  2. 最简单的例子就是计数器,注意不要在渲染过程中读取或者写入ref.current,就像下面所示那样

      // 会引起更新
      const [count, setCount] = useState(0);
    
      function handleClick() {
        setCount(count + 1);
      }
      return (
        <button onClick={handleClick}>
          你点击了 {count}</button>
      );
    --------------------------------------
      // 不会引起更新
        let countRef = useRef(0);
    
      function handleClick() {
        // 这样并未重新渲染组件!
        countRef.current = countRef.current + 1;
        console.log(countRef.current); //这个值会增加
      }
        return (
        <button onClick={handleClick}>
          你点击了 {countRef.current}</button>
      );
    
  3. 何时需要使用Ref:通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:

    • 存储 timeout ID(就是SetInterval)
    • 存储和操作 DOM 元素
    • 存储不需要被用来计算 JSX 的其他对象。

Ref操作DOM

  1. 注意ref只能操作自己组件的DOM,不允许操作子组件,如果想要操作子组件,则需要用props去传递,比如
    const MyInput = forwardRef((props, ref) => {
      return <input {...props} ref={ref} />;
    });
    

使用Effect进行同步

在谈到 Effect 之前,你需要熟悉 React 组件中的两种逻辑类型:

  • 渲染逻辑代码(在 描述 UI 中有介绍)位于组件的顶层。你将在这里接收 props 和 state,并对它们进行转换,最终返回你想在屏幕上看到的 JSX。渲染的代码必须是纯粹的——就像数学公式一样,它只应该“计算”结果,而不做其他任何事情。

  • 事件处理程序(在 添加交互性 中介绍)是嵌套在组件内部的函数,而不仅仅是计算函数。事件处理程序可能会更新输入字段、提交 HTTP POST 请求以购买产品,或者将用户导航到另一个屏幕。事件处理程序包含由特定用户操作(例如按钮点击或键入)引起的“副作用”(它们改变了程序的状态)。

而上述这些可能在功能实现上不能满足所有的需求,这里举了个例子,就是比如有一个chatroom的app,当他显示在屏幕上时,就应该连接上了服务器。那么这一个动作(连接上服务器)不是一个主动事件(点击按钮),他是一个被动过程。

于是乎,Effect 允许你指定由渲染本身(当chatroom被渲染到屏幕上时产生的连接服务器这一副作用),而不是特定事件引起的副作用。

side effect是一个程序设计中的属于,一般翻译为副作用,指的是如果一个函数修改了自己范围之外的资源,那就叫做有副作用,反之,就是没有副作用。

在聊天中发送消息是一个“事件”,因为它直接由用户点击特定按钮引起。然而,建立服务器连接是 Side Effect。因为它应该在组件出现时始终发生,无论是由哪种交互引起的。(because it should happen no matter which interaction caused the component to appear) 这是一个很好的时机,可以将 React 组件与某个外部系统(如网络或第三方库)同步。(原文档什么奇怪的翻译

如何编写Effect

  1. 声明 Effect。默认情况下,Effect 会在每次渲染后都会执行
  2. 指定 Effect 依赖。大多数 Effect 应该按需执行,而不是在每次渲染后都执行。例如,淡入动画应该只在组件出现时触发。连接和断开服务器的操作只应在组件出现和消失时,或者切换聊天室时执行。
  3. 必要时添加清理(cleanup)函数。有时 Effect 需要指定如何停止、撤销,或者清除它的效果。例如,“连接”操作需要“断连”,“订阅”需要“退订”,“获取”既需要“取消”也需要“忽略”。你将学习如何使用 清理函数 来做到这一切。

一个例子

step1:声明Effect
  1. 下述代码是有问题的,问题出在,这段代码中的 ref.current.play()ref.current.pause() 是在函数组件的主体内(即组件函数的顶层)直接调用的。在 React 中,函数组件的主体部分在组件渲染期间被执行,因此这些代码也就是在渲染期间被执行。因此,每次重新渲染时,都会调用 ·ref.current.play()· 或· ref.current.pause()·,这样就会在渲染阶段尝试执行 DOM 操作,从而导致错误。
    	import { useState, useRef, useEffect } from 'react';
    	function VideoPlayer({ src, isPlaying }) {
    	  const ref = useRef(null);
    	  if (isPlaying) {
    	    ref.current.play();  // 渲染期间不能调用 `play()`。 
    	  } else {
    	    ref.current.pause(); // 同样,调用 `pause()` 也不行。
    	  }
    	  return <video ref={ref} src={src} loop playsInline />;
    	}
    	
    	export default function App() {
    	  const [isPlaying, setIsPlaying] = useState(false);
    	  return (
    	    <>
    	      <button onClick={() => setIsPlaying(!isPlaying)}>
    	        {isPlaying ? '暂停' : '播放'}
    	      </button>
    	      <VideoPlayer
    	        isPlaying={isPlaying}
    	        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
    	      />
    	    </>
    	  );
    	}
    
  2. 解决方法就是:把调用 DOM 方法的操作封装在 Effect 中,你可以让 React 先更新屏幕,确定相关 DOM 创建好了以后然后再运行 Effect。
    当 VideoPlayer 组件渲染时(无论是否为首次渲染),都会发生以下事情。首先,React 会刷新屏幕,确保 <video> 元素已经正确地出现在 DOM 中;然后,React 将运行 Effect;最后,Effect 将根据 isPlaying 的值调用 play() pause()
      useEffect(() => {
        if (isPlaying) {
          ref.current.play();
        } else {
          ref.current.pause();
        }
      });
    
  3. 当然上述问题还有一个解法,就是把对ref的操作放到一个回调函数中,在触发阶段去更改是否播放,首先第一次渲染时,不会调用handleClick,之后改变播放暂停状态也是在触发阶段进行的
    import { useState, useRef } from 'react';
    export default function VideoPlayer() {
      const [isPlaying, setIsPlaying] = useState(false);
      const ref = useRef(null);
      function handleClick() {
        const nextIsPlaying = !isPlaying;
        setIsPlaying(nextIsPlaying);
        if (nextIsPlaying) {
          ref.current.play();
        } else {
          ref.current.pause();
        }
      }
      return (
        <>
          <button onClick={handleClick}>
            {isPlaying ? '暂停' : '播放'}
          </button>
          <video width="250" ref={ref} onPlay={() => setIsPlaying(true)} onPause={() => setIsPlaying(false)}>
            <source
              src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
              type="video/mp4"
            />
          </video>
        </>
      )
    }
    
step2:指定Effect的依赖
  1. 一般来说,Effect 会在 每次 渲染时执行。但更多时候,并不需要每次渲染的时候都执行 Effect。再回到上面的代码,我们增加了一个input,由于const [text, setText] = useState('');的存在,如果不指定依赖,每次输入一个文字就会执行一次useEffect中的逻辑,显然时不行的,因此需要放入一个[依赖项],这里依赖的时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);
      const [text, setText] = useState('');
      return (
        <>
          <input value={text} onChange={e => setText(e.target.value)} />
          <button onClick={() => setIsPlaying(!isPlaying)}>
            {isPlaying ? 'Pause' : 'Play'}
          </button>
          <VideoPlayer
            isPlaying={isPlaying}
            src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
          />
        </>
      );
    }
    
  2. 依赖项参数的区别
    useEffect(() => {
      // 这里的代码会在每次渲染后执行
    });
    
    useEffect(() => {
      // 这里的代码只会在组件挂载后执行
    }, []);
    
    useEffect(() => {
      //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
    }, [a, b]);
    
  3. 为什么依赖项省略了ref,这是因为 ref 具有 稳定 的标识:React 保证 每轮渲染中调用 useRef 所产生的引用对象时,获取到的对象引用总是相同的,即获取到的对象引用永远不会改变,所以它不会导致重新运行 Effect。当然实际上你也可以添加ref进去,但是因为他是稳定的,所以没什么影响
step3:按需添加清理函数
  1. 注意在开发环境下,react会挂在两次组件,以帮你找到一些错误,比如示例中控制台实际打印 “✅ 连接中……” 了两次。
  2. 我们可以在return中进行cleanup

你可能不需要Effect?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值