前端必备面试基础

220 阅读24分钟

遍历二叉树

有两种方式:

  • 一种是深度优先遍历也就是我们常说的 DFS,深度优先遍历实现的方法有俩种,一种是递归还有一种是迭代
  • 另一种是广度优先遍历我们常用 BFS 来称呼, 而广度优先遍历则是利用队列来实现的,我们称之为层序遍历。
深度优先遍历

先从递归开始,总结一下二叉树深度优先遍历的三种方法,分别是前序遍历,中序遍历和后序遍历。

  • 二叉树前序遍历的特性是:根节点,左节点,右节点。
  • 二叉树中序遍历的特性是:左节点,根节点,右节点。
  • 二叉树后序遍历的特性是:左节点,右节点,根节点。

在二叉树的中,如果使用迭代去遍历,则必须要借助栈来实现。

  • 由于栈的特性是先进后出,所以我们要先 push 右孩子,再push 左孩子,这样出栈的顺序,才会是:中-左-右。
  • 中序遍历的特性是:左-中-右,所以我们要先找到位于二叉树最底部的左孩子,于是我们就要一层一层的找下去,直到到达树左面的最底层,这步操作,我们可以借用指针来帮助访问节点,利用栈来存储节点上的元素。
  • 由于后序遍历的顺序是:左-右-中,前序遍历的顺序是:中-左-右,我们可以先调换下,左孩子,右孩子入栈的顺序,然后再翻转 List 数组,即可在前序遍历的代码基础上实现后序遍历。
广度优先遍历

层序遍历二叉树的方式,就是从左到右,一层一层的去遍历二叉树。这时候,就需要借助队列,队列先进先出的特性符合一层一层的去遍历二叉树。

原文: juejin.cn/post/702478…

浏览器页签之间的通信

有四种方式,主要分为浏览器数据存储,以及服务器方式;而浏览器主要用本地存储方式解决,即调用localStorage、Cookie等本地存储方式。服务器则通过使用websocket技术使多页签都监听服务器推送事件来获得其他页签发送的数据。

浏览器存储
  • 第一种:localStorage:

在一个标签页里面使用localStorage.setItem(key,value)添加(修改、删除)内容;在另一个标签页里面监听storage事件。即可得到localstorge存储的值,实现不同标签页之间的通信。

  • 第二种:cookie+setInterval:

将要传递的信息存储在cookie中,每隔一定时间读取cookie信息, 即可随时获取要传递的信息。 在A页面将需要传递的消息存储在cookie当中 在B页面设置setInterval,以一定的时间间隔去读取cookie的值。(不停地问cookie)

监听服务器事件
  • 第一种:websocket通讯:

WebSocket 是全双工(full-duplex)通信自然可以实现多个标签页之间的通信(服务器可以主动发数据给浏览器;浏览器也可以主动发数据给服务器)。 WebSocket 是HTML 5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。

  • 第二种:html5浏览器的新特性SharedWorker:

SharedWorker可以被多个window共同使用,但必须保证这些标签页都是同源的(相同的协议,主机和端口号);webworker端(暂且这样称呼)的代码就如上,只需注册一个onmessage监听信息的事件,客户端(即使用sharedWorker的标签页)发送message时就会触发。

客户端和webworker端的通信不像websocket那样是全双工的,所以客户端发送数据和接收数据要分成两步来处理。分别对应的向sharedWorker发送数据的请求以及获取数据的请求,但他们本质上都是相同的事件--发送消息。

webworker端会进行判断,传递的数据为'get'时,就把变量data的值回传给客户端,其他情况,则把客户端传递过来的数据存储到data变量中。

页面A发送数据给worker,然后打开页面B,调用window.worker.port.postMessage('get'),即可收到页面A发送给worker的数据。

原文:juejin.cn/post/689482… juejin.cn/post/684490…

