【3月面经】用ChatGpt来回答前端八股文🔥

11,769 阅读42分钟

一、前言

趁着复习,总结一些最近面试遇到的问题。后续还有两篇正在写:

  • 前端常考JS编程
  • 前端常考算法题(不写了,实用性不大)
  • 都有对应的解法,基本也都是自己遇到的题。

有内推机会可以call me(武汉or杭州),VX:V798595965

有正在找工作的同学,也可以进群分享行情

二、chatGpt

  • 真的很好用,我怕我习惯后,收费太多我就离不开了。。。
  • 90%的问题,问它比问google要快得多,而且gpt支持连续对话,可以针对不懂的问题连环拷问
  • AIGC盛行的年代,不想被淘汰就往前走一步,把AI当做一个好工具,狠狠赚ta一笔

关于科学上网 & openAi注册,不懂的也可以问我,提供国外短信验证码接收服务

image.png

三、JS

3.1、Promise原理

Promise 是 JavaScript 中一种常见的异步编程解决方案,其基本思想是通过链式调用的方式使代码更具有可读性和可维护性。下面是 Promise 基本原理:

  1. Promise 的状态

Promise 对象有三种状态:pending、fulfilled 和 rejected,状态只能由 pending 转变为 fulfilled 或 rejected,状态变更后不可再变更。

  1. Promise 的基本结构

Promise 是一个类,构造函数中传入一个执行器函数,执行器函数中接收两个函数参数,分别是 resolve 和 reject,用来改变 Promise 的状态。

class Promise {
  constructor(executor) {
    this.status = 'pending'; // 初始状态
    this.result = undefined; // 成功返回的结果
    this.reason = undefined; // 失败返回的原因
    const resolve = (res) => { // 改变状态为 fulfilled
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.result = res;
      }
    }
    const reject = (err) => { // 改变状态为 rejected
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = err;
      }
    }
    executor(resolve, reject);
  }
}

执行器函数接收 resolve 和 reject 两个函数作为参数,在执行器函数中调用 resolve 函数表示成功,调用 reject 函数表示失败,从而实现状态的改变。

  1. Promise 的 then 方法

Promise 对象的 then 方法用于添加状态确定后的回调函数,then 方法接收两个函数参数,分别是 onFulfilled 和 onRejected,分别表示状态变更时的成功回调和失败回调。then 方法返回一个新的 Promise 对象。

resolvePromise的定义gpt并没有给出

class Promise {
  // ...
  then(onFulfilled, onRejected) {
    const promise2 = new Promise((resolve, reject) => {
      if (this.status === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.result);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);
      } else if (this.status === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        }, 0);

      } else if (this.status === 'pending') {
        this.resolveCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.result);
              resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
        this.rejectCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
      }
    });
    return promise2;
  }
}

then 方法中需要根据当前 Promise 对象的状态来判断是否应立即执行回调函数,如果当前 Promise 对象状态为 pending,则需要将回调函数加入回调队列,直到状态变更后才执行回调函数。执行回调函数时需要对其返回值进行处理,如果返回值为 Promise 对象,则需要等待其状态变更后再执行后续操作。

  1. Promise 的链式调用

链式调用是 Promise 的重要特性之一,可以通过链式调用的方式实现异步操作的串联。Promise 的 then 方法返回一个新的 Promise 对象,因此可以通过链式调用的方式,将多个异步操作串联执行。

asyncFunc1()
  .then(res1 => {
    // dosomething
    return asyncFunc2(res1)
  })
  .then(res2 => {
    // dosomething
    return asyncFunc3(res2)
  })
  .then(res3 => {
    // dosomething
  })

在链式调用中,每个 then 方法会返回一个新的 Promise 对象,这个 Promise 对象的状态会根据其上一个 then 的返回值(可能是图中的 x,也可能是一个新的 Promise 对象)而决定。

图中的? 不知道gpt从哪里抄的,hhh

  1. 错误处理

在 Promise 中,错误处理可以通过传递一个可选的 error 函数来实现。在使用 then 方法时,可以定义 onFulfilled 和 onRejected 两个回调函数,onRejected 用于捕获错误,因此在 then 方法后通常都会调用一个 catch 方法来统一处理错误。

asyncFunc()
  .then(res => {
    // do something
  })
  .catch(err => {
    // handle error
  })

以上就是 Promise 的基本原理,Promise 的链式调用和异步操作的串联执行使得代码更加可读性和可维护性,使异步编程不再繁琐。

3.2、ES6继承的底层实现原理是什么

在 ES6 中,使用 classextends 关键字实现继承,底层是基于原型的继承实现的。一个子类继承父类时,实际上是在子类的原型对象上创建了一个父类的实例。这个实例包含了父类中的所有属性和方法。这样,子类就可以通过原型链来访问父类中的方法和属性。

具体来说,ES6 中的继承是这样实现的:

  1. 在子类构造函数中调用 super() 方法,相当于调用了父类的构造函数并返回了一个实例对象。
  2. 子类的原型对象即为这个实例对象,此时子类就可以在原型对象上访问父类的方法和属性了。
  3. 在子类原型对象上添加新的方法和属性,这些方法和属性会在子类实例这一级别被访问到。 例如:
