面试问题总结
1. 父子组件生命周期执行顺序
Vue的父子组件钩子函数的执行顺序可以归类为4个部分
- 第一部分:首次加载渲染
父beforeCreate -> 父crearted -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
- 第二部分:子组件更新
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父 updated
- 第三部分: 父组件更新- 不会影响子组件
父 beforeUpdate -> 父 updated
- 第四部分:组件销毁
父 beforeDestroy -> 子 beforeDestory -> 子 destoryed -> 父 destoryed
2. Vue 路由钩子
2.1 路由钩子分为三种
- 全局钩子: beforeEach、afterEach、 beforeResolve
- 当个组件里面的钩子:beforeEnter
- 组件路由:beforeRouteEnter、 beforeRouteUpdate、beforeRouteLeave
2.2 全局钩子
- beforeEach: 全局前置守卫 进入路由之前
- beforeResolve: 全局解析守卫(2.5.0 +)在beforeRouteEnter调用之后调用
- afterEach: 全局后置钩子
2.3 路由的三个参数
- to: 将要进入的对象
- from: 将要离开的对象
- next: 这是一个函数,且必须使用,否则不能进入路由-可以携带参数 (path/name)
2.4 路由组件内的守卫
- beforeRouteEnter 进入路由前
- beforeRouteUpdate (2.2) 路由复用同一个组件时
- beforeRouteLeave 离开当前路由时
2.5 触发钩子的顺序
将路由导航、keep-alive、和组件生命周期钩子结合起来,触发顺序。假设是从A组件离开,第一次进入B组件
- beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
- beforeEach: 路由全局前置守卫,可用于登录验证、全局路由loading等
- beforeEnter: 路由独享守卫
- beforeRouteEnter: 路由组件的组件进入路由前钩子
- beforeResolve: 路由全局解析守卫
- afterEach: 路由全局后置钩子
- beforeCreate: 组件生命周期,不能访问this
- created: 组件生命周期,可以访问this、不能访问dom
- beforeMount: 组件生命周期
- deactivated: 离开缓存组件A,或者触发A的beforeDestory和 destoryed 组件销毁钩子
- mounted: 组件生命周期, 可以访问和操作dom
- activated: 进入缓存组件,进入A的嵌套父组件-前提是有
- 执行 befoerRouterEnter 回调函数next
3. Vue组件间的参数传递
3.1. 父组件和子组件传值
- 父向子传值使用
props
- 子向父传值使用
$emit
- 通过
$parent
/$children
通信; - 使用
$ref
也可以访问组件实例; - 使用 provide / inject ;
3.2. 兄弟组件之间的传值
- 通过
$emte
和props
结合的方式 - 通过一个空的vue实例 也就是常说的
EventBus
- 使用
vuex
3.3. 跨级通信
- 通过一个空的vue实例 也就是常说的
EventBus
- 使用
vuex
- 使用 provide / inject
- 使用 $attrs / $listeners
4. Vue路由的实现
Vue路由有两种模式:1.hash模式 2. history模式
4.1 hash模式 在浏览器中有个符号#
, #及#以后的字符成为hash, 用window.location.hash 读取
- hash虽然在URL中,但不被包含在http请求中
- 用来指导浏览器动作,对服务端安全无用
- hash不会重载页面
- hash模式下,仅hash符合之前的内容会被包含在请求中,如 http://www.xxx.com
- 对于后端来说,即使没有找到对应路由的全覆盖,也不会返回404错误
4.2 history模式 history采用了HTML5的新特性
- 有两个方法“ pushState() 和 replaceState()
- 可以对浏览器记录进行修改,以及popState事件的监听到状态变更
- history模式下,前端的URL必须和实际向后端发起请求的URL一致
- 如 ‘http://www.xxx.com/item/id'。后端如果缺少对 /items/id 的路由处理,将返回404错误
5. 什么是 Vuex
5.1 vuex有哪几种属性
有五种,分别是 State、Getter、Mutation、Action、Module
5.2 Vuex中的State特性是什么
- Vuex就是一个仓库,仓库里面放了很多对象。其中State就是数据存放地,对应于与一般Vue对象的data
- state里面存放的数据是响应式的。Vue组件从store中读取数据,若是store中数据发生改变,依赖这个数据的组件也会发生更新
- 它通过mapState 把全局的state 和 getters 映射到当前组件的 computed 计算属性中
5.3 Vuex中的Getter特性是什么
- getters 可以对State进行计算操作,简而言之就是state的计算属性
- 虽然在组件中也可以做计算属性,但是getters 可以在多组件复用
- 如果一个状态只在一个组件内使用,是可以不用getters
5.4 Vuex中的Mutation特性是什么
- mutation是一个对象,包含多个直接更新state的方法(回调函数)
- 在vue严格模式中, mutation是vuex改变state的唯一途径
- mutation只能执行同步操作, 不能写异步代码
- 通过 store.commit() 调用 Mutation
5.5 Vuex中的Action特性是什么
- Action 类似于 mutation,不同在于:
- Action 提交的是mutation,而不是直接变更state值
- Action可以包含任意异步操作
- 通过 store.dispatch() 调用 Action
5.6 Vuex中的Module特性是什么
- 用来分割store,生成不同的模块
- 每一个模块都拥有自己的state,mutation,action,getter
- 防止同名变量污染
5.7 Vue.js中ajax请求代码应该写在组件的methods中还是vuex的actions中?
- 先看请求的数据是不是呗其他组件共用,仅仅在请求的组件内使用,则不需要放入vuex的state里面
- 如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用,并包装成promise返回。在调用处用async await处理返回的数据。如果不需要复用,则直接卸载vue组件文件里面就可以
6. 什么是原型 和 原型链
6.1 原型
在JavaScript中,每定义一个函数的数据类型(普通函数、类)时候,都会天生自带一个属性·prototype·,这个属性指向了函数的原型对象,并且这个属性是一个对象数据类型。
让我用一张图表示构造函数和实例、原型对象之间的关系:
6.2 原型链
6.2.1 __proto__
和 prototype
、constructor
__proto__
是每一个子对象(除null外)都会有的一个属性,指向该对象的原型prototype
这是每个函数都有的一个属性,指向了该函数的原型对象constructor
每个原型都拥有一个constructor属性, 它指向了关联的构造函数
6.2.2 原型链
每个对象拥有一个原型对象,通过__ptoto__
属性指向其原型对象,并从中继承方法和属性。同时原型对象也可能拥有原型,这样一层一层,最终指向null(通常也就是Object.prototype.ptoto).这种关系就被称为原型链。通过原型链一个对象可以拥有定义在其他对象中的属性和方法
7. JavaScript 设计模式
8. 深拷贝和浅拷贝
8.1 浅拷贝
浅拷贝只能拷贝复杂数据类型的指针,并不能改变复杂数据类型的地址,只能拷贝外层,并不能彻底拷贝,例如数组中还有数组(对象)。(准确来说是外层引用数据类型)
8.2 深拷贝
不光外层的引用地址改变了 内层的引用数据类型也发生改变
- 用递归依次遍历赋值所有属性
1 | obj = { |
- JSON.parse(JSON.stringify())
这个方法最简单方便,但是只能对JSON对象、数组使用。当时有个问题
对比图:
JSON.parse(JSON.stringify())拷贝后的缺陷:
- NaN ===> null
- undefined ===> 空
- 时间戳或者date对象 ===> 字符串时间
- 错误信息 ===> 空对象
- Infinity ===> null
9. vue2 实现双向绑定的原理
vue.js
则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持setter,getter.在数据变动时发布消息给订阅者,触发相应的监听回调
Object.defineProperty():方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
1 | var obj = {}; |
object.defineProperty的缺点
- 因为es5的object.defineProperty无法监听对象属性的删除和添加
- 不能监听数组的变化,除了push/pop/shift/unshift/splice/spObject.definert/reverse,其他都不行
- Object.defineProperty 只能遍历对象属性直接修改(需要深拷贝进行修改)
10 vue3实现双向绑定的原理
vue3 主要通过es6的proxy 和 Reflect实现的双向绑定
proxy:用于修改某些操作的默认行为,等同于在语言层面做出修改。相当于在目标前假设一个拦截层,外层对该对象的访问都必须经过这层拦截。
reflect:es6为操作对象而提供的新的api。
目的有:
- 将Object对象的一些明显属于语言内部的方法,放到Reflect对象上。如Object.defineProperty
- 修改某些Object方法的返回结果,让其变得更合理。比如利用Object.defineProperty给基本数据类型添加一个属性,就无法对属性进行定义,这时会抛出一个错误。而Reflect.defineProperty(obj,name,desc)会返回false。
- 将让Object操作都变成函数行为。(Reflect.has(obj,name) Reflect.deleteProperty(obj,name))
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
proxy 优点
- 直接监听对象而非属性
- 直接监听数组的变化
- 拦截的方式有很多种(有13种,set, get,has)
- proxy 返回一个新对象,可以操作新对象达到目的
proxy缺点
- proxy 有兼容性的问题,不能用polyfill来兼容(polyfill主要抚平不同浏览器之间对js实现的差异)
1 |
|
11. 为什么Vue中data一定要是函数
Vue中data必须是函数是为了保证组件的独立性和可复用性,data是一个函数,组件实例化的时候这个函数将会被调用,返回一个对象,计算机会给这个对象分配一个内存地址,你实例化几次,就分配几个内存地址,他们的地址都不一样,所以每个组件中的数据不会相互干扰,改变其中一个组件的状态,其他组件不变
12. vue 插槽
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于父组件 => 子组件
12.1 匿名插槽(默认插槽) 定义一个插槽不指定name属性,就是通过替换占位符达到在父组件中更改子组件中内容的效果
语法:
<slot></slot>
在子组件中放置一个占位符(slot):
1 | <template> |
然后在父组件中引用这个子组件,并给这个占位符(slot)填充内容:
1 | <template> |
这时页面展现的内容会是【父组件:我就是默认插槽内容】。
12.2. 具名插槽 就是当需要多个插槽时,给子组件中不同的插槽取一个名字,而父组件就可以在引用子组件的时候,根据这个名字对号入座,将对应内容填充到相应的插槽中
语法:
<slot name="名称"></slot>
在子组件中放置两个具名插槽:
1 | <template> |
在父组件中引用该子组件,并通过v-slot:[name]的方式将相应的内容填充到相应的插槽中:
1 | <template> |
这时页面展示的内容会是【两个插槽:第一个插槽one,第二个插槽two】。v-slot:one
和v-slot:two
可以简写成#one
和#two
语法糖
使用默认插槽和具名插槽的注意事项
- 如果子组件中存在多个默认插槽,那么父组件中所有指定到默认插槽的填充内容(未指定具名插槽),会全部填充子组件的每个默认插槽中。
- 即使父组件中将具名插槽的填充顺序打乱,只要具名插槽的名字对应上了,填充的内容就能被正确渲染到相应的具名插槽中,一个萝卜一个坑。
- 如果子组件中同时存在默认插槽和具名插槽,但是子组件中找不到父组件中指定的具名插槽,那么该内容会被直接丢弃,而不会被填充到默认插槽中
12.3. 作用域插槽 在子组件的插槽中带入参数(数据)提供给父组件使用,该参数(数据)仅在插槽内有效,父组件可以根据子组件中传过来的参数(数据)对展示内容进行定制
语法:
<slot name="自定义插槽名" :自定义name=data中的属性或对象></slot>
- 带有插槽 的组件 TipsText.vue(子组件)
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<template>
<div>
<div>hello world</div>
<!-- ⼦组件中告诉⽗组件,要实现obj⾥的信息 -->
<slot name="default" :info="info.title"></slot>
<slot name="other" :info="info.msg"></slot>
<slot name="text" :info="info.text"></slot>
</div>
</template>
<script>
import { reactive } from "vue";
export default {
setup() {
const info = reactive({
title:'作用域插槽',
msg:'这是一个作用域插槽的内容哦',
text:'how are you'
})
return {
info
}
}
}
</script> - 父组件 Test.vue 引入 TipsText.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<tips-text>
<template v-slot:default="slotProps" >
<div>{{slotProps.info}}</div>
</template>
<template v-slot:other="slotProps" >
<div>{{slotProps.info}}</div>
</template>
<template v-slot:text="slotProps" >
<div>{{slotProps.info}}</div>
</template>
</tips-text>
</div>
</template>
<script>
import TipsText from './TipsText.vue'
export default {
components:{ TipsText }
}
</script>
13. 事件绑定,事件类型,事件委托
13.1 事件绑定的方法
一是直接在标签内直接添加执行语句,二是绑定函数。第三种是事件监听(addEventListener)
13.2 DOM事件两种类型
- 事件类型分两种: 事件冒泡、事件捕获
- 事件冒泡就是有内往外,从具体的目标节点元素触发逐级向上传递,直到根节点。
- 事件捕获就是由外往内,从事件发生的定点开始,逐级向下查找,一直到目标元素
- 怎么阻止事件冒泡:vue里使用stop修饰符,原生js可以return false 还有 stopPropagation
13.3 事件委托
又名事件代理。事件委托就是利用事件冒泡,就是把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托也就没法实现了
好处:提高性能,减少了事件绑定,从而减少内存占用
示例:瀑布流:无限上拉列表中,如果给每一个图片绑定点击事件,非常繁琐且消耗内存。所以我们通常可以吧每张图片上的点击事件委托个共同的父元素
14. class和function 的区别
14.1 相同点: 都可以作为构造函数
14.2 不同点
- class构造函数必须使用new操作符。而普通
function
构造函数可以把window
作为this
来创建实例 - class声明不可以提升。
function
构造函数声明存在提升,也就是定义构造函数部分可以写在实例化对象的后面 - class不可以用call、apply、bind改变执行上下文。普通的
function
函数,调用时可以使用call
、apply
、bind
改变其this
指向 - class不能重复定义,会报语法错误
- class定义的类没有私有属性和私有方法
- class静态方法和静态属性
15.TCP三次握手和四次挥手
15.1.1三次握手
图中字符的含义:
SYN
: 连接请求/接收 报文段seq
: 发送的第一个字节的序号ACK
: 确认报文段ack
: 确认号。希望收到的下一个数据的第一个字节的序号
刚开始客户端处于 Closed 的状态,而服务端处于 Listen 状态:
CLOSED
:没有任何连接状态
LISTEN
:侦听来自远方 TCP 端口的连接请求
第一次握手: 客户端向服务端发送一个 SYN 报文(SYN = 1),并指明客户端的初始化序列号 ISN(x),即图中的 seq = x,表示本报文段所发送的数据的第一个字节的序号。此时客户端处于 SYN_Send
状态。
SYN-SENT
:在发送连接请求后等待匹配的连接请求
第二次握手: 服务器收到客户端的 SYN 报文之后,会发送 SYN 报文作为应答(SYN = 1),并且指定自己的初始化序列号 ISN(y),即图中的 seq = y。同时会把客户端的 ISN + 1 作为确认号 ack 的值,表示已经收到了客户端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 x + 1,此时服务器处于 SYN_REVD
的状态。
SYN-RECEIVED
:在收到和发送一个连接请求后等待对连接请求的确认
第三次握手: 客户端收到服务器端响应的SYN报文之后,会发送一个ACK报文,也是一样把服务器的ISN + 1作为ack的值,表示已经收到了服务端发来的SYN报文,希望收到的下一个数据的第一个字节的序号是 y + 1, 并指明此时客户端的序列号seq = x + 1(初始为seq = x,所以第二个报文段要 + 1),此时客户端处于Establised
状态
服务端收到了客户端的ACK报文后,也进入了Establised
状态,至此,双方建立了TCP连接、
ENTABLISHED
: 代表一个打开的连接,数据可以传送给用户
==注意:第三次握手是可以携带数据的,因为发送端已经确认了服务端有接受和发送的能力。而其他两次并没有确认对方的接受能力==
15.1.2 为什么要三次握手:
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
只有经过三次握手才能确认双发的收发功能都正常,缺一不可:
- 第一次握手(客户端发送 SYN 报文给服务器,服务器接收该报文):客户端什么都不能确认;服务器确认了对方发送正常,自己接收正常
- 第二次握手(服务器响应 SYN 报文给客户端,客户端接收该报文):客户端确认了:自己发送、接收正常,对方发送、接收正常;服务器确认了:对方发送正常,自己接收正常
- 第三次握手(客户端发送 ACK 报文给服务器):客户端确认了:自己发送、接收正常,对方发送、接收正常;服务器确认了:自己发送、接收正常,对方发送、接收正常
15.2.1 四次挥手
建立一个 TCP 连接需要三次握手,而终止一个 TCP 连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这是由于 TCP 的半关闭(half-close)特性造成的,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
TCP 连接的释放需要发送四个包(执行四个步骤),因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作。
上图中符号的意思:
FIN
: 连接终止位seq
: 发送的第一个字节的序号ACK
: 确认报文段ack
: 确认号。希望收到的下一个数据的第一个字节的序号
刚开始双方都处于ESTABLISHED 状态,假设是客户端先发起关闭请求。四次挥手的过程如下:
第一次挥手: 客户端发送一个FIN 报文(请求连接终止: FIN = 1),报文中会指定一个序列号seq = u。并停止再发送数据,主动关闭TCP连接。此时客户端处于FIN_WAIT1
状态,等待服务端的确认。
FIN-WAIT-1
: 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
第二次挥手: 服务端收到 FIN 之后,会发送ACK报文,且把客户端的序号值 + 1作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WA IT
状态.
CLOSE-WAIT
: 等待冲本地用户发来的连接中断请求;
此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN-WAIT-2
(终止等待2)状态,等待服务端发出的连接释放报文段。
FIN-WAIT-2
: 从远处TCP等待连接中断的请求;
第三次挥手: 如果服务端也想断开连接了(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送FIN报文,且指定一个序列号。此时服务端处于LAST_ACK
状态,等待客户端的确认,
LAST-ACK
: 等待原来发向远程TCP的连接中断请求的确认;
第四次挥手: 客户端收到FIN之后,一样发送一个ACK报文作为应答(ack = w + 1),且把服务端的序列值 + 1作为自己ACK 报文的序号值(seq = u + 1),此时客户端处于TIME_WAIT
(时间等待)状态
TIME-WAIT
: 等待足够的时间以确保远程TCP接收到连接中断请求的确认;
🚨 注意 !!!这个时候由服务端到客户端的 TCP 连接并未释放掉,需要经过时间等待计时器设置的时间 2MSL(一个报文的来回时间) 后才会进入 CLOSED 状态(这样做的目的是确保服务端收到自己的 ACK 报文。如果服务端在规定时间内没有收到客户端发来的 ACK 报文的话,服务端会重新发送 FIN 报文给客户端,客户端再次收到 FIN 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文给服务端)。服务端收到 ACK 报文之后,就关闭连接了,处于 CLOSED 状态。
15.2.2 为什么要四次挥手
由于 TCP 的半关闭(half-close)特性,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。
举个例子:A 和 B 打电话,通话即将结束后,A 说 “我没啥要说的了”,B 回答 “我知道了”,于是 A 向 B 的连接释放了。但是 B 可能还会有要说的话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,于是 B 向 A 的连接释放了,这样整个通话就结束了。
16. Vue封装组件
- 数据从父组件传入
- 在父组件中处理事件
- 子组件要预留一个插槽slot
- 不要依赖vuex
- 合理使用scoped 分割样式
- 组件具有单一职责
17. Vue中 mixin实现原理
Vue中的mixin的方法也是开发中一个很常用的方法,特别是在组件封装中,我们可以依赖它实现方法的复用
实现 思路
mixin 的本质还是对象之间的合并,但是对不同对象和方法有不同的处理方式,对于普通对象,就是简单的对象合并类似于Object.assign,对于基础类型就是后面的覆盖前面的,而对于生命周期上的方法,相同的则是合并到一个数组中,调用的时候依次调用。
代码实现
- 定义mixin 方法
1
2
3
4
5
6
7
8
9
10
11import { mergeOptions} from "./util/index.js";
/*
初始化全局的API
*/
export function initGlobalApi(Vue){
Vue.options={};
Vue.mixin=function(mixin){
this.options=mergeOptions(this.options,mixin);
return this;
}
} - 合并对象的方法
1 | export function mergeOptions(parent,child){ |
其实源码中合并的方法很复杂,包括对合并对象的响应式数据处理,还有组件中 minins的合并策略,如有兴趣,可查看源码中的options.js 文件。
18. Vue中 eventBus 实现原理
18.1 概念
EventBus是消息传递的一种方式,基于一个消息中心,订阅和发布消息的模式,称为发布订阅者模式。
- on(‘name’, fn)订阅消息,name:订阅的消息名称, fn: 订阅的消息
- emit(‘name’, args)发布消息, name:发布的消息名称 , args:发布的消息
18.2 实现
1 | class Bus { |
使用:
1 | const EventBus = new Bus() |
注: 只不过Vue中已经替我们实现好了
$emit
和$on
这些方法,所以直接用的时候去new Vue()
就可以了
19. v-for和v-if 为什么不能一起
Vue2
在Vue2中,v-for的优先级是高于v-if的,如果作用在同一元素上,输出的渲染函数中可以看除会先执行循环再判断条件,哪怕只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会造成性能的浪费
Vue3
而在Vue3中,v-if的优先级时高于v-for的,因此v-if执行时要调用的变量可能还不存在,会导致报错。
使用场景
通常有两种情况导致需要v-if和v-for同时使用:
- 为了过滤列表中的项目,例如
v-for = 'user in users' v-if = 'user.isActive'
。此时可以定义出一个计算属性,例如activeUsers
,让其返回过滤后的列表即可,users.filter( u => u.isActive)
- 为了避免渲染本应该被隐藏的列表,例如
v-for = 'user in users' v-if = 'shouldShowUsers'
。此时可以把v-if绑定在容器元素上,例如ul,ol
或在外包一层template
20. v-for 为什么要加key, 如果key重复会发生什么
key的作用
- key是给每一个vnode(虚拟dom)的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点
- 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
- 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed
- 用+new Date()生成的时间戳作为key,手动强制触发重新渲染
key的概念
- 在写v-for的时候,都需要给元素加上一个key属性,并且最好不要用索引
- key的主要作用就是用来提高渲染性能的!
- key属性可以避免数据混乱的情况出现 (如果元素中包含了有临时数据的元素,如果不用key就会产生数据混乱),为了更好地区别各个组件 key的作用主要是为了高效的更新虚拟DOM
21. 闭包是什么,有啥好处坏处
闭包就是当一个函数中嵌套了一个函数以后,由于作用域嵌套形成的一种特殊现象;包含了那个局部变量的容器它被内部函数对象引用着。
闭包的好处:
- 保护了私有变量(将变量定义在局部,不会污染全局,保证了数据的安全)
- 全局也能操作局部的变量了
- 函数在 调用栈中的内存一直没有被销毁。
- 延长了变量的生命周期
闭包的坏处:
如果闭包用的不好,就会造成内存泄漏、溢出(类似于死循环或递归函数没有结束
22. vue 哪些地方用了闭包
- 数据响应化Observer中使用闭包如果觉得上面的代码还是不容易理解,那么,我们换种方式再看看:
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// 注:由于Vue源码中有大量与闭包不相关的代码可能会影响阅读和理解,
// 故此处不照抄Vue源码,而是本人根据Vue源码的实现原理实现的简易版响应式代码,方便阅读与理解
function defineRealive(target, key, value){
return Object.defineProperty(target, key, {
get(){
console.log(`通过getter获取数据:${value}`);
return value;
},
set(val){
console.log(`通过setter设置数据:新值-${val};旧值-${value}`);
// 很多人会疑问,value明明是形参,为什么给他赋值就能够达到数值改变的效果呢?形参不是出了这个函数就没用了么?
// 其实,这就用到了闭包的原理,value是外层函数defineRealive的参数,而我们实际上使用value确是在内层的get或set方法里面
// 这样就形成了一个闭包的结构了。根据闭包的特性,内层函数可以引用外层函数的变量,并且当内层保持引用关系时外层函数的这个变量
// 不会被垃圾回收机制回收。那么,我们在设置值的时候,把val保存在value变量当中,然后get的时候再通过value去获取,这样,我们再访问
// obj.name时,无论是设置值还是获取值,实际上都是对value这个形参进行操作的。
value = val;
}
});
}
let obj = {
name: 'kiner',
age: 20
};
Object.keys(obj).forEach(key=>defineRealive(obj, key, obj[key]));
obj.name = 'kanger';// 控制台输出:通过setter设置数据:新值-kanger;旧值-kiner
obj.age = 18;// 控制台输出:通过setter设置数据:新值-18;旧值-20
// 控制台输出:通过getter获取数据:kanger
// 控制台输出:通过getter获取数据:18
// 控制台输出:kanger 18
console.log(obj.name,obj.age);
1 | // 这样是不是就比较好理解了呢,形参也是一个普通的局部变量,只是可能我们平时使用的时候, |
- 全局Api中的$watch方法中使用闭包
1
2
3
4
5
6
7// 根据Vue的$watch原理实现的简易版$watch
Vue.prototype.$watch = function(exp, cb, options = {immediate: true, deep: false}) {
let watcher = new Watcher(this, exp, cb, options);
return () => {
watcher.unWatch();
};
}
从$watch方法的实现中,我们可以看出,这其实也是一个闭包的结构,用户调用方法时,会实例化一个Watcher对象并保存在变量watcher,然后返回一个函数,在这个函数里面,调用了watcher下的unWatch方法。也就是说,用户可以通过以下方式进行监听/移除监听属性变化
1 |
|
这样实现的巧妙之处在于,我们无须关心要调用谁去取消监听,你怎么监听的,他就给你返回一个取消监听的方法,直接调用这个方法取消就可以了。
23. 内存溢出和内存泄漏
23.1. 什么是内存溢出?
内存溢出比较好理解,当我们程序使用的内存大于剩余的内存,就会造成内存溢出,好比一个300毫升的水杯倒了500毫升的水。就会报错
23.2. 什么是内存泄露?
不再使用的内存未能被程序释放,就是当一块内存不再用到,但是垃圾回收机制又无法释放这块内存的时候,就导致内存泄漏。内存泄露会因为减少可用内存数量从而降低计算机性能,严重的可能导致设备停止正常工作,或者应用程序崩溃。
- 全局变量
1
2
3
4
5
6function fn(){
var name = '张三'
age = 18
}
fn()
//age没有被var定义,所以是全局变量。fn调用完age还会保留在内存当中 - 没有及时的清除定时器
1
2
3
4
5var timer = setInterval(()=>{
console.log(new Date()) //每隔一秒输出一次中国的时间
},1000)
clearInterval(timer)
//clearInterval是 结束定时器的循环调用函数 - 没有及时清理闭包函数
1
2
3
4
5
6
7
8
9
10
11
12function fn(){
var i=0;
return function(){
console.log(i++); //变量不会被内存回收机制回收
i=null; //释放掉内存
}
}
var f1=fn()
f1(); // 0
f1(); // 0
// 不管调用几次f1都是0 - 没有及时清理dom元素的引用
1
2console.log(dom)
var dom = document.getElementById('box')
24. 关于’ESM’ | ‘CJS’ | ‘AMD’ | ‘UMD’ | ‘LIFE’ 怎么理解
ESM
CMAScript Module,现在使用的模块方案,使用 import export 来管理依赖浏览器直接通过 <script type="module">
即可使用该写法。NodeJS 可以通过使用 mjs 后缀或者在 package.json 添加 "type": "module"
来使用
1 | import pmutils from '../../lib/pm-utils.es.js' // 测试在es模式 node.js中环境 |
观看上图可以发现 不支持
在package.json中添加
1 | { |
编译通过
CJS
CommonJS,只能在 NodeJS 上运行,使用 require(“module”) 读取并加载模块
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见
AMD
Asynchronous Module Definition(异步模块加载机制),可以看作 CJS 的异步版本,制定了一套规则使模块可以被异步 require 进来并在回调函数里继续使用,然后 require.js 等前端库也可以利用这个规则加载代码了,目前已经是时代的眼泪了。
UMD
同时兼容 CJS 和 AMD,并且支持直接在前端用 的方式加载.现在还在广泛使用,不过可以想象 ESM 和 LIFE 逐渐代替它。
LIFE
因为我们的应用程序可能包含来自不同源文件的许多函数和全局变量,所以限制全局变量的数量很重要。如果我们有一些不需要再次使用的启动代码,我们可以使用LIFE模式。由于我们不会再次重用代码,因此在这种情况下使用LIFE比使用函数申明或函数表达式更好
1 | const makeWithdraw = (balance) => ((copyBalance) => { |
25. Vue的new Vue()过程和参数
25.1 前言
介绍的vue版本是2.6.12
在项目中我们引入了Vue:import Vue from ‘vue’。那么问题是vue到底从哪里来的?从node_modules中来。在node_modules路径下存在vue文件夹,vue文件夹中存在一个package.json文件。在这个文件中存在两个配置字段,它们都是程序的主入口文件。
1 | "main": "dist/vue.runtime.common.js", |
其中module的优先级大于main的优先级。在module不存在时,main对应的配置项就是主入口文件。可以看到 dist/vue.runtime.esm.js 才是主入口文件。
为了方便,我们还是去GitHub上下载vue的源代码到本地查看 https://github.com/vuejs/vue
下载完成后(或者直接在地址栏github后面拼接1s就可以网页预览vscode查看),我们在编辑器打开,它的目录结构如下:
其中Vue.js 的源码都在 src ⽬录下,源码的⽬录结构如下:
1 | src |
compiler
compiler 目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 AST 语法树,AST语法树优化,代码生成等功能。
编译的工作可以在构建时做(可以借助 webpack、vue-loader 等插件);也可以在运行时做,使用包含构建功能的 Vue.js。编译是一项耗性能的工作,所以更推荐前者——离线编译。
core
core 目录包含了 Vue.js 的核心代码,包括有内置组件、全局 API 封装,Vue 实例化、Obsever、Virtual DOM、工具函数 Util 等等。
platform
Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。
server
Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。
服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序。
sfc
通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单文件来编写组件。
这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。
shared
Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。
接下来我们来找一下Vue的入口文件,我们接下来的分析都是基于platform为web的环境下进行的分析,从 package.json 和 config的 的打包配置中里可以看出,运行在web环境 (Runtime only (CommonJS))
的入口文件在 web/entry-runtime.js
下。
25.2. Vue入口文件
Vue入口文件目录 vue/src/core/instance/index.js
1 | // vue/src/core/instance/index.js |
采用的是ES5的写法,并不是ES6的Class写法的优点,是因为:
- 使用混入Mixin的方式传入Vue,为Vue的原型prototype上增加方法。class难以实现这种方法
- 此种方式将代码模块合理划分,将扩展分散到多个模块中去实现,使得代码文件不会过于庞大,便于维护和管理。这个编程技巧以后可以用于代码开发实现中。
initGlobalAPI
在 vue/src/core/index.js 中,调用的initGlobalAPI(Vue),是为Vue增加静态方法的,
在路径 vue/src/core/global-api/ 目录下的文件中,都是给Vue添加的静态方法
比如:
1 | Vue.use // 使用plugin |
有了这些基础的了解和一步步的跟踪查找后,我们一步一步找到了 new Vue 所在的位置,接下来我们来看下 new Vue 到底做了什么?
25.3 new Vue 做了什么
从入口的文件看来,通过new关键字初始化,调用了
1 | // src/core/instance/index.js |
然后从Mixin增加的原型方法看,initMixin(Vue),调用的是为Vue增加的原型方法_init
1 | // src/core/instance/init.js |
所以,从上面的函数看来,new vue所做的事情,就像一个流程图一样展开了,分别是
- 合并配置
- 初始化生命周期
- 初始化事件中心
- 初始化渲染
- 调用beforeCreate 钩子函数
- init injections and reactivity(这个阶段属性都已注入绑定,而且被
$watch
变成reactivity, 但是$el
还是没有生成,也就是dom没有生成) - 初始化state状态(初始化了data、props、computed、watcher)
- 调用created 钩子函数
在初始化的最后,检测到如果有el属性,则调用vm.$mount 方法挂载vm,挂载的目标就是把模板渲染成最终的DOM.
Vue代码初始化的主线逻辑非常分明,是的逻辑和流程非常清楚,这种编程方法值得我们学习
26. Javascript this指向问题
26.1 在全局作用域中
=> this => window
1 | <script> |
26.2 在普通函数中
=> this取决于谁调用,谁调用我this 就指向谁,跟如何定义无关
1 | var obj = { |
26.3 箭头函数中的this
箭头函数没有自己的this,箭头函数的this 就是上下文中定义的this, 因为箭头函数没有自己的this 所以不能用作构造函数
1 | var div = document.querySelector('div') |
26.4 事件绑定中的this
事件源: onclick = function(){} // this => 事件源
事件源: addEventListener(‘事件’, function() {}) // this => 事件源
1 | var div = document.querySelector('div') |
26.5 定时器中的this
定时器中的this => window, 因为定时器中采用回调函数作为处理函数,而回调函数的this => window
1 | setInterval(function () { |
26.6 构造函数中的this
构造函数配合new 使用,而new 关键字会将构造函数中的this指向实例化对象,所以构造函数中的this => 实例化对象
26.6.1 new 关键字
1 | // 定义构造函数 |
25.6.2 伪代码演示过程
通过new关键字实例化对象p
,具备了构造函数Person
中this的属性: name
、age
, 也具备了构造函数Person
的原型prototype
的属性color
和satBye
方法。下面我们来通过伪代码看具体的实现过程
初始化新对象
1
var o = {}
原型的执行,确定o对象的原型链
1
o.__proto__ = Person.prototype
绑定this对象位o,传入参数;执行Person构造函数,进行属性和方法赋值操作
1
Person.call(o, '无奈', 18)
25.6.3 构造函数中的this实例:
1 | function Person(name,age) { |
27. 说说var、let 、const 之间的区别
27.1 var
在es5中,顶层对象的属性和全局对象是等价的,用var
声明的变量既是全局变量,也是顶层变量
注意:顶层对象,在浏览器环境指的是window
对象,在node
指的是global
对象
- 使用var 声明的变量存在变量提升的情况
- 使用var 我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
- 在函数中使用var 声明变量时候,该变量是局部的
- 但是如果在函数内不使用var, 该变量是全局的
27.2 let
let 是 es6 新增的命令, 用来声明变量
用法类似于var ,但是所声明的变量,只在let 命令所在的代码块内有效
- 不存在变量提升
- 只要块级作用域内存在let命令,这个区域将不再受外部影响
- 使用let 声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”
- let 不予许在相同作用域中重复声明
- 不能在函数内部重新声明参数
27.3 const
const 声明一个只读的变量,一旦声明,常量的值就不能改变,这就意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值
- 之前用var 或 let 声明过的变量,再用const 声明会报错
- const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
- 对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量
- 对于复杂类型的数据,变量指向的内存地址,保存的是一个指向实际数据的指针,const 只能保证这个指针是固定的,并不能确保变量的结构不变
- 其他情况 const与 let一致
27.4 区别
27.4.1 变量提升:
- var 声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
- let 和 const 不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
27.4.2 暂时性死区
- var 不存在暂时性死区
- let 和 const 存在暂时性死区,只有在等到声明变量的那一行代码出现,才可以获取和使用该变量
27.4.3 块级作用域
- var 不存在块级作用域
- let 和 const 存在块级作用域
27.4.4 重复声明
- var 允许重复声明变量
- let 和 const 在同一作用域不允许重复声明变量
27.4.5 修改声明的变量
- var 和 let可以
- const 声明一个只读的变量。一旦声明,常量的值就不能改变
27.5 使用
能用const 的情况尽量使用const.其他情况下大多使用let。避免使用var
28 Vue nextTick使用场景及实现原理
29 普通函数和async await区别
30 typescript 中 type 和 interface 区别
30.1 interface VS type
大家使用 typescript 总会使用到 interface 和 type,官方规范 稍微说了下两者的区别
接口可以在extends或implements子句中命名,但对象类型字面量的类型别名不能。
接口可以有多个合并声明,但对象类型文字的类型别名不能。
30.2 相同点
都可以描述一个对象或者函数
interface
1 | interface User { |
type
1 | type User { |
都允许拓展(extends)
interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说interface可以extends type, type 也可以extends interface。虽然效果差不多,但是两者语法不同
interface extends interface
1 | interface Name { |
type extends type
1 | type Name { |
interface extends type
1 | type Name { |
type extends interface
1 | interface Name { |
30.3 不同点
30.3.1 type可以而interface 不行
1 | // 基本数据类型 |
- type语句中还可以使用 typeof 获取实例的 类型进行赋值
1 | // 当你想获取一个变量的类型时,使用 typeof |
- 其他骚操作
1 | type StringOrNumber = string | number |
30.3.2 interface可以而type 不行
interface 能够合并声明
1 | interface User { |
30.4 总结
一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用type。 其他更多详情参看 官方规范文档
31 Javascript NaN 类型是什么
Javascript 中的数字类型包含整数和浮点数:
1 | const integer = 4 |
另外还有两个特殊数值: (Infinity
大于任何其他数字的数字) 和 NaN
(表示“非数字”概念):
1 | const infinite = Infinity |
虽然直接使用的 NaN
很少见,但在对数字进行失败的操作后却会令人惊讶地出现。
让我们仔细看一下 NaN
特殊值: 如何检查变量是否具有 NaN
, 并重要地了解创建 “非数字”值的方案。
31.1 NaN 号
Javascript 中的数字类型时所有数字值的集合,包含“非数字”, 正无穷大和负无穷大。
可以使用特殊表达式 NaN
或作为全局对象 或 Number
函数的属性来访问 “非数字”:
1 | typeof NaN // => 'number' |
也尝试解析无效的数字字符串,例如 ‘Joker’
导致 NaN
1 | parseInt('Joker', 10) // => NaN |
31.2 检查与NaN的相等性
NaN
有个有趣的特性是,即使它本身也不等于任何值 NaN
1 | NaN === NaN // => false |
此行为对于检测变量是否为有用 NaN
:
1 | const someNumber = NaN |
someNumber !== someNumber
表达式true
仅当someNumber
为NaN
时,因此,以上代码片段将记录到控制台”Is NaN”。