websocket 的心跳?

  • 心跳机制:顾名思义,就是客户端每隔一段时间向服务端发送一个特有的心跳消息,每次服务端收到消息后只需将消息返回,此时,若二者还保持连接,则客户端就会收到消息,若没收到,则说明连接断开,此时,客户端就要主动重连,完成一个周期;
  • 心跳的实现: 只需在第一次连接时用回调函数做延时处理,此时还需要设置一个心跳超时时间,若某时间段内客户端发送了消息,而服务端未返回,则认定为断线。 原文:juejin.cn/post/723342…

深拷贝-循环引用

  • 我们只需要在每次对复杂数据类型进行深拷贝前保存其值,如果下次又出现了该值,就不再进行拷贝,直接截止。
function deepCopy3(obj) {
    // hash表,记录所有的对象的引用关系
    let map = new WeakMap();
    function dp(obj) {
        let result = null;
        let keys = Object.keys(obj);
        let key = null,
            temp = null,
            existobj = null;

        existobj = map.get(obj);
        //如果这个对象已经被记录则直接返回
        if(existobj) {
            return existobj;
        }

        result = {}
        map.set(obj, result);

        for(let i =0,len=keys.length;i<len;i++) {
            key = keys[i];
            temp = obj[key];
            if(temp && typeof temp === 'object') {
                result[key] = dp(temp);
            }else {
                result[key] = temp;
            }
        }
        return result;
    }
    return dp(obj);
}

const obj= {
    a: {
        name: 'a'
    },
    b: {
        name: 'b'
    },
    c: {

    }
};
c.d.e = obj.a;

const copy = deepCopy3(obj);

原文:juejin.cn/post/723342…

TS常问问题

  • TS你用来做什么?熟练程度?
  • interface和type的区别?
  • 联合类型和交叉类型区别?
联合类型:希望一个变量可以支持多种类型 |

tip: 联合类型大大提高了类型的可扩展性,但当 TS 不确定一个联合类型的变量到底是哪个类型的时候,只能访问他们共有的属性和方法

交叉类型:对象形状进行扩展,可以使用交叉类型 &

tip:交叉类型和 interface 的 extends 非常类似,都是为了实现对象形状的组合和扩展。

比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性,就可以这么写,

interface Person {
    name: string
    age: number
}

type Student = Person & { grade: number }

这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性;

二者的区别:联合类型 | 是指可以取几种类型中的任意一种,而交叉类型 & 是指把几种类型合并起来。
type(类型别名):听名字就很好理解,就是给类型起个别名;
type Name = string
type NameResolver = () => string
type NameOrResolver = Name | NameResolver          // 联合类型
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n
    }
    else {
        return n()
    }
}

调用时传字符串和函数都可以。

getName('lin') getName(() => 'lin')
type的适用于基本类型、联合类型、交叉类型、元组;
type Name = string                              // 基本类型

type arrItem = number | string                  // 联合类型

const arr: arrItem[] = [1,'2', 3]

type Person = { 
  name: Name 
}

type Student = Person & { grade: number  }       // 交叉类型

type Teacher = Person & { major: string  } 

type StudentAndTeacherList = [Student, Teacher]  // 元组类型

const list:StudentAndTeacherList = [
  { name: 'lin', grade: 100 }, 
  { name: 'liu', major: 'Chinese' }
]

