LOADING

高频题整理

2024/9/19

事件循环(运行机制)

事件循环是浏览器渲染主线程的工作方式,每次循环从消息队列中取出第⼀个任务执行,当然代码在执行过程中,会遇到⼀些无法立即处理的任务(网络请求,I/O 操作,计时器等),主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加⼊到消息队列的末尾排队,等待主线程调度执行。每个任务有不同的类型,同类型的任务必须在同⼀个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在⼀次事件循环中,由浏览器⾃行决定取哪⼀个队列的任务。但浏览器必须有⼀个微队列,微队列的任务⼀定具有最⾼的优先级,必须优先调度执行。

浏览器一个页面会有一个进程,然后一个页面中进程中会有一个渲染主线程

js 是运行在渲染主线程的。这才是导致 js 是单线程的原因

队列:计时队列,交互队列,渲染队列,微任务队列

在浏览器地址栏输入地址,并按下回车键后,发生了哪些事情?

  1. 浏览器自动补全协议、端口,完成 url 编码
  2. 浏览器根据 url 地址查找本地缓存,根据缓存规则看是否命中缓存,若命中缓存则直接使用缓存,不再发出请求
  3. 通过 DNS 解析找到服务器的 IP 地址
  4. 浏览器向服务器发出建立 TCP 连接的申请,完成三次握手后,连接通道建立
  5. 若使用了 HTTPS 协议,则还会进行 SSL 握手,建立加密信道。
  6. 浏览器决定要附带哪些 cookie 到请求头中
  7. 浏览器自动设置好请求头、协议版本、cookie,发出 GET 请求
  8. 服务器处理请求,进入后端处理流程。完成处理后,服务器响应一个 HTTP 报文给浏览器。
  9. 浏览器根据使用的协议版本,以及 Connection 字段的约定,决定是否要保留 TCP 连接。
  10. 浏览器根据响应状态码决定如何处理这一次响应
  11. 浏览器根据响应头中的 Content-Type 字段识别响应类型,如果是 text/html,则对响应体的内容进行 HTML 解析,否则做其他处理
  12. 浏览器根据响应头的其他内容完成缓存、cookie 的设置
  13. 浏览器开始从上到下解析 HTML,若遇到外部资源链接,则进一步请求资源
  14. 解析过程中生成 DOM 树、CSSOM 树,然后一边生成,一边把二者合并为渲染树(rendering tree),随后对渲染树中的每个节点计算位置和大小(reflow),最后把每个节点利用 GPU 绘制到屏幕(repaint)
  15. 在解析过程中还会触发一系列的事件,当 DOM 树完成后会触发 DOMContentLoaded 事件,当所有资源加载完毕后会触发 load 事件

浏览器的渲染原理

1.解析 dom 树和 cssom 树(预解析线程下载外部的 css 文件和 js 文件),最终解析两棵树

2.样式计算(经历 css 属性值的计算过程【单位转化和层叠】),生成带样式的 dom 树

3.布局,生成布局树【计算盒子大小和页面上 x,y,伪元素,隐藏元素】

4.分层【z-index】

5.绘制【生成绘制指令,类似 canvas】

6.分块—合成线程

7.光栅化 –gpu 进程 将块变成位图

8.画 真正的展示

浏览器缓存

发送 http 请求之后,先看本地有没有缓存,如果有就直接使用,没有再发送,发送接收到数据以后,根据响应头保存数据。当有一天过期的时候,再发起协商缓存,如果服务器响应 304 就继续使用。现在基本都会把文件的本地缓存时间设置的很长,因为由于工程化,上传的文件都是 hash 文件,只要有变动就会请求新的地址

Cache-Control:max-age=3600(缓存方式)

ETag: W/“121-171ca289ebf”(资源编号)

Date:Thu, 30 Apr 2020 12:39:56 GMT(响应时间)

Last-Modified:Thu, 30 Apr 2020 08:16:31 GMT(资源最后一次修改时间)

如果缓存过期,携带这两个头文件,再次询问服务器,这个资源能否接着使用

If-Modified-Since: 资源最后一次修改时间

If-None-Match: 资源编号

讲讲三次握手与四次挥手

这个事情发生的在网络的传输层,在 tcp 连接上

主要目的是保证发送和请求的顺序

握手过程:

浏览器———>syn=1——–>服务器

服务器——–>ack=1———>浏览器

浏览器——–>ack=1———>服务器

ack 表示确认

挥手过程:

