闭包

236 阅读4分钟

什么是闭包

函数 + 作用域

垃圾回收

参考: Garbage collection (javascript.info)

索引切换导致的垃圾回收

let fiber = { type: 'div' }
fiber = null // 此时 { type: 'div' } 会被垃圾回收

函数执行

  • 当函数执行时 let a被创建
  • 当函数执行完毕, let a被垃圾回收
function fn() {
    let a = 1 
}

此时let a不会被垃圾回收

function fn() {
    let a = 1 
    return b => a = a+b
}
const add = fn() // 此时变量a将无法被垃圾回收

标记清除算法

  • 全局变量不会被垃圾回收
  • 和全局变量有关系的会被打上勾
  • 没打勾的会被垃圾回收

image.png

新生代

  • 垃圾回收发生得相对频繁
  • 新生代主要用于存放新创建的对象。
  • 由于大多数对象在创建后不久就变得不再需要,
  • 因此新生代中的垃圾回收发生得相对频繁。

旧生代

  • 旧生代主要用于存放存活时间较长的对象。
    • 这些对象在经过多次新生代垃圾回收后仍然存活(全局变量的关系户)
    • 因此被移动到旧生代。
  • 旧生代的垃圾回收频率通常较低,
    • 因为其中的对象生命周期较长。
    • 当旧生代的内存空间不足时,
    • 会触发Major GC(老年代垃圾回收)来回收不再使用的对象。

内存泄漏

满足内存泄漏的两个条件

  • 这个变量没有用了
  • 这个变量无法被垃圾回收

内存泄漏的场景1

被全局引用, 组件卸载时未及时清理

  • 被全局变量引用
  • 被全局函数使用
function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    // 下面两种情况都会导致内存泄漏
    window.arr = arr
    window.fn = () => console.log(arr)
  }, []);

  return <h1>app</h1>;
}
  • 改进
function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    window.arr = arr
    window.fn = () => console.log(arr)
    return () => {
      window.arr = null
      window.fn = null
    }
  }, []);

  return <h1>app</h1>;
}

内存泄漏的场景2

被定时器使用, 组件卸载时未及时清理

function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    setInterval(() => {
      console.log(arr);
    }, 1000);
  }, []);

  return <h1>app</h1>;
}

改进

function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    const timeId = setInterval(() => {
      console.log(arr);
    }, 1000);
    return () => clearInterval(timeId);
  }, []);

  return <h1>app</h1>;
}

内存泄漏的场景3

注册事件, 组件卸载时未及时清理

function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    const print = () => console.log(arr)
    window.addEventListener('resize', print)
  }, []);

  return <h1>app</h1>;
}

改进

function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    const print = () => console.log(arr)
    window.addEventListener('resize', print)
    return window.removeEventListener('resize', print)
  }, []);

  return <h1>app</h1>;
}

闭包不会导致内存泄漏

  • 如果我们将闭包的相关依赖设置为null
  • 就能触发垃圾回收
  • 但是由于相关依赖太多(一千多个), 造成一部分依赖被遗漏
  • 那么这个闭包就无法被垃圾回收掉

const cleanUp = new FinalizationRegistry((key) => {
  console.log('出发垃圾回收:', key)
})

const createFn = () => {
  let o = {a: 1}
  cleanUp.register(o, '变量o被垃圾回收了')
  return () => {
    return o
  }
}

let fn = createFn() // fn使用到了o, 导致o无法被垃圾回收
let b = fn() // 全局变量b指向o, 导致o无法被垃圾回收

document.addEventListener('click', () => {
  // 斩断o的一切联系, 将o垃圾回收掉
  fn = null
  b = null
})

闭包: 无内存泄漏

  • 变量n一直是有用的, 不存在什么内存泄漏
function createAdd() {
    let count = 0
    return () => {
        return count++
    }
}

const add = createAdd()

const click = () => {
    const n = add()
    console.log(n)
}

window.addEventListenner('click', click)

闭包: 容易造成内存不足

  • 下面代码由于忘记将相关依赖设置为null, 导致闭包无法被垃圾回收
  • 点击三次后, 变量count已经没有存在的必要了
  • 应该将变量count垃圾回收掉
  • 但变量count不会自动被垃圾回收
  • 如果不将相关依赖设置成null
  • 变量count永远不会被垃圾回收
function createAdd() {
    let count = 0
    return () => {
        return count++
    }
}

// 函数add使用到了变量count, 导致count无法被垃圾回收
// 点击三下后, 函数add就没有用了
// 需要我们手动触发垃圾回收, 不然就是内存泄漏
const add = createAdd()

const click = () => {
    const n = add()
    console.log(n)
    if(n>3) {
        window.removeEventListenner('click')
    }
}

window.addEventListenner('click', click)

将上面代码优化, 避免内存泄漏

function createAdd() {
    let count = 0
    return () => {
        return count++
    }
}

const add = createAdd()

const click = () => {
    const n = add()
    console.log(n)
    if(n>3) {
        window.removeEventListenner('click')
        add = null
    }
}

window.addEventListenner('click', click)

形成闭包的场景

{
    var count
    windown.add = () => count++
}
let fn

{
  let a = 1
  fn = () => a
}
const createFn = () => {
  let n = 1
  
  return () => {
    return n
  }
}
  • 虽然是闭包,
  • 但是立即函数执行完成后
  • count会被垃圾回收掉
(function fn(){
    var count = 0
    return ()=> count++
})()

闭包的应用

useEffect

为什么 useEffect 能打印出 n 变化前的值

  const [n, setN] = useState(0)

  useEffect(() => {
    console.log(n, '------ after');
    return () => console.log(n, '-------- before'); // 打印变化前的值
  })

useEffect原理

let before: Function = null

function useEffect(after: Function) {
  if (!before) before = after()
  before()
  before = after()
}

解释

image.png

let before: Function = null
// 第一次执行
{
    const n = 0
    const after = () => {
        console.log(n)
        return () => console.log(n)
    }
    before = after() // 打印0
    before() // 打印0
    before = after() // 打印0
}
// 第二次执行
{
    const n = 1
    const after = () => {
        console.log(n)
        return () => console.log(n)
    }
    before() // 打印: 0, 这是第一次执行after返回的函数
    before = after() // 打印: 1
}
// 第三次执行
{
    const n = 2
    const after = () => {
        console.log(n)
        return () => console.log(n)
    }
    before()
    before = after()
}

更多