使用React Hooks你可能会忽视的作用域问题

前言

其实React Hooks已经推出来一段时间了,直到前一阵子才去尝试了下,看到的一些博客都是以API的使用居多,还有一些是对于原理的解析。而我这篇文章想写的是关于React Hooks使用中的作用域问题,希望可以帮助到曾经有过困惑的你。

useEffect基础使用

在讲作用域之前,首先帮助你熟悉或者复习一下useEffect的使用,useEffect的基本使用如下:

useEffect(() => {
    // do something
    return () => {
        // release something
    };
}, [value1, value2...])
复制代码

useEffect接受两个参数:一个函数和一个值数组,第二个参数是指在下次render的时候,如果这个数组中的任意一个值发生变化,那么这个effect的函数(第一个参数)会重新执行。

这么讲可能比较抽象,我们以下面的一个例子来说明:

如图,页面中有1个按钮,当点击 "+" 按钮时count要加1,computed始终要为count + 1(实际业务中,这个计算往往不会是这么简单的),现在我们就用useEffect来计算computed:

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

export default () => {
    const [count, setCount] = useState(0);
    const [computed, setComputed] = useState(0);

    useEffect(() => {
        setComputed(count + 1);
        // return () => {};
    }, [count]);

    return View代码略;
};

复制代码

代码很简单,useEffect的第二个参数为[count],表示当count变化时,函数需要执行,在这个函数里面我们去设置computed为count+1,这样就完成了我们的需求。

下面我们深入讲解下useEffect的执行流程。

useEffect执行流程

我们利用console.log来帮助大家理解执行流程,上面代码改为:

export default () => {
    const [count, setCount] = useState(0);
    const [computed, setComputed] = useState(0);
    
    console.log('render before useEffect', count, computed);
    
    useEffect(() => {
        console.log('in useEffect', count, computed);
        setComputed(count + 1);
        return () => {
            console.log('just log release')
        };
    }, [count]);
    
    console.log('render after useEffect', count, computed);

    return View代码略;
};

复制代码

首次刷新时,打印日志为:

我们来看发生了什么事情:

1、第一次render执行的时候,useEffect的函数是异步执行的,是在render后执行的,准确的说,在第一个render的时候是在DOM生成后执行的,相当于类组件的componentDidMount和componentDidUpdate。

2、render后开始执行useEffect的函数,这时候我们执行了setComputed函数,触发state的修改,触发重新render。

3、第二次render的时候,useEffect的函数本来应该是要异步执行的,但是这时候注意了,useEffect是有第二个参数的,第二次render的时候,count不变,所以useEffect的函数不执行。

我们点击下 "+" 按钮,再看下打印日志:

1、setCount触发render,首先执行render

2、检测useEffect第二个参数,发现count已经变化,所以这个effect要重新执行,执行effect之前,会去看前一次effect执行时是否返回了函数,如果返回了函数,那么会首先执行这个函数(主要让我们释放副作用)。

3、执行完release函数后,开始执行effect函数,这时候执行setComputed

4、setComputed再次触发render,这次的render,useEffect检测到count没有发生变化,所以不会重新再执行effect。

如果你没看懂这其中render、effect函数、release函数的执行顺序,那么对于后续的一些作用域问题你可能无法理解,麻烦多看几遍这个日志打印的例子。

作用域问题

首先我们看段代码:

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

export default () => {
    const [state, setState] = useState({
        count: 0,
        computed: 1,
    });
    useEffect(() => {
        const buttonNode = document.getElementById('button');
        
        function handler() {
            console.log('in handler', state.count, state.computed);
            setState({
                count: state.count + 1,
                computed: state.count + 2,
            });
        }
        
        buttonNode.addEventListener('click', handler);
        
        return () => buttonNode.removeEventListener('click', handler);
    }, []);

    console.log('render', state.count, state.computed);

    return (
        <div className="app">
            <p>count: {state.count}, computed: {state.computed}</p>
            <button id="button"> + </button>
        </div>
    );
};
复制代码

我们把之前的例子改造了下,把button的点击事件改成了在useEffect里面绑定,useEffect的第二个参数传入空数组[],表示这个effect函数只在componentDidMount的时候执行。我们不断点击 "+" 按钮,期待的结果应该是和上面的例子一样,count不断增加,computed始终为count + 1,我们看下打印日志:

你猜对结果了吗?我们期待的count并没有不断增加,而handler里获取到的state.count居然始终为0。

按照我们的习惯,handler里面用到了state,在handler这个函数作用域里面没有这个变量,那么应该去render这个函数里面找,在第二次点击按钮的时候,state.count应该已经是1了,但是为什么拿到的还是0呢?

如果你看到这个结果没有一刻的困惑,那么你应该是个基础异常扎实的人,很不容易。

这个问题的答案要用作用域来解释。

静态作用域

关于作用域的详细解释大家自己去google,好文章很多,这里不展开讲太多,简单看段代码:

