更全的面试题

vue 生态面试题

HTML

新增的 h5 属性

  • 标签类 header,footer,aside,nav,article
  • 媒体播放标签 audio,video
  • canvas svg 等
  • 存储属性 localStorage,sessionStorage
  • input 标签新增type 属性 email,password,file,tel,date,url

CSS

em/rem/px/vw/vh

  • em 相对长度单位 是根据当前对象文本的字体尺寸,如果没有设置,则取默认尺寸
  • rem 相对单位,只根据 HTML 根元素的 font-size的值
  • px 指的是像素,为绝对单位,不会受到其他单位的影响
  • vw 屏幕宽度,最大为100,最小为0
  • vh 屏幕高度,最大为100,最小为0

BFC 块级格式化上下文

是块级盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

特性:

  • 块级元素会在垂直方向一个接一个排列
  • 避免子元素的margin 属性影响父元素
  • 保证子元素的属性只在BFC盒内起作用,不会影响盒外的其他元素

触发方式:

  • float: left/right
  • position: absolute/fixed
  • overflow: hidden/hidden-x/hidden-y
  • display: inline-block/flex/gid

如何解决高度塌陷

  • :after { clear: both;} 或者在父元素内部最后新增一个元素设置 style clear: both;
  • 设置 position: absolute
  • overflow: hidden

元素水平垂直居中的方法

  • display: flex; align-items: center;justify-content: space-around;
  • position:absolute; top:50%;left:50%;transform: translate(-50%, -50%)
  • display: grid; align-items: center;justify-content: space-around;
  • margin-top:50%;margin-left:50%;transform: translate(-50%, -50%)
  • 定高的话可以使用 margin-left偏移自身一般宽度和高度

flex 布局

有哪些属性

  • flex-direction 布局方向 row/row-reverse/column/column-reverse
  • flex-wrap 是否换行
  • flex-flow flex-direction 和 flex-wrap 的简写模式
  • justify-content 主轴上的对齐方式 flex-start/flex-end/space-around/space-between/space-evenly/center
  • align-items: 交叉轴 flex-start/flex-end/center/base-line(以第一行文字基线对齐)/stretch
  • align-content 多根轴线对齐 flex-start/flex-end/space-around/space-between/space-evenly/center

子元素属性

  • order 定义项目排列顺序,数值越大越靠后
  • flex 设置 flex-grow/flex-shrik/flex-basic 的缩写
  • flex-grow 是否放大 默认为0
  • flex-shrink 是否缩小 默认为1
  • flex-basic 占据主轴空间
  • align-self 单独设置子元素的对齐方式,可以使部分子元素与其他子元素对齐方式不同 flex-start/flex-end/center/base-line(以第一行文字基线对齐)/stretch

css 选择器优先级

css 选择器有以下几种

  • ID 选择器 id="abc"
  • 类(class)选择器 class="abc"
  • 标签选择器 div
  • 标签属性选择器 div[title="abc"]
  • 伪类和伪元素 after,before,selection,focus,hover,active,
  • 父子选择器 div > p
  • 后代选择器 div p

其中 ID 选择器的权重最高,接着是类选择器,最后是标签选择器

!important 和 行内样式 的权重最高,权重高的的样式会覆盖掉权重低的样式

常用的 css 库

  • reset.css 是一个css文件,用来重置css样式的。
  • normalize.css 为了增强跨浏览器渲染的一致性,一个CSS 重置样式库。

    JavaScript

    数据类型

基础数据类型有

  • Number
  • String
  • Boolean
  • Null
  • Undefined
  • Symbol
  • Bigint (新增的,因为是为了兼容大数)

引用数据

  • Object

如何判断数据类型

  • typeof 需要注意的是 typeof null = object, typeof func() {} = function
  • instanceof 原理是根据原型链来查找的
  • Object.prototype.toString.call() 会返回对应元素类型 [object Object/Array/RegExp/...]

数据类型转换

  • 显示转换,比如使用 parseInt,Number,String,Boolean 方法强制进行转换
  • 隐式转换,进行数据运算和 === 操作时,对象会调用 valueOf 方法进行转换,基础数据类型会根据内容进行隐式转换
1
2
3
4
5
6
7
8
9
10
11
12
let a = {
valueOf: () => 4
}
a + 1 // 5
let b = {}
b + 1 // "[object Object]1"

let c = []
c + 1 // "1"
true + 1 // 2
false + 1 // 1
"" + 1 // "1"

你能写出多少种数组的方法

1
2
3
4
5
6
7
map       filter        reduce        reduceRight       some
every from of fill join
splice slice find findIndex findLast
indexOf lastIndexOf forEach concat findLastIndex
includes contains values keys entries
at flat isArray sort pop
push unshift shift toString toLocaleString

你能写出多少种字符串方法

1
2
3
4
5
split     substr        substring     match             parseInt
repeat replace replaceAll concat parseFloat
endsidth startsWidth padStart padEnd toLowerCase
toUpperCase toLocaleLowerCase toLocaleUpperCase codeAt charCodeAt
valueOf includes trim trimStart

多维数组转成一维数组的方法

  • flat 传参为 number,平铺层级
  • reduce + 递归 使用 reduce + 递归的方式
  • for + 递归 for 循环加递归

数组去重的方法

  • es6 Set 方法会自动去重
  • 使用 hash,对象的键是具有唯一性的
  • 简单的遍历,使用新的数组来承载去重后的数组,可以用 indexOf,includes 等方法判断元素是否存在

中心思想是如何去重,使用方法并不固定

闭包