class Parent {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

class Child extends Parent {
  constructor(name, type) {
    super(name);
    this.type = type;
  }
  getType() {
    return this.type;
  }
}

let child = new Child('foo', 'bar');
console.log(child.getName());  // 'foo'
console.log(child.getType());  // 'bar'

在上面的例子中,Child 类继承了 Parent 类,并通过 super() 方法调用了父类的构造函数。在这个过程中,Child 类创建了一个 Parent 类的实例对象,这个实例对象就包含了 Parent 类的所有属性和方法。然后,Child 类的原型对象就指向了这个实例对象,从而实现了继承。

因此,ES6 中的继承实际上是基于原型和构造函数来实现的。在子类的原型对象上创建了父类的实例,子类实例可以通过原型链访问父类的方法和属性,同时也可以在子类原型对象中添加新的方法和属性。

3.3、for forEach map哪个性能最好

一般来说,在处理数组时,forEachmap 都比 for 循环更简洁、易读。但是性能上存在差异,具体取决于具体的使用情况和需求,不一定每次都是相同的。

对于大规模数组的处理,for 循环的性能通常比 forEachmap 更好。这是因为 for 循环遍历数组时不需要创建新的函数作用域,forEachmap 都需要额外创建函数作用域,同时 map 还需要创建新数组,这些都会带来一定的性能开销。

一般来说,map 不仅仅能够遍历数组,还能够修改数组元素并返回新的数组,往往会比 forEach 在性能上稍微差一些。而 forEach 的遍历操作只是对数组进行操作,一般只用于遍历数组中的元素并执行一些操作,不会改变数组本身,因此它比 map 在性能上稍微好一点。

综上所述,在不同场景和需求下,三者的性能情况都可能是不同的。在遍历和处理数组时,应该根据实际情况,权衡其可读性和性能优劣,选择最合适的方法。

追问:for forEach map搭配async await 都能实现任务的依次输出吗?各位大哥试试看 

// for
async function loop(arr) {
  for (let i = 0; i < arr.length; i++) {
    let result = await asyncFunction(arr[i]);
    console.log(result);
  }
}
loop([1, 2, 3]);
async function asyncFunction(value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value * 2), 200);
  });
}

// forEach
async function loop(arr) {
  arr.forEach(async (value) => {
    let result = await asyncFunction(value);
    console.log(result);
  });
}
loop([1, 2, 3]);
async function asyncFunction(value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value * 2), 200);
  });
}

3.4、详细说说前端模块化

前端模块化是指将复杂的前端代码按照一定规则和标准划分为相对独立和可重用的模块,以便管理和维护。模块化使得代码更加易于理解、调试、维护和扩展,同时也促进了代码的重用和团队协作。

前端模块化的发展主要经历了以下几个阶段:

  1. 命名空间: 最初,前端代码的组织方式是通过定义全局变量或对象来实现,这种方式容易造成变量名冲突和代码耦合,导致不易于管理和维护。为了解决命名冲突和代码耦合的问题,开发者通常会通过命名空间的方式来实现代码组织。命名空间是一个全局的对象,用于封装相关的函数和变量。通过这种方式,可以将代码划分为相对独立的模块,但依然存在命名冲突和依赖管理等问题。

  2. CommonJS: 是最常见的前端模块化规范之一,其主要特点是以 “require()” 加载模块和以 “module.exports” 导出模块,同步加载。它适用于服务器端的 JavaScript,同时也被很多前端框架所采用,比如 Node.js 和 Browserify 等。

CommonJS 能够解决命名冲突和模块依赖的问题,同时可以实现模块的复用。但在浏览器端,需要通过打包工具将模块打包成一个文件,然后在页面中引入,这使得 CommonJS 在浏览器端的使用不够方便。

优点:在服务器端的 JavaScript 应用广泛,可以实现模块的复用和依赖管理。

缺点:需要使用打包工具将模块打包成一个文件,使用不太方便,对于浏览器端使用存在局限性。

  1. AMD: (Asynchronous Module Definition)也是一种前端模块化规范,与 CommonJS 相比,它更适合浏览器端使用。AMD采用异步模块加载的方式,可以在模块依赖关系确定后才加载模块,从而提高页面加载速度和性能。

AMD 使用 define() 函数定义模块,使用 require() 来加载模块,并且支持异步加载和非同步加载两种方式。 RequireJS 是 AMD 的一种实现方式。

优点:可以实现异步加载模块,提高页面加载速度和性能。 缺点:需要使用 require.js 等模块加载器来实现,使用稍有些复杂。

  1. ES Modules: 是 ECMAScript 6 中引入的原生模块化规范。它的主要特点是使用 import 和 export 语法来导入和导出模块,可以在编译时静态分析模块的依赖关系,实现高效的资源管理和代码组织。

ES Modules 可以直接在现代浏览器中使用,无需打包工具的支持,同时与 CommonJS 和 AMD 之间也可以进行互操作。

优点:原生支持,无需第三方库,使用方便,可以静态分析模块依赖,使代码组织更清晰。

缺点:一些浏览器不支持,需要使用构建工具进行打包。

总之,前端模块化可以提高代码的可维护性和可读性,利于团队协作和代码重用。目前,CommonJS, AMD 和 ES Modules 都是主要的前端模块化规范,开发者可以根据具体的场景和需求来选择合适的规范和工具。

CMD不要了?