interface:(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。
  • 定义 interface 一般首字母大写
  • 属性必须和类型定义的时候完全一致。(属性少、多,都会报错)
  • 在属性上加个 ?,这个属性就是可选的(可选属性)
  • 希望某个属性不被改变,可以这么写:readonly id: number(只读属性)
  • interface 也可以用来描述函数类型,代码如下:
interface ISum {
    (x:number,y:number):number
}

const add:ISum = (num1, num2) => {
    return num1 + num2
}

  • 属性必须和类型定义的时候完全一致,如果一个对象上有多个不确定的属性,可以这么写:
interface RandomKey {
    [propName: string]: string
}

const obj: RandomKey = {
    a: 'hello',
    b: 'lin',
    c: 'welcome',
}

tip:注意:interface 不是 JS 中的关键字,所以 TS 编译成 JS 之后,这些 interface 是不会被转换过去的,都会被删除掉,interface 只是在 TS 中用来做静态检查。

两者相同点:
  • 都可以定义一个对象或函数
type addType = (num1:number,num2:number) => number

interface addType {
    (num1:number,num2:number):number
}
// 这两种写法都可以定义函数类型

const add:addType = (num1, num2) => {
    return num1 + num2
}

  • 都允许继承 interface 使用 extends 实现继承, type 使用交叉类型实现继承

tip:interface可以继承 interface或者type;type可以继承 interface或者type

两者不同点:
  • type 可以声明基本类型、联合类型、交叉类型、元组,interface 不行
  • interface可以合并重复声明,type 不行
interface Person {
    name: string
}

interface Person {         // 重复声明 interface,就合并了
    age: number
}

const person: Person = {
    name: 'lin',
    age: 18
}

重复声明 type ,就报错了

type Person = {
    name: string
}

type Person = {     // Duplicate identifier 'Person'
    age: number
}

const person: Person = {
    name: 'lin',
    age: 18
}

两者总结

其实本不该把这两个东西拿来做对比,他们俩是完全不同的概念。

interface 是接口,用于描述一个对象。

type 是类型别名,用于给各种类型定义别名,让 TS 写起来更简洁、清晰。

只是有时候两者都能实现同样的功能,才会经常被混淆

平时开发中,一般使用组合或者交叉类型的时候,用 type。

一般要用类的 extendsimplements 时,用 interface。

其他情况,比如定义一个对象或者函数,就看你心情了。

类型保护(typeof)

如果有一个 getLength 函数,入参是联合类型 number | string,返回入参的 length,

function getLength(arg: number | string): number {
    return arg.length
}

从上文可知,这么写会报错,因为 number 类型上没有 length 属性。

  • 代码改造
function getLength(arg: number | string): number {
    if(typeof arg === 'string') {
        return arg.length
    } else {
        return arg.toString().length
    }
}

类型断言(值 as 类型)

使用类型断言来告诉 TS,我(开发者)比你(编译器)更清楚这个参数是什么类型,你就别给我报错了,

tip:注意,类型断言不是类型转换,把一个类型断言成联合类型中不存在的类型会报错。

function getLength(arg: number | string): number {
    return (arg as number[]).length
}

字面量类型

这样就只能从这些定义的常量中取值,乱取值会报错,例如:const sex:Sex = '卡卡'

type ButtonSize = 'mini' | 'small' | 'normal' | 'large'

type Sex = '男' | '女'

泛型
泛型的基本使用
  • 处理函数参数 泛型的语法是 <> 里写类型参数,一般可以用 T 来表示。
function print<T>(arg:T):T {
    console.log(arg)
    return arg
}

这样,我们就做到了输入和输出的类型统一,且可以输入输出任何类型。 如果类型不统一,就会报错:

  • type 和 interface 都可以定义函数类型,也用泛型来写一下
type Print = <T>(arg: T) => T
const printFn:Print = function print(arg) {
    console.log(arg)
    return arg
}

interface Iprint<T> {
    (arg: T): T
}

function print<T>(arg:T):T {
    console.log(arg)
    return arg
}


const myPrint: Iprint<number> = print

处理多个函数参数

现在有这么一个函数,传入一个只有两项的元组,交换元组的第 0 项和第 1 项,返回这个元组。

泛型改造

function swap<T, U>(tuple: [T, U]): [U, T]{
    return [tuple[1], tuple[0]]
}

函数副作用操作

我们希望调用 API 都清晰的知道返回类型是什么数据结构,就可以这么做:

interface UserInfo {
    name: string
    age: number
}

function request<T>(url:string): Promise<T> {
    return fetch(url).then(res => res.json())
}

request<UserInfo>('user/info').then(res =>{
    console.log(res)
})

这样就能很舒服地拿到接口返回的数据类型,开发效率大大提高:

约束泛型

假设现在有这么一个函数,打印传入参数的长度,我们这么写: 现在我想约束这个泛型,一定要有 length 属性

interface ILength {
    length: number
}

function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}

这其中的关键就是 <T extends ILength>,让这个泛型继承接口 ILength,这样就能约束泛型。

我们定义的变量一定要有 length 属性,比如下面的 str、arr 和 obj,才可以通过 TS 编译。