闭包是一个函数及其捆绑的周边环境状态的引用的组合,闭包可以使开发者从内部函数访问外部函数的作用域。

  • 可以用来实现函数柯里化(将多个参数转变成单个参数传入func(a,b,c) => func(a)(b)(c)
  • 可以用来模拟私有方法,因为闭包内部的部分变量和方法,函数外部无法访问
  • 闭包还可以用来解决变量提升的问题。

使用闭包需要注意内存泄露问题,因为这些作用域不会随着函数的执行而清除掉

原型链

javascript对象上有一个属性,可以通过这个属性查找到她的原型,而且原型上也有这个属性,可以一级一级的向上查找

1
2
3
4
5
6
7
8
9
10
11
let obj = new Object()
obj.prototype === undefined
obj.__proto__ === Object.prototype
Object.prototype.__proto__ === null

function func () {

}
let f = new func()
f.__proto__ === func.prototype
func.prototype.__proto__ === Object.prototype

当我们在查找一个对象的属性的时候,如果当前对象不存在这个属性,则会沿着原型链一级一级的向上查找,直查找到最后一级,也就是 Object 对象上

需要注意的是在原型链上查找比较耗时和消耗性能,如果层级比较深的话,可以选择暂存下来

原型链上的属性和方法最好不要随意修改,否则可能会影响其他继承于该原型的函数

this 指向

  • 直接执行 this 指向 window

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function func() {

    }
    func() // window
    const obj = {
    func: function() {

    }
    }
    let f1 = obj.func
    f1() // window
  • 挂载到对象上执行 this指向当前对象

    1
    2
    3
    4
    5
    6
    const obj = {
    func: function() {

    }
    }
    obj.func() // obj
  • new
    使用new 的时候会执行以下几个操作

    1. 创建一个新的空对象
    2. 将当前空对象的 prototype 指向当前函数的 prototype
    3. 将前端对象替换当前函数的执行上下文(this)
    4. 判断函数是否存在返回值,返回值如果是基础数据类型,则this 还是 创建的空对象,否则是return 的对象
  • call,bind,apply
    call 和 apply 会立即执行函数,同时将函数的this 指向传入的this
    bind 不会立刻执行,而且参数可以不用一次性传入

    • call(this, …args)
    • bind(this, …args)
    • apply(this, args)

事件循环 (Event Loop)

  • 宏任务(setTimeout,setInterval,script,ajax)
  • 微任务(Promise.then, requestAnimationFrame, process.nextTic, Object.observer, MutationObserver(监察dom节点变化))
  • 同步任务 正常js解析,js代码执行等

函数的执行顺序是

  • 如果存在同步任务,会先执行同步任务
  • 同步任务执行完成后,先查找微任务队列中是否包含任务,如果包含需要执行的任务,会立即执行
  • 微任务队列执行完成之后才会去判断宏任务中是否存在需要执行的任务,宏任务每次执行的时候都需要判断微任务队列中是否包含需要执行的任务

Promise

  • Promise.resolve() 返回一个以给定值解析后的 Promise 对象
  • Promise.reject() 返回一个带有拒绝原因的 Promise 对象。
  • Promise.then() 返回一个 Promise 对象,有两个回调函数,一个成功的回调,一个失败情况的回调
  • Promise.catch() 返回一个 Promise 对象,只有一个失败情况的回调
  • Promise.finally() 无论成功和失败都会执行
  • Promise.all() 当所有的 Promise 成功后才回返回成功,结果按顺序返回,有一个失败就返回失败
  • Promise.race() 只要有一个 Promise 成功或者失败,就是返回一个成功或者失败的结果
  • Promise.allSettled() 当所有的 Promise 的结果都获取到才返回,不管成功还是失败
  • Promise.any() 只要有一个 Promise 成功就会返回成功的结果,全部失败才会返回失败的结果

手写一个 Promise 方法

Proxy/Rflect

Proxy 对象用于创建一个对象的代理,实现对基本操作的拦截

类似于 Object.defineProperties,但是比她的功能更强大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {
a: 1
}
let o = new Proxy(obj, {
set: function(obj, key, value) {
console.log(obj, key, value)
},
get: function(obj, key, value) {
console.log(obj, key, value)
return obj[key]
},
})
obj.a = 20
o.a
o.a = 30

需要注意的是,只有通过代理后的对象修改和获取属性时,才会触发代理,原对象修改和删除并不会触发

handler 配置属性

  • get 属性读取操作
  • set 属性设置操作
  • has in操作符
  • deleteProperty delete 操作符
  • ownKeys Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
  • apply 函数调用时触发
  • construct 使用 new 操作符触发
  • getPrototypeOf 获取原型对象扑捉器 Object.getPrototypeOf
  • setPrototypeOf 设置原型对象扑捉器 Object.setPrototypeOf
  • isExensible Object.isExtensible 方法的捕捉器 是否是可扩展的
  • preventExtensible Object.preventExtensions 方法的捕捉器。设置对象不可扩展
  • getOwnPropertyDescriptor 获取对象的属性描述(value,configurable,writable,get,set,enumerable)
  • defineProperty Object.defineProperty 方法的捕捉器。

Reflect 方法同理,主要用来对 Proxy 对应属性的映射

能说一下 Set和 weakSet Map和 weakMap 有什么区别吗?

主要区别在 weakMap 和 weakSet 存储的键值对是不会被垃圾清理进行标记和计数的,也就是说,在进行垃圾清理的时候,如果浏览器判断这个元素需要被清除掉,那么它就会从 weakMap 和 weakSet 中清除掉

还有一个比较重要的区别就是,weakSet和weakMap(key) 只能使用引用类型的值,而不像set和map可以使用任意类型的值

而map 和 set 并没有这种操作

方法都是一样的

  • Map有 set,has,get,keys,values,entries
  • set有 add, has, get, size, keys, values, entries

Symbol 的一些注意事项

  1. 使用 new Symbol 的时候,都会创建一个新的Symbol 无论传入的参数是否一样,也就是说
    1
    2
    3
    let a = Symbol("123")
    let b = Symbol("123")
    b===a // false
  2. 如果想要获取到同一个 Symbol,可以采用 Symbol.for 方法创建 Symbol,它会去查找 是否已经创建了 Symbol,如果没有创建则会新建一个 Symbol,如果已创建,则会返回已创建的Symbol
1
2
3
let a = Symbol.for("123")
let b = Symbol.for("123")
a === b // true
  • 需要注意的是 Symbol 不能使用 new 关键字

ES6 新增了哪些内容

ES6 现在统一作为 ECMA2015 以后的 javascript 版本的统称,主要新增了一下内容

  • 新增let,const 命名变量(解决变量提升问题)
  • 箭头函数(没有this,this的执行为定义的时候上下文,无法使用new,call,bind,apply 等操作更改this)
  • 解构赋值(包含有 symbol.iterator 方法的对象都支持)
  • class 语法糖(继承和封装)
  • promise,async/await(将异步函数转成同步方法)
  • map/set(将对象的键值对分别存储,允许使用对象作为键)
  • 数组新增了 map,filter,reduce,some,every 等方法
  • for … of …

class 语法糖

  • constructor 构造方法,new 的时候会执行 constructor 里面的代码
  • get set 方法 对某个属性设置存值函数和取值函数,拦截该属性的存取行为
  • static 静态方法 静态方法只能在类上直接调用,无法再实例上调用
    ,并且可以被子类继承
  • 私有属性和私有方法,只能在类内部访问的属性和方法,可以用 # 来标记,不可以在类的外面使用,可以通过this来访问
  • new.target 在类里面指向当前类

面对对象编程

面对对象编程

  • 面向过程编程:会将事情分为一个个函数和数据,按照一定的是顺序执行。
  • 面向对象编程:将一个事情抽象成对象的概念,然后给对象赋予一些属性和方法,然后将每个对象执行自己的方法。因此易于维护,易复用,易扩展

面对对象编程 三大基本特性:封装,继承,多态

五大基本原则:单一职责,开放封闭,里式替换,依赖倒置,接口分离

函数式编程

函数式变成属于结构化编程的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用

  • 闭包和高阶函数
  • 递归
  • 惰性计算

有以下五个鲜明特点

  1. 函数是第一等公民
  2. 只用表达式,不用语句
  3. 没有副作用(函数只会返回一个新的值,不会修改外部变量的值,内容保持独立)
  4. 不修改状态(不会修改函数外变量或者属性的值)
  5. 引用透明性(同样的输入会返回同样的结果)

优点是:

  1. 代码简洁,开发快速
  2. 接近自然语言,便于理解
  3. 方便代码管理,调试起来更方便
  4. 支持并发操作(没有副作用)
  5. 修改方法内部时,不需要修改其他地方,升级方便

setTimeout和setInterval 那个用来计时最准确,为什么?

setTimeout 要更准确一些。

原因如下:

  • 如果setInterval 中函数执行时间超过等待事件,在函数执行完后后,由于任务已经被加入队列,所以,并不会等待一段时间后再执行,而是会立即执行
  • 而setTimeout 则是等函数执行完成后,才会向异步任务队列添加回调,等待一段时间后,才会再次执行回调函数,代码执行间隔事件会比预期学校

但是具体哪个比较好,还是需要看具体需求的

AMD CommonJs,ES6 mdoule 之间的区别

  • AMD 异步模块加载,通过 define 定义模块,require 引用模块,模块加载完成后通过 回调函数执行代码

  • commonJs 通过 module.exports 导出模块,require 引入模块,模块内部变量只在内部起作用,不会污染全局,并且是同步加载的

  • ES6 module 通过 export 和 export default 导出模块,import 引入模块,加入了treeShaking,只会编译和打包被使用的模块。也支持动态加载

浏览器

垃圾回收

  • 引用计数

    引用计数的原理当创建和引用对象时,都会给该对象的引用次数加1,当给变量重新赋值或者执行的上下文清除时,引用次数 -1,当引用次数为0的时候,会被清除的。

    缺点是当出现循环引用的时候,由于引用计数一直不为0,所以无法被清除

  • 标记清除

    当变量进入执行环境时,会标记状态为进入,当弹出执行环境时,会标记状态为清除,标记状态为清除的会自动被js引擎清除

    缺点是容易造成内存碎片化

  • 标记整理法

    同标记清除,会在清除后重新整理内存地址

  • 垃圾回收优化

    将内存分为 新生代和老生代,新生代缓存的是存活时间比较短的地址,老生代缓存的是存活时间久的地址

    同时新生代又分为使用区和空闲区(双缓存),在进行清除时,会将使用区的变量的标记进行判断,如果被标记,则放到空闲区内,并且进行内存整理,最后将使用区的内存清空,并将使用区变成空闲区,空闲区变成使用区,并重复上述操作

  • 内存泄露

    • 循环引用
    • 闭包
    • 没有终止的 setInterval

性能优化

最全的页面性能优化

输入一个链接地址到显示到页面上具体经过了那些步骤

  • 浏览器首先会根据输入的网址,进行 DNS 域名解析找到对应服务器的ip地址

  • 建立连接 (TCP/IP 协议)三次握手 客户端向服务端发送SYN 服务器恢复 SYN-ACK,最后客户端发送 ACK 报文

  • 发送请求(请求头,请求体)到服务器,服务器会根据传入的地址,找到对应的文件返回到客户端

  • 客户端下载文件后,会解析文件,将标签和样式通过对应的parse 转化为 dom 树和 CSSOM 树

  • 如果在解析的过程中遇到 script 标签,如果script 标签没有引用外部文件,则会停止文件解析,等待 script 内部代码执行完成后才会继续解析,如果引入了外部文件,则会根据 defer 属性和 async 属性,来判断是否延迟加载 js文件。

  • 将生成的 DOM 树和 CSSOM 树合并形成渲染树,进行布局计算,将计算结果会渲染屏幕上

  • 渲染时涉及到图层的概念(position,translae3d,will-change, transform),启动GPU加速,将所有图层最后组合到页面上正成最终结果

浏览器缓存

  • cache-control

    • max-age 最大缓存时间
    • no-store 浏览器不会进行缓存,也不会进行协商缓存
    • no-cache 浏览器进行协商缓存通过 ETag/If-Matchlast-modified
    • public 可以被任何缓存例如共有缓存(CDN)和私有缓存(浏览器)
    • private 仅可被私有缓存(浏览器)缓存
  • 强缓存:

    强缓存主要是向浏览器缓存区获取文件,并且根据文件是否失效(max-age/exproes)来判断是否需要重新重服务器拿取文件

  • 协商缓存:

    当强缓存失效时,会走协商缓存,协商缓存不会直接从浏览器缓存中拿取数据,而是直接向服务器发送请求,服务器根据 ETag 和 last-Modified 参数和服务器文件进行对比,如果发生变更,则代表文件已修改,返回给客户端最新的文件,如果没有变更再去读取本地缓存的文件

  • 缓存类型

    • server-work (sw.js)走的是server-work 的缓存逻辑
    • memory cache 存在内存中,如果关闭tab页缓存会失效
    • disk cache 存在磁盘缓存中

重绘和重排(回流)

  • 重绘 是指元素自身属性发生变化时,并不会影响其他元素位置和属性变化,例如:color,opciaty,backgound等
  • 重排是指元素本身的物理尺寸发生变化时,引起页面重新排列,列如修改宽,高,外边距,内边距,边框,添加和删除元素,display: none等

如何避免频繁触发重绘和重排

  • 如果涉及到频繁修改样式,可以采用统一修改的方式,例如,添加 class 或者 cssText
  • 设置 position: absolute 脱离文档流
  • 这是 transform 和 will-change 属性,启动 gpu加速

  • 修改元素时批量修改

    • 设置 display: none;修改完成后在显示出来
    • 使用文档片段 (document.createDocumentFragment())
    • 创建一个熄灯节点,用来更新内容,更新换成后替换旧节点
  • 获取以下属性和方法时,页面会被强制更新,因为这些属性需要根据页面上元素的最新状态进行更新
    offsetTop、offsetLeft、offsetWidth、offsetHeight
    scrollTop、scrollLeft、scrollWidth、scrollHeight
    clientTop、clientLeft、clientWidth、clientHeight
    getComputedStyle()
    getBoundingClientRect

HTTP1/HTTP2

  • HTTP1.0 在 HTTP1.0 中,每发送一次请求,都要与服务器建立一次连接,请求完成后,连接会立即中断,再加上浏览器对于请求并发的限制,如果有一个请求一直处于padding 状态,则会影响后续接口的请求

  • HTTP1.1 则支持长连接,以及多个请求共用一次连接,然后返回结果根据请求顺序进行返回,虽然减少了连接次数,但是由于需要按照顺序返回,因此一旦有一个请求处于padding 状态,后面的接口即使结果已经拿到也不会返回给前端,而是等padding 结束后才返回给前端

  • HTTP2.0 支持多路复用以及二进制传输,首部压缩等操作,

    • 多路复用 同一个tcp连接,可以同时发送多个请求和回应
    • 二进制分帧 每一个请求和回应都生成一个独特的id,客户端和服务端根据id拿取数据,这样不会发生 堵塞影响后续接口数据传输
    • 头部压缩 客户端和服务端同时维护一套 头部信息键值对,可以减少数据传输量

HTTP/HTTPS

  • HTTP 超文本运输协议,是实现网络通信的一种规范,可以将信息从一个设备发送到另一个设备

  • HTTPS 在 HTTP 的基础上添加了加密协议(SSL/TLS)传输的信息是1进行加密过的,但是由于需要进行加密验证,所以性能方法消耗比较大

跨域出现的原因及解决方法

出现原因:主要是同源策略,避免其他服务器的攻击,什么是同源策略呢?就是域名,端口号,还有协议必须要一样,如果有一个不一样,就不满足同源策略

解决方式:

  • jsonp 主要利用script 标签不会被同源策略影响
  • postMessage
  • cors 后端通过设置 Access-Control-Allow-Origin 参数,允许白名单域名进行访问
  • document.domain

事件流

浏览器事件触发会经历三个阶段

  1. 事件捕获阶段(由最外层元素,向目标元素查找)
  2. 目前事件阶段(触发事件元素)
  3. 事件冒泡阶段(由目标元素向直至最外层元素)

事件类型又分为三种

  1. 原始事件 (on + 事件名) 相同事件只能存在一个,后面的会覆盖前面的
  2. 标准事件 (addEventListener) 相同事件可以绑定多个函数,这些函数都会触发

web 常见的攻击方式

  • XSS (跨站脚本攻击)
  • CSRF 跨站请求伪造
  • SQL 注入

解决方式:

  1. 尽量不要使用 innerHTML,如需使用,需要对内容进行转码或者过滤等操作
  2. 对于发送给服务端数据,需要进行转码操作
  3. 针对 CSRF 进行同源检测,token验证等
  4. 针对SQL 注入,过滤和转义特殊字符串,检查输入内容,提前进行拦截

CDN 缓存

CDN (全称 Content Delivery Network),即内容分发网络,构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。

当开启 CDN 缓存时,用户访问文件时,会首先访问 CDN 距离用户地区最近站点查找,如果查找到文件,会根据文件缓存时间是否过期等信息判断是返回缓存的文件还是向服务器重新拿取文件,如果没有找到则会向服务拉取最新文件进行缓存,并且发送给客户端

可以通过修改文件名称,修改路径地址,清空对应文件 CDN 缓存。

SEO 优化

  • title,meta标签(description,keywords,og:title…)
  • image alt 和 src 属性
  • link conial
  • a 标签的 href 属性
  • p 标签和 H1-6 标签
  • 结构化数据(HowTo,Prodduct,Score…)

浏览器线程和进程之间的关系

  • 进程是Cpu 最小的资源分配单位
  • 线程是 CPU 最小的调度单位

一个进程可以同时存在多个线程,同一个进程下的所有线程共用一个存储空间

浏览器常用的进程大致分为以下几种

  1. 浏览器进程
  2. GPU 进程
  3. 浏览器渲染进程
  4. 每个标签页都有一个单独的进程
  5. 浏览器的扩展插件也有自己的一个单独进程

相比于单进程浏览器,多进程有如下优点:

  1. 避免单个page crash影响整个浏览器
  2. 避免第三方插件crash影响整个浏览器
  3. 多进程充分利用多核优势
  4. 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

浏览器内核有哪些线程

  1. GUI 渲染进程
  2. js 引擎(js内核)
  3. 事件触发线程
  4. 定时触发器线程
  5. 异步HTTP 请求

Vue2

常用指令

  • v-if
  • v-else
  • v-else-if
  • v-show
  • v-for
  • v-html
  • v-text
  • v-bind
  • v-on
  • v-model
  • v-once
  • v-slot 插件

常用修饰符

  • number
  • .lazy v-model 监听change 而非 input
  • keyup/keydown
  • trim
  • prevent/stop 阻止默认事件
  • once 只执行一次
  • right/left 鼠标左右键点击触发
  • camel 将属性转驼峰

生命周期

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestory
  • destoryed
  • errorCaptured 错误监听
  • activited keep-alive
  • deactivited keep-alive

常用属性

  • name
  • data
  • props
  • provide
  • inject
  • computed
  • watch
  • directives
  • filters
  • methods
  • mixin

组件通讯

  • 父子组件通讯
    • props
    • provide/inject
    • $emit
    • $parent/$children
    • $ref
    • event bus
  • 兄弟组件
    • 通过父组件传值
    • $parent
    • event bus
  • 不相关组件
    • event bus
    • Vuex

Vue 组件之间通讯的各种情况

computed 和 watch

  • computed 计算属性,计算后的结果值会被缓存下来,如果相关联的属性没有发生变化时,会取缓存后的结果
  • watch 监听属性,当属性发生变化时,会重新执行监听函数,针对对象可以使用 immediate 和 deep 来启动立即监听和深度监听

在日常使用中更推介使用computed 属性,因为会对得到的结果进行缓存,减少计算量

diff 算法 (双指针遍历)

当组件创建和更新时,vue会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom

对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程

在对比时,vue采用深度优先、同级比较的方式进行比对。同级比较就是说它不会跨越结构进行比较

在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的

具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。

在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。

这样一直递归的遍历下去,直到整棵树完成对比。

请阐述vue的diff算法

Vue diff 算法双指针逻辑总结

新创建的 子节点数组 和 旧的子节点数组分别有一个 头节点指针和尾结点指针。

针对旧节点的两个指针,如果新节点的两个指针有匹配到的,则将该指针所对应的 Vnode 转移到对应的位置(头对头,尾队尾),对应头结点和尾结点的位置进行变化(头结点后移,尾结点前移)

知道头尾节点均不符合匹配时,这样就筛选出来两类节点,一种是旧子节点数组上需要删除的子节点,一种是新的子节点上需要新增的子节点。

这样做的目的是最大可能性的复用已有的真实dom节点,对于新增的,需要创建新的 dom节点。

MVVM 框架

MVVM 是指双向数据流,即 View-Model 之间的双向通信,由 ViewModel 作桥接,当model 发生变化时,会通过 view-model 修改 view 视图,当 view 视图发生变化时,通过 view-model 修改 model,比较典型的就是 vue 的双向数据流绑定,通过 Object.defineProperty和 Proxy 对数据修改和获取进行拦截,收集依赖。进行绑定

Mvvm 前端数据流框架精讲

v-for 和 v-if

v-for 的优先级比v-if 的优先级要高,页面会首先根据 v-for 渲染列表,然后在根据 v-if 来判断列表元素是否显示和隐藏,因此会造成性能浪费

因此推荐使用 computed 属性,提前过滤需要隐藏后的数据后,在使用 v-for 进行渲染

v-show 和 v-if

v-show 会进行dom 树的构建,但是 display 属性为 none

v-if 不会进行组件渲染,自然也不会构建 dom 树,对性能有优化

如果一个元素经常会触发显示和隐藏操作,可以使用 v-show,如果不会经常触发,推介使用v-if

v-for 循环时 key的作用是什么

进行diff 算法时,方便进行对比组件是否更新,以及组件复用

会首先对比key 是否一样,如果一样在判断标签或者 类型是否一致(input)这样可以节省一部分对比操作,提升性能

$nextTick 原理

主要是利用了 javascript 引擎的事件循环机制,是的 $nextTick 传入的参数在同步事件之后触发,可以拿到 修改后的dom和 data 里面的值

使用 Promise.then,setImediate, setTimeout, mutationObserve 来实现

Vue 是如何监听属性变化的

使用 Object.defineProperty 来监听属性的变化,重写get,set 方法,并且新增 Watcher 方法,当修改属性值时,触犯到 set 方法,在set 方法中会收集到当前这个属性所关联的依赖,并通知这些依赖进行更新.

每个组件被创建的时候($mount,mountComponent,$watch)都会创建一个新的 Watcher 实例,并且赋值给 当前组件内进行数据监听时创建的 Dep 对象的 target,当我们初始化实例的时候,会调用 depend 方法,会将当前 Dep 添加到 Watcher 里面,会通过 Watcher 的 addDep 方法,把当前Watcher 添加到 Dep 的依赖(dep) 中,修改属性值的时候会调用 notify 方法,去执行该属性关联的 Watcher 的 update 方法,更新数据。

那几个方法会创建 new Watcher

纵观源码,有以下几个地方会创建 Watcher

  • computed,在非 SSR 的情况下会给每个 监听的变量创构建一个 watcher
  • $mount 方法 mountComponent 在挂载组件的时候也会创建一个 Watcher
  • $watch 方法
1
2
创建 Watcher
使用 Object.defineProperty 对数据获取和更新进行拦截,get操作时添加依赖(watcher),set 时,notify 依赖执行 update 操作,更新数据和对应页面

data 为什么要用一个函数的模式

是因为组件是可以重复使用的,如果data 不用一个函数模式的话,可以会造成数据污染,无法获取到想要的数据

Vuex 有几个模块

  • module 用来区分模块
  • state 用来存储值
  • mutations 用来修改state 的值
  • actions 用来异步操作,通过 mutations 修改值
  • getters 计算属性,会根据state最新的值来计算

Vuex 提供了 mapGetters,mapState,mapActions 等方法,方便我们快速获取Vuex 的值

Vuex 主要时通过Vue.use 注册到 Vue 实例上面的,其原理和 vue 的双向绑定原理类似,都是如果组件使用了Vuex 里面的属性,则会将该组件添加到属性的依赖上面,因此如果属性发生变化,就可以监控到那些组件需要执行更新操作

Vue-router 有几个模式

设置 router 的 mode 属性,默认为 hash 模式,设置 history 后,为history模式

  • hash模式 在网址后面以 # 模式
  • history 模式 直接跟在网址后面

Vue-router 可以使用懒加载的方式

Vue 项目如何优化

  • 路由懒加载 () => import(“”)
  • 组件懒加载 () => import(“”)
  • v-if 和 v-for 避免一起使用,可以用计算属性过滤部分数据
  • 针对经常显示隐藏的页面结构,可以使用 v-show,否则应使用 v-if

等等

自定义指令 (directive)

有以下几个周期,可以对dom 元素的各个状态进行监听

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestory
  • destoryed

Vue3

Vue3 相对比 Vue2 新增了哪些东西?

  • composition API
    ref,reactive,watch,watchEffect,computed 等
  • setup

    setup 执行顺序在 beforeCreate 生命周期之前,因此也不能获取到 DOM,因此提供了 生命周期钩子函数 (on + 生命周期) ,通过 return 把参数和方法暴露出来

  • beforeDestory -> beforeUnmount, Destoryed -> unmount
  • 数据监听方式由 Object.defineProperty 变更为 Proxy,这样的话一个是针对多层 object 对象不需要进行递归监听,其次是针对数组的新增和修改操作也能更加方便的监听

vue3 相比于 vue2 针对性能做了哪些优化

  • 对组件类型进行区分,对静态节点进行了优化,会对静态节点保存下来,进行更新操作时,不需要重新创建节点
  • 碰见连续静态节点的时候,会将这一段静态节点转成 html 字符串,直接使用 innerHTML 方法添加到元素内,减少了递归操作
  • 缓存事件处理函数
  • 对静态节点进行优化,虚拟dom对比时,直接跳过虚拟节点,减少对比和递归次数

React

生命周期

挂载阶段

  • constructor
  • componentWillMount(新版 getDerivedtateFromProps)
  • render
  • componentDidMount

更新阶段

  • componentWillReceiveProps (getDerivedtateFromProps))
  • shouldComponentUpdate
  • componentWillUpdate (null)
  • render
  • (getSnapshotBeforeUpdate)
  • componentDidUpdate