追问:CommonJS能否像ES Modules一样做Tree Shaking?

  • 通过让构建工具执行装换,将 CommonJS 模块转换为 ES Modules 格式,前提是该模块的导出内容是静态可知的。
  • 通过 Rollup.js 这种构建工具,它使用了一个基于模块依赖关系的体系结构,可以将模块打包成一个纯粹的 ES Module 格式的 JavaScript 文件,再进行 Tree Shaking 处理,输出仅包含应用中实际使用的 JavaScript 代码的新文件。

3.5 jsbridge 原理

JSBridge 是一种在 WebView 中使用的 JavaScript 与 Native 代码交互的桥接技术。具体来说,JSBridge 的原理如下:

  1. WebView 加载一个 HTML 页面,其中包含 JavaScript 程序和 Native 程序。
  2. Native 为 WebView 注入一段 JSBridge 的 JavaScript 代码,并且把当前 WebView 实例保存到一个全局变量中。这段 JavaScript 代码提供了 WebView 调用 Native 方法的接口。
  3. JSBridge JavaScript 程序想要调用 Native 中的方法,首先调用注入的 JavaScript 代码,把参数传递给该代码。
  4. 注入的 JavaScript 代码通过保存的 WebView 实例中的 JavaScriptInterface,将数据传递给 Native 代码。
  5. Native 代码接收到数据后,根据指定的方法进行处理,并将需要返回给 JavaScript 的数据封装成字符串格式返回。
  6. JavaScript 代码接收到 Native 返回的数据后,再将数据传递给原始的 JavaScript 方法。

JSBridge 就是利用 WebView 提供的 JavaScript 和 Native 交互的接口,将 JavaScript 的调用传递到 Native 方法中,再将 Native 方法的返回值传递回 JavaScript 中。通过这种方式,实现了 JavaScript 和 Native 之间的互通。

四、浏览器

4.1、301和302的区别

301 和 302 都是 HTTP 的状态码之一,表示重定向。它们的主要区别在于重定向的永久性和临时性。

  • 301: 永久性跳转。当访问某个网址时,服务器返回 301 状态码,则浏览器会自动把当前的地址缓存下来,下一次访问相同的 URL 时,就会直接跳转到缓存的结果页。如果需要更改目标地址,必须要使用户清空浏览器缓存才能生效。

  • 302: 临时性跳转。当访问某个网址时,服务器返回 302 状态码,则浏览器会自动暂时不跳转,而是根据响应头部的 “Location” 信息,再发送一次请求给 Location 指定的地址,来获取新的资源。所以当访问相同的 URL 时,浏览器总是会向服务器发送新的请求。302 主要应用于临时性的、短期的、不经常被访问的页面跳转。

在实际开发中,一般建议使用 301 来进行重定向,因为使用 301 会让搜索引擎将权重自动转移给新的网址。而 302 虽然使用较为广泛,但一般只用于短期重定向。

4.2、http1.1,http2,http3对比

以下是 HTTP/1.1、HTTP/2 和 HTTP/3 的一些主要区别和优势:

  1. HTTP/1.1   - 采用文本协议,数据传输时需要解析HTTP头部信息,耗费带宽和CPU。   - 限制了浏览器并行请求同一域名的最大数量(在同一时间内只能发送有限个数的请求)。   - 队头堵塞,需要等待前面的请求响应完成之后才能继续发送下一个请求。   - 不支持请求/响应的多路复用,需要建立多次TCP连接才能同时处理多个请求。
  2. HTTP/2   - 二进制分帧层解决了 HTTP/1.1 协议中文本解析及麻烦的整个请求顺序问题。   - 在一个TCP连接上支持全双工通信,允许多个请求并发处理,减少了网络延迟时间。   - 针对队头堵塞问题,HTTP/2 引入了流的概念,将一个连接拆分成多个流,实现流的独立处理。   - 多路复用机制允许在一个连接上并行请求/响应多个资源,提高了网络性能。
  3. HTTP/3   - HTTP/3 使用 QUIC 作为底层传输协议,旨在通过减少网络延迟时间来提高性能。   - 将 TLS 插入到 QUIC 层中,实现了零轮延迟(Zero Round Trip Time)连接建立,客户端和服务器只需交换一条消息即可完成TLS握手并建立一个加密连接。   - 同时支持多路请求和响应,允许在同一连接上处理多个请求和响应。   - 支持更优的流控和拥塞控制算法,适合高质量网络的应用场景。

追问:HTTP/3中,UDP丢包问题如何解决

HTTP/3 采用的是基于 UDP 的 QUIC 协议,在传输数据时可能会出现 UDP 丢包的情况。为了应对这种情况,HTTP/3 采取了多种措施,包括无连接建立、恢复机制、流量控制等。其中,QUIC 提供了自己的机制来处理丢包和重传,通过唯一的 ID 和 Cookie 对数据包进行辨别和管理,保证数据包的实时传输和准确性。同时,QUIC 还实现了自适应流量控制,防止过度拥塞,保证数据传输的稳定性。这些措施共同组成了 HTTP/3 处理 UDP 丢包的解决方案。

