关于react.js:转载React-Hooks-中的闭包问题

48次阅读

共计 5888 个字符,预计需要花费 15 分钟才能阅读完成。

前言

明天中午在领完盒饭,吃饭的时候,正吃着深海鳕鱼片,蘸上番茄酱,那美味,几乎无以言表。忽然产品急匆匆的跑过来说:“明天需要能上线吧?”我突然虎躯一震,想到本人遇到个问题迟迟找不到起因,怯怯的答复道:“能 … 能吧 …”,产品听到‘能’这个字便哼着小曲扬长而去, 留下我独自一人,面对着曾经变味的深海鳕鱼片 … 一遍又一遍的想着问题该如何解决 …

一、从 JS 中的闭包说起

JS的闭包实质上源自两点,词法作用域和函数以后值传递。

闭包的造成很简略,就是在函数执行结束后,返回函数,或者将函数得以保留下来,即造成闭包。

对于 词法作用域 相干的知识点,能够查阅 《你不晓得的 JavaScript》 找到答案。

React Hooks中的闭包和咱们在 JS 中见到的闭包并无不同。

定义一个工厂函数 createIncrement(i), 返回一个increment 函数。每次调用 increment 函数时,外部计数器的值都会减少i

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
    }
    return increment
}
const inc = createIncrement(10)
inc() // 10
inc() // 20
复制代码

createIncrement(10) 返回一个增量函数,该函数赋值给 inc 变量。当调用 inc() 时,value 变量加 10。

第一次调用 inc() 返回 10,第二次调用返回 20,依此类推。

调用 inc() 时不带参数,JS 依然能够获取到以后 valuei 的增量,来看看它是如何工作的。

原理就在 createIncrement() 中。当在函数上返回一个函数时,就会有闭包产生。闭包捕捉了词法作用域中的变量 valuei

词法作用域是定义闭包的内部作用域 。在本例中,increment() 的词法作用域是createIncrement() 的作用域,其中蕴含变量 valuei

无论在何处调用 inc(),甚至在 createIncrement() 的作用域之外,它都能够拜访 valuei

闭包是一个能够从其 词法作用域 记住和批改变量的函数,不论执行的作用域是什么。

二、React Hooks 中的闭包

通过简化状态重用和副作用治理,Hooks 取代了基于类的组件。此外,咱们能够将反复的逻辑提取到自定义 Hook 中,以便在应用程序之间重用。Hooks 重大依赖于 JS 闭包, 然而闭包有时很辣手。

当咱们应用一个有多种副作用和状态治理的 React 组件时,可能会遇到的一个问题是过期的闭包,这可能很难解决。

三、过期的闭包

工厂函数 createIncrement(i) 返回一个 increment 函数。increment 函数对 value 减少 i,并返回一个记录以后value 的函数

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState 相当于 logValue 函数
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(10)
const log = inc() // 10,将以后的 value 值固定
inc() // 20
inc() // 30

log() // "Current value is 10" 未能正确打印 30
复制代码

在这里还要提一下 useRef, 为什么当你应用let 申明的 useRef 的时候不会遇到这个问题, 而用 let 申明的变量的时候却会遇到这个问题, 这是因为 useRef 其实并不是一个根底类型变量,而是一个对象,每次在批改值的时候,你理论批改的是对象的值,而对象是以指针的形式进行援用的,因而不论在任何中央取值都能获取到最新的值!

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState 相当于 logValue 函数
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(1) // i 被固定为 1, 输出几就被固定为几
inc() // 1
const log = inc() // 2
inc() // 3

log() // "Current value is 2" 未能正确打印 3
复制代码

过期的闭包捕捉具备过时值的变量

四、修复过期闭包的问题

(1) 应用新的闭包

解决过期闭包的第一种办法是找到捕捉最新变量的闭包。

找到捕捉了最新 message 变量的闭包,就是从最初一次调用 inc() 返回的闭包。

const inc = createIncrement(1)
inc() // 1
inc() // 2
const latestLog = inc()
latestLog() // "Current value is 3"
复制代码

以上就是 React Hook 解决闭包新鲜度的办法了。

Hooks实现假如在组件从新渲染之前,最为 Hook 回调提供的最新闭包 (例如useEffect(callback)) 曾经从组件的函数作用域捕捉了最新的变量。也就是说在 useEffect 的第二个参数 [] 退出监听变动的值,在每次变动时,执行function,获取最新的闭包。

(2) 敞开已更改的变量

第二种办法是让 logValue() 间接应用 value

让咱们挪动行 const message = ...;logValue() 函数体中:

