Vue 系列


对 SPA(单页面应用)的理解

1.什么是 SPA

SPA,翻译过来就是单页面应用, 是以中网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换影响用户体验,在单页应用中所偶有必要的代码(HTML,JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(响应用户操作)动态装载适当的资源并添加到页面,页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。

2.SPA 和 MPA 的区别

上面大家已经对单页面有所了解,下面来说多页面应用,在多页面应用中,每个页面都是一个主页面,都是独立的,当我们访问另一个页面的时候,都需要重新加载html,css,js文件,公告文件比如HeaderFooter按需加载。

单页面应用(SPA) 多页面应用(MPA)
组成 一个主页面和多个页面片段 多个主页面
刷新方式 局部刷新 整夜刷新
url 模式 哈希模式 历史模式
SEO 搜索引擎优化 难实现,可使用 SSR 方式改善 容易实现
数据传递 容易 通过 url、cookie、localStorage 等传递
页面切换 速度快,用户体验良好 切换加载资源,速度慢,用户体验差
维护成本 相对容易 相对复杂

v-if 和 v-show 怎么理解

1.v-show 与 v-if 的区别

控制手段:v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除
编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于 css 切换
编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。渲染条件为假时,并不做操作,直到为真才渲染

  1. v-showfalse变为true的时候不会触发组件的生命周期
  2. v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗;

2.v-show 和 v-if 的使用场景

v-ifv-show 都能控制 dom 元素在页面的显示与隐藏,v-if相比 v-show 开销更大的(直接操作 dom 节点增加与删除)
如果需要非常频繁地切换,则使用 v-show 较好,如果在运行时条件很少改变,则使用 v-if 较好

Vue 的 v-if 和 v-for 不建议一起使用

1. 作用

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true 值的时候被渲染
v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名
v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于 diff 算法进行优化

2. 优先级

v-for优先级比v-if

注意事项:

1.永远不要把 v-ifv-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)

2.如果避免出现这种情况,则在外层嵌套template(页面渲染不生成 dom 节点),在这一层进行v-if判断,然后在内部进行v-for循环

1
2
3
<template v-if="isShow">
<p v-for="item in items">
</template>

3.如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项

1
2
3
4
5
6
7
computed: {
items: function() {
return this.list.filter(function (item) {
return item.isShow
})
}
}

SPA(单页应用)首屏加载速度慢怎么解决

1.什么是首屏加载

首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

首屏加载可以说是用户体验中最重要的环节
通过DOMContentLoad或者performance来计算出首屏时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
name: "first-contentful-paint",
entryType: "paint",
startTime: 507.80000002123415,
duration: 0,
};

2.加载慢的原因

在页面渲染的过程,导致加载速度慢的因素可能如下:

  1. 网络延时问题
  2. 资源文件体积是否过大
  3. 资源是否重复发送请求去加载了
  4. 加载脚本的时候,渲染内容堵塞了

3.解决方案

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化
下图是更为全面的首屏优化的方案

SSR 解决了什么问题?

1.SSR 是什么

Server-Side Rendering我们称其为 SSR,意为服务端渲染

指由服务器完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程.

先来看看 Web3 个阶段的发展史:

  1. 传统服务端渲染 SSR
  2. 单页面应用 SPA
  3. 服务端渲染 SSR

Vue官方对SSR的解释:

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记”激活”为客户端上完全可交互的应用程序.
服务器渲染的 Vue.js 应用程序也可以被认为是”同构”或”通用”,因为应用程序的大部分代码都可以在服务器和客户端上运行

我们从上面解释得到以下结论:

  1. Vue SSR 是一个在 SPA 上进行改良的服务端渲染
  2. 通过 Vue SSR 渲染的页面,需要在客户端激活才能实现交互
  3. Vue SSR 将包含两部分:服务端渲染的首屏,包含交互的 SPA

2.解决了什么

SSR 主要解决了以下两种问题:

  1. seo:搜索引擎优先爬取页面 HTML 结构,使用 ssr 时,服务端已经生成了和业务想关联的 HTML,有利于 seo
  2. 首屏呈现渲染:用户无需等待页面所有 js 加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)
    但是使用 SSR 同样存在以下的缺点:
  3. 复杂度:整个项目的复杂度
  4. 库的支持性,代码兼容
  5. 性能问题: 1.每个请求都是 n 个实例的创建,不然会污染,消耗会变得很大 2.缓存 node serve、 nginx 判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。 3.降级:监控 cpu、内存占用过多,就 spa,返回单个的壳
  6. 服务器负载变大,相对于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用

所以在我们选择是否使用 SSR 前,我们需要慎重问问自己这些问题:

  1. 需要 SEO 的页面是否只是少数几个,这些是否可以使用预渲染(Prerender SPA Plugin)实现
  2. 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢

Vue 中给对象添加新属性界面不刷新

Vue 组件间通信方式都有哪些

1.组件间通信的概念

开始之前,我们把组件间通信拆分

  1. 组件
  2. 通信

都知道组件是vue最强大的功能之一,vue中每一个.vue我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信组件间通信即指组件(.vue)通过某种方式来传递信息以达到某个目的.举个栗子我们在使用 UI 框架中的table组件,可能会往table组件中传入某些数据,这个本质就形成了组件之间的通信

2.组件间通信解决了什么

通信的本质是信息同步,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统

3.组件间通信的分类

组件间通信的分类可以分成以下

  1. 父子组件之间的通信
  2. 兄弟组件之间的通信
  3. 祖孙与后代组件之间的通信
  4. 非关系组件间之间的通信

关系如图

4.组件间通信的方案

整理vue中 8 种常规的通信方案

  1. 通过 props 传递
  2. 通过$emit 触发自定义事件
  3. 使用 ref
  4. EvenBus
  5. $parent或$root
  6. attrs 与 listeners
  7. Provide 与 inject
  8. Vuex

