JS 面试高频题

1.Js中this 的指向?

this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象

2.闭包

  • 简单描述

    1. 能够读取其他函数内部变量的函数。
    2. 函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
  • 稍微完整的描述
    在 js 中变量的作用域属于函数作用域,在函数执行完后,作用域会被清理,内存会随之被回收,但是由于闭包函数式建立在函数内部子函数,由于其可访问上级作用域,即使,上级函数执行完,作用域也不会随之销毁,这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完毕后作用域内的值也不会被销毁。

  • 闭包解决了什么问题

    1. 可以读取函数内部的变量
    2. 让这些变量的值始终保持在内存中。不会再函数调用后被清除。
  • 闭包的缺点

    1. 由于闭包会使得函数中的变量都被保存到内存中,滥用闭包很容易造成内存消耗过大,导致网页性能问题。解决方法是在推出函数之前,将不再使用的变量全部删除
    2. 闭包可以使得函数内部的值可以在函数外部进行修改。所以,如果你把父函数当做对象(object)使用,把闭包当做他的公共方法(public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

      3.原型及原型链

      原型

  • 在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

    原型链

  • 当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

    4.作用域及作用域链

  1. 全局作用域
    ● 最外层函数和最外层函数外面定义的变量拥有全局作用域
    ● 所有未定义直接赋值的变量自动声明为全局作用域
    ● 所有window对象的属性拥有全局作用域
    ● 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
  2. 函数作用域
    ● 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
    ● 作用域是分层的,内层作用域可以访问外层作用域,反之不行
  3. 块级作用域
    ● 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{}包裹的代码片段)
    ● let和const声明的变量不会有变量提升,也不可以重复声明
    ● 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部

    5.深拷贝,浅拷贝

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

    浅拷贝的实现方式

  1. Object.assign
  2. 数组的 slice 和 concat 方法
  3. 数组静态方法 Array.from
  4. ...解构运算符

    深拷贝的实现方式

    递归方法:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

    6.js的 (GC)垃圾回收

    回收概念:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
    垃圾回收的方式

  • 浏览器通常使用的回收方式有两种:标记清除引用计数
    1. 标记清除(分为标记阶段和清除阶段)
  • (标记)标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放
  • (清除)垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。它的作用就是会去掉环境中的变量以及被环境中的变量引用的标记。而在清除阶段的时候而再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
    1. 引用计数
  • 引用计数就是跟踪记录每个值被引用次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数+1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就-1。当这个引用次数变为0时,这个变量所占有的内存空间就会被释放出来。
    • 缺点:如果两个值进行相互引用,两个对象的引用次数都是2,因此他们的引用次数永远不会为0,就会引起循环引用,这样的话会造成内存泄漏
      
      //例子
      function fun() {
      let obj1 = {};
      let obj2 = {};
      obj1.a = obj2; // obj1 引用 obj2
      obj2.a = obj1; // obj2 引用 obj1
      }

//解决方法手动释放内存 obj1.a = null obj2.a = null

**优化垃圾回收的方式:**

1. 不再使用的值和对象可以赋值为null。 obj = null。
2. 对象object优化 可以进行逐个删除 delete obj[key]。
3. 数组array优化 讲length赋值为0  arr.length = 0。
4. function中在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
<a name="MK6gJ"></a>
## 7.js中的内存泄漏

1. **闭包**:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
2. **意外的全局变量**:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
3. **被遗忘的计时器和回调函数**:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
4. **脱离DOM的引用**:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
5. **被遗忘的事件监听器**:window.addEventListener("resize", this.doSomething) 需要window.removeEventListener("resize", this.doSomething)
6. 如果用**发布订阅模式**做的类似于eventbus的函数需要on之后off取消监听它
7. **遗忘的Map和Set**,因为他们是对引用值的强引用,如果需要做内存优化则需要用weakMap和weakSet来替代他们,让GC回收他们