function createIncrementFixed(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {const message = `Current value is ${value}`;
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // 打印 1
inc();             // 打印 2
inc();             // 打印 3
// 失常工作
log();             // 打印 "Current value is 3"
复制代码

logValue()敞开 createIncrementFixed() 作用域内的 value 变量。log()当初打印正确的音讯。

五、Hook 中过期的闭包

useEffect()

在应用 useEffect Hook 时呈现闭包的常见状况。

在组件 WatchCount 中,useEffect每秒打印 count 的值。

function WatchCount() {const [count, setCount] = useState(0)
    useEffect(function() {setInterval(function log() {console.log(`Count is: ${count}`)
        }, 2000)
    }, [])
    
    return (
      <div>
      {count}
      <button onClick={() => setCount(count + 1)}> 加 1 </button>
      </div>
    )
}
复制代码

点击几次加 1 按钮,咱们从控制台看,每 2 秒打印的为Count is: 0

在第一渲染时,log()中闭包捕捉 count 变量的值 0。过后,即便count 减少,log()中应用的依然是初始化的值 0log() 中的闭包是一个过期的闭包。

解决办法:让 useEffect()晓得 log()中的闭包依赖于 count:

function WatchCount() {const [count, setCount] = useState(0);

  useEffect(function() {const id = setInterval(function log() {console.log(`Count is: ${count}`);
    }, 2000);
    return function() {clearInterval(id);
    }
  }, [count]); // 看这里,这行是重点,count 变动后从新渲染 useEffect

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}
复制代码

设置依赖项后,一旦 count 更改,useEffect()就更新闭包。

正确治理 Hook 依赖关系是解决过期闭包问题的要害。举荐装置 eslint-plugin-react-hooks, 它能够帮忙咱们检测被忘记的依赖项。

useState()

组件 DelayedCount 有 2 个按钮

  • 点击按键“Increase async”在异步模式下以 1 秒的提早递增计数器
  • 在同步模式下,点击按键“Increase sync”会立刻减少计数器
function DelayedCount() {const [count, setCount] = useState(0);

  function handleClickAsync() {setTimeout(function delay() {setCount(count + 1);
    }, 1000);
  }

  function handleClickSync() {setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  )
}
复制代码

点击“Increase async”按键而后立刻点击“Increase sync”按钮,count 只更新到 1

这是因为 delay() 是一个过期的闭包。

来看看这个过程产生了什么:

初始渲染:count 值为 0。点击 ‘Increase async‘ 按钮。delay() 闭包捕捉 count 的值 0setTimeout() 1 秒后调用 delay()。点击“Increase async”按键。handleClickSync() 调用 setCount(0 + 1)count 的值设置为 1,组件从新渲染。1 秒之后,setTimeout() 执行 delay() 函数。然而 delay() 中闭包保留 count 的值是初始渲染的值 0,所以调用 setState(0 + 1),后果 count 放弃为 1。

delay() 是一个过期的闭包,它应用在初始渲染期间捕捉的过期的 count 变量。

为了解决这个问题,能够应用函数办法来更新 count 状态:

function DelayedCount() {const [count, setCount] = useState(0);

  function handleClickAsync() {setTimeout(function delay() {setCount(count => count + 1); // 这行是重点
    }, 1000);
  }

  function handleClickSync() {setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  );
}
复制代码

当初 setCount(count => count + 1) 更新了 delay() 中的 count 状态。React 确保将最新状态值作为参数提供给更新状态函数,过期的闭包的问题就解决了。

useLayoutEffect()

useLayoutEffect 能够看作是 useEffect 的同步版本。

useLayoutEffect其函数签名与 useEffect 雷同,但它会在所有的 DOM 变更之后同步调用 effect。能够应用它来读取DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect外部的更新打算将被同步刷新。

因而,要尽可能应用规范的 useEffect 以防止阻塞视觉更新。因为 useLayoutEffect 是同步的,如果咱们要在 useLayoutEffect 调用状态更新,或者执行一些十分耗时的计算,可能会导致 React 运行工夫过长,阻塞了浏览器的渲染,导致一些卡顿的问题

如果你正在将代码从 class 组件迁徙到应用 Hook 的函数组件,则须要留神 useLayoutEffectcomponentDidMountcomponentDidUpdate 的调用阶段是一样的。然而,咱们举荐你一开始先用 useEffect,只有当它出问题的时候再尝试应用 useLayoutEffect

如果你应用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无奈在 Javascript 代码加载实现之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,须要将代码逻辑移至 useEffect 中(如果首次渲染不须要这段逻辑的状况下),或是将该组件提早到客户端渲染实现后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的状况下)。

若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,能够通过应用 showChild && <Child /> 进行条件渲染,并应用 useEffect(() => { setShowChild(true); }, []) 提早展现组件。这样,在客户端渲染实现之前,UI 就不会像之前那样显示错乱了。

总结

闭包是一个函数,它从定义变量的中央 (或其词法范畴) 捕捉变量。

当闭包捕捉 过期的变量 时,就会呈现过期闭包的问题。

解决闭包的无效办法

  1. 正确设置 React Hook 的依赖项
  2. 对于过期的状态,应用函数形式更新状态

作者:FruitBro
链接:https://juejin.cn/post/684790…
起源:掘金
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

正文完
 0