泛型约束接口

使用泛型,也可以对 interface 进行改造,让 interface 更灵活。

interface IKeyValue<T, U> {
    key: T
    value: U
}

const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = { key: 'lin', value: 18}

泛型定义数组

定义一个数组,我们之前是这么写的:

const arr: number[] = [1,2,3]

现在这么写也可以:

const arr: Array<number> = [1,2,3]

泛型总结

泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。

泛型中的 T 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出

泛型在成员之间提供有意义的约束,这些成员可以是:函数参数、函数返回值、类的实例成员、类的方法等。

原文:juejin.cn/post/706808…

vue2升vue3的区别?

  • 响应式原理:使用Proxy取代Object.defineProperty()
- Proxy可以直接监听对象而非属性 
- Proxy可以直接监听数组的变化 
- Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的 
- Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改 
- Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利 

- Object.defineProperty的优势如下: ● 兼容性好,支持IE9
  • 代码组织方式:采用了Compositon API取代Option API
`Compositon API`缺陷
- 由于所有数据都挂载在 `this` 之上,因而 `Options API` 的写法对 `TypeScript` 的类型推导很不友好,并且这样也不好做 `Tree-shaking` 清理代码。
- 新增功能基本都得修改 `data``method` 等配置,并且代码上 300 行之后,会经常上下反复横跳,开发很痛苦。
- 代码不好复用,Vue2 的组件很难抽离通用逻辑,只能使用 `mixin`,还会带来命名冲突的问题。
  • 工程化工具:采用Vite代替Webpack
- `Vite`的兴起是现代浏览器已经默认支持了 ES6import 语法,`Vite` 就是基于这个原理来实现的。
- 在调试环境下,我们不需要全部打包,只是把你首页依赖的文件,依次通过网络请求去获取,整个开发体验得到巨大提升,做到了复杂项目的秒级调试和热更新。
- 而传统的`Webpack`要把所有的依赖打包后,才能开始调试
  • Vue3提供了新的组件
- `Vue3` 还内置了 `Fragment``Teleport` 和 `Suspense` 三个新组件。
- `Fragment`: Vue 3 组件不再要求有一个唯一的根节点,清除了很多无用的占位 `div`。
- `Teleport`: 允许组件渲染在别的元素内,主要开发弹窗组件的时候特别有用。
- `Suspense`: 异步组件,更方便开发有异步请求的组件。

原文:juejin.cn/post/724360…

服务端渲染(Next.js && Nuxt.js)?

前端如何排查内存泄漏?

  • 可以使用chrome 开发工具的 Performance 选项和 Memory 选项来排查内存泄漏。
  • 打开 Chrome 的开发者面板,点开 Performance 面板,拍下快照,如下图所示。

image.png

  • 但是光是这么一个快照无法看出究竟是哪里出错了,毕竟实际项目中函数调用错综复杂。
  • 这时就可以打开 Mermory 面板,这里显示了每一项的内存占用情况,如下图所示。

image.png

  • 如果存在占用内存非常大且远超其他项,这时我们就可以重点排查一下是怎么产生的。 原文:juejin.cn/post/708512…

Vue 插槽(具名插槽 && 作用域插槽)

插槽
  • 插槽就是在组件当中留出一些位置,进行内容的填充
  • 具名插槽:区分插槽与内容的对应关系,我们可以分别给 slot 和内容都加上一个名字,插入插槽的时候大家按照名字区分好就可以了。

我们给 child 组件添加上多个 slot,并且给每个 slot 取上一个名字。

<template>
  <div class="child-box">
    <p>我是子组件</p>
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

接下来就是我们父组件 App.vue 填充内容了。

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <child>
    <template v-slot:header>
      <div>我是 header:{{ message }}</div>
    </template>
    <div>我没有名字:{{ message }}</div>
    <template v-slot:footer>
      <div>我是 footer:{{ message }}</div>
    </template>
  </child>
</template>

具名插槽简写:
  • 原写法
<template v-slot:footer>
</template>

  • 简写:
<template #footer>
</template>