4.3、输入URL到页面渲染完成的过程

  1. URL解析:浏览器会先对输入的URL进行解析,包括检查URL的格式、协议、主机名等,确定要访问的站点地址。
  2. DNS解析:浏览器会向本地 DNS 服务器请求解析出该站点的 IP 地址。
  3. 建立连接:浏览器使用 TCP 协议与服务器建立连接,服务器回应确认建立连接。
  4. 发送请求:浏览器向服务器发送 HTTP 请求,请求的内容包括请求头和请求体。
  5. 服务器处理请求:服务器接收到请求后,会根据请求的内容进行处理,确定要返回的内容。
  6. 返回响应:服务器把处理好的内容通过 HTTP 响应报文返回给浏览器,响应的内容包括响应头和响应体。
  7. 浏览器渲染:浏览器拿到返回的响应后,对响应内容进行解析,构建DOM树、CSSOM树和 JavaScript 内存模型,然后把它们组合成渲染树,最后把渲染树展示给用户,页面呈现完成。

值得注意的是,上述过程并不是一条线性的路径,它可能会发生很多个并发的操作和优化,比如浏览器可能会在接收到响应数据的同时就开始解析渲染,以提高页面加载速度。

追问1:什么是options请求?如何减少发送次数?

在CORS(跨源资源共享)中,OPTIONS请求被用作跨域请求的预检测,用于查询服务器所支持的CORS安全头和方法,并确认客户端请求是否被服务器允许。通过CORS中的OPTIONS请求,可以让浏览器在发送实际请求前,对服务端进行预检测。

减少发送次数:

Access-Control-Max-Age: 3600

追问2:CSS3动画 会引起回流和重绘吗?

CSS3动画在浏览器中的实现通常是通过GPU加速的方式来提高性能,因此它不会引起回流,但仍然会引起重绘。

重绘是指元素的样式发生变化,需要重新绘制其在页面上的部分或全部区域。在CSS3动画中,改变元素的CSS属性会触发重绘操作,因此CSS3动画仍然会造成一定的性能开销。

然而,相比于回流(reflow)来说,重绘的性能开销要小得多。回流指的是页面的排版渲染发生变化,需要重新计算元素的位置和大小等信息,并重新进行布局。回流是非常消耗性能的操作,因为它会导致整个页面的重新渲染,因此应该尽量避免触发回流。

对于页面中需要进行动画效果的元素,应该尽量采用CSS3动画来实现,并且避免频繁地更改元素的样式属性,以减少重绘的开销。同时,也应该注意控制页面中动画元素的数量和复杂度,以避免过多的开销影响页面性能。

追问3:讲讲浏览器渲染过程中的合成层

浏览器的渲染引擎有一个渲染管线,负责将HTML、CSS、JavaScript等资源组合成最终的渲染结果。在这个过程中,会生成若干个图层(Layer),用于优化页面的渲染性能。

合成层是其中的一种图层,它具有以下特点:

  1. 独立绘制:合成层可以单独绘制,不受其他元素的影响,因此可以独立进行GPU加速处理。
  2. 减小重绘区域:合成层可以包含多个元素,当其中某个元素发生变化时,只需要对该元素进行重绘,不会影响其他元素的渲染,从而减小了重绘的范围。
  3. 提高渲染性能:利用GPU加速处理,可以提高渲染性能,尤其在动画等场景下,更能体现其优势。

当页面中的某个元素需要被创建为合成层时,可以使用以下方法:

  1. 使用will-change属性:will-change属性用于告诉浏览器该元素将要发生哪些变化,可以让浏览器提前创建合成层,从而优化渲染性能。
.element {
  will-change: transform;
}

上述CSS代码可以让浏览器将.element元素创建为一个合成层,从而优化transform属性的渲染性能。

  1. 使用CSS transform、opacity属性:CSS中的transform和opacity属性也会触发合成层创建,因此可以通过使用这些属性来优化渲染性能。

  2. 使用JavaScript:通过调用API(例如createImageBitmap)等方法来生成位图,也可以创建合成层。

需要注意的是,虽然合成层可以优化页面的渲染性能,但也不是越多越好,因为创建合成层也会有一定的开销,过多的合成层会导致GPU显存不足、内存泄漏等问题,因此需要合理使用。

五、Vue

5.1、项目中多个.vue文件生成了多少个vue实例

一开始gpt没给出解释,我以为它忽悠我😂

image.png

准确的说

所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。 - Vue官网

严谨的说:

  • 一个单页应用就是一个 vue 的实例
  • 每个自定义组件就是一个 vueComponent 实例

5.2 vue2中v-for为什么不推荐和v-if一起用?vue3又可以了

在 Vue 2 中,v-for 和 v-if 的优先级问题是造成性能问题的主要原因之一。因为 v-for 是在渲染时将数组所有元素都渲染出来,而 v-if 则是根据条件决定是否渲染当前元素。如果在一个 v-for 循环内使用 v-if,就需要对每个元素进行判断,判断是否满足条件,造成了一定的计算资源浪费。

当 v-for 和 v-if 连用时,Vue 会先对所有数据进行渲染,然后再根据 v-if 中的条件进行过滤,这意味着在一些情况下,许多不需要渲染的元素仍然会被渲染出来,导致性能不佳。

而在 Vue 3 中,由于引入了新的编译优化技术,重构了虚拟 DOM 的实现方式,可以更好地利用现代浏览器的功能,因此 v-for 和 v-if 一起使用的性能问题已经得到了优化,Vue 3 中已经可以放心使用。

在实际开发中,如果需要在一个列表中仅显示某些符合特定条件的项,可以在数据层面进行过滤,或者使用计算属性来完成过滤操作,而不是将 v-for 和 v-if 一起使用。这样既可以避免性能问题,也可以更好地把逻辑和视图分离。

5.3 vue provide / inject 实现原理