卸载阶段

  • componentWillUnmount

为什么新版的react 的生命周期要加上 UNSAFE_ 前缀

这个是因为这个函数不安全,原因是因为 react 16 以后 react 支持 fiber 切片后,组件会根据执行优先级来进行编译和渲染,因此可能会出现一个组件正在执行更新或者创建操作时,有一个优先级更高的任务插进来,这个时候,当前任务会被暂停掉,更换优先级更高的任务执行操作,操作完成后,才会回到之前的操作,继续执行,这个时候,部分生命周期有可能会执行两次,造成数据混乱

虚拟dom(virtual DOM)

virtual DOM 又称虚拟DOM, 是使用 JavaScript 对象来描述真实DOM节点,virtual DOM 内存储了真实DOM的各种信息,标签名,属性名包括子元素,virtual DOM 之间通过树形结构连接,与真实DOM一一对应,构成了最终的虚拟DOM树

virtual DOM 的修改不会触发页面的重绘和重排,可以有效避免真实DOM的频繁更新,通过对应的编译器可以实现一套代码,多端运行,但是由于 还需要渲染成真实DOM显示到页面上面,因此首次渲染时速度较慢

diff 算法

React技术揭秘-卡颂

React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中n是树中元素的数量

React diff 算法做了以下优化

  1. 只进行同级比较,如果一个节点在更新前后层级不一样,则不会进行比较
  2. 不同类型的子元素是一个新的树,旧的树会被直接销毁,新的树会被创建
  3. 可以通过key 值来判断那些子元素保持稳定