function foo() {
    console.log(a); 
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;

bar();
复制代码

这段代码执行打印结果为:2

为什么呢?因为JS的函数会创建一个作用域,这个作用域是在函数被定义的时候就定好的,在上面的代码中,foo函数定义的时候,它的外层作用域是global,global里面a变量是2,所以打印出来的结果是2,如果是动态作用域,那么打印出来的就是3。

记住了吗?

模拟useEffect的作用域问题

由于React Hooks的内部原理需要去看源码才能知道,这里我们用原生JS来模拟,这样你就可以更纯粹地理解。

let init = true;

const value = {count: 0};

function render() {
    let count = value.count;
    if (init) {
        function handler() {
            console.log(count);
            value.count = count + 1;
            render();
        }
        document.addEventListener('click', handler);
        init = false;
    }
}

render();
复制代码

这段代码定义了一个函数render,render里面绑定了document点击事件,回调函数里面执行了value.count为count + 1,然后触发render,模拟修改state后触发render行为。

这里handler的count也是始终为0,为什么呢?

我们把上面说过的作用域概念引入就很好解释了,当第一次执行render的时候,render函数创建了一个作用域,这个作用域中count = value.count,也就是0,这时init为true,所以handler被定义,词法作用域被创建,它的上层作用域就是刚才执行render的创建的作用域。

根据静态作用域的特性,handler里面的count在它被定义的时候就决定是0了,所以它始终是0.

理解吗?

如果理解了,那么我们返回来看useEffect的作用域。

useEffect作用域问题

仍然是这段代码:

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

export default () => {
    const [state, setState] = useState({
        count: 0,
        computed: 1,
    });
    
    useEffect(() => {
        const buttonNode = document.getElementById('button');
        
        function handler() {
            setState({
                count: state.count + 1,
                computed: state.count + 2,
            });
        }
        
        buttonNode.addEventListener('click', handler);
        
        return () => buttonNode.removeEventListener('click', handler);
    }, []);

  
    return View省略;
};
复制代码

1、在第一次render的时候,执行到useEffect函数的时候,可以想象成React内部是类似下面的代码:

const fnArray = [];
const consArray = [];

function useEffect(callback, conditions) {
    const index = <该useEffect对应的index>;
    if (<首次render>) {
        fnArray.push(callback);
        consArray.push(conditions);
    } else if (<根据conditions判定需要重新执行effect>) {
        fnArray[index] = callback;
        consArray[index] = conditions;
    }
}
复制代码

源码肯定不是这样的,但是可以这么理解,是用数组在维护hooks,所以useEffect的函数的作用域在执行useEffect的时候就定好了,当你传入的conditions(第二个参数)判定不需要重新执行时,effect函数的作用域的外层为前面某个render创建的作用域,这次render中,conditions发生了变化,判定需要重新执行effect,

普通的useEffect,也就是第二个参数不传,每次都update的effect,这样的effect在每次render执行后,都会更新最新的effect函数,因此可以拿到最新的state

useEffect(() => {
    // do something
})
复制代码

一个技巧

利用effect执行时机来记录前一个render的值

export function usePrevious(value) {
    const ref = useRef();
    useEffect(() => {
        ref.current = value;
    });
    return ref.current;
}

复制代码

然后你在你的组件中就可以这么用:

const Component = () => {
    const [count, setCount] = useState(0);
    const prevCount = usePrevious(count); // 获取上一次render的count

    return (View代码);
}
复制代码

转载于:https://juejin.im/post/5ca0eec8f265da30c347841f

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Hooks 是 React 16.8 版本引入的一种新特性,它是为了使函数组件能够拥有状态和其他 React 特性而设计的。 React Hooks 的原理基于两个核心概念:闭包和钩子函数。 1. 闭包:在函数组件内部,可以通过闭包的方式引用外部作用域的变量。React Hooks 利用了闭包的特性,使得可以在函数组件内部存储和更新状态。 2. 钩子函数:React Hooks 提供了一系列的钩子函数,如 useState、useEffect、useContext 等。这些钩子函数是预定义的特殊函数,可以在函数组件中使用,通过调用这些钩子函数,可以获取和操作组件的状态、副作用和上下文等。 当一个组件使用React Hooks,React 在底层创建一个与该组件实例相关联的 Fiber 对象,并在组件渲染时执行组件函数。在执行组件函数时,React 跟踪每个组件函数内部所有的钩子函数调用,并将其与该组件实例相关联。 当组件函数执行时,如果遇到 useState 钩子调用,React 查找该钩子函数对应的状态值,并将其返回给组件函数。组件通过 useState 返回的状态值可以读取和更新组件的状态。 当组件函数执行完毕后,React 将该组件的状态和副作用存储在 Fiber 对象中,并将 Fiber 对象添加到更新队列中。之后,React 根据更新队列中的 Fiber 对象,对组件进行批量更新,实现页面的重新渲染。 通过这种方式,React Hooks 实现了函数组件的状态管理和副作用处理,使得开发者可以更方便地编写和维护 React 组件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值