<a name="j6LrQ"></a>
## 8.Js中的evet loop(事件循环)
**事件循环**:会将**同步代码**按顺序排在某个地方(**执行栈**),然后**依次**执行里面的函数。当遇到**异步**任务的时候,就交给**其他线程**去处理,等待当前执行栈里面的**所有同步代码**执行**完成**后,从一个队列中去取出**已完成的异步任务的回调**,加入执行栈继续执行,遇到异步任务又交给其他线程,然后放入执行栈里面再来执行。通过这样的一个循环来执行完整个的代码,这就叫做**事件循环**,每一次的**循环**其实就是一个**事件周期,**或称作一次tick。

- 事件循环中**宏任务**、**微任务**:
   - **宏任务(macro task)**:setTimeout,setInterval,setImmediate等主代码块,请求任务,常见的鼠标点击事件和键    盘按下事件。
      - **特征:宏任务有明确异步任务需要执行和回调,需要其他异步线程去支持**。如**setTimeout**需要**定时器线程**去支持,**Ajax**的‘发送请求’任务,需要**Http线程**去支持执行。
   - **微任务(micro task)**:promise.then()、promise.catch()、 new MutaionObserver()、process.nextTick()。
      - **特征:微任务没有明确的异步任务需要执行,只有回调,不需要其他异步线程去支持。**
```javascript
console.log('同步代码1');
setTimeout(()=>{
    console.log('setTimeout');//异步但为宏任务
},0)
new Promise((re)=>{
    console.log('同步代码2')
    re();
}).then(()=>{
    console.log('promise.then')//异步但为微任务
})
console.log('同步代码3');
/**
最后输出:
同步代码1
同步代码2
同步代码3
promise.then
setTimeout
**/
  • 执行顺序为:同步>异步>微任务>宏任务

    9.Js中的防抖与节流

  • 防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay(延迟)时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发

    • 应用场景:输入框搜索功能,多次输入会增加查询的负担,按钮多次点击事件也会发送多次请求。
      // func是用户传入需要防抖的函数
      // wait是等待时间
      const debounce = (func, wait = 50) => {
      // 缓存一个定时器id
      let timer = 0;
      // 这里返回的函数是每次用户实际调用的防抖函数
      // 如果已经设定过定时器了就清空上一次的定时器
      // 开始一个新的定时器,延迟执行用户传入的方法
      return function (...args) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
          func.apply(this, args)
      }, wait)
      }
      }
      debounce(fun,500);
  • 节流:使得一定时间触发一次函数。原理是通过判断是否到达一定时间来触发函数。

    • 应用场景:在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
      
      function throttle(fun, delay = 500) {
      let last, deferTimer
      return function (args) {
      let that = this
      let _args = arguments
      let now = +new Date()
      if (last && now < last + delay) {
          clearTimeout(deferTimer)
          deferTimer = setTimeout(function () {
              last = now
              fun.apply(that, _args)
          }, delay)
      }else {
          last = now
          fun.apply(that,_args)
      }
      }
      }

throttle(ajax, 1000)

<a name="ukFrR"></a>
## 10.Js中的异步及Promise
<a name="uqYAW"></a>
### javascript中的异步实现

- **回调方式**:使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
- **Promise**:使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
- **generator**:它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以**同步**的顺序来书写。
```javascript
// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
    let a = 1 + 2;
    yield 2;
    yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }
  • async:async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

    setTimeout、Promise、Async/Await的区别?

(1) setTimeout

console.log('script start') //1. 打印 script start
setTimeout(function(){
    console.log('settimeout')   // 4. 打印 settimeout
})  // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end')   //3. 打印 script start
// 输出顺序:script start->script end->settimeout

(2) Promise

对promise的理解
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
promise的状态和过程:

  1. Promise的实例有三个状态
    • Pending(进行中)
    • Resolved(已完成)
    • Rejected(已拒绝)

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。

  1. promise的实例有两个过程
    • pending -> fulfilled : Resolved(已完成)
    • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
对promise的特点:

  • 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——承诺。
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从pending变为fulflilled,或者从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。

对promise的缺点:

  • 无法取消promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行。

console.log('script start')
setTimeout(function(){
    console.log('settimeout')
})
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})

console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