通过 prop 传递

适用场景:父组件传递数据给子组件
子组件设置props属性,定义接收父组件传递过来的参数
父组件在使用子组件标签中通过字面量来传递值
Children.vue:

1
2
3
4
5
6
7
8
9
10
props:{
// 字符串形式
name:String // 接收的类型参数
// 对象形式
age:{
type:Number, // 接收的类型为数值
defaule:18, // 默认值为18
require:true // age属性必须传递
}
}

Father.vue组件:

1
<Children name="jack" age=18 />

通过$emit 触发自定义事件

适用场景:子组件传递数据给父组件
子组件通过$emit触发自定义事件,$emit 第二个参数为传递的数值
父组件绑定监听器获取到子组件传递过来的参数
Children.vue:

1
this.$emit('add', good)

Father.vue组件:

1
<Children @add="cartAdd($event)" />

ref

父组件在使用子组件的时候设置 ref
父组件通过设置子组件 ref 来获取数据
父组件:

1
2
;<Children ref="foo" />
this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据

EventBus

使用场景:兄弟组件传值
创建一个中央事件总线 EventBus
兄弟组件通过$emit触发自定义事件,$emit 第二个参数为传递的数值
另一个兄弟组件通过$on 监听自定义事件

Bus.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {} // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args))
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能

Children1.vue

1
this.$bus.$emit('foo')

Children2.vue

1
this.$bus.$on('foo', this.handle)

$parent 或$ root

通过共同祖辈$parent或者$root搭建通信桥连
兄弟组件

this.$parent.on('add',this.add)

另一个兄弟组件

this.$parent.emit('add')

$attrs 与$ listeners

适用场景:祖先传递数据给子孙
设置批量向下传属性$attrs$listeners
包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
可以通过 v-bind="$attrs" 传⼊内部组件

1
2
3
4
5
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo"/>
1
2
3
4
5
6
7
8
// 给Grandson隔代传值,communication/index.vue
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2>

// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>

// Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">{{msg}}</div>

provide 与 inject

在祖先组件定义 provide 属性,返回传递的值
在后代组件通过 inject 接收组件传递过来的值
祖先组件

1
2
3
4
5
provide(){
return {
foo:'foo'
}
}

后代组件

1
inject: ['foo'] // 获取到祖先组件传递过来的值

vuex

适用场景: 复杂关系的组件数据传递

Vuex 作用相当于一个用来存储共享变量的容器


state 用来存放共享变量的地方

getter,可以增加一个getter 派生状态,(相当于 store 中的计算属性),用来获得共享变量的值

mutations 用来存放修改 state 的方法。

actions 也是用来存放修改state 的方法,不过 action 是在mutations 的基础上进行。常用来做一些异步操作

小结

  1. 父子关系的组件数据传递选择 props$emit 进行传递,也可选择 ref
  2. 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent 进行传递
  3. 祖先与后代组件数据传递可选择 attrslisteners 或者ProvideInject
  4. 复杂关系的组件数据传递可以通过 vuex 存放共享的变量

双向绑定的理解

对 nexttick 的理解

1.NextTick 是什么

官方定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新
举个栗子:

1
<div id="app">{{ message }}</div>
1
2
3
4
5
6
const vm = new Vue({
el: '#app',
data: {
message: '原始值',
},
})

修改message,并获取。

1
2
3
4
this.message = '修改后的值1'
this.message = '修改后的值2'
this.message = '修改后的值3'
console.log(vm.$el.textContent) // 原始值

这个时候获取页面最新的DOM节点,却发现获取到的是旧值
这是因为message数据在发现变化的时候,vue 并不会立刻去更新Dom,而是将修改数据的操作放在了一个异步操作队列中

如果我们一直修改相同数据,异步操作队列还会进行去重

等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM的更新

为什么要有 nexttick

1
2
3
4
{{num}}
for (let i = 0; i < 100000; i++) {
num = i
}

如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新 10 万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略

2.使用场景

如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM结构)
第二个参数为:执行函数上下文

1
2
3
4
5
6
7
8
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {
// DOM 更新了
console.log(vm.$el.textContent) // 修改后的值
})

组件内使用 vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上

1
2
3
4
5
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '修改后的值'
})

$nextTick() 会返回一个 Promise 对象,可以是async/await完成相同作用的事情

1
2
3
4
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'

对 slot 的理解?使用场景

1.slot 是什么?

在 HTML 中 slot 元素 ,作为 Web Components 技术套件的一部分,是 Web 组件内的一个占位符

该占位符可以在后期使用自己的标记语言填充

1
2
3
4
5
6
7
8
9
<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>

template不会展示到页面中,需要用先获取它的引用,然后添加到DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
customElements.define(
'element-details',
class extends HTMLElement {
constructor() {
super()
const template = document.getElementById(
'element-details-template'
).content
const shadowRoot = this.attachShadow({ mode: 'open' }).appendChild(
template.cloneNode(true)
)
}
}
)

在 Vue 中的概念也是如此
Slot艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口

可以将其类比为插卡式的 FC 游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容)

2.使用场景

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理

如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情

通过 slot 插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

3.分类

slot可以分来以下三种

  1. 默认插槽
  2. 具名插槽
  3. 作用域插槽

默认插槽

子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面

父组件在使用的时候,直接在子组件的标签内写入内容即可
子组件Child.vue

1
2
3
4
5
<template>
<slot>
<p>插槽后备的内容</p>
</slot>
</template>

父组件

1
2
3
<Child>
<div>默认插槽</div>
</Child>

具名插槽

子组件用name属性来表示插槽的名字,不传为默认插槽

父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值
子组件Child.vue

1
2
3
4
<template>
<slot>插槽后备的内容</slot>
<slot name="content">插槽后备的内容</slot>
</template>