react 在进行 diff 算法时,会首先针对 节点的 类型,key进行对比,如果都一样,在对比传入的 props,state 等属性是否有变化,有变化执行更新操作,没有变化就直接复用,不过不一样,则会创建一个新的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 之前
abcd

// 之后
dabc

===第一轮遍历开始===
d(之后)vs a(之前)
key改变,不能复用,跳出遍历
===第一轮遍历结束===

===第二轮遍历开始===
newChildren === dabc,没用完,不需要执行删除旧节点
oldFiber === abcd,没用完,不需要执行插入新节点

将剩余oldFiber(abcd)保存为map

继续遍历剩余newChildren

// 当前oldFiber:abcd
// 当前newChildren dabc

key === d 在 oldFiber中存在
const oldIndex = d(之前).index;
此时 oldIndex === 3; // 之前节点为 abcd,所以d.index === 3
比较 oldIndex 与 lastPlacedIndex;
oldIndex 3 > lastPlacedIndex 0
则 lastPlacedIndex = 3;
d节点位置不变

继续遍历剩余newChildren

// 当前oldFiber:abc
// 当前newChildren abc

key === a 在 oldFiber中存在
const oldIndex = a(之前).index; // 之前节点为 abcd,所以a.index === 0
此时 oldIndex === 0;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 0 < lastPlacedIndex 3
则 a节点需要向右移动