当js主线程执行到异步任务时:

  • promise1.then() 的回调就是一个 task
  • promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue(微任务队列)
  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue (微任务队列)中
  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue(微任务队列) 即使是 0ms 的情况

总结:
Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
Promise的方法
Promise有五个常用的方法:then()、catch()、all()、allSettled()、race()、finally。

//第一种普通书写方式:
const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then(data=>{
  console.log(data)
},err=>{
  console.log(err)
}).finally(()=>{//不管最后状态如何,都会执行这个函数。

})
//等同于
promise.then(data=>{
  console.log(data);
}).catch(err=>{
  console.log(err);
}).finally(()=>{////不管最后状态如何,都会执行这个函数。
})

Promise.all和Promise.race的区别的使用场景

  • Promise.all

Promise.all可以将多个Promise实例包装成一个新的promise实例,同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败返回的是最先被reject失败状态的值。
Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

  • Promise.race

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多少时间就不做了,可以用这个方法来解决。

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

Promise.allSettled()与Promise.all()的区别

  1. 它们所返回的数据不太一样,all()返回一个直接包裹resolve内容的数组,则allSettled()返回一个包裹着对象的数组。
    
    //allSettled
    0:
    status: "fulfilled"
    value: "p1"
    __proto__: Object
    1:
    status: "fulfilled"
    value: "p2"
    __proto__: Object
    length: 2
    __proto__: Array(0)

//all ["p1", "p2"]


2. 如果是all()的话,如果有一个Promise对象报错了,则all()无法执行,会报错你的错误,无法获得其他成功的数据。则allSettled()方法是不管有没有报错,把所有的Promise实例的数据都返回回来,放入到一个对象中。如果是resolve的数据则status值为fulfilled,相反则为rejected。
<a name="hrv4U"></a>
#### (3) Async/Await
async/await其实就是generator的 语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

await的含义为等待,也就是 async 函数需要s等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

**async/await对比Promise的优势**