Vue 的 provide / inject 是一种祖先组件向子孙组件传递数据的机制。可以理解为 Vue 中的一种依赖注入方式,父组件通过 provide 选项提供数据,之后任意后代组件都可以通过 inject 选项来使用这些数据。 这个特性在一些场合下特别方便,例如在 Vue 应用中使用某些第三方库,此时我们可以在应用的根级注入,并通过 inject 在其他组件中使用。 

provide 和 inject 的实现原理是通过 Vue 的上下文机制实现的,每个组件在实例化时都会创建一个上下文,这个上下文可以在组件树中向上查找提供的数据,也可以向下注入数据。当一个组件需要使用数据时,它会在上下文中查找 inject 对应的键值,如果查找不到,会一直上溯到整个组件树的根实例,如果仍然没有找到,就会返回默认值。

在实现过程中,Vue 会在组件内部创建一个 provide 对象,这个对象中包含了绑定数据的键值对。在 Vue 实例化时,会根据 provide 中的数据创建一个新的上下文对象,并在组件树建立一个父子关系。当子组件需要获取数据时,会先在自己的上下文查找 inject 绑定的键值,如果找到了就直接返回,如果没有找到就向上一级上下文查找。这个查找过程会一直递归到根组件为止,如果还没有找到就返回默认值。正是由于上下文机制的存在,使得 provide / inject 能够顺利地向组件传递数据。

需要注意的是,provide 和 inject 最好在开发时就规划好,不要动态更改或者依赖于生命周期钩子函数,这样可以保证数据在整个组件树中的传递正确与可靠。另外,为了避免出现数据混乱或者冲突,推荐在提供的数据中使用复杂类型或者对象时,将数据进行浅拷贝或者深拷贝。

六、React

6.1、react 受控组件和非受控组件优缺点

React 组件可分为受控组件和非受控组件,它们各自具有不同的特性和用途。在选择受控组件还是非受控组件时,需要根据具体的场景和需求进行权衡和选择。

受控组件

受控组件是指其属性值和状态受应用程序的状态管理,并受 React 的渲染控制的组件。其通常采用了事件处理器来控制输入元素的渲染和属性更新,比如 onChange 事件用于监听元素的值变化,并依赖于外部 state 或 props 中的数据来更新输入元素的值。

  • 优点:
    • 具有强类型和严格的数据流,数据流动更可控,使用更加安全稳定。
    • 可以方便地获取当前元素的状态值,便于进行表单校验和数据提交。
    • 完全由 React 控制和管理,更加可靠和可预测,并且支持更好的测试和调试。
  • 缺点:
    • 编码难度较大,需要编写大量的事件处理程序代码,对开发效率会有一定的影响。
    • 每个表单元素需要维护自己的 state,这会占用一定的内存空间,并且可能会导致项目性能下降。
  • 例子:
function ControlledInput() {
  const [value, setValue] = useState('');
  function handleChange(event) {
    setValue(event.target.value);
  }
  return (
    <input
      type="text"
      value={value}
      onChange={handleChange}
    />
  );
}

非受控组件

非受控组件指的是其状态不受管理,通常输入元素的值由 DOM 直接控制,而不是由 React 管理。这种组件主要靠原生的 DOM 事件和 callback 来控制用户输入的数据,其数据流与 state 和 props 主要是单向的。

  • 优点:
    • 简单易用,不需要管理组件的 state,不会占用额外的内存空间。
    • 可以使用更加简洁和直观的方法和方式来处理表单数据,对开发效率提高有一定的帮助。
    • 适用于比较简单的场景和应用,对于表单元素数量较多的复杂应用,可以有效降低开发难度。
  • 缺点:
    • 缺乏强类型和严格的数据流,数据的流动更易出现问题,使用不够安全和稳定。
    • 缺乏 React 的控制和管理,可能不可靠或不可预测,同时也不太容易进行单元测试和集成测试。
function UncontrolledInput() {
  const inputRef = useRef(null);
  function handleClick() {
    alert(inputRef.current.value);
  }
  return (
    <>
      <input type='text' ref={inputRef} />
      <button onClick={handleClick}>Alert the value!</button>
    </>
  );
}

6.2、useEffect实现原理

useEffect 是 React Hooks 提供的一个副作用挂载钩子,它可以在 React 组件挂载、更新和卸载时执行各种副作用操作,比如发送网络请求、订阅事件、修改 DOM 等。

在技术层面上,useEffect 实现的原理主要是使用了 React Fiber 架构中的异步调度机制。当组件 mount、update 或 unmount 时,React 将会使用 time slicing 和 concurrent rendering 技术来尽可能地将工作分摊到多个帧中,从而避免长时间阻塞主线程。

具体而言,useEffect 采用了以下几个步骤来实现副作用的调度和执行:

  1. 首先,React 会在渲染阶段(Render Phase)中收集组件内部所有的 useEffect 副作用函数,然后将其存放到组件的 Effect list 中。
  2. 在任务交由 React Scheduler 处理调度的时候,React 会根据内部的调度策略来决定是否可以执行该 Effect。
  3. 如果可执行,React 会将该 Effect 加入到一个 effect 队列中,然后将该组件置于 Dirty 队列中,以在下一次 update 或者 commit 阶段中重新处理该组件的 Effect。
  4. 当 React 处理该组件的 Effect 时,会按照各个 Effect 的定义顺序来省略或执行每一个 Effect,同时会将副作用函数的返回值存储到 Component instance 里面,以便在下次更新时使用。

