React入门

函数式编程

副作用

1、前端基本都是和副作用打交道,所以不可能所有东西都是pure 的,副作用其实不可避免, pure只能尽量而不要强求。

Sideeffect 就是除了函数计算之外的一切操作,例如请求数据,修改dom 修改 state等输入不等于输出操作

2、就是任何情况下函数输入什么,输出就是什么。这就是pure。假如函数依赖了外部变量,或者调用了其他函数,或者中途改变了外部变量,那就不pure 。

Sideeffect 就是除了函数计算之外的一切操作,例如请求数据,修改dom 修改 state等输入不等于输出操作 。

3、 如果有副作用,我们将其称为过程,或者命令式。因此函数没有副作用。我们认为,如果函数修改了可变数据结构或变量,使用I/O,引发异常或中止错误,则将产生副作用。所有这些东西都被认为是副作用。

副作用之所以不好,是因为(如果有)副作用,取决于系统状态,功能可能是不可预测的。当一个函数没有副作用时,我们可以随时执行它,在给定相同的输入的情况下,它将始终返回相同的结果

但是要声明一点,函数式编程并不是不需要副作用,只是在需要时限制它们。需要有副作用,因为没有它们,我们的程序将只能进行计算。 我们经常必须写数据库,与外部系统集成或写文件。与外界通过接口的形式交互才能将我们的计算展示出去。所以很多倾向无副作用的语言的中心细想是把“作用”与“副作用”分离开来处理。

函数式组件与类组件的区别

1、一个例子

pjqnl16lm7 - CodeSandbox

  • 类组件中发生了什么?

    如果我们说UI在概念上是当前应用状态的一个函数,那么事件处理程序则是渲染结果的一部分 —— 就像视觉输出一样。我们的事件处理程序“属于”一个拥有特定 props 和 state 的特定渲染。

    调用一个回调函数读取 this.props 的 timeout 会打断这种关联。我们的 showMessage 回调并没有与任何一个特定的渲染“绑定”在一起,所以它“失去”了正确的 props。从 this 中读取数据的这种行为,切断了这种联系。

  • 类组件如何捕获渲染用的值?

    ①在调用事件处理函数之前读取this.props,存到另一个变量中。

    ②利用闭包

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    class ProfilePage extends React.Component {
      render() {
        // Capture the props!    const props = this.props;
        // Note: we are *inside render*.
        // These aren't class methods.
        const showMessage = () => {
          alert('Followed ' + props.user);    };
    
        const handleClick = () => {
          setTimeout(showMessage, 3000);
        };
    
        return <button onClick={handleClick}>Follow</button>;
      }
    }
    

2、函数式组件捕获了渲染所用的值。(Function components capture the rendered values.)

上面的例子是正确的,但是看起来很奇怪。如果你在render方法中定义各种函数,而不是使用class的方法,那么使用类的意义在哪里?

事实上,我们可以通过删除类的“包裹”来简化代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

就像上面这样,props仍旧被捕获了 —— React将它们作为参数传递。

不同于thisprops对象本身永远不会被React改变。

3、 读取并不属于这一次特定渲染的,最新的props和state呢?

在类中,你通过读取this.props或者this.state来实现,因为this本身时可变的。React改变了它。

在函数式组件中,你也可以拥有一个在所有的组件渲染帧中共享的可变变量。它被成为“ref”:

React渲染

JSX

1、如果在 JSX 中使用的属性不存在于 HTML 的规范中,这个属性会被忽略。如果要使用自定义属性,可以用 data- 前缀。

2、一个 DOM 元素包含的信息其实只有三个:标签名,属性,子元素。 HTML 所有的信息我们都可以用合法的 JavaScript 对象结构来表示。 所谓的 JSX 其实就是 JavaScript 对象。

3、组件的render()方法

条件返回:在render()中加入变量来判断return的内容 元素变量:JSX作为元素

4、render( )根据JSX生成react element

这个React.createElement的表达式会在render函数被调用的时候执行,换句话说,当render函数被调用的时候,会返回一个element。 这个element是个对象,但不一定是Object类型。 element.children的类型

5、列表数据

之前说过 JSX 的表达式插入 {} 里面可以放任何数据,如果我们往 {} 里面放一个存放 JSX 元素的数组会怎么样? React.js 会帮你把数组里面一个个元素罗列并且渲染出来。

element生成真实节点

初始化element:如果element是Object类型,则创建ReactDOMComponent的实例对象。 接着ReactComponentComponentWrapper通过递归,最终调用ReactDOMComponent的mountComponent方法来帮助生成真实DOM节点。

事件监听

1、合成事件