- 代码阅读和书写起来更加同步代码化,会减轻阅读负担
- 传递中间值很方便,不像Promise.then一样麻烦
- 错误处理比较友好,可以用try catch 
- 调试比较优化,更加容易打断点调试
<a name="zA7Zc"></a>
## 11.什么是函数柯里化?
能把一个带有**多个参数**的函数转换成**一系列的嵌套函数**。它**返回一个新函数**,这个**新函数期望传入下一个参数**。<br />它不断地返回新函数(像我们之前讲的,这个新函数期望当前的参数),直到所有的参数都被使用。参数会一直保持 alive (通过**闭包**),当柯里化函数链中最后一个函数被返回和调用的时候,它们会用于执行。<br />优点是:通常可用于在不侵入函数的前提下,为函数**预置通用参数**,供多次重复调用。
```javascript
const add = function add(x) {
    return function (y) {
        return x + y
    }
}

const add1 = add(1)

add1(2) === 3
add1(20) === 21

CSS

常见的水平垂直居中的方式

1.
//利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,
//然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。
.parent {
    position: relative;
}

.child {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
}
2.
//利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,
//由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该
//方法适用于盒子有宽高的情况:
.parent {
    position: relative;
}

.child {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
}
3.
//利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心
//,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况
.parent {
    position: relative;
}

.child {
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -50px;     /* 自身 height 的一半 */
    margin-left: -50px;    /* 自身 width 的一半 */
}
4.
//使用 flex 布局,通过 align-items:center 和 justify-content:center 
//设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。
//该方法要**考虑兼容的问题**,该方法在移动端用的较多:
.parent {
    display: flex;
    justify-content:center;
    align-items:center;
}
5.
//另外,如果父元素设置了flex布局,只需要给子元素加上`margin:auto;`
//就可以实现垂直居中布局
.parent{
    display:flex;
}
.child{
    margin: auto;
}

React

React 事件机制

React并不是将click事件绑定到了div的真实DOM上,而是在document(react 17 是在root根节点上)处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行(是由react自己实现的合成事件(SyntheticEvent))进行处理。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。

好处

  • 兼容其他浏览器,更好的跨平台

  • 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。

  • 方便 react 统一管理和事务机制。

    原理

    在React底层,主要对合成事件做了两件事:

  • 事件委派:React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。

  • 自动绑定:React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。

    React如何阻止默认事件和冒泡事件

  1. 在react中现在return false无法阻止默认事件了(官网有说),阻止默认事件e.preventDefault ,阻止冒泡事件e.stopPropagation

  2. 阻止document(root)上的原生事件:e.nativeEvent.stopImmediatePropagation()

  3. 要想阻止所有的冒泡事件,只能通过ref获得dom节点监听,用 原生事件对象e的e.stopPropagation()去阻止冒泡(除非是本身dom节点的其他原生监听事件 比如:dom.addeventlister('click', ...))

  4. e.target.matches方法也可以用来解决事件冒泡。

    React合成事件与原生事件有什么不同

  5. 命名不一样,合成事件是小驼峰,原生是非驼峰

  6. 事件的值不同,合成事件是函数,原生是字符串

  7. react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

  8. 事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。

Hook使用规范和一些注意事项

1.只能在顶层使用,不能在循环、条件或嵌套函数中调用

https://juejin.cn/post/6939766434159394830
每个 hook 都会有一个 next 指针hook 对象之间以单向链表的形式相互串联, 同时也能发现 useState 底层依然是 useReducer ,所以总结一下初始化阶段构建链表,更新阶段按照顺序遍历之前构建好的链表,取出对应的数据信息进行渲染,(类似于数组索引值)当两次顺序不一样的时候就会造成渲染上的差异

解决方法

//这样,通过 _hooks['key'] 来查找,就无所谓前序的 Hook 出现的任何意外情况了。

const [value, setValue] = useState(0, "key2");

这样,通过 _hooks['key'] 来查找,就无所谓前序的 Hook 出现的任何意外情况了。

2.useState初始化值,只能第一次有效

import React, { useState } from 'react'

// 子组件
function Child({ userInfo }) {
    // render: 初始化 state
    // re-render: 只恢复初始化的 state 值,不会再重新设置新的值
    //            只能用 setName 修改
    const [ name, setName ] = useState(userInfo.name)

    return <div>
        <p>Child, props name: {userInfo.name}</p>
        <p>Child, state name: {name}</p>
    </div>
}

function App() {
    const [name, setName] = useState('双越')
    const userInfo = { name }

    return <div>
        <div>
            Parent &nbsp;
            <button onClick={() => setName('慕课网')}>setName</button>
        </div>
        <Child userInfo={userInfo}/>
    </div>
}

export default App

点击setName后:
只会改变props name 不会改变state name
re-render: 只恢复初始化的 state 值,不会再重新设置新的值,只能用 setName 修改
image.png

3.useEffect内部不能修改state

包括interval

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

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

    // 模拟 DidMount
    const countRef = useRef(0)
    let countA = 0;
    useEffect(() => {
        console.log('useEffect...', count)

        // 定时任务
        const timer = setInterval(() => {
            console.log('setInterval...', countRef.current)
            setCount(count + 1)

            //两种方法都可行
            // setCount(++countRef.current)
            // setCount(++countA)
        }, 1000)

        // 清除定时任务
        return () => clearTimeout(timer)
    }, []) // 依赖为 []

    // 依赖为 [] 时: re-render 不会重新执行 effect 函数
    // 没有依赖:re-render 会重新执行 effect 函数

    return <div>count: {count}</div>
}

export default UseEffectChangeState

如果依赖项是[],一直返回的都是0,如果删掉[]依赖项或者[count]的话就是正常的累加
image.png
解决方法:

  • 删掉[]依赖项
  • 依赖项为[count]
  • 用useRef或者定义一个let变量 来进行++自增累加(不建议,打破了纯函数的规则)

    4.useEffect可能出现死循环

    依赖为项为[],{}引用类型的话 就会出现死循环,因为内部是用Object.is()去比较的,
    Object.is({},{}) false

    useEffect(() => {
    xxx
    }, []) // 依赖为项为[],{}引用类型的话 就会出现死循环