浏览器——–>fin=1——>服务器

服务器——->ack=1——>浏览器

服务器——->ack=1 fin=1——>浏览器

浏览器—–>ack=1——->服务器

http 和 https

HTTPS 是建立在 SSL 协议之上的 HTTP 协议

如果没有第三方机构(ca),那么你收到的信息可能会被篡改(中间人攻击)

我们交给第三方机构:钱+要认证的域名+一个公钥(服务器生成的)

服务器花钱去买一个证书

第三方机构给我们一个证书包括:域名+证书颁发机构+加密的你发给他的公钥信息(私钥加密)+加密的证书签名(私钥加密)

由于公钥全世界有所以我就用公钥解密,一旦被篡改,公钥就解密不了

服务器由于有证书了,于是客户端通信的时候,把这个证书交给客户端(客户端验证一下)

用服务器的公钥进行加密进行传输一个密钥,后续就用这个密钥传输

作用域链

作用域在定义时就确定,并且不会改变

当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用

如果没有则向上一级作用域中寻找,直到找到全局作用域,如果使用了上级的作用域则形成了闭包

垃圾回收

浏览器会被不被使用的变量自动进行清理【清理的时机是异步的】

一般现在使用的原理是标记清除

闭包

闭包 = 执行函数 + 词法作用域

执行函数时,只要在函数中使用了外部的数据,就创建了闭包。作用域链是实现闭包的手段。

闭包让开发者可以从内部函数访问外部函数的作用域。闭包会随着函数的创建而被同时创建。

闭包可以防止内部的变量污染全局

闭包内存泄漏:

词法作用域会膨胀

function test() {
  let a;
  let b;
  function sub() {
    a;
  }
  function sub2() {
    b;
  }
  return sub;
}

虽然只用到了 sub 与 a,但是 b 变量的由于跟 a 在同一个词法作用域,所以他也不会销毁

原型链

原型的作用就是要保证对象的类型可溯

每个函数都会自动附带一个属性prototype,这个属性的值是一个普通对象,称之为原型对象

每个对象都会自动附带一个属性proto,这个属性的值是一个普通对象,称之为实例对象

当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用, 如果没有则会去原型对象中寻找,如果找到则直接使用,于是就这样一直找下去,也就是我们平时所说的原型链的概念。

继承

组合继承

1.继承构造方法

2.继承原型

function Parent(name) { this.name = name; } Parent.prototype.sayName = function
() { console.log(this.name); }; function Child(name) { Parent.call(this, name);
} Child.prototype = new Parent(); Child.prototype.constructor = Child;

es6 extends 继承(本质也是用这种方式)

模块化

目的

不污染全局变量

保证依赖的顺序

细分与复用代码

common.js 和 es module 的区别

1.动态与静态

静态可以进行 tree shaking

CommonJs 导出值是拷贝,可以修改导出的值,Es Module 导出的是引用值,并且值都是只读的,不能修改

Es Module 有符号绑定【共享一块内存地址】

3.标准不同

一个社区规范,一个官方模块化标准

Promise async

promise 解决的问题:

1.回调地狱

2.统一了异步模型

有了 Promise,异步任务就有了一种统一的处理方式,解决了回调地狱,统一了异步模型

每一个异步任务,都是一个 promise 对象(两个阶段、三个状态)

then 方法必定会返回一个新的 Promise,可理解为后续处理也是一个任务

新任务的状态取决于前一个任务的后续处理(因为 then 方法是没有 resolve 和 reject,所以他会有自己的规则)

-  若前一个任务没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据
-  若前一个任务有后续处理但还未执行,新任务挂起。
-  若前一个任务后续处理执行了,则根据后续处理的情况确定新任务的状态
    * 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
    * 后续处理执行有错,新任务的状态为失败,数据为异常对象
    * 后续执行后返回的是一个任务对象(promise),新任务的状态和数据与该任务对象一致

async 关键字用于修饰函数,被它修饰的函数,一定返回一个新的 Promise,如果返回的是值,则会包装成 promise。

webpack

webpack 是基于模块化的打包(构建)工具,它把一切视为模块(图片,css 等)

编译过程

  1. 初始化【形成一个最终的配置对象】
  2. 编译

根据入口创建 chunk,转化 loader,转化为抽象语法树,分析依赖(转化代码,webpck_require)【chunk】

放入执行模板中(立即执行函数)【bundle】

  1. 输出

利用 fs 模块将内存中的数据写入文件