动态插槽名
<template>
  <div class="child-box">
    <p>我是子组件</p>
    <header>
      <slot name="{dynamicSlotName}"></slot>
    </header>
  </div>
</template>

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

具名插槽总结
  • 插槽内容可以访问到父组件的数据作用域,就好比上述中的 message 是父组件的。
  • 插槽内容无法访问到子组件的数据,就好比上述 App.vue 中的插槽内容拿不到子组件 child 的数据。
作用域插槽:插槽内容访问子组件数据

上段代码中我们在子组件中 slot 标签上添加了一些自定义属性,属性值就是我们想要传递给父组件的一些内容。

<template>
  <div class="child-box">
    <p>我是子组件</p>
    <slot text="我是子组件小猪课堂" :count="1"></slot>
  </div>
</template>

在父组件 App.vue 中通过 v-slot="slotProps"等形式接收子组件传毒过来的数据,slotProps 的名字是可以任意取的,它是一个对象,包含了所有传递过来的数据。

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <child v-slot="slotProps">
    <div>{{ slotProps.text }}---{{ slotProps.count }}</div>
  </child>
</template>

解构写法

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <child>
    <template #header="{ text, count }">
      <div>{{ text }}---{{ count }}</div>
    </template>
  </child>
</template>

原文:juejin.cn/post/710936…

浏览器渲染流水线

从输入URL到页面呈现的整体流程:
-   浏览器进程构建完整URL,并通过进程间通信将URL提交给网络进程
-   网络进程:
    -   检查缓存
    -   DNS解析
    -   建立TCP连接(三次握手)
    -   发送请求数据
    -   接收响应数据,并根据响应数据类型进行解析
-   网络进程将处理好的数据提交给浏览器进程,浏览器进程准备好渲染进程
-   渲染进程:
    -   将HTML解析为DOM树
    -   将CSS解析为CSSOM树
    -   将DOM树和CSSOM树构建成布局树
    -   进行分层和绘制
    -   合成

image.png

渲染流程特点:回流和重绘
  • 回流:当元素的几何属性(尺寸)、隐藏属性等改变而触发重新布局的渲染,这个过程就是回流。回流需要更新完整的渲染流程(布局-分层-绘制-图块-栅格化-合成-显示),所以开销较大,需要尽量避免。
-   盒子模型相关属性(width、padding、margin、display、border等)
-   定位属性和浮动(position、top、float等)
-   文字结构(text-align、font、white-space、overflow等)
  • 重绘:当元素的外观、风格等属性发生改变但不会影响布局的渲染,这个过程就是重绘。重绘省去了布局和分层阶段(绘制-图块-栅格化-合成-显示),所以性能比回流要好。回流必将引起重绘,重绘不一定会触发回流。
color、border-style、background、outline、box-shadow、visibility、text-decoration
避免重绘和回流
-   避免使用触发重绘和回流的CSS属性
-   将频繁重绘回流的元素创建为一个独立图层
-   使用transform实现效果:可以避开回流和重绘,直接进入合成阶段(图块-栅格化-合成-显示)
-   用opacity替代visibility:visibility会触发重绘
-   使用class替代DOM频繁操作样式
-   DOM离线后修改,如果有频繁修改,可以先把DOM隐藏,修改完成后再显示
-   不要在循环中读取DOM的属性值:offsetHeight会使回流缓冲失效
-   尽量不要使用table布局,小改动会造成整个table重新布局
-   动画的速度:200~500ms最佳
-   对动画新建图层
-   启用GPU硬件加速:启用translate3D

HTML解析特点
  • 顺序执行、并发加载
-   顺序执行:HTML的词法分析是从上到下,顺序执行
-   并发加载:当 HTML 解析器被脚本阻塞时,解析器虽然会停止构建DOM,但仍会识别该脚本后面的资源,并进行预加载。
-   并发上限:浏览器对同域名的并发数是有限制的(HTTP/2则没有这个限制)
  • 阻塞
css阻塞
-   css在head中阻塞页面的渲染:避免页面闪动
-   css会阻塞js的执行:CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。
-   css不阻塞外部脚本的加载