继续遍历剩余newChildren

// 当前oldFiber:bc
// 当前newChildren bc

key === b 在 oldFiber中存在
const oldIndex = b(之前).index; // 之前节点为 abcd,所以b.index === 1
此时 oldIndex === 1;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 1 < lastPlacedIndex 3
则 b节点需要向右移动

继续遍历剩余newChildren

// 当前oldFiber:c
// 当前newChildren c

key === c 在 oldFiber中存在
const oldIndex = c(之前).index; // 之前节点为 abcd,所以c.index === 2
此时 oldIndex === 2;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 2 < lastPlacedIndex 3
则 c节点需要向右移动

===第二轮遍历结束===

总结来说就是尽量复用已有的节点,将已有的节点按照新的节点数组进行排序,没有的节点再新增

总共进行了两次遍历,第一次遍历是为了找到相同的前缀子节点数组,第二次遍历是为了找到剩余子节点数组中可以复用的子节点。

fiber 分片

React技术揭秘-卡颂

React Fiber可以理解为:React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。

总的来说,每一个 fiber 节点都对应着一个 react element,并且存储着当期节点的 属性,状态和值,fiber 与 fiber 之间通过 return,child,sibling 参数连接

在开始遍历的时候,会首先判断 child 是否有值,如果有值,会遍历child 的第一个值,然后逻辑同上。