只需要给需要监听事件的元素加上属性【类似于】(指被react改造封装过) onClick、onKeyDown 这样的属性:这些 on* 的事件监听只能用在普通的 HTML 的标签上,而不能用在组件标签上。

2、event对象

和普通浏览器一样,事件监听函数会被自动传入一个 event 对象,这个对象和普通的浏览器 event 对象所包含的方法和属性都基本一致。

3、类组件中,React.js 的事件监听方法需要手动 bind 到当前实例:

这是因为 React.js 调用你所传给它的方法的时候,并不是通过对象方法的方式调用(this.handleClickOnTitle),而是直接通过函数调用 (handleClickOnTitle),所以事件监听函数内并不能通过 this 获取到实例。

4、 事件处理程序则是渲染结果的一部分

关于用户界面性质的一个有趣观察:如果我们说UI在概念上是当前应用状态的一个函数,那么事件处理程序则是渲染结果的一部分 —— 就像视觉输出一样。我们的事件处理程序“属于”一个拥有特定 props 和 state 的特定渲染。

组件化

我们将组件划分成三种组件:容器组件、无状态组件和UI组件。这样,我们就可以按照既定的原则去处理组件。同时,在拿到产品的原型稿时,我们也需要去认真地思考这个问题。我们可以将页面分成几个模块。

需要明白的是: 越小的单元,state就越需要单一 不要在UI组件和无状态组件中进行数据的请求,应该将之放入容器组件中 单向原则,子组件不应该影响父组件

react渲染思想

它统一描述了初始渲染和之后的更新。这降低了你程序的

React会根据我们当前的props和state同步到DOM。“mount”和“update”之于渲染并没有什么区别。

状态与数据

单向数据流

单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会【使用接收到的新值,而不是去修改已有的值】。 在React中,默认只是单项数据流,也就是只能把state上的数据绑定到页面上,但无法把页面中数据的变化自动同步回state中。如果需要把页面上的数据的变化保存到state中,需要程序员手动监听onChange事件,拿到最新的数据,手动调用this.setState({ })更改回去。

state vs prop

state 代表了随时间会产生变化的数据,应当仅在实现交互时使用。所以构建应用的静态版本时,你不会用到它。 组件的显示形态和行为可以由数据状态(state)和配置参数(props)共同决定。

Yep, the property is passed to the component, similar to how an argument is passed to a function. props can come from the parent, or can be set by the component itself. Since props are passed in, and they cannot change, you can think of any React component that only uses props (and not state) as “pure,” that is, it will always render the same output given the same input. This makes them really easy to test - win! ... props are a way of passing data from parent to child. ... State is reserved only for interactivity, that is, data that changes over time.

state vs ref

  1. 更新 state 会触发组件重新渲染,更新 ref 则不会
  2. state 更新是异步的(state 会在重新渲染之后更新),ref 更新是同步的(ref 立即更新)
  3. 适合存储在 ref 中的是那些包含了副作用(side-effect)的基础结构信息(译者按:类似全局变量),例如,计时器 id、各种指针(如 socket id)等。
  4. Believe it or not, that’s it: use react state for values that (a) are used in the JSX or (b) trigger side-effects via use*Effect or in lifecycle hooks. In all other cases, you can safely store them anywhere you want.

setState

setState 的第二种使用方式,可以接受一个函数作为参数。React.js 会把【上一个 setState 的结果】传入这个函数,你就可以使用该结果进行运算、操作,然后【返回一个对象】作为更新 state 的对象。

props

1、作用:让组件适应不同场景下的需求,具有一定的【可配置性】。props可以达到这个效果: 每个组件都可以接受一个 props 参数,它是一个对象,包含了所有你对这个组件的配置。 2、默认配置:defaultProps,设置的之后不需要再用 || 运算符

ref

1、作用

  • 存储可变值: mutable & persist

​ 在组件重新渲染时,ref 的值依然保持不变。更新 ref 不同于 state,不会触发组件重新渲染。

  • 访问DOM元素 当 ref 属性用于 HTML 元素时,构造函数中使用react.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。 函数组件:useRef()

2、理解

一个ref与一个实例字段扮演同样的角色。这是进入可变的命令式的世界的后门。你可能熟悉’DOM refs’,但是ref在概念上更为广泛通用。它只是一个你可以放东西进去的盒子。

甚至在视觉上,this.something就像是something.current的一个镜像。他们代表了同样的概念。

你可以自行查看 demo来比较有无ref的不同。ref是一种**“选择退出”渲染一致性**的方法,在某些情况下会十分方便。