tips:默认情况下,CSS会阻塞渲染,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。不过,使用媒体查询可以让CSS资源不在首次加载中阻塞渲染。
js阻塞
-   直接引入的js会阻塞页面的渲染:当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行
-   js不阻塞资源的加载
-   js顺序执行,会阻塞后续js的执行
-   js可以查询和修改 DOMCSS

解决js的阻塞:
`defer``async`属性可以改变js的阻塞情形,不过这两个只对src方式引入的script有效,对于inline-script无效。

原文:juejin.cn/post/684490…

react15升16.2区别在哪??

  • 文件体积更小
  • 重写核心算法,渲染性能更优
它可以支持异步渲染(16.0不支持,16.x支持),异步渲染能够将渲染任务划分为多块,这意味着几乎所有的行为都是同步发生的。React 16使用原生的浏览器 API来间歇性地检查当前是否还有其他任务需要完成,从而实现了对主线程和渲染过程的管理。例如拖动、onChange等在不考虑防抖情况,频繁setState的场景,相对于之前版本,有一定性能的提升。
  • render支持Array、String渲染
render() {
  return [
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}
render() {
  return 'Look ma, no spans!';
}

  • Portals API-提供一种新的方式,支持DOM节点渲染在父组件之外
  • 引入误差边界
- 如果类组件定义了新的生命周期函数 componentDidCatch(error, info),该类组件代表错误边界
- 捕获子组件树中任何位置的 JavaScript 错误,记录这些错误,并显示后备 UI
- 错误边界仅捕获树中位于其下方的组件中的错误,错误边界无法捕获自身内部的错误,这也类似于`catch {}`JavaScript 中 block 的工作方式。
  • 更好的服务端渲染
新的实现非常快,接近3倍性能于React 15,它支持流(streaming),可以很快的向客户端更快地发送字节
  • 支持自定义DOM属性
// 你的代码:
<div mycustomattribute="something" />
// React15 输出:
<div />
// React 16 输出:
<div mycustomattribute="something" />

  • 不再支持ES5语法

原文:juejin.cn/post/684490…

模块的循环引用?

  • CommonJs的循环引用的重要原则:一旦出现某个模块被”循环引用”,就只输出已经执行的部分,还未执行的部分不会输出。
  • ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。  等到真的需要用到时,再到模块里面去取值。因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

原文:juejin.cn/post/684490…

前端大文件上传

前端

我们使用el-upload去获取文件,对于文件切片,核心就是利用Blob.prototype.slice(),和数组的slice相似,我们可以使用这个方法获取文件的某一部分的片段,文件切片后,我们将这些切片进行并发发给服务端,由服务端进行合并,因为是并发,所以传输的顺序肯定是会变的,所以这个时候我们需要去记录片段的顺序,以便服务端去合并

服务端

服务端接收切片后,需要去合并切片。那么产生如下两个问题

  • 怎么取合并切片
  • 什么时候知道切片上传完成了 对于第一个问题我们可以使用fs-extra的读写流进行合并。 第二个问题我是解决办法是在每个请求的参数加一个文件总切片长度,对于每个切片我是的命名规则是name.suffixName_index,其中suffixName是后缀名,name可以使用你的文件名,index是上传的当前分片的index值,这样交给后端,后端沟通好就使用这个去区分。接下来我们就实现吧。
实现
/**
 * 创建切片
 */
const createFileChunk = (file, size = 1024 * 10 * 1024) => {
  //定义一个数组用来存储每一份切片
  const fileChunkList = [];
  //存储索引,以cur和cur+size作为开始和结束位置利用slice方法进行切片
  let cur = 0;
  while (cur < file.size) {
    fileChunkList.push({ file: file.slice(cur, cur + size) });
    cur += size;
  }
  upload.total = fileChunkList.length;
  return fileChunkList;
};

/**
 * @description: 文件上传 Change 事件  选择文件
 * @param {*}
 * @return {*}
 */
const handleFileChange = async (file, files) => {
  console.log("[Log] file-->", file, files);
  upload.fileList = files;
  upload.currentFile = file;
  upload.name = file.name;
};
/**
 * @description: 文件上传 Click 事件
 * @param {*}
 * @return {*}
 */
const handleUploadFile = async () => {
  percentage.value = 0;
  controller = new AbortController();
  if (!upload.fileList.length) return;
  const fileChunkList = createFileChunk(upload.currentFile.raw); // 这里上传文件的时候进行分片
  // calculateHash ---- 计算hash
  const fileHash = await calculateHash(fileChunkList);
  // 获取后缀名
  let suffixName = upload.currentFile.name.split(".")[1];
  upload.currentFile.fileHashName = fileHash + "." + suffixName;
  upload.fileArr = fileChunkList.map(({ file }, index) => ({
    chunk: file,
    hash: fileHash + "." + suffixName + "_" + index, // 文件名  数组下标
  }));
  let result = await fileIsTransmission("http://localhost:8100/bigFile");
  console.log("[Log] result-->", result);
  if (result.code === 201) {
    handleUploadChunks();
  } else {
    ElMessageBox.alert("文件秒传成功", "文件上传", {
      confirmButtonText: "OK",
    });
  }
};
/**
 * 上传切片
 */
const handleUploadChunks = async () => {
  //设置请求头和监听上传的进度
  let configs = {
    headers: {
      "Content-Type": "multipart/form-data",
    },
    //设置超时时间
    timeout: 600000,
  };
  const requestList = upload.fileArr.map(({ chunk, hash }) => {
    const formData = new FormData();
    formData.append("file", chunk);
    formData.append("hash", hash);
    formData.append("filename", upload.currentFile.name);
    formData.append("total", upload.total);
    // console.log("[Log] formData-->", formData.get("hash")); // 直接打印formata是空的你需要使用get或者getAll的方法去打印
    return { formData };
  });

  let result = null;

  if (switchControl.value) {
    console.log("上面 ---- 并发");
    result = await ajax(
      "http://localhost:8100/bigFile",
      5,
      requestList,
      configs
    );
  } else {
    console.log("下面 ---- 遍历");
    result = await noConcurrency(
      "http://localhost:8100/bigFile",
      requestList,
      configs
    );
  }

  // return;
  if (result.code == 200) {
    let {
      data: { consumTime },
    } = await mergeRequest(upload.currentFile.fileHashName);
    console.log("[Log] consumTime-->", consumTime);
    timeLog.value.push({
      consumTime: consumTime,
      date: new Date().toLocaleString(),
      size: upload.currentFile.size,
    });
  }
};


变量upload是用来保存文件数据用的

const upload = reactive({
  //文件列表
  fileList: [],
  //存储当前文件
  currentFile: null,
  //当前文件名
  name: "",
  //存储切片后的文件数组
  fileArr: [],
  //切片总份数
  total: 0,
  timeLog: [], // 耗时记录
});

生成hash

无论是前端还是服务端,都必须要生成文件和切片的 hash,之前我们使用文件名 + 切片下标作为切片 hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 hash 的生成规则,所以上面我们使用了spark-md5,它可以根据文件内容计算出文件的 hash 值。另外一个问题是如果文件过大,可能会导致计算hash进行ui阻塞,导致页面假死,其实我们这边可以使用web-work方法进行去计算补补课

/**
 * 使用spark-md5计算hash
 */
const calculateHash = function (fileChunkList) {
  return new Promise((resolve) => {
    // 阵列缓冲区 ArrayBuffer
    const spark = new SparkMD5.ArrayBuffer();
    const reader = new FileReader();
    const file = fileChunkList;
    // 文件大小
    const size = upload.currentFile.size;
    let offset = 2 * 1024 * 1024;
    let chunks = [file.slice(0, offset)];
    // 以某个大小,进行文件组装
    // 前面100K
    let cur = offset;
    while (cur < size) {
      // 最后一块全部加进来[]
      if (cur + offset >= size) {
        chunks.push(file.slice(cur, cur + offset));
      } else {
        // 中间的 前中后去两个字节
        const mid = cur + offset / 2;
        const end = cur + offset;
        chunks.push(file.slice(cur, cur + 2));
        chunks.push(file.slice(mid, mid + 2));
        chunks.push(file.slice(end - 2, end));
      }
      // 前取两个字节
      cur += offset;
    }
    // 拼接
    reader.readAsArrayBuffer(new Blob(chunks));
    reader.onload = (e) => {
      spark.append(e.target.result);
      resolve(spark.end());
    };
  });
};

文件秒传

人话:我们在文件上传之前,去问一下服务器,你有没有这个文件呀,你没有的话我就开始上传,你要是有的话我就偷个懒,用你有的我就不上传了。

  • 需要实现一个检测接口(verify),去询问服务器有没有这个文件,因为我们之前是计算过文件的 hash的,能保证文件的唯一性。就用这个hash就能唯一的判断这个文件。
进度条功能
  • 当一个分片上传完成的时候,后端返回相应的状态码,进而前端根据沟通好的状态进行叠加
断点续传
  • 其实就是当你暂停了上传,后续的请求就不会再去上传了

tips:CancelToken文档已经说了在v0.22.0后续版本已经不再使用了,所以使用上面的abortController,那么使用abortController可能会遇到一个问题就是当你取消再去请求的时候就用不了,所以我们需要进行改造,怎么改造呢?

用事件的时候再去进行赋值,

image.png 进行请求的时候再去给signal进行赋值 取消请求我们直接去判断有没有这个controller,没有就不需要调用,如果有,我们需要使用abort事件还需要将controller进行赋值为null,如果对signal不熟悉的可以去补补课

恢复上传

对于断点续传和恢复重传是一个相通的事件,那么问题来了,我如何去判断

  • 我哪个地方已经上传了
  • 哪个地方没有去上传
  • 从哪里开始去上传

针对这些问题,所以我对代码进行了改动我把上传的所有切片用一个全局变量去存储requestList,后端因为对于每个分片都去成功返回成功的代码,所以当我暂停传的时候会接收到最开始的状态码,我拿到状态码根据文件名获得index,利用index去splice数组requestList就可以了。

node.js

实时数据更新?

一键换肤(Vue)?

ES6-ES12新增哪些新特性?

前端工程化?

前端实现文件预览(pdf、excel、word、图片)?

CSS3新特性

HTML5新特性

JS的高级API

给你十万条数据,丝滑加载出来

forEach能不能跳出循环--不能

Array.prototype.customForEach = function (fn) { 
    for (let i = 0; i < this.length; i++) 
        { 
            fn.call(this, this[i], i, this)
        } 
}

- forEach 内侧嵌套着一个函数,即使你retrun 也无法阻断外面的for循环执行
- 例如
function demo(){
    return 'demo' 
} 

function demo2(){ 
    demo() 
    return 'demo2' 
} 

demo()

demo函数返回的结果 跟demo2无关

let list = [1,2,3,4,5]

try {
    list.forEach((item, index, list) => {
        if (index === 2) {
            throw new Error('demo')
        }
        console.log(item)
    })
} catch (e) {
    // console.log(e)
}



let list = [1,2,3,4,5]

 list.forEach((item, index, list) => {
       list.length = 0 // 直接把数组置空
        console.log(item)
    })
}

let list = [1,2,3,4,5]

 list.forEach((item, index, list) => {
       list.splice(1,list.length - 1)
        console.log(item)
    })
}

深拷贝

function deepClone(obj) {  
      if (typeof obj !== 'object' || obj === null) {  
        return obj;  
      }  
  
      let copyObj;  
      if (Array.isArray(obj)) {  
        copyObj = [];  
        for (let i = 0; i < obj.length; ++i) {  
          copyObj.push(deepClone(obj[i]));  
        }  
      } else if (obj instanceof Set) {  
        copyObj = new Set([ ...obj ]);  
      } else if (obj instanceof Map) {  
        copyObj = new Map([ ...obj ]);  
      } else {  
        copyObj = {};  
        Reflect.ownKeys(obj).forEach(key => {  
          copyObj[key] = deepClone(obj[key]);  
        });  
      }  
  
      return copyObj;  
    }