如果child 没有值,会通过 sibling 查找当前节点的兄弟节点,兄弟节点查找完成后,会通过 return,回到父节点,然后继续执行操作(感觉像是深度优先遍历)

在进行 dom 挂载和更新的操作,fiber 采用的是双缓存。也就是说,react 在进行更新操作的时候,是存在两个fiber 树的,一个是当前页面渲染的旧的fiber 树(current Fiber),另一个是页面更新后需要渲染的 fiber 树(workInProcess Fiber),这两个 fiber 之间通过 alternate 参数连接

在更新过程中,会根据diff 算法判断是否需要复用原来的 fiber 节点(节省时间,提高效率)

页面更新完成后,workInProcess Fiber 将会变成 current Fiber,原来的 fiber 树将会被舍弃掉

常用 hooks

  • useState
  • useEffect
  • useLayoutEffect 在 页面渲染时触发,应该是拿不到dom 对象
  • useRef 既可以用来存值,也可以用来获取dom对象
  • useMemo 缓存值
  • useCallback 缓存函数
  • useContext 上下文
  • useReducer
  • useImperativeHandle/forwardRef 可以将子组件的方法暴露给父组件来使用

    useImperativeHandle 可以将子组件中部分函数暴露给父组件的 ref使用

useMemo 和 useCallback 的区别