需要注意的是,由于 useEffect 可能会触发更新和重新渲染,因此可能存在性能问题和潜在的逻辑错误。为了避免这种问题,我们应该尽可能减少副作用的操作和依赖项,只在必须时才使用 useEffect,并且要合理设置到底是触发 update 还是 unmount 时执行 Effect。同时,React 提供了一些优化和 API,比如 useMemouseCallback、React.memo 等,来应对处理大量数据、避免重复渲染等常见问题。

6.3、class类组件能享受到fiber带来的性能提升吗?

是的,Class 类组件也能享受到 Fiber 带来的性能提升。Fiber 的主要目标之一就是提高组件的渲染性能,包括 Class 类组件和 Function 类组件。具体来说,Fiber 通过实现增量渲染,又称为异步渲染,来提高 React 应用的整体性能,并且可以避免组件的阻塞和界面卡顿等问题。

在 React Fiber 中,所有的组件(包括 Class 类组件和 Function 类组件)都会被转换成 Fiber 节点。Fiber 节点是 React 用来描述组件渲染过程的一种数据结构,它可以在不同的渲染阶段中保存和更新组件的状态和属性。

为了支持增量渲染,React Fiber 在调度更新时引入了异步优先级的概念,可以根据组件的优先级来安排更新的顺序,以此来提高整体性能。这个机制在 Class 类组件中同样适用,因此 Class 类组件同样能够享受到 Fiber 带来的性能提升。

总之,虽然 Fiber 的主要目标是提高 Function 类组件的渲染性能,但它同样适用于 Class 类组件,能够带来明显的性能提升。

七、其他

7.1、webpack优化

Webpack 是一个强大的前端打包工具,但是在项目体积较大的情况下,其构建速度和输出文件大小可能会对开发者造成一定的困扰。因此,下面介绍一些 Webpack 的优化技巧。

  1. 优化 Loader 的使用
  • 使用 includeexclude,明确指定 Loader 转换的文件范围,避免转换无关文件。
  • 使用 cacheDirectory,开启 Loader 的缓存功能,减少编译时间。
  1. 优化插件的使用
  • 使用 tree shakingScope Hoisting,只打包使用的模块,减少打包文件大小。
  • 使用 webpack-bundle-analyzer,可视化分析打包文件体积,找到文件过大的原因。
  1. 缩小解析范围
  • 使用 resolve.alias,将常用模块的路径映射到一个短的别名,缩小解析范围。
  • 使用 resolve.extensions,指定不需要写的文件扩展名,缩小解析范围。
  • 使用 resolve.modules,指定模块搜索路径的顺序,缩小解析范围。
  1. 开启多线程打包
  • 使用 happypack,将 Loader 的解析过程通过多线程来处理,加快编译速度。
  1. 使用 DLLPlugin
  • 使用 DLLPluginDLLReferencePlugin,将基础库(如 React、Vue 等)从主应用中分离出来,经过预编译后缓存,加快二次构建速度。
  1. 代码分离
  • 使用 code splitting,将代码拆分为多个文件,可以根据业务逻辑、路由等分为多个块,实现按需加载,减少首屏加载时间。

除此之外,还可以针对具体场景进行优化,例如针对网络情况合理使用文件压缩、静态资源 CDN 加速等等。这些优化策略都可以结合实际项目需求来选择使用,提高 Webpack 的打包构建速度和性能。

7.2、如何保证Node Bff稳定性

为了保证 Node.js BFF(Backend for Frontend)的稳定性,可以采取以下方法:

  1. 异常处理:在 Node.js 应用程序中处理错误十分重要。这可以通过不同的方式实现,如 try-catch、错误处理中间件、Promise rejection 等。一定要对错误进行适当处理,以保持系统的稳定性。

  2. 日志记录:生产环境下要使用日志记录工具,可以针对不同的级别进行记录,比如调试、信息、警告和错误。使用好的日志记录工具能够帮助开发团队发现并解决潜在问题,提高问题解决的速度和效率。主要通过traceId查找链路。

  3. 监控和告警:在生产环境中,需要使用监控工具对 BFF 服务进行监测。在出现问题时,能够快速发现并向相关人员发出报警通知,以便及时处理。可以使用一些监控工具如 Prometheus、Grafana 等进行监控。

  4. 代码质量:代码质量往往直接影响系统的稳定性。为了保证 BFF 的质量,应该对代码进行规范、格式化和注释,使用合理的设计模式和架构,通过单元测试和集成测试进行验证和检验。

  5. 性能优化:性能问题是导致 BFF 稳定性问题的重要因素之一。在前期规划时要合理规划和设计 API ,遵循 RESTful 接口的标准,减少 API 响应时间和降低系统的响应延迟。

综上所述,保证 Node.js BFF 的稳定性需要多方面进行考虑与维护,包括错误处理、日志记录、监控和告警、代码质量和性能优化等方面。只有综合考虑,才能确保 BFF 服务的稳定性。

7.3、微前端相关

qiankun的优缺点