3、注意点

  • React.forwardRef 允许某些组件接收 ref,并将其向下传递给子组件

  • 但是记住一个原则:能不用 ref 就不用。特别是要避免用 ref 来做 React.js 本来就可以帮助你做到的页面自动更新的操作和事件监听。多余的 DOM 操作其实是代码里面的“噪音”,不利于我们理解和维护。 ref:命令式编程

  • Updating references restriction:

The function scope of the functional component should either calculate the output or invoke hooks.

That's why updating a reference (as well as updating state) shouldn't be performed inside the immediate scope of the component's function.

The reference must be updated either inside a useEffect() callback or inside handlers (event handlers, timer handlers, etc).

HOOKS

HOOKS概述

1、Hooks的调用顺序?

“React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。” 为什么hooks的声明要在顶部?参考文档解释。

2、用自变量与因变量来理解hooks

useState保存自变量,useEffect保存含有副作用的因变量,useMemo/UseCallback保存普通的因变量。

useRef保存中间变量。

3、 钩子(hook)的作用

一句话,钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。

4、分层

React内置的hook提供了基础的能力,虽然本质上它也有一些分层,比如:

  • useState是基于useReducer的简化版。
  • useMemouseCallback事实上可以基于useRef实现。

5、函数式组件的生命周期:

jsx的渲染比useEffect早 !

函数组件被调用 -> 执行代码 ->根据return的JSX渲染DOM -> 执行useEffect ->

6、没看懂但是抄抄

Hooks 是完全封装的 —— 你每次调用 Hooks 函数, 它都会从当前执行组件中获取到独立的本地状态。对这个特殊的例子来说并不重要(所有组件的窗口宽度是相同的!),但这正是 Hooks 如此强大的原因。它们不仅是一种共享状态的方式,更是共享状态化逻辑的方式。我们不想破坏自上而下的数据流!

每个 Hooks 都可以包含一些本地状态和副作用。你可以在不同 Hooks 之间传值,就像在通常在函数之间做的那样。Hooks 可以接受参数并返回值,因为它们就是JavaScript 函数。

作者:小小木锤链接:https://juejin.cn/post/6844903712347930638

useState

useState()使用时的数组解构? const [fruit, setFruit] = useState('banana'); 等价于: var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组 var fruit = fruitStateVariable[0]; // 数组里的第一个值 var setFruit = fruitStateVariable[1]; // 数组里的第二个值

不要在 render 里面 setState, 否则会触发死循环导致内存崩溃

useEffect

0、参考文章

useEffect 完整指南 — Overreacted

1、概述

  • effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。
  • Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
  • 记住,effects的心智模型和componentDidMount以及其他生命周期是不同的,试图找到它们之间完全一致的表达反而更容易使你混淆。想要更有效,你需要“think in effects”,它的心智模型更接近于实现状态同步,而不是响应生命周期事件。

2、依赖数组参数(deps)

(1)用法

传递数组作为 useEffect 的第二个可选参数:某些特定值(数组中的值)在两次重渲染之间没有发生变化时,则不会调用effect。

  • 如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。

(2)注意点

建议把不依赖props和state的函数提到你的组件外面,并且把那些仅被effect使用的函数放到effect里面。 要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要【在 effect 内部 去声明它所需要的函数。】这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值 。

万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹进 useCallback Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变 :