父组件

1
2
3
4
5
<child>
<template v-slot:default>具名插槽</template>
<!-- 具名插槽⽤插槽名做参数 -->
<template v-slot:content>内容...</template>
</child>

作用域插槽

子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上

父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用
子组件Child.vue

1
2
3
4
5
<template>
<slot name="footer" testProps="子组件的值">
<h3>没传footer插槽</h3>
</slot>
</template>

父组件

1
2
3
4
5
6
7
8
9
<child>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
<template #default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
</child>

小结

  1. v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用
  2. 默认插槽名为default,可以省略default直接写v-slot
  3. 缩写为#时不能不写参数,写成#default
  4. 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

Vue 中 key 的理解

1. key 是什么

开始之前,我们先还原两个实际工作场景

1.当我们在使用v-for时,需要给单元加上key

1
2
3
<ul>
<li v-for="item in items" :key="item.id">...</li>
</ul>

2.用+new Date()生成的时间戳作为key,手动强制触发重新渲染

1
<Comp :key="+new Date()" />

一句话来讲,key是给每一个 vnode 的唯一 id,也是 diff的一种优化策略,可以根据 key,更准确, 更快的找到对应的 vnode 节点

背后的逻辑

当我们在使用 v-for 时,需要给单元加上 key

如果不用 key,Vue 会采用就地复地原则:最小化 element 的移动,并且会尝试尽最大程度在同适当的地方对相同类型的 element,做 patch 或者 reuse。

如果使用了 key,Vue 会根据 keys 的顺序记录 element,曾经拥有了 key 的 element 如果不再出现的话,会被直接 remove 或者 destoryed

用+new Date()生成的时间戳作为 key,手动强制触发重新渲染

当拥有新值的 rerender 作为 key 时,拥有了新 key 的 Comp 出现了,那么旧 key Comp 会被移除,新 key Comp 触发渲染

对 keep-alive 的理解

1. keep-alive 是什么

keep-alive是 vue 中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染 DOM

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

keep-alive可以设置以下 props 属性:

  1. include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  2. exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  3. max - 数字。最多可以缓存多少组件实例
    关于keep-alive的基本用法:
1
2
3
<keep-alive>
<component :is="view"></component>
</keep-alive>

使用includesexclude

1
2
3
4
5
6
7
8
9
10
11
12
13
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  1. 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated

  2. 再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > deactivated

2.使用场景

使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive
首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive

在路由中设置keepAlive属性判断是否需要缓存

1
2
3
{ path: 'list', name: 'itemList', // 列表页 component (resolve) {
require(['@/pages/item/list'], resolve) }, meta: { keepAlive: true, title:
'列表页' } }

使用<keep-alive>:

1
2
3
4
5
6
7
8
<div id="app" class="wrapper">
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

3.缓存后如何获取数据

解决方案可以有以下两种:

  1. beforeRouteEnter
  2. actived

beforeRouteEnter

每次组件渲染的时候,都会执行beforeRouteEnter

1
2
3
4
5
6
7
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
}

actived

keep-alive缓存的组件被激活的时候,都会执行actived钩子

1
2
3
activated(){
this.getData() // 获取数据
},

注意:服务器端渲染期间avtived不被调用

什么是虚拟 DOM?如何实现一个虚拟 DOM

1.什么是虚拟 DOM