优点:

  1. 微服务化:qiankun 的微服务化方案,可以将多个独立的应用整合成一个整体,并通过主应用进行路由分发和状态管理,提高应用整体的可维护性和可扩展性。
  2. 稳定性:qiankun 的应用隔离和独立运行机制,能够保证各个独立的子应用之间的互不干扰,保证了应用的稳定性和安全性。
  3. 模块化:qiankun 的微前端框架可实现不同应用间的模块化管理,优化代码复用与维护。
  4. 低耦合:不同应用之间的通信采用事件、自定义函数组件、props等方式,减少应用间的耦合。主应用可以抽象出通用组件和方法,其他应用可以直接调用,降低了应用间的耦合度。

缺点:

  1. 学习曲线:qiankun 框架采用了 Vue、React、Angular 等多种前端框架,使用前需要完成相关框架的学习。
  2. 部署需求高:由于 qiankun 的微前端架构,需要更高的部署要求和部署成本。包括要求每个应用都能独立运行和部署,需要一定的部署技术和经验。
  3. 被使用限制:qiankun 对前端框架版本和技术栈有一定要求,需要能够使用 qiankun 的前端框架才能进行使用。

qiankun 子应用加载过程

qiankun 子应用加载过程分为两部分:主应用加载子应用、子应用渲染过程。

主应用加载子应用的过程:

  1. 主应用配置子应用:在主应用的配置文件中定义子应用的基本信息,如名称、路由、入口、应用类型等。
  2. 主应用启动 qiankun 插件:通过调用 start 函数启动 qiankun 插件,并加载所配置的子应用信息。
  3. 加载子应用:在 qiankun 插件的生命周期中,执行 loadMicroApp 函数,异步加载子应用代码。加载包括子应用的代码、样式和资源,同时在加载完代码后执行子应用的 bootstrap 生命周期钩子函数。
  4. 路由切换:切换子应用路由,通过主应用渲染容器控制路由的切换,将子应用容器插入到主应用的路由中。

子应用渲染过程:

  1. bootstrap 生命周期:子应用的 bootstrap 生命周期主要用于子应用独立运行时的初始化操作,如对子应用的全局环境初始化、基座应用与应用间通讯方式的初始化等。
  2. mount 生命周期:子应用的 mount 生命周期主要用于渲染子应用,及子应用的静态资源和组件的加载。
  3. update 生命周期:子应用的 update 生命周期与 React 的 componentWillReceiveProps、Vue 的 beforeUpdate 等钩子函数类似,用于在子应用发生更新时进行相应的操作。
  4. unmount 生命周期:子应用的 unmount 生命周期用于子应用从主应用中卸载时进行一些清理工作,如清除定时器、取消监听等操作。

在以上的生命周期函数中,主要用到的是 bootstrap 和 mount 生命周期,这两个生命周期可以控制子应用的加载和渲染过程,保证子应用的正常运行。

子应用动态加载阶段:

在用户访问子应用时,主应用会根据子应用的entry信息,动态地加载其JS代码,并创建一个script标签引入子应用的JS文件。这个script标签的模板大概长这样: <script src="./{entry}?{query}"></script> 其中,{entry}就是子应用的入口文件,{query}可以用于传递一些额外的参数。

同时,在这个script标签中,我们会为该子应用建立独立的沙箱环境,确保子应用的运行环境与主应用相互隔离,互相不会产生影响。

qiankun 主应用和子应用路由是如何管理的

在 qiankun 中,主应用和子应用的路由是通过 registerMicroApps 和 start 两个 API 实现的。主应用通过 registerMicroApps 函数来注册子应用,并设置子应用的路由信息。而在 start 函数中,主应用会启动路由监听,监控路由变化,然后根据当前的路由切换子应用。在子应用中,路由一般由子应用自己管理。

具体来说,主应用的路由管理主要包括以下几个步骤:

  1. 配置子应用信息:在主应用中,需要配置子应用的基本信息,包括名称、路由、入口等。
  2. 注册子应用:通过调用 registerMicroApps API,将子应用注册到主应用中。
  3. 设置路由信息:在注册子应用的过程中,需要设置子应用的路由信息,包括前缀、激活函数等。
  4. 监听路由变化:调用 start 函数来启动主应用的路由控制器,此时主应用会开始监听路由变化,并自动加载和卸载子应用。

而在子应用中,路由的管理与普通的单页应用相同,一般使用 React-Router、Vue-Router 等路由管理工具。子应用没有控制路由的权利,只需要将路由的定义暴露给主应用即可,主应用负责控制路由,选择合适的子应用进行渲染。

总之,qiankun 通过主应用的路由来管理子应用的路由,实现应用间的无缝嵌套。同时,子应用可以采用自己喜欢的路由解决方案,不会受到主应用的限制。

qiankun CSS隔离方案是怎么做的

gpt回答还是shadowDOM,问它最新版本的方案,它才说下面的

最新版本的 qiankun(2.x及以上)已经放弃了之前的样式隔离方案,采用了 CSS 变量方案来实现子应用之间的样式隔离。

具体来说,qiankun 会为每个子应用创建一个独立的样式变量作用域(scope),该作用域会将该子应用的样式代码转换为自定义属性(CSS 变量)并注入到 HTML 中。该作用域只作用于其所管理的子应用内,不会对其他子应用和主应用产生影响。

在子应用中,我们可以使用 CSS 变量来定义样式属性,这些属性会被解析为自定义属性,从而避免了全局的样式污染问题。同时,我们也可以使用全局的 CSS 变量来覆盖子应用的样式属性。

这种方案的好处是,它不需要使用 shadowDOM,也不需要进行样式前缀的处理,而且支持动态修改全局变量。而缺点则是需要所有子应用中的样式都使用 CSS 变量进行定义,推荐使用 CSS 预处理器来实现这一点。