useMemo 缓存的是值

useCallback 缓存的是具体函数

因为 React 函数式组件每次更新的时候,都会销毁上一次的上下文,然后重新创建新的上下文,也就是说在函数式组件内创建的函数,都会被重新创建

组件通讯

  • mobx/redux 等状态管理工具
  • props
  • ref/useImperativeHandle
  • 发布订阅模式
  • React.createContext/useContext(React.createContext/Provider/Consumer)

React 如何监听数据变化

主要是通过 setState,useState,forceUpdate,会生成一个 Update 对象,会和原属性的值进行对比,如果发生改变,则会触发更新操作,这个对比采用的是浅对比的方式。

Update 有一个更新队列,存放着这个组件本次需要执行的所有更新操作

Vue 和 React 有什么异同点?

  • 共同点如下:

    1. 都是javascript 的一个框架,定义了一套属于自己的方法来实现项目搭建
    2. 都采用了 虚拟DOM 的方式来对真实DOM 进行描述
    3. 都采用diff 算法来对组件更新时候的节点比较进行优化
    4. 都实现了自己的一套模板,vue(template),react(jsx)
  • 不同点如下:

    1. vue 是采用 Object.defineProperty(Proxy) 来实现双向绑定原理的,,react 是单向数据流,修改值的话,只能通过 setState,useState,focusUpdate,render才会出现页面的更新操作
    2. 模板实现原理不一样,vue(template),react(jsx)
    3. diff 算法实现逻辑不一样,vue 采用的是双指针的方式,react 则采用单指针比较,同时检测出可复用的节点(两次循环)
    4. react 实现了 fiber,支持根据优先级进行渲染,优先级低的组件渲染可被中断,采用了双缓存的逻辑,方便进行一部分 fiber 的复用