function ProductPage({ productId }) {
  // ✅ 用 useCallback 包裹以避免随渲染发生改变  
  const fetchProduct = useCallback(() => {    // ... Does something with productId ...  }, [productId]); // ✅ useCallback 的所有依赖都被指定了
  return <ProductDetails fetchProduct={fetchProduct} />;
}

function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
  // ...
}

3、结合渲染过程来理解

  • **在任意一次渲染中,props和state是始终保持不变的。**如果props和state在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的count值。

  • Effect函数如何读取到最新的count状态值呢?

    并不是React会在我们组件内部修改它以使我们的effect函数总能拿到最新的值? NO

    并不是count的值在“不变”的effect中发生了改变,而是【effect 函数本身】在每一次渲染中都不相同。

React: 给我状态为 0时候的UI。

你的组件:
- 给你需要渲染的内容: <p>You clicked 0 times</p>。
- 记得在渲染完了之后调用这个effect: () => { document.title = 'You clicked 0 times' }。

React: 没问题。开始更新UI,喂浏览器,我要给DOM添加一些东西。

浏览器: 酷,我已经把它绘制到屏幕上了。

React: 好的, 我现在开始运行给我的effect
运行 () => { document.title = 'You clicked 0 times' }。
  • 在effect的回调函数里读取最新的值而不是捕获的值

class中的this.state总是读取最新的值。

在FC中,最简单的实现方法是使用refs,这篇文章的最后一部分介绍了相关内容。

  • effect的清除发生在什么时候?

    React只会在浏览器绘制后运行effects。这使得应用更流畅因为大多数effects并不会阻塞屏幕的更新。Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除。

4、在effect中传递最小信息

虽然我们effect的情况不尽相同,但可以应用类似的思想。**只在effects中传递最小的信息会很有帮助。**类似于setCount(c => c + 1)这样的更新形式比setCount(count + 1)传递了更少的信息,因为它不再被当前的count值“污染”。它只是表达了一种行为(“递增”)。“Thinking in React”也讨论了如何找到最小状态。原则是类似的,只不过现在关注的是如何更新。

useRef

1、概述

ref是一个对象,ref.current是引用

2、useRef vs createRef

  • useRefreact hook 中的作用, 正如官网说的, 它像一个变量, 类似于 this , 它就像一个盒子, 你可以存放任何东西.
  • createRef每次渲染都会返回一个新的引用,而useRef` 每次都会返回【相同的引用(persist)】。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const UseRefDemoa = () => {
    const [renderIndex, setRenderIndex] = useState(1);
    const refFromUseRef = useRef<number | null>(null);
    const refFromCreateRef:any = createRef<HTMLDivElement>();
    if (!refFromUseRef.current) {
        refFromUseRef.current = renderIndex;
    }
    if (!refFromCreateRef.current) {
        refFromCreateRef.current = renderIndex;
    }
    return (
        <div ref={refFromCreateRef} className="demoa">
            <span className="item_title">当前的index是: {renderIndex}</span>
            <span className="item_title">使用useRef来获取renderIndex {refFromUseRef.current}</span>
            <span className="item_title">使用createRef来获取renderIndex {refFromCreateRef.current}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
        </div>
    );
}

3、useRef()可以拿到前一次渲染的值:

疑问:【拿到前一次的值】这个结果主要是基于useEffect而非useRef吧?如果将对ref的赋值提到外面,就能拿到最新值啊。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const UseRefDemod = () => {
    const [renderIndex, setRenderIndex] = useState(1);
    const ref = useRef<number | null>(null);
    useEffect(() => {
        ref.current = renderIndex
    })

    return (
        <div className="demoa">
            <span className="item_title">当前的index是: {renderIndex}</span>
            <span className="item_title">上一个index是: {ref.current}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
        </div>
    );
}

4、Alert弹出点击button时的快照

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const UseRefDemob = () => {
    const [renderIndex, setRenderIndex] = useState(1);

    const handleClick = ():void => {
        setTimeout(() => {
            alert(renderIndex)
        }, 3000)
    }

    return (
        <div className="demoa">
            <span className="item_title">当前的index是: {renderIndex}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
            <Button onClick={handleClick}>
                点击让弹出renderIndex
            </Button>
        </div>
    );
}

5、Alert弹出实时状态(与2对比?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const UseRefDemoc = () => {
    const [renderIndex, setRenderIndex] = useState(1);
    const ref = useRef<number | null>(null);
    useEffect(() => {
        ref.current = renderIndex
    })
    const handleClick = ():void => {
        setTimeout(() => {
            alert(ref.current)
        }, 3000)
    }

    return (
        <div className="demoa">
            <span className="item_title">当前的index是: {renderIndex}, {ref.current}</span>
            <Button style={{marginLeft: 0}} onClick={() => setRenderIndex(prev => prev + 1)}>
                点击让renderIndex加1
            </Button>
            <Button onClick={handleClick}>
                点击让弹出renderIndex
            </Button>
        </div>
    );
}

useReducer

当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们。

当你写类似setSomething(something => ...)这种代码的时候,也许就是考虑使用reducer的契机。reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。

useMemo

返回一个 memoized 值。

1、关键词:高开销

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

2、关键词:纯函数

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const WithMemo = function() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    const expensive:number = useMemo(() => {
        // 加入此处是一段大量运算的逻辑,实现了只有依赖项count变化时才会重新触发。达到了性能优化的目的
        console.log('执行了');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count])

    return <div>
        <h4>{count}-{val}-{expensive}</h4>
        <div>
            <Button onClick={() => setCount(count + 1)}>+c1</Button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

useCallback

返回一个 memoized 回调函数。 可以看出,两者的关系如下:

1
useCallback(fn, deps) === useMemo(() => fn, deps))

useCallback第一个参数是一个函数,返回一个 memoized 回调函数。只有当第二个参数也就是依赖项发生变化的情况下,memoized 回调函数才会更新,否则将会指向同一块内存区域。

这种情况通常用在子组件中,比如把memoized 回调函数传给子组件后,子组件就可以通过shouldComponentUpdate或者React.memo来避免不必要的更新。

updatedupdated2024-07-162024-07-16