总之,通过使用 CSS 变量方案,qiankun 实现了更加灵活和高效的样式隔离,该方案已经成为 qiankun 的默认实现,并且能够满足大部分场景的需要。

如何解决qiankun内存占有率高的问题

在使用qiankun微前端框架时,出现内存占用率高的问题很常见。这主要是由于每个子应用都有自己的独立沙箱环境,会导致大量的内存占用。为了解决这个问题,我们可以考虑以下几种方案:

  1. 优化子应用的资源文件

对于子应用的资源文件,我们需要尽量缩减它们的体积。不能将所有的资源都打包在一起,最好的方式是将可复用的代码抽离为公共模块,使得不同的子应用共享公共模块,同时减少代码的冗余。

  1. 使用动态加载的方式

在qiankun框架中,我们可以使用动态加载的方式加载子应用的代码和资源,以减少内存占用。通过动态加载,我们可以按需加载子应用的资源文件,在不需要使用子应用时释放内存,从而减少内存占用。

  1. 使用keep-alive技术

可以使用keep-alive技术缓存组件实例,以便在需要时快速恢复组件状态。这样我们就可以减少重复渲染的次数,从而减少内存占用。

  1. 使用路由切换页面

在qiankun框架中,可以通过路由切换页面的方式来切换子应用。这种方式可以避免在同一页面中同时加载多个子应用的情况,从而降低内存占用。

针对不同的场景,我们可以采用不同的解决方案。通过以上的方式,我们可以有效地降低qiankun框架所占用的内存,提高应用的性能,同时提升用户的体验。

如何优化qiankun子应用的加载速度

  1. 优化代码打包和编译:使用代码分割和懒加载等技术,减少不必要的资源和文件加载量。可以使用打包工具(如 Webpack、Rollup等),配置代码分割、tree shaking、code splitting 等优化选项,以减少代码体积和加载时间。
  2. 优化资源加载:在加载资源时,可以通过设置缓存策略、离线缓存、资源预加载等优化手段,以优化子应用加载速度。例如,在主应用加载时就提前加载子应用需要的资源,在子应用启动时就可以快速加载资源,从而提高应用的加载速度。
  3. 基础依赖项/eslint缩小打包体积:如果子应用加载的依赖项过多,可能会影响加载速度。建议只引入必要的依赖项,并尽可能采用现代化的技术和框架,以减少不必要的资源和代码体积。在代码开发过程中,也要注意使用 eslint 等工具对代码进行格式规范和优化,以减少代码冗余和错误。
  4. 将公共资源提取到主应用中:将常用的公共资源,如 React、Vue 等提前提取到主应用中,在子应用启动时直接使用主应用中的资源,减少子应用的代码体积和依赖项加载,从而加速子应用的启动和加载速度。
  5. 使用 CDN:使用 CDN 加速,可以通过将资源分布到多个服务器,使用最近的服务器来响应请求,以达到加速网络请求的目的。
  6. 可以考虑使用 qiankun 提供的 preFetch 预加载子应用的功能,将子应用的资源预加载到浏览器中,可以大幅提高子应用的启动速度和性能表现,减少空载入时间和内存占用。

综上所述,通过代码优化、资源优化、依赖项优化、公共资源提取和使用 CDN 等手段,可以优化 qiankun 子应用的加载速度,提高应用的用户体验和性能。

qiankun的通信方案有哪些

在实际应用中,常用的 qiankun 子应用通信方案主要有 props、全局状态管理工具和 postMessage,具体应用根据实际情况而定。下面是各种方案的使用场景和优缺点:

  1. props:适用于父子之间传递数据,数据量较小且变化不频繁的情况,使用方便且易于维护,但不适用于应用之间的通信。

  2. 全局状态管理工具:适用于数据共享较多、需要交互较为复杂的场景,如多个子应用共享一个用户身份认证信息或购物车信息等。使用全局状态管理工具可以将状态数据存储在全局状态容器中,实现方便快捷的状态共享,但是需要注意状态与应用之间的正确划分。

  3. postMessage:适用于需要在不同的应用之间传输少量数据,且需要实现双向通信的情况。通过 postMessage 实现跨应用之间的数据通信,具有较高的灵活性和可扩展性,但需要注意数据格式和安全性等问题。

综上所述,qiankun 的通信方案有多种,可以根据具体场景和需求选择合适的通信方式,以实现高效、稳定和灵活的应用通信。

qiankun的js沙箱实现方案

  1. 隔离全局变量和属性,确保不同子应用之间的 js 代码不会相互干扰。
  2. 隔离 DOM 环境,确保不同子应用之间的 DOM 操作不会相互干扰。
  3. 拦截全局方法和事件,确保不同子应用之间的方法调用和事件触发不会相互干扰。
  4. 隔离 http 请求和 websocket 连接,确保不同子应用之间的网络请求不会相互干扰。
  5. 禁止不安全代码的执行,确保沙箱环境的安全性。

js 沙箱主要是通过创建一个独立的子上下文,将子应用所需要的全局变量和属性注入到这个子上下文中,从而实现隔离。同时,沙箱还可以通过动态代理和拦截的方式,对全局方法和事件进行拦截和代理,从而确保子应用之间的沟通和协作是安全可靠的。

八、往期回顾

image.png

本文正在参加「金石计划」