Webpack

多入口页面

通过 entry 和 output 参数可以设置多入入口文件及打包后的文件名称和位置

接下来通过 html-webpack-plugin 插件指定文件渲染模板,此外还可以设置生成文件的 title meta 标签以及link 标签等内容,以满足部分 SEO 需求

loaders 的使用

  • 常用loader
    具有那些loader 可以查看这里
    • style-loader
    • css-loader
    • sass-loader
    • less-loader
    • postcss-loader
    • ts-loader
    • babel-loader
    • vue-loader

webpack 打包流程

  • 首先通过 entry 找到入口文件
  • 通过各种loader(ts-loader,js-loader,style-loader,sass-loader) 等将需要打包的文件转成浏览器识别的语言
  • 接下来通过 output 将转化成的代码生成js文件,如果有分包操作,将会被拆解成多个文件
  • 然后通过 plugin 将 文件写入到html文档中,如果有html-webpack-plugin 则会根据对应的chunk 和模板文件写入

Vite

深入理解Vite核心原理

Other

你在工作中觉得有没有什么比较难的事情

我觉得这个其实挺蛇皮的,这种问题看起来没有一点可以问的价值,正常来说只要能完成,我觉得都没什么难的,以下列一下我在工作中认为比较难的事情

  • 项目底层架构升级,需要对项目各个功能,参数,还有调用方式都有一个清晰的认知,否则在进行升级的过程中会出现各种各样的问题
  • 多页面打包配置,基于 gitlab 实现多页面项目按需打包,避免发版影响页面过多
  • 移动端样式适配,适配不同浏览器及移动端浏览器适配(样式,功能(下载))

这种东西其实在做的时候感觉挺难的,其实会想之后感觉也就那样

设计模式

用最简便的方法描述

  • 工厂模式
  • 抽象模式
  • 单例模式
  • 观察者模式
  • 状态模式
  • 组合模式
  • 享元模式
  • 原型链模式
  • 桥接模式
  • 装饰器模式
  • 代理模式
  • 策略模式
  • 访问者模式
  • 委托模式
  • 数据访问对象模式
  • 节流模式
  • 参与者模式

npm,pnpm,yarn 之间的区别

三者目前都是主流的包管理工具

一文看懂npm、yarn、pnpm之间的区别

  • npm

使用 npm 添加安装插件的时候,会在版本号前面有一个 ^ 符号,这个符号告诉npm 可以下载当前主版本号的任意一个版本,因此会出现 不同开发人员下载三方包的时候,下载下来的包不是同一个版本的包。

npm必须首先遍历所有的项目依赖关系,然后再决定如何生成扁平的node_modules目录结构。npm必须为所有使用到的模块构建一个完整的依赖关系树,这是一个耗时的操作,是npm安装速度慢的一个很重要的原因。

npm是有本地缓存的,它保存了已经下载的每个版本的压缩包。

  • pnpm

pnpm采用了一种巧妙的方法,利用硬链接和符号链接来避免复制所有本地缓存源文件。

同时pnpm运行起来速度也是很快的

  • yarn

使用yarn 进行安装的时候,会对下载的插件版本号和package.json 中的版本号进行匹配,因此不会出现版本不同的情况

yarn 同样也提供了本地缓存的功能

  • cnpm

cnpm 主要是 npm 的镜像,主要用于国内下载资源加速。

1
2
// 安装cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org

GIT

正常提交流程

1
2
3
4
git add .
git commit -m "message"
git pull
git push

合并分支

1
git merge branch-name

切换,创建,删除分支

1
2
3
git checkout <branch-name> // 切换分支
git checkout -b <branch-name> // 创建本地新分支
git checkout -d <branch-name> // 删除分支

拉取代码

1
2
git clone github.com/sample.git // 拉取代码
git clone -b <branch-name> github.com/sample.git // 拉取指定分支代码

git 回滚

1
2
git revert -n [hash] // 在这个版本基础上重新生成一条记录
git reset --hard [hash] // 本地强制回滚至上一次提交,并覆盖记录

设置用户

1
2
git config --global user.name [name]
git config --global user.email [email]

其他用法

1
2
3
git log // 查看提交记录 q 退出查看
git diff branch1 branch2 // 对比分支修改记录
git cherry-pick [hash] // 合并末次commit 内容到当前分支

手写算法题

数组方法重写

后续持续更新,敬请期待