虚拟 DOM(Virtual DOM )这个概念相信大家都不陌生,从 ReactVue ,虚拟 DOM 为这两个框架都带来了跨平台的能力(React-NativeWeex

实际上它只是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上

Javascript对象中,虚拟DOM 表现为一个 Object对象。并且最少包含标签名 (tag)属性 (attrs)子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别

创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点真实DOM的属性一一照应

在 vue 中同样使用到了虚拟 DOM 技术
定义真实DOM

1
2
3
4
<div id="app">
<p class="p">节点内容</p>
<h3>{{ foo }}</h3>
</div>

实例化vue

1
2
3
4
5
6
const app = new Vue({
el: '#app',
data: {
foo: 'foo',
},
})

观察renderrender,我们能得到虚拟DOM

1
2
3
4
5
6
7
8
9
;(function anonymous() {
with (this) {
return _c('div', { attrs: { id: 'app' } }, [
_c('p', { staticClass: 'p' }, [_v('节点内容')]),
_v(' '),
_c('h3', [_v(_s(foo))]),
])
}
})

通过VNodevue可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff算法得出一些需要修改的最小单位,再更新视图,减少了dom操作,提高了性能

2.为什么需要虚拟 DOM

DOM是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起的
真实的 DOM 节点,哪怕一个最简单的 div 也包含着很多属性


由此可见,操作 DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验

举个例子:
用传统的原生apijQuery去操作 DOM 时,浏览器会从构建DOM树开始从头到尾执行一遍流程

当你在一次操作时,需要更新 10 个 DOM 节点,浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行10次流程

而通过 VNode,同样更新 10 个 DOM 节点,虚拟DOM不会立即操作DOM,而是将这 10 次更新的 diff 内容保存到本地的一个js对象中,最终将这个js对象一次性attachDOM树上,避免大量的无谓计算

很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。这是虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种 GUI

diff 算法

1.是什么

diff 算法是一种通过同层的树节点进行比较的高效算法
其有两个特点:
比较只会在同层级进行, 不会跨层级比较
在 diff 比较的过程中,循环从两边向中间比较

diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

2.比较方式

diff整体策略为:深度优先,同层比较

1.比较只会在同层级进行, 不会跨层级比较

2.循环从两边向中间靠拢

下面举个 vue 通过 diff 算法更新的例子:
新旧 VNode节点如下图所示:


第一次循环后,发现旧节点 D 与新节点 D 相同,直接复用旧节点 D 作为 diff 后的第一个真实节点,同时旧节点 endIndex 移动到 C,新节点的 startIndex 移动到了 C

第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E

第三次循环中,发现 E 没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动

第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的 startIndex 移动到了 B

第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex 移动到了 C,新节点的 startIndex 移动到了 F

新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdx 和 newEndIdx 之间的所有节点,也就是节点 F,直接创建 F 节点对应的真实节点放到 B 节点后面

vue 要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?

1.是什么

权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源
而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发:

  1. 页面加载触发
  2. 页面上的按钮点击触发

总的来说,所有的请求发起都触发自前端路由或视图,所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:

  1. 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页
  2. 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
  3. 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截

2.如何做

前端权限控制可以分为四个方面:

  • 接口权限
  • 按钮权限
  • 菜单权限
  • 路由权限
  • 接口权限

    接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回 401,跳转到登录页面重新进行登录
    登录完拿到 token,将 token 存起来,通过 axios 请求拦截器进行拦截,每次请求的时候头部携带 token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    axios.interceptors.request.use(config => {
    config.headers['token'] = cookie.get('token')
    return config
    })
    axios.interceptors.response.use(res=>{},{response}=>{
    if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误
    router.push('/login')
    }
    })

    路由权限控制

    方案一

    初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

    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
    const routerMap = [
    {
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true, // will always show the root menu
    meta: {
    title: 'permission',
    icon: 'lock',
    roles: ['admin', 'editor'], // you can set roles in root nav
    },
    children: [
    {
    path: 'page',
    component: () => import('@/views/permission/page'),
    name: 'pagePermission',
    meta: {
    title: 'pagePermission',
    roles: ['admin'], // or you can only set roles in sub nav
    },
    },
    {
    path: 'directive',
    component: () => import('@/views/permission/directive'),
    name: 'directivePermission',
    meta: {
    title: 'directivePermission',
    // if do not set roles, means: this page does not require permission
    },
    },
    ],
    },
    ]

    这种方式存在以下四种缺点:

  • 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。
  • 全局路由守卫里,每次路由跳转都要做权限判断
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
  • 方案二
    初始化的时候先挂载不需要权限控制的路由,比如登录页,404 等错误页。如果用户通过 URL 进行强制访问,则会直接进入 404,相当于从源头上做了控制

    登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用 addRoutes 添加路由

    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
    68
    69
    70
    71
    import router from './router'
    import store from './store'
    import { Message } from 'element-ui'
    import NProgress from 'nprogress' // progress bar
    import 'nprogress/nprogress.css' // progress bar style
    import { getToken } from '@/utils/auth' // getToken from cookie

    NProgress.configure({ showSpinner: false }) // NProgress Configuration

    // permission judge function
    function hasPermission(roles, permissionRoles) {
    if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
    if (!permissionRoles) return true
    return roles.some((role) => permissionRoles.indexOf(role) >= 0)
    }

    const whiteList = ['/login', '/authredirect'] // no redirect whitelist

    router.beforeEach((to, from, next) => {
    NProgress.start() // start progress bar
    if (getToken()) {
    // determine if there has token
    /* has token*/
    if (to.path === '/login') {
    next({ path: '/' })
    NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
    } else {
    if (store.getters.roles.length === 0) {
    // 判断当前用户是否已拉取完user_info信息
    store
    .dispatch('GetUserInfo')
    .then((res) => {
    // 拉取user_info
    const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
    store.dispatch('GenerateRoutes', { roles }).then(() => {
    // 根据roles权限生成可访问的路由表
    router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
    next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
    })
    })
    .catch((err) => {
    store.dispatch('FedLogOut').then(() => {
    Message.error(err || 'Verification failed, please login again')
    next({ path: '/' })
    })
    })
    } else {
    // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
    if (hasPermission(store.getters.roles, to.meta.roles)) {
    next() //
    } else {
    next({ path: '/401', replace: true, query: { noGoBack: true } })
    }
    // 可删 ↑
    }
    }
    } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
    // 在免登录白名单,直接进入
    next()
    } else {
    next('/login') // 否则全部重定向到登录页
    NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
    }
    })

    router.afterEach(() => {
    NProgress.done() // finish progress bar
    })

    按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限

    这种方式也存在了以下的缺点:

  • 全局路由守卫里,每次路由跳转都要做判断
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
  • 菜单权限

    菜单权限可以理解成将页面与路由进行解耦

    方案一
    菜单与路由分离,菜单由后端返回

    前端定义路由信息

    1
    2
    3
    4
    5
    {
    name: "login",
    path: "/login",
    component: () => import("@/pages/Login.vue")
    }

    name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验

    全局路由守卫里做判断

    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
    function hasPermission(router, accessMenu) {
    if (whiteList.indexOf(router.path) !== -1) {
    return true
    }
    let menu = Util.getMenuByName(router.name, accessMenu)
    if (menu.name) {
    return true
    }
    return false
    }

    Router.beforeEach(async (to, from, next) => {
    if (getToken()) {
    let userInfo = store.state.user.userInfo
    if (!userInfo.name) {
    try {
    await store.dispatch('GetUserInfo')
    await store.dispatch('updateAccessMenu')
    if (to.path === '/login') {
    next({ name: 'home_index' })
    } else {
    //Util.toDefaultPage([...routers], to.name, router, next);
    next({ ...to, replace: true }) //菜单权限更新完成,重新进一次当前路由
    }
    } catch (e) {
    if (whiteList.indexOf(to.path) !== -1) {
    // 在免登录白名单,直接进入
    next()
    } else {
    next('/login')
    }
    }
    } else {
    if (to.path === '/login') {
    next({ name: 'home_index' })
    } else {
    if (hasPermission(to, store.getters.accessMenu)) {
    Util.toDefaultPage(store.getters.accessMenu, to, routes, next)
    } else {
    next({ path: '/403', replace: true })
    }
    }
    }
    } else {
    if (whiteList.indexOf(to.path) !== -1) {
    // 在免登录白名单,直接进入
    next()
    } else {
    next('/login')
    }
    }
    let menu = Util.getMenuByName(to.name, store.getters.accessMenu)
    Util.title(menu.title)
    })

    Router.afterEach((to) => {
    window.scrollTo(0, 0)
    })

    每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的

    如果根据路由 name 找不到对应的菜单,就表示用户有没权限访问

    如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载

    这种方式的缺点:

  • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
  • 全局路由守卫里,每次路由跳转都要做判断
  • 方案二

    菜单和路由都由后端返回

    前端统一定义路由组件

    1
    2
    3
    4
    5
    6
    const Home = () => import('../pages/Home.vue')
    const UserInfo = () => import('../pages/UserInfo.vue')
    export default {
    home: Home,
    userInfo: UserInfo,
    }

    后端路由组件返回以下格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;[
    {
    name: 'home',
    path: '/',
    component: 'home',
    },
    {
    name: 'home',
    path: '/userinfo',
    component: 'userInfo',
    },
    ]

    在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件

    如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理

    这种方法也会存在缺点:

  • 全局路由守卫里,每次路由跳转都要做判断
  • 前后端的配合要求更高
  • 按钮权限

    方案一
    按钮权限也可以用 v-if 判断

    但是如果页面过多,每个页面都要获取用户权限 role 和路由表里的 meta.btnPermissions,然后再做判断。

    方案二
    通过自定义指令进行按钮权限的判断

    首先配置路由

    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
    {
    path: '/permission',
    component: Layout,
    name: '权限测试',
    meta: {
    btnPermissions: ['admin', 'supper', 'normal']
    },
    //页面需要的权限
    children: [{
    path: 'supper',
    component: _import('system/supper'),
    name: '权限测试页',
    meta: {
    btnPermissions: ['admin', 'supper']
    } //页面需要的权限
    },
    {
    path: 'normal',
    component: _import('system/normal'),
    name: '权限测试页',
    meta: {
    btnPermissions: ['admin']
    } //页面需要的权限
    }]
    }

    自定义权限鉴定指令

    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
    import Vue from 'vue'
    /**权限指令**/
    const has = Vue.directive('has', {
    bind: function (el, binding, vnode) {
    // 获取页面按钮权限
    let btnPermissionsArr = []
    if (binding.value) {
    // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
    btnPermissionsArr = Array.of(binding.value)
    } else {
    // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
    btnPermissionsArr = vnode.context.$route.meta.btnPermissions
    }
    if (!Vue.prototype.$_has(btnPermissionsArr)) {
    el.parentNode.removeChild(el)
    }
    },
    })
    // 权限检查方法
    Vue.prototype.$_has = function (value) {
    let isExist = false
    // 获取用户按钮权限
    let btnPermissionsStr = sessionStorage.getItem('btnPermissions')
    if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
    return false
    }
    if (value.indexOf(btnPermissionsStr) > -1) {
    isExist = true
    }
    return isExist
    }
    export { has }

    在使用的按钮中只需要引用v-has指令

    1
    <el-button @click='editClick' type="primary" v-has>编辑</el-button>

    小结

    关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离

    权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断

    跨域问题

    1.跨域是什么?

    跨域本质是浏览器基于同源策略的一种安全手段

    同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能

    所谓同源(即指在同一个域)具有以下三个相同点:

  • 协议相同(protocol)
  • 主机相同(host)
  • 端口相同(port)
  • 反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

    一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用 postman 请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。

    2.如何解决

    解决跨域的方法有很多,下面列举了三种:

  • JSONP
  • CORS
  • Proxy
  • 而在 vue 项目中,我们主要针对CORSProxy这两种方案进行展开

    CORS
    CORS(Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应

    CORS实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

    只要后端实现了 CORS,就实现了跨域

    Proxy

    代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

    方案一
    如果是通过 vue-cli 脚手架工具搭建项目,我们可以通过 webpack 为我们起一个本地服务器作为请求的代理对象

    通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域

    在 vue.config.js 文件,新增以下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    amodule.exports = {
    devServer: {
    host: '127.0.0.1',
    port: 8084,
    open: true, // vue项目启动时自动打开浏览器
    proxy: {
    '/api': {
    // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
    target: 'http://xxx.xxx.xx.xx:8080', //目标地址,一般是指后台服务器地址
    changeOrigin: true, //是否跨域
    pathRewrite: {
    // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
    '^/api': '',
    },
    },
    },
    },
    }

    通过axios发送请求中,配置请求的根路径

    1
    axios.defaults.baseURL = '/api'

    方案二

    此外,还可通过服务端实现代理请求转发
    express框架为例

    1
    2
    3
    4
    5
    6
    var express = require('express')
    const proxy = require('http-proxy-middleware')
    const app = express()
    app.use(express.static(__dirname + '/'))
    app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false }))
    module.exports = app

    方案三

    通过配置nginx实现代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    server {
    listen 80;
    # server_name www.josephxia.com;
    location / {
    root /var/www/html;
    index index.html index.htm;
    try_files $uri $uri/ /index.html;
    }
    location /api {
    proxy_pass http://127.0.0.1:3000;
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    }

    Vue2 和 Vue3 的区别

    1.Vue3 介绍

    关于vue3的重构背景,尤大是这样说的:

    「Vue 新版本的理念成型于 2018 年末,当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久,但在这段时期,前端世界已经今昔非比了

    在我们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题」

    简要就是:

  • 利用新的语言特性(es6)
  • 解决架构问题
  • 哪些变化

    我们可以概览 Vue3 的新特性,如下:

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原生
  • 更易使用
  • 速度更快

    vue3相比vue2

  • 重写了虚拟dom实现
  • 编译模板的优化
  • 更高效的组件初始化
  • update性能提高1.3-2倍
  • SSR 提高提高了2-3倍
  • 体积更小

    通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的
    能够tree-shaking,有两大好处

  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大
  • 对使用者,打包出来的包体积变小了
  • vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多

    更易维护

  • 可与现有的Options API一起使用
  • 灵活的逻辑组合与复用
  • Vue3模块可以和其他框架搭配使用
  • 更好的 Typescript 支持
    VUE3 是基于 typescipt 编写的,可以享受到自动的类型定义提示

    更接近原生
    可以自定义渲染 API

    更易使用
    响应式 Api 暴露出来


    轻松识别组件重新渲染原因

    2.Vue3 新特性

    Vue 3 中需要关注的一些新功能包括:

  • framents
  • Teleport
  • composition Api
  • createRenderer
  • framents

    Vue3.x中,组件现在支持有多个根节点

    1
    2
    3
    4
    5
    6
    <!-- Layout.vue -->
    <template>
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
    </template>

    Teleport

    Teleport 是一种能够将我们的模板移动到 DOMVue app之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”

    在 vue2 中,像 modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位z-index和样式就会变得很困难

    通过Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它

    1
    2
    3
    4
    5
    6
    7
    <button @click="showToast" class="btn">打开 toast</button>
    <!-- to 属性就是目标位置 -->
    <teleport to="#teleport-target">
    <div v-if="visible" class="toast-wrap">
    <div class="toast-msg">我是一个 Toast 文案</div>
    </div>
    </teleport>

    createRenderer
    通过 createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台

    我们可以将其生成在 canvas 画布上

    关于 createRenderer,我们了解下基本使用,就不展开讲述了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { createRenderer } from '@vue/runtime-core'

    const { render, createApp } = createRenderer({
    patchProp,
    insert,
    remove,
    createElement,
    // ...
    })

    export { render, createApp }

    export * from '@vue/runtime-core'

    composition Api
    composition Api,也就是组合式 api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理
    关于 compositon api 的使用,这里以下图展开


    简单使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    export default {
    setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    function increment() {
    count.value++
    }
    onMounted(() => console.log('component mounted!'))
    return {
    count,
    double,
    increment,
    }
    },
    }

    3.非兼容变更

    Global Api

  • 全局 Vue API 已更改为使用应用程序实例
  • 全局和内部 API 已经被重构为可 tree-shakable
  • 模板指令

  • 组件上 v-model 用法已更改
  • (template v-for)和 非 v-for节点上key用法已更改
  • 在同一元素上使用的 v-if 和 v-for 优先级已更改
  • v-bind="object" 现在排序敏感
  • v-for 中的 ref 不再注册 ref 数组
  • 组件

  • 只能使用普通函数创建功能组件
  • functional 属性在单文件组件 (SFC)
  • 异步组件现在需要 defineAsyncComponent 方法来创建
  • 其他小改变

  • destroyed 生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount
  • [prop default工厂函数不再有权访问 this 是上下文
  • 自定义指令 API 已更改为与组件生命周期一致
  • data 应始终声明为函数
  • 来自 mixin 的 data 选项现在可简单地合并
  • attribute 强制策略已更改
  • 一些过渡 class 被重命名
  • 组建 watch 选项和实例方法 $watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数
  • (template) 没有特殊指令的标记 (v-if/else-if/else、v-for 或 v-slot) 现在被视为普通元素,并将生成原生的 (template>)元素,而不是渲染其内部内容。
  • 在Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML,这意味着容器本身不再被视为模板的一部分
  • 移除 api

  • keyCode 支持作为 v-on 的修饰符
  • $on,$off和$once 实例方法
  • 过滤filter
  • 内联模板 attribute
  • $destroy 实例方法。用户不应再手动管理单个Vue 组件的生命周期
  • —————-


    ES6 系列

    说说 var let const 之间的区别

    1.var

    在 ES5 中,顶层对象的属性和全局变量是等价的,用 var 声明的变量既是全局变量,也是顶层变量

    顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象

    1
    2
    var a = 10
    console.log(window.a) // 10

    使用 var 声明的变量存在变量提升的情况

    1
    2
    console.log(a) // undefined
    var a = 20

    在编译阶段,编译器会将其变成以下执行

    1
    2
    3
    var a
    console.log(a)
    a = 20

    使用 var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明

    1
    2
    3
    var a = 20
    var a = 30
    console.log(a) // 30

    在函数中使用使用 var 声明变量时候,该变量是局部的

    1
    2
    3
    4
    5
    6
    var a = 20
    function change() {
    var a = 30
    }
    change()
    console.log(a) // 20

    而如果在函数内不使用 var,该变量是全局的

    1
    2
    3
    4
    5
    6
    var a = 20
    function change() {
    a = 30
    }
    change()
    console.log(a) // 30

    2. let

    letES6新增的命令,用来声明变量
    用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效

    1
    2
    3
    4
    {
    let a = 20
    }
    console.log(a) // ReferenceError: a is not defined.

    不存在变量提升

    1
    2
    console.log(a) // 报错ReferenceError
    let a = 2

    这表示在声明它之前,变量 a 是不存在的,这时如果用到它,就会抛出一个错误

    只要块级作用域内存在 let 命令,这个区域就不再受外部影响

    1
    2
    3
    4
    5
    var a = 123
    if (true) {
    a = 'abc' // ReferenceError
    let a
    }

    使用 let 声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”

    最后,let 不允许在相同作用域中重复声明

    1
    2
    3
    let a = 20
    let a = 30
    // Uncaught SyntaxError: Identifier 'a' has already been declared

    注意的是相同作用域,下面这种情况是不会报错的

    1
    2
    3
    4
    let a = 20
    {
    let a = 30
    }

    因此,我们不能在函数内部重新声明参数

    1
    2
    3
    4
    5
    function func(arg) {
    let arg
    }
    func()
    // Uncaught SyntaxError: Identifier 'arg' has already been declared

    3.const

    const声明一个只读的常量,一旦声明,常量的值就不能改变

    1
    2
    3
    const a = 1
    a = 3
    // TypeError: Assignment to constant variable.

    这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值

    1
    2
    const a;
    // SyntaxError: Missing initializer in const declaration

    如果之前用 var 或 let 声明过变量,再用 const 声明同样会报错

    1
    2
    3
    4
    5
    var a = 20
    let b = 20
    const a = 30
    const b = 30
    // 都会报错

    const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
    对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量

    对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的,并不能确保改变量的结构不变

    1
    2
    3
    4
    5
    6
    7
    8
    const foo = {}

    // 为 foo 添加一个属性,可以成功
    foo.prop = 123
    foo.prop // 123

    // 将 foo 指向另一个对象,就会报错
    foo = {} // TypeError: "foo" is read-only

    其它情况,constlet一致

    4.区别

    varletconst三者区别可以围绕下面五点展开:

  • 变量提升
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量
  • 使用
  • 变量提升

    var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
    letconst不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // var
    console.log(a) // undefined
    var a = 10

    // let
    console.log(b) // Cannot access 'b' before initialization
    let b = 10

    // const
    console.log(c) // Cannot access 'c' before initialization
    const c = 10

    暂时性死区

    var不存在暂时性死区
    letconst存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // var
    console.log(a) // undefined
    var a = 10

    // let
    console.log(b) // Cannot access 'b' before initialization
    let b = 10

    // const
    console.log(c) // Cannot access 'c' before initialization
    const c = 10

    块级作用域

    var不存在块级作用域
    letconst存在块级作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // var
    {
    var a = 20
    }
    console.log(a) // 20

    // let
    {
    let b = 20
    }
    console.log(b) // Uncaught ReferenceError: b is not defined

    // const
    {
    const c = 20
    }
    console.log(c) // Uncaught ReferenceError: c is not defined

    重复声明

    var允许重复声明变量
    letconst在同一作用域不允许重复声明变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // var
    var a = 10
    var a = 20 // 20

    // let
    let b = 10
    let b = 20 // Identifier 'b' has already been declared

    // const
    const c = 10
    const c = 20 // Identifier 'c' has already been declared

    修改声明的变量

    varlet可以
    const声明一个只读的常量。一旦声明,常量的值就不能改变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // var
    var a = 10
    a = 20
    console.log(a) // 20

    //let
    let b = 10
    b = 20
    console.log(b) // 20

    // const
    const c = 10
    c = 20
    console.log(c) // Uncaught TypeError: Assignment to constant variable

    使用

    能用const的情况尽量使用const,其他情况下大多数使用let,避免使用var

    ES6 中数组新增了哪些扩展

    1.扩展运算符的应用

    ES6 通过扩展元素符...,好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列

    1
    2
    3
    4
    5
    6
    7
    8
    console.log(...[1, 2, 3])
    // 1 2 3

    console.log(1, ...[2, 3, 4], 5)
    // 1 2 3 4 5

    [...document.querySelectorAll('div')]
    // [<div>, <div>, <div>]

    主要用于函数调用的时候,将一个数组变为参数序列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function push(array, ...items) {
    array.push(...items)
    }

    function add(x, y) {
    return x + y
    }

    const numbers = [4, 38]
    add(...numbers) // 42

    可以将某些数据结构转为数组

    1
    ;[...document.querySelectorAll('div')]

    能够更简单实现数组复制

    1
    2
    3
    const a1 = [1, 2]
    const [...a2] = a1
    // [1,2]

    数组的合并也更为简洁了

    1
    2
    3
    4
    5
    const arr1 = ['a', 'b']
    const arr2 = ['c']
    const arr3 = ['d', 'e']
    ;[...arr1, ...arr2, ...arr3]
    // [ 'a', 'b', 'c', 'd', 'e' ]

    注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组
    下面看个例子就清楚多了

    1
    2
    3
    4
    5
    const arr1 = ['a', 'b', [1, 2]]
    const arr2 = ['c']
    const arr3 = [...arr1, ...arr2]
    arr[1][0] = 9999 // 修改arr1里面数组成员值
    console.log(arr[3]) // 影响到arr3,['a','b',[9999,2],'c']

    扩展运算符可以与解构赋值结合起来,用于生成数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const [first, ...rest] = [1, 2, 3, 4, 5]
    first // 1
    rest // [2, 3, 4, 5]

    const [first, ...rest] = []
    first // undefined
    rest // []

    const [first, ...rest] = ['foo']
    first // "foo"
    rest // []

    如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错

    1
    2
    3
    4
    5
    const [...butLast, last] = [1, 2, 3, 4, 5];
    // 报错

    const [first, ...middle, last] = [1, 2, 3, 4, 5];
    // 报错

    可以将字符串转为真正的数组

    1
    2
    ;[...'hello']
    // [ "h", "e", "l", "l", "o" ]

    定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let nodeList = document.querySelectorAll('div')
    let array = [...nodeList]

    let map = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three'],
    ])

    let arr = [...map.keys()] // [1, 2, 3]

    如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错

    1
    2
    const obj = { a: 1, b: 2 }
    let arr = [...obj] // TypeError: Cannot spread non-iterable object

    2. 构造函数新增的方法

    关于构造函数,数组新增的方法有如下:

  • Array.from()
  • Array.of()
  • Array.from()

    将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 SetMap

    1
    2
    3
    4
    5
    6
    7
    let arrayLike = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    }
    let arr2 = Array.from(arrayLike) // ['a', 'b', 'c']

    还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组

    1
    2
    Array.from([1, 2, 3], (x) => x * x)
    // [1, 4, 9]

    Array.of()

    用于将一组值,转换为数组

    1
    Array.of(3, 11, 8) // [3,11,8]

    没有参数的时候,返回一个空数组

    当参数只有一个的时候,实际上是指定数组的长度

    参数个数不少于 2 个时,Array()才会返回由参数组成的新数组

    1
    2
    3
    Array() // []
    Array(3) // [, , ,]
    Array(3, 11, 8) // [3, 11, 8]

    ES6 中新增的 Set,Map 两种数据结构怎么理解

    如果要用一句来描述,我们可以说

    Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构

    什么是集合?什么又是字典?

  • 集合
  • 是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
  • 字典
  • 是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同 区别:
  • 共同点:集合、字典都可以存储不重复的值
  • 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储
  • 1.Set

    Set是 es6 新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合
    Set本身是一个构造函数,用来生成 Set数据结构

    1
    const s = new Set()

    增删改查

    Set的实例关于增删改查的方法:

  • add()
  • delete()
  • has()
  • clear()
  • add()

    添加某个值,返回 Set 结构本身
    当添加实例中已经存在的元素,set不会进行处理添加

    1
    s.add(1).add(2).add(2) // 2只被添加了一次

    delete()
    删除某个值,返回一个布尔值,表示删除是否成功

    1
    s.delete(1)

    has()

    返回一个布尔值,判断该值是否为 Set 的成员

    1
    s.has(2)

    clear()

    清除所有成员,没有返回值

    1
    s.clear()

    遍历

    Set实例遍历的方法有如下:

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员
  • 2.Map

    Map类型是键值对的有序列表,而键和值都可以是任意类型
    Map本身是一个构造函数,用来生成 Map 数据结构

    1
    const m = new Map()

    增删改查

    Map 结构的实例针对增删改查有以下属性和操作方法:

  • size 属性
  • set()
  • get()
  • has()
  • delete()
  • clear()
  • 遍历

    Map结构原生提供三个遍历器生成函数和一个遍历方法:

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员
  • ES6 中的 Promise 以及试用场景

    1.介绍

    Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大
    在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    doSomething(function (result) {
    doSomethingElse(
    result,
    function (newResult) {
    doThirdThing(
    newResult,
    function (finalResult) {
    console.log('得到最终结果: ' + finalResult)
    },
    failureCallback
    )
    },
    failureCallback
    )
    }, failureCallback)

    阅读上面代码,是不是很难受,上述形成了经典的回调地狱

    现在通过Promise的改写上面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    doSomething()
    .then(function (result) {
    return doSomethingElse(result)
    })
    .then(function (newResult) {
    return doThirdThing(newResult)
    })
    .then(function (finalResult) {
    console.log('得到最终结果: ' + finalResult)
    })
    .catch(failureCallback)

    瞬间感受到promise解决异步操作的优点:

  • 链式操作减低了编码难度
  • 代码可读性明显增强
  • 下面我们正式来认识promise

    状态
    promise 对象仅有三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)
  • 特点

  • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
  • 一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果
  • 流程
    认真阅读下图,我们能够轻松了解promise整个流程

    2.用法

    Promise对象是一个构造函数,用来生成Promise实例

    1
    const promise = new Promise(function (resolve, reject) {})

    Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”
  • 实例方法

    Promise构建出来的实例存在以下方法:

  • then()
  • catch()
  • finally()
  • then()

    then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数
    then方法返回的是一个新的Promise实例,也就是promise能链式书写的原因

    1
    2
    3
    4
    5
    6
    7
    getJSON('/posts.json')
    .then(function (json) {
    return json.post
    })
    .then(function (post) {
    // ...
    })

    catch()

    catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数

    1
    2
    3
    4
    5
    6
    7
    8
    getJSON('/posts.json')
    .then(function (posts) {
    // ...
    })
    .catch(function (error) {
    // 处理 getJSON 和 前一个回调函数运行时发生的错误
    console.log('发生错误!', error)
    })

    Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    getJSON('/post/1.json')
    .then(function (post) {
    return getJSON(post.commentURL)
    })
    .then(function (comments) {
    // some code
    })
    .catch(function (error) {
    // 处理前面三个Promise产生的错误
    })

    一般来说,使用 catch 方法代替then()第二个参数
    Promise对象抛出的错误不会传递到外层代码,即不会有任何反应

    1
    2
    3
    4
    5
    6
    const someAsyncThing = function () {
    return new Promise(function (resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2)
    })
    }

    浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程
    catch()方法之中,还能再抛出错误,通过后面 catch 方法捕获到

    finally()

    finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

    1
    2
    3
    4
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});

    构造函数方法

    Promise构造函数存在以下方法:

  • all()
  • race()
  • allSettled()
  • resolve()
  • reject()
  • try()
  • 怎么理解 ES6 中的 Proxy 以及使用场景

    1.介绍

    定义: 用于定义基本操作的自定义行为
    本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

    元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
    一段代码来理解

    1
    2
    3
    4
    5
    6
    7
    #!/bin/bash
    # metaprogram
    echo '#!/bin/bash' >program
    for ((I=1; I<=1024; I++)) do
    echo "echo $I" >>program
    done
    chmod +x program

    这段程序每执行一次能帮我们生成一个名为 program 的文件,文件内容为 1024 行 echo,如果我们手动来写 1024 行代码,效率显然低效

  • 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
  • Proxy亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等

    2.用法

    Proxy为 构造函数,用来生成 Proxy实例

    1
    var proxy = new Proxy(target, handler)

    参数

    target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))

    handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

    3. 使用场景

    Proxy其功能非常类似于设计模式中的代理模式,常用功能如下:

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理
  • —————-