使用React Hooks你可能会忽视的作用域问题
前言
其实React Hooks已经推出来一段时间了,直到前一阵子才去尝试了下,看到的一些博客都是以API的使用居多,还有一些是对于原理的解析。而我这篇文章想写的是关于React Hooks使用中的作用域问题,希望可以帮助到曾经有过困惑的你。
useEffect基础使用
在讲作用域之前,首先帮助你熟悉或者复习一下useEffect的使用,useEffect的基本使用如下:
useEffect(() => {// do somethingreturn () => {// 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的countreturn (View代码);
}
复制代码
转载于:https://juejin.im/post/5ca0eec8f265da30c347841f
使用React Hooks你可能会忽视的作用域问题相关推荐
- 【译】什么是React Hooks
原文:What are React Hooks? 作者:Robin Wieruch 译者:博轩 React Hooks 于 2018年10月的React Conf 中引入,作为在 React 函数组件 ...
- React Hooks 完全使用指南
大家好,我是若川.最近组织了源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列> ...
- [译] React Hooks: 没有魔法,只是数组
[译] React Hooks: 没有魔法,只是数组 原文链接: medium.com/@ryardley/r- 我是 React 新特性 Hooks 的粉丝.但是,在你使用 React Hooks的 ...
- 使用React Hooks 时要避免的5个错误!
作者:Shadeed 译者:前端小智 来源:dmitripavlutin 点赞再看,微信搜索**[大迁世界],B站关注[前端小智]**这个没有大厂背景,但有着一股向上积极心态人.本文 GitHub h ...
- React Hooks核心原理与实战
React Hooks核心原理与实战 一.Hooks的优点 1.1 Hooks的含义 1.2 优点 二.常用的Hooks 2.1 useState 2.2 useEffect 2.3 useCallb ...
- React Hooks 使用详解
本文对 16.8 版本之后 React 发布的新特性 Hooks 进行了详细讲解,并对一些常用的 Hooks 进行代码演示,希望可以对需要的朋友提供点帮助. 一.Hooks 简介 Hooks 是 Re ...
- 探React Hooks
前言 众所周知,hooks在 React@16.8 中已经正式发布了.而下周周会,我们团队有个同学将会仔细介绍分享一下hooks.最近网上呢有不少hooks的文章,这不免激起了我自己的好奇心,想先行探 ...
- 通过 React Hooks 声明式地使用 setInterval
2019独角兽企业重金招聘Python工程师标准>>> 本文由云+社区发表 作者:Dan Abramov 接触 React Hooks 一定时间的你,也许会碰到一个神奇的问题: se ...
- mounty不可重新挂载因为先前没有完全卸载_【译】React Hooks测试完全指南
原文地址:https://www.toptal.com/react/testing-react-hooks-tutorial 2018年底,React在16.8版本中引入了Hooks.它们(译注:指R ...
最新文章
- STM32固件库文件树及构成详解
- pandas修改数据类型_如何正确在pandas里使用inplace参数
- SpringBoot 基础拦截器
- SyntaxError: Non-UTF-8 code starting with '\xba' in file 错误的解决方法!!
- Leetcode--215. 数组中第K个最大元素
- 特征工程系列之降维:用PCA压缩数据
- 构造器2(Java)
- 【“达观杯”冠军分享】预训练模型彻底改变了NLP,但也不能忽略传统方法带来的提升...
- 42.数据库 SQL 操作
- android手机如何拥有苹果表情包,怎样让安卓emoji显示iPhone的emoji样式
- 2021软考软件设计师真题
- linux cgroup 学习的一些总结
- 服务器机箱 改造 桌面,你的显卡站起来了吗?——桌面改造分享装机篇
- 【转】图片热点链接使用方法
- 大数据技术学习带来的思考
- RNN(三) 在SLU中的应用
- 美食杰 login的实现效果
- 设备树文件里的aliases和chosen
- 小米4 miui6 android,小米4怎么刷miui6?小米4刷miui6三种方法详解
- 大数定律与蒙特卡罗法