loader

loader 本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回

如果要执行多个规则,从后向前执行

postcss-loader 处理 css 兼容性

babel-loader 处理 js 兼容性

file-loader 生成依赖文件到输出目录

plugin

当 webpack xxx 时,就 xxx。

有各种各样的钩子,可以在各个时机做事

class MyPlugin{ apply(compiler){ //这里一次启动只会运行一次 类型window.onload
//name是用来调试的,随便填写 compiler.hooks.事件名称.事件类型(name,
function(compilation){ //事件处理函数 }) } }

copy-webpack-plugin(复制静态资源)

mini-css-extract-plugin(生成 css 文件)

vite

依赖预构建

第三方模块会进行依赖预构建

解决的问题:

不同的第三方包会有不同的导出格式

对路径的处理上可以直接使用.vite/deps, 方便路径重写

叫做网络多包传输的性能问题(也是原生 esmodule 规范不敢支持 node_modules 的原因之一), 有了依赖预构建以后无论他有多少的额外 export 和 import, vite 都会尽可能的将他们进行集成最后只生成一个或者几个模块

开发阶段

es build 打包进行依赖预构建,而且还会进行缓存,es build 打包速度快

打包阶段

rollup 打包

插件

vue(), VueSetupExtend(), AutoImport({ resolvers: [ElementPlusResolver()] }),
Components({ resolvers: [ ElementPlusResolver({ importStyle: "sass", }),
IconsResolver({ prefix: "icon", alias: { antd: "ant-design", // iconify
图标集合名称过长可设置别名 https://icon-sets.iconify.design/ant-design/ ven:
"ven-icon" }, customCollections: ["ven-icon"], //
使用自定义本地图标需要定义集合名称 }) ] }),

优化问题

构造性能

webpack

1.设置不需要解析的模块

2.cache-loader 缓存

3.热更新

4.开发阶段不计算文件 hash

传输性能

分包(split-chunk)

tree-shaking

terser 代码压缩

gzip 压缩

cdn

懒加载

NDS 解析优化

其他

webpack scope hoisting

webpack 内置优化,减少执行上下文,合并函数

webpack-bundle-analyzer

分析打包体积

speed-measure-webpack-plugin

分析打包耗时

rollup-plugin-visualizer

分析打包体积

rollup-plugin-visualizer

用来分析各文件的编译结果

项目难点亮点

请求库

之前对 lib 库参与过封装请求,这里面还是相对复杂的,我们公司希望能有一个通用库,对一些重复请求,请求缓存,有些串行等进行封装,同样也是给大文件上传打下了很好的基础。这个库分为三层,一层是 base 层,里面会注册一些发送前和得到请求后的钩子,会注册一些权鉴的 token 携带,会对请求的地址和超时时间进行封装,中间有 core 层然后可以对串行请求,重复请求,缓存请求进行操作。最上层就是业务请求层,由于请求的接口很多,很多都是重复代码,于是我写了一个脚本,在 api fox 里面先把接口的参数是否缓存之类的全部定义好,然后导出成 json 格式,然后根据 json 格式去生成代码,当然有些代码可能还需要再次修改,也可以进行打补丁。当然中间由于缓存的地方很多,于是可以运用依赖倒置原则由上层来决定存储位置

大文件上传

把大文件分片上传需要有各种各样的协议,创建文件的协议,服务器给一个 token 标识,hash 校验协议,如果没有这个分片再发送,上传文件碎片协议,最后碎片上次完毕合并协议。中间用到了发布订阅模式,用来监控上传的进度,中间还用到了并发请求控制。

动态路由

将 views 下的所有 vue 文件生成路由,当然需要补充 meta 数据,其他的依靠 import.meta.glob 读到所有路径和组件,然后还有嵌套路由,需要一级一级展开。其他还有一些细节,比如有些 vue 文件不需要生成路由,有一些层级嵌套等。这样的好处是不用写一个文件就重复些路由了。同时右边菜单运用 el-menu 同时递归有子路由的地方,这样就可以要有的页面的时候只需要写 vue 即可

封装动态表单

就是表单项之间是有关联的,上一项的选择会决定下一项如何去显示

思路:把一个表单项就看成是一个对象,对象里面就是表单的配置,然后把这一个个对象形成一个链表结构,对象里面加一个 next(当前选择,之前的选择)函数,调用得到下一项是什么。

然后当在页面中选择一项后,就会自动调用 next,next 函数返回的是一个组件

权限控制指令

app.directive('permiss', { mounted(el, binding) { if
(!permiss.key.includes(String(binding.value))) { el['hidden'] = true; } }, });

后台系统有权限控制,于是封装了指令,用在 el-menu 上

resize 指令

将监听的 dom 放入 weakMap,当后面尺寸变化的时候执行传进来的函数

const map = new WeakMap(); const ob = new ResizeObserver((entries)=>{ for(const
entry of entries){ const handler = map.get(entry.target); handler &&
handler(entry.contentRect) } //dom尺寸发生变化后,运行这里面的函数 }) export
default{ mounted(el,binding){ ob.observe(el) map.set(el,binding.value) },
unmounted(el){ ob.unobserve(el) } }

图片懒加载

1.判断视口

2.Intersectionobserver

封装方法如上

白屏优化

首页元素太多

思路:

一开始只渲染用户看得见的部分

第一帧渲染第一个组件,第二帧渲染第二个组件

每执行一帧,count 的数字就会增加,一旦 count 的数字大于传进来的数字就说明你可以加载了

<template>
  <comp1 v-if="defer(1)"></comp1>
  <comp2 v-if="defer(2)"></comp2>
</template>

defer(){ const count = ref(0); function update(){ count.value++;
requestAnimationFrame(update); } update(); return function(n){ return
count.value >= n; } }

虚拟滚动(长列表优化)

有一些后端里面,渲染的下拉项特别多,非常卡顿,就使用了虚拟滚动

如果有白屏问题可以增加缓冲区,多加载一些数据

<!-- 可视区域容器 -->
<div class="infinite-list-container">

  <!-- 这是容器里面的占位,高度是总列表高度,用于形成滚动条 -->
  <div class="infinite-list-phantom"></div>

  <!-- 列表项渲染区域 -->
  <div class="infinite-list">
    <!-- item-1 -->
    <!-- item-2 -->
    <!-- ...... -->
    <!-- item-n -->
  </div>
</div>

组件接受参数

1.列表所有的元素

2.每个元素的高度

提供一个插槽 item 给予外面如何展示这个元素

内部其实是一个绝对定位

根据监听滚动条来生成的一个只有几个元素的列表,看上去是在滚动,其实只有几个元素

具体原理就是根据滚动条现在的位置来确定展示哪些数据

websocket

我们那个软件采用的是 electron,然后后端先向阿里云请求模板数据(流)

const { data, headers } = await axios.get(url, { headers: { 'Connection':
'keep-alive', }, responseType: 'stream', timeout: 60000, })

然后 pipe 一点一点保存到本地

data.on('data', (chunk) => { curSize += chunk.length; if (Date.now() -
lastUpdateTime > 100) { onDownloadProgress && onDownloadProgress(curSize /
totalLength); lastUpdateTime = Date.now(); } }) .pipe(writer)

通过 track 和 emit 交换当前的下载进度

const socket = await getSocket(); socket.on("connect", () => {
socket.on("progress", (data) => { progress.value = data * 100; });
socket.on("downloaded", (projectMeta: ProjectMeta) => { progress.value = 100;
emit("created",projectMeta) emit("update:modelValue",false); }, 320) });
track("create-project", {templateId: themeMd5}); });

组件透传

二次封装往往只改动一部分内容

1.透传属性和事件 v-bind=attrs

2.透传插槽

import { useSlots } from 'vue';

3.透传 ref

把内部组件 ref 里面的方法提到自己的这个组件,循环遍历,暴露

右键菜单封装

1.右键后调出的位置计算

通过鼠标的位置,响应式视口的大小,菜单的大小决定调出框的位置

要用到一个方法对监控某个 dom 的尺寸变化

2.弹出的框必须要放到元素的最外部,通过,否则定位不一定准确

3.组件的设计,应该是类似插槽方式,内容放入组件的内部,同时传递选中事件和菜单

4.右键事件弹出(contextMenu 事件),阻止冒泡,阻止默认事件

5.卸载组件 取消监听,右键其他区域隐藏

6.动画,要计算高度

css 动效

轮播图

全屏视频

视差滚动

响应式布局

利用 sass 混合

.header{ width:100%; @inclue reposeTo('phone'){ height:100px; } @inclue
reposeTo('pad'){ height:200px; } } @mixin reposeTo($name){ if($name === 'phone){
@media(min-with:320px) and (max-width:400px){ @content } } }