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

前言

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

useEffect基础使用

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

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

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

这么讲可能比较抽象,咱们如下面的一个例子来讲明:数组

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

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,这样就完成了咱们的需求。app

下面咱们深刻讲解下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代码略;
};

复制代码

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

咱们来看发生了什么事情:google

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

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

三、第二次render的时候,useEffect的函数原本应该是要异步执行的,可是这时候注意了,useEffect是有第二个参数的,第二次render的时候,count不变,因此useEffect的函数不执行。

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

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

二、检测useEffect第二个参数,发现count已经变化,因此这个effect要从新执行,执行effect以前,会去看前一次effect执行时是否返回了函数,若是返回了函数,那么会首先执行这个函数(主要让咱们释放反作用)。

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

四、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省略;
};
复制代码

一、在第一次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代码);
}
复制代码
相关文章
相关标签/搜索