Vue2
推荐一个学习vue的网站:Vue3
说说你对vue的理解
前端发展背景
最早的网页是没有数据库的,可以理解成就是一张可以在网络上浏览的报纸,就是纯静态页面
直到CGI
技术的出现通过 CGI Perl 运行一小段代码与数据库或文件系统进行交互
(前后端交互)
后来JSP(Java Server Pages)技术取代了CGI技术,其实就是Java + HTML
1 | <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> |
JSP有一个很大的缺点
,就是不太灵活。JSP使用 Java
而不是 JavaScript
,并且 Java 代码只能在服务器端运行。我们每次的请求:获取的数据、内容的加载,服务器都会做对应的处理,并渲染dom然后返回渲染好的dom,简单的来说,JSP把页面的渲染工作完全交给后端服务器。
后来ajax
火了,它允许用户在不刷新整个页面的前提下,和后端服务器交换数据,并由浏览器执行js代码,更新部分页面。
随后移动设备的普及,Jquery的出现,以及SPA(Single Page Application 单页面应用)的雏形,Backbone EmberJS AngularJS 这样一批前端框架随之出现,但当时SPA的路不好走,例如SEO问题,SPA 过多的页面、复杂场景下 View 的绑定等,都没有很好的处理经过这几年的飞速发展,节约了开发人员大量的精力、降低了开发者和开发过程的门槛,极大提升了开发效率和迭代速度。我们可以看到Web技术的变化之大与快,每一种新的技术出现都是一些特定场景的解决方案,那我们今天的主角Vue又是为了解决什么呢?
vue是什么
是一个用于创建用户界面
的开源JavaScript框架
,也是一个创建单页应用(SPA)
的前端框架。
Vue核心特性
数据驱动视图更新
数据驱动(MVVM)
MVVM表示的是 Model-View-ViewModel
- Model:模型层,负责处理业务逻辑以及和服务器端进行交互
- View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面
- ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁,在vue中这个桥梁是vue实例
组件化
降低了代码的耦合度,可维护性,可扩展性高,便于调试
指令系统
指令 (Directives) 是带有
v- 前缀
的特殊属性
,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM常用的指令
- 条件渲染指令
v-if
- 列表渲染指令
v-for
- 属性绑定指令
v-bind
- 事件绑定指令
v-on
- 双向数据绑定指令
v-model
- 条件渲染指令
说说你对vue双向绑定的理解
双向绑定
不等同于响应式
了,这两个东西是有区别的。
响应性
是一种可以使我们声明式地处理变化的编程范式。简单来讲就是当更改响应式数据
时,视图会随即自动更新
。而实现这个功能的原理就是劫持数据,收集依赖,当数据发生变化时,执行相应的依赖(副作用/更新视图)。响应式的具体实现原理可以参考后面的文章《说说Vue实例挂载过程中发生了什么》,《手写一个简单的vue》,《说说你对Vue.observable的理解》。
双向绑定
是数据变化驱动视图更新,视图更新触发数据变化。其实就是v-model
的功能,而我们知道v-model
只是一个语法糖。因此如果要问双向绑定的原理,思路应该是如何实现这个语法糖。其原理是把input
的value绑定data的一个值,当原生input的事件触发时,用事件的值来更新data的值。
1 | <!-- 使用 v-model --> |
Vue的学习路线
原生开发
通过script标签引入vue.js,src属性通常是http链接,或者下载到本地的vue.js文件的路径。
1 | <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> |
如果是http链接,当浏览器加载这个脚本会发送一个get请求获取并执行vue的js代码,类似jsonp请求。
引入vue.js后,Vue这个构造函数成为全局变量,挂载到window对象上
然后我们在页面的script标签中写些代码,创建一个vue实例,传入一个配置对象
1 | const app = new Vue({ |
此时我们还未引入组件的概念,但是我们已经能够学习vue的大部分知识点了。包括模板语法,数据绑定,数据代理如何实现,vue的常用指令,计算属性,数据监听,vue的生命周期等等。
原生开发之组件化开发
什么组件?组件化开发有什么好处?
组件就是能实现局部功能的html,css,js代码的集合,组件化开发有利于代码复用,提高开发效率,同时把功能上密切相关的html,css,js代码放到一起,依赖关系明确,易于维护。
vue的组件可分为单文件组件和非单文件组件,非单文件组件就是通过Vue.extend({}),返回一个VueComponent构造函数,这个
构造函数被用来创建组件实例,依赖的配置对象就是Vue.extend({})传入的对象,这个配置对象的结构和new Vue()传入的配置对象的结构几乎一致。存在如下关系,即Vuecomponent是Vue的子类
:
1 | 组件实例._proto_ = VueComponent.prototype |
非单文件组件使用
1 | <div id="app" :name="str"> |
1 | const School = Vue.extend({ |
单文件组件
单文件组件就是我们熟知的.vue
文件, 单文件组件解决了非单文件组件无法把css代码与html,js代码放到一起的问题。
显然,.vue文件是vue团队开发的文件,无法在浏览器上运行,所以我们需要借助打包工具webpack来处理这个文件,webpack又是基于nodejs的,nodejs是使用模块化开发的。这样vue的开发就过渡到了基于nodejs+webpack的模块化开发,为了简化模块化开发过程中webpack的配置,vue团队就开发了vue-cli
,即vue的脚手架
谈谈对el和template属性的理解
当我们在学习Vue的基础语法,vue的组件的适合一定涉及到了这两个容易混淆的属性。
创建Vue根实例必须要给出el属性,指明要为哪个容器服务,这个容器会成为模板;创建
组件实例
不能传入el属性。如果创建vue根实例同时配置了el和template属性,则template将替换el指定的容器成为模板(可以参考
vm.$mount
源码,template属性优先级更高),不过要注意的是nodejs开发环境中,通过import
导入的vue
是精简版的,没有模板解析器的, 模板解析器被单独提取出来作为一个开发环境的包,用来处理.vue
文件中的template
,所以在创建vue根实例的时候不能使用template
,所以无法借助它实现在页面中自动插入Vue.app的效果。1
2
3
4
5
6
7import App from './App.vue'
import Vue from 'vue'
new Vue({
el:'#root',
template:'<App></App>',
components:{App}
})上述代码会报错,不能配置
template
应当修改为:
1
2
3
4
5
6import App from './App.vue'
import Vue from 'vue'
new Vue({
el:'#root',
render:h => h(App)//传入的h是createElement函数,用来创建VNode
})或者引入完整版的vue.js
1
import Vue from 'vue/dist/vue.js'
创建组件必须指定组件的结构,即template,组件的模板,不必指定组件为哪个容器服务(不要el)
el指定的容器中的
结构
可以被抽离为一个一个单独的模板template
,一个个单独的组件,也就是说模板中可以不写实际结构,只写组件标签,这些组件标签会在模板解析的时候被解析。其实组件中的
template
也能被拆分,从而形成一个一个组件,这就是组件的嵌套。
说说Vue实例挂载过程中发生了什么
我们都听过知其然知其所以然这句话
那么不知道大家是否思考过new Vue()
这个过程中究竟做了些什么?
过程中是如何完成数据的绑定
,又是如何将数据渲染到视图
的等等。
流程图一览
详细分析
首先找到vue
的构造函数
vue构造函数源码
1 | //源码位置:src\core\instance\index.js |
options
是用户传递入的配置对象,包含data、methods
等常用属性。
vue
构建函数调用了_init
方法,并传入了options
:
_init方法源码
1 | //位置:src\core\instance\init.js |
分析后得出如下结论:
- 在调用
beforeCreate
之前,主要做一些数据初始化的工作,数据初始化
并未完成,像data
、props
这些对象内部属性无法通过this
访问到 - 执行
created
的时候,数据已经初始化完成,能够通过this
访问data
、props
这些对象的属性,但这时候并未完成dom
的挂载,因此无法访问到dom
元素 - 通过调用
vm.$mount
方法实现了dom挂载
我们主要进一步分析initState
方法
initState方法源码
1 | //源码位置:src\core\instance\state.js |
分析后发现,initState
方法依次,统一初始化了props/methods/data/computed/watch
,说明在created
的时候,这些东西都准备好了
我们继续分析initState
中的initData
方法
initData方法源码
1 | function initData (vm: Component) { |
阅读源码后发现:
props
和method
在data
之前就被初始化了,所以data
中的属性值不能与props
和methods
中的属性值重复
,之所以要防止重复,因为它们都会被代理到this(vm)
上,都是直接通过this
来访问,重复了就会产生冲突。data
定义的时候可选择函数形式或者对象形式(组件只能为函数形式)initData
方法把vm._data
中的属性代理到vm
上并给vm._data
添加了响应式
(实现了数据的代理,给数据添了响应式)。
vue的数据代理核心在于proxy
方法,我们来看看它做了什么
proxy方法
1 | function proxy(target, sourceKey, key) { |
其实还是蛮容易理解的。
vue数据监听的核心在于observe
方法,我们来分析一下这个方法
observe方法源码
1 | function observe(obj) { |
Observer类的源码
1 | class Observer { |
我们很容易发现这个类的核心是defineReactive方法,那么这个方法内部到底做了些什么呢?
这里我自己实现了一个简易版
的defineReactive
方法来帮助理解它的原理。
1 | const obj = { name: 'tom', age: 22 } |
可以看到,defineReactive
方法的核心在于使用Object.defineProperty
给对象属性添加监听
,且没有借助其他源对象。而是闭包中的数据。真正的defineReactive
方法要做的比上述代码多的多,比如,如果value
的值是对象,则递归添加响应式,还有数据改变要触发视图更新等。
修改后的Obj对象结构如图,可以看到原来的属性被覆盖
了,变得不可枚举。
分析之后我们发现,vue2中的``数据代理和
数据监听都是通过
Object.defineProperty`实现的。
vue的构造函数中使用的挂载方法是vm.$mount
,我们尝试分析它的源码:
vm.$mount方法源码
1 | Vue.prototype.$mount = function ( |
阅读上面代码,我们能得到以下结论:
- 根元素不能是
body
或者html
,也就是说el
的指向不能是这两个元素。 $mount
方法的工作流程就是,如果没有render
函数,则解析template(模板),如果没有template(模板),只有el
(选择器),则通过选择器获取template
,然后解析template
模板,即HTML 字符串,得到render
方法。
对template
的解析步骤大致分为以下几步:
- 将
html
文档片段解析成ast
描述符 - 将
ast
描述符解析成字符串 - 生成
render
函数
生成render
函数,挂载到vm
上后,再调用mountComponent
开始开始渲染
mountComponent方法源码
1 | export function mountComponent ( |
方法主要执行在vue
初始化时声明的_render
,_update
方法,_render
的作用主要是生成vnode
_render
方法内部其实使用的是我们模板解析后得到的render
函数,最终返回一个vnode。
_render方法源码
1 | Vue.prototype._render = function (): VNode { |
_update
主要功能是调用patch
,将vnode
转换为真实DOM
,并且更新到页面中。
手写一个简单的vue
了解了Vue实例的挂载过程后,我们应该就能够模拟实现一个简单的Vue。
1 | <body> |
模板解析
1 | class Vue{ |
生命周期
vue2的生命周期函数很好实现,无非就是在特定时期调用特定的函数。
1 | class Vue{ |
添加事件
1 | class Vue{ |
给数据添加代理
1 | class Vue{ |
添加响应式
至此,我们修改数据,视图显然是不会更新的,即没有实现数据驱动视图更新的效果
,简单来说,就是没有实现响应式。
想要实现vue的响应式,我们需要收集依赖,并监听数据变化
所谓收集依赖,就是要知道data
中的某个属性,到底在哪些文本结点中使用过了,换句话说,就是这些文本结点依赖data中的那个数据,这个数据改变时,我们需要监听到这个数据的变化,然后通知依赖这个数据的文本结点更新内容。
依赖收集是在模板解析
过程中进行的,我们定义一个Watcher
类来记录文本结点
,文本结点内部未编译前的字符串
,和它依赖的数据(vm.key)
。
1 | class Watcher{ |
1 | class Vue{ |
双向绑定
双向绑定
并不等同于响应式
了,这两个东西是有区别的。
响应性
简单来讲就是当更改响应式数据时,视图会随即自动更新,即数据驱动视图更新
。而实现这个功能的原理就是劫持(监听)数据
,收集依赖
,当数据发生变化时,执行相应的依赖(副作用/更新视图)。
双向绑定
是数据变化驱动视图更新,视图更新触发数据变化。其实就是v-model
的功能,而我们知道v-model
只是一个语法糖。因此如果要问双向绑定
的原理,思路应该是如何实现这个语法糖
。
只需完善$compile
方法和update
方法,完整代码如下
1 | class Watcher { |
Vue.observable你有了解过吗?说说看
Vue.observable
,让一个对象变成响应式数据。Vue
内部会用它来处理 data
函数返回的对象
在 Vue 2.x
中,被传入的对象会直接被 Vue.observable
变更,它和被返回的对象是同一个对象,这一点,看看前面对defineReactive
方法的介绍就很容易理解了。
在 Vue 3.x
中,则会返回一个可响应的代理对象,而对源对象直接进行变更仍然是不可响应的,因为在vue3中响应式的实现是基于Proxy
这个构造函数,传入一个对象,会返回一个新的
代理对象,对代理对象的修改会映射到源对象。
使用场景
创建一个js
文件
1 | // 引入vue |
在.vue
文件中直接使用即可
1 | <template> |
详细解释
依赖收集
是组件初始化过程中,模板解析时候的工作,组件模板解析的时候,如果使用到了某个响应式对象
的某个属性,就会new
一个watcher
,存储到Dep.target
中,然后取值的时候会触发getter
,getter
内部会判断Dep.target
是否为空,不为空则收集依赖,把这个watcher
取出来,push
到这个属性(key)的deps
(依赖数组)中。然后某个属性值修改的时候就会触发对应的setter
,通知这些依赖的watcher
更新内容,即调用依赖这个属性(key)的watcher
的update
方法。
所以说Vue.observable
只能给数据添加响应式,但是想要实现数据修改,依赖这些数据的组件也重新渲染,就需要在模板解析过程中收集依赖。
说说Vue的生命周期
vue的生命周期指的是vue实例从创建到销毁的过程,可分为vue实例创建前后,dom挂载前后,数据更新前后,vue实例销毁前后四个阶段。这四个阶段分别对应了8
个生命周期函数。
生命周期函数指的是在vue实例特定时间段执行的函数。
这里拿vue2的生命周期函数举例。
beforeCreate:vue实例刚被创建,能拿到this,部分初始化工作完成,但是数据代理还未开始(未调用
initState
方法),此时无法通过this获取data和methods等created: 数据代理结束,能通过this拿到data和methods,但是模板解析还未开始(未调用
vm.$mount
方法),页面展示的是未经vue编译的dom。beforeMount:template模板解析结束,但是虚拟dom还未转化成真实dom挂载到页面中。
mounted:把
初始的
真实DOM放入页面,此时对dom的操作是有效的。beforeUpdate:此时数据是新的,页面展示的内容是旧的, 因为vue视图是异步更新的。
updated: 此时
新旧虚拟dom比较
完毕,页面已更新。beforeDestroy: 实例被销毁前调用,此时实例
属性与方法
仍然有效destroyed: 完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器;并不能清除DOM,仅仅销毁实例.
数据请求在created和mouted的区别
- 这两个阶段
数据
和方法
都已经初始化,都能通过this
访问到,因为created
的执行时期更早,所以能更早的发送请求,更快的返回数据。 - 一个组件中有子组件,它们的生命周期函数的执行顺序是先执行父组件的前三个声明周期函数,再执行子组件的前四个生命周期函数,然后在执行哦父组件的
mouted
函数。
说说你对slot的理解?slot使用场景有哪些?
slot
的作用就是用来自定义组件内部的结构
slot
可以分来以下三种:
- 默认插槽
- 具名插槽
- 作用域插槽
默认插槽
子组件用<slot>
标签来确定渲染的位置,标签里面可以放DOM
结构,当父组件没有往插槽传入内容,标签内DOM
结构就会显示在页面
父组件在使用的时候,直接在子组件的标签内写入内容即可
子组件Child.vue
1 | <template> |
父组件
1 | <Child> |
具名插槽
默认插槽形如
1 | <slot> |
当我们给slot
标签添加name
属性,默认插槽就变成了具名插槽
当我们需要在子组件内部的多个位置使用插槽的时候,为了把各个插槽区别开,就需要给每个插槽取名。
同时父组件传入自定义结构的时候,也要指明是传递给哪个插槽的。
子组件Child.vue
1 | <template> |
父组件
1 | <child> |
template
标签是用来分割,包裹自定义结构的。v-slot
属性用来指定这部分结构用来替换哪个插槽。
v-slot:default
可以简化为#default
,v-slot:content
可以简化成#content
作用域插槽
子组件在slot
标签上绑定属性来将子组件的信息传给父组件使用,所有绑定的属性会被收集成一个对象,被父组件的v-slot
属性接收。
子组件Child.vue
1 | <template> |
父组件
1 | <child> |
可以通过解构获取v-slot={user}
,还可以重命名v-slot="{user: newName}"
和定义默认值v-slot="{user = '默认值'}"
所在slot
中也存在’’双向数据传递’’,父组件给子组件传递页面结构
,子组件给父组件传递子组件的数据。
你有写过自定义指令吗?自定义指令的应用场景有哪些?
什么是指令
在vue
中提供了一套为数据驱动视图
更为方便的操作,这些操作被称为指令系统
。简单的来说,指令系统
能够帮助更方便的实现数据驱动视图更新
。
我们看到的v-
开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能
除了核心功能默认内置的指令
(v-model
和 v-show
),Vue
也允许注册自定义指令
指令使用的几种方式:
1 | //会实例化一个指令,但这个指令没有参数 |
注意:指令中传入的都是表达式,无论是不是自定义指令,比如v-bind:name = ‘tom’,传入的是tom这个变量的值,而不是tom字符串,除非写成“ ‘tom’ ”,传入的才是字符串。
如何实现
关于自定义指令,我们关心的就是三大方面,自定义指令的定义,自定义指令的注册,自定义指令的使用。
自定义指令
的使用方式和内置指令
相同,我们不再研究,其中的难点就是定义自定义指令
部分。
注册自定义指令
注册一个自定义指令有全局注册
与局部注册
两种方式
全局注册主要是通过Vue.directive
方法进行注册
Vue.directive
第一个参数是指令的名字(不需要写上v-
前缀),第二个参数可以是对象数据,也可以是一个指令函数
1 | //全局注册一个自定义指令 `v-focus` |
局部注册通过在组件配置对象中设置directives
属性
1 | directives: { |
然后就可以使用
1 | <input v-focus /> |
定义自定义指令
自定义指令本质就是一个包含特定钩子函数的js对象
在vue2中,这些常见的钩子函数包括:
bind()
只调用一次
,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置,此时无法通过el
拿到父级元素inserted()
绑定指令的元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中),此时可以通过
el
拿到父级元素mounted()
指令绑定的元素被插入到
文档
中之后update()
传入指令的值改变后触发
unbind()
只调用一次,指令与元素
解绑
时调用
注意:上述钩子函数再vue3中并不都有效,vue3中的自定义指令钩子函数和生命周期函数一致,具体见官方文档,https://cn.vuejs.org/guide/reusability/custom-directives#directive-hooks
所有的钩子函数的参数都有以下:
el:指令所绑定的元素,可以用来直接操作
DOM
binding:
一个对象,包含以下property
name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
vnode
:Vue
编译生成的虚拟节点oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用
应用场景
给某个元素添加节流
1 | // 1.设置v-throttle自定义指令 |
图片懒加载
Vue常用的修饰符有哪些有什么应用场景
修饰符是什么
在Vue
中,修饰符是用来修饰Vue中的指令的,它处理了许多DOM
事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理。
vue
中修饰符分为以下五种:
- 表单修饰符
- 事件修饰符
- 鼠标按键修饰符
- 键值修饰符
- v-bind修饰符
修饰符的具体作用
表单修饰符
在我们填写表单的时候用得最多的是input
标签,指令用得最多的是v-model
关于表单的修饰符有如下:
- lazy
- trim
- number
lazy
在我们填完信息,光标离开标签的时候,才会将值赋予给value
,也就是在change
事件之后再进行信息同步
1 | <input type="text" v-model.lazy="value"> |
trim
自动过滤用户输入的首尾空格字符,而中间的空格不会过滤
1 | <input type="text" v-model.trim="value"> |
number
自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat
解析,则会返回原来的值
1 | <input v-model.number="age" type="number"> |
事件修饰符
stop
阻止了事件冒泡
,相当于调用了event.stopPropagation
方法
1 | <div @click="shout(2)"> |
prevent
阻止了事件的默认行为
,相当于调用了event.preventDefault
方法
1 | <form @submit.prevent="onSubmit"></form> |
self
只当在 event.target
是当前元素自身时触发处理函数,既不是冒泡触发,也不是捕获触发,简单来说就是点击的就是这个元素。
1 | <div @click.self="doThat">...</div> |
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用
@click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素自身的点击
once
绑定了事件以后只能触发一次,触发一次之后立即解除事件监听。
1 | <button @click.once="shout(1)">ok</button> |
capture
使事件触发从包含这个元素的顶层开始往下触发
1 | <div @click.capture="shout(1)"> |
passive
在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll
事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll
事件整了一个.lazy
修饰符
1 | <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --> |
不要把
.passive
和.prevent
一起使用,因为.prevent
将会被忽略,同时浏览器可能会向你展示一个警告。
passive
会告诉浏览器你不想阻止事件的默认行为
native
让组件变成像html
内置标签那样监听根元素的原生事件,否则组件上使用 v-on
只会监听自定义事件
1 | <my-component v-on:click.native="doSomething"></my-component> |
使用.native修饰符来操作普通HTML标签是会令事件失效的
Vue中组件和插件有什么区别
组件是什么
在vue中,组件就是能实现部分功能
的html,css,js代码的集合。
优势
降低整个系统的
耦合度
在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
提高代码的
可维护性
,和可复用性
由于每个组件的职责单一,并且组件在系统中是被复用的。
插件是什么
插件通常用来为 Vue
添加全局功能,比如通过全局混入来添加一些组件选项。如vue-router
区别
两者的区别主要表现在以下几个方面:
- 编写形式
- 注册形式
编写形式
编写一个组件,可以有很多方式,我们最常见的就是vue
单文件的这种格式,每一个.vue
文件我们都可以看成是一个组件。
vue
插件就是一个实现了 install
方法的对象。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象(options)。
1 | MyPlugin.install = function (Vue, options) { |
注册形式
vue
组件注册主要分为全局注册
与局部注册
局注册通过Vue.component
方法,第一个参数为组件的名称,第二个参数为传入的配置项
1 | Vue.component('my-component-name', { /* ... */ }) |
局部注册只需在用到的地方通过components
属性注册一个组件
1 | const component1 = {...} // 定义一个组件 |
插件的注册通过Vue.use()
的方式进行注册(安装),第一个参数为插件的名字
,第二个参数是可选择的配置项
1 | Vue.use(插件名字[,options]) |
注册插件的时候,需要在调用
new Vue()
启动应用之前
完成,Vue.use
会自动阻止多次注册相同插件,只会注册一次。
Vue组件通信的方式有哪些
vue
中,每个组件之间的都有独自的作用域
,组件间的数据是无法共享的\,但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的,要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统。
组件间通信的分类
- 父子组件之间的通信
- 兄弟组件之间的通信
- 祖孙与后代组件之间的通信
- 非关系组件间之间的通信
组件间通信的方案
props传递数据
适用场景:父组件传递数据给子组件(父子组件之间的通信)
子组件设置props
属性,定义接收父组件传递过来的参数,父组件在使用子组件标签中添加属性来传递值。
1 | //Children.vue |
1 | <Children name="jack" age=18 /> |
要注意的是,props中的数据是父组件的,子组件 不能直接修改
$emit 触发自定义事件
适用场景:子组件传递数据给父组件(父子组件通信)
子组件通过$emit
触发自定义事件,$emit
第一个参数为自定义的事件名,第二个参数为传递给父组件的数值
父组件在子组件上绑定事件监听,通过传入的回调函数
拿到子组件的传过来的值。
1 | //Children.vue |
1 | //Father.vue |
ref
在 Vue 2 中,this.$refs
是一个对象,它包含了所有通过 ref
属性注册的 DOM 元素
或组件实例
。你可以使用 this.$refs
来直接访问这些元素或组件,从而进行操作,如获取DOM节点、调用子组件的方法,数据等。
注意:this.$refs
只能
在父组件中用来引用通过 ref
属性标记的子组件
或 DOM 元素
1 | <Children ref="foo" /> |
同时,子组件也可通过this.$parent
拿到父组件实例
EventBus(事件总线)
使用场景:兄弟组件传值
通过共同祖辈
$parent
或者$root
搭建通信兄弟组件
1
this.$parent.on('add',this.add)
另一个兄弟组件
1
this.$parent.emit('add')
本质就是要找到一个两个兄弟组件都能访问到的vue实例,这个vue实例的作用好像连接这两个组件的管道,通过这个Vue实例来通行。
provide 与 inject
跨层级传递数据,传递方向是单向
的,只能顶层向底层传递。
在祖先组件
定义provide
属性,返回
传递的值,在后代组件通过inject
接收祖先组件传递过来的值
1 | // 普通类型是响应式的复杂类型则不是,这和vue2数据响应式的实现方式有关 |
1 | export default { |
Vuex
SPA
什么是SPA,和MPA有什么区别?
SPA指的是只有一个页面的web应用程序,与MPA(多页面应用程序相比),通过局部更新来更新页面内容,而不是通过页面切换,整页刷新,刷新速度更快,用户体验更好,同时把页面渲染工作交给客户端,减轻了服务端的压力。
缺点是不利于搜索引擎优化,首次加载速度较慢。
面试官:你对SPA单页面的理解,它的优缺点分别是什么?如何实现SPA应用呢 | web前端面试 - 面试官系列
如何提高首屏加载速度?
首屏加载时间,指的是浏览器从响应用户输入网址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容。
首屏加载慢的原因
- 网络延时问题
- 资源文件体积是否过大
- 资源是否重复发送请求去加载了
- 加载脚本的时候,渲染内容堵塞了
提高首屏加载速度的方法:
使用路由懒加载
对于非首屏组件,使用路由懒加载,当需要访问这些组件的时候再加载对应的资源。
开发单页面应用程序时,只有一个
html
页面,打包后也只有一个index.html
页面,其他所谓的页面
,都是通过JavaScript
动态地修改DOM
来实现的。开发过程中,一个页面对应一个或者多个组件
,在打包后,每个组件都会转化成对应的css
,js
代码,其中的js代码
不光包括业务逻辑,也负责修改dom,构建页面。如果使用
路由懒加载
,我们可以观察到,打包后的js,css文件数量
变多了,每个文件的体积也变小了,是因为使用懒加载的组件
都被打包成独立的
css,js文件了。这样,index.html
引入的的js
,css
文件的体积也会变小,因为只包含首屏组件
需要的js,css
代码。缓存静态资源
对于已经请求过的资源,再次请求直接使用缓存。
压缩图片等静态资源的大小
使用服务端渲染SSR
将首页的html结构的拼接工作交给后端服务器
对于vue,推荐使用
nuxt.js
使用CDN加速
内联首屏关键css
如何提高SPA的SEO
- 使用服务端渲染SSR
SSR
什么是SSR
指由服务端
完成页面的 HTML结构拼接
的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。
传统web开发,一般就是多页面应用程序,每个页面的html结构都在服务端拼接好。
单页面应用程序(SPA)通过浏览器执行js代码来实现页面的html结构的替换,拼接。
解决了什么
有利于SPA的SEO
使用服务端渲染,返回的页面就已经包含了一定的页面结构,能够被搜索引擎爬取。
提高的首屏渲染速度
使用服务端渲染,将
首屏结构
交给服务端来拼接,这样不必等待页面所有js
加载完成,就可以看到首屏视图。
怎么实现
给对象添加属性视图不刷新
为什么
在vue2中,数据的响应式是在vue实例创建的时候,使用Object.defineProperty来实现的,后续添加属性并没有使用这个方法
来添加响应式。
如何解决
可以通过**Vue.set()或者this.$set()**来给新增属性添加响应式。
Vue.set( target, propertyName/index, value )
target:{Object | Array}
propertyName/index:{string | number}
value:{any}
这个方法的本质就是使用Object.defineProperty()给新添加的属性赋予响应式
v-if和v-for的优先级是什么
我们查看下vue
源码
源码位置:\vue-dev\src\compiler\codegen\index.js
1 | export function genElement (el: ASTElement, state: CodegenState): string { |
可以观察到,v-for
的优先级是比v-if
高的,这就意味着,当要渲染某个列表项的时候,会先渲染全部的列表项,再判断是否改保留某个列表项。
但在实际开发中,有时可能希望根据某个条件
来决定是否渲染整个列表
,而不是对列表中的每个项都应用条件,此时如果我们把v-for
和v-if
写到同一个元素上,即便最终我们并不要渲染整个列表,也会由于v-for
的优先级更高,仍然会创建这些元素的 VNode
,造成不必要的性能浪费。在这种情况下,可以考虑调整指令的位置或结构。
1 | <ul v-if="shouldShowList"> |
1 | <template v-if="isShow"> |
即便不是根据某个条件来判断是否渲染整个列表
,而是每个列表项都使用不同的判断条件
,也不建议把v-if
和v-for
写到一起,而是先筛选
出那些需要渲染的列表项
,然后再使用v-for
渲染。
1 | <li v-for="item in filteredItems" :key="item.id">{{ item.name }}</li> |
1 | computed: { |
而在 Vue 3.x 中,虽然 v-if
的优先级理论上高于 v-for
,但官方仍然建议不要在同一元素上同时使用这两个指令,以防止可能出现的逻辑混淆和不必要的计算。
v-if和v-for如何理解
共同点
二者都是用来控制页面中元素的显示与隐藏,当表达式值为false
的时候,都不会占据页面的位置。
区别
v-show
本质是通过切换css样式
来实现元素的显示与隐藏,令display:none
让元素隐藏,dom元素还存在。
v-if
本质则是通过控制dom元素的删除与创建
来实现元素的显示与隐藏,因为v-if
直接操作dom
,所以v-if
有更高的性能消耗。
v-if
才是真正的条件渲染
,v-show
的值为false
的元素,也会被渲染。
nextTick
vue数据改变,并不会立即操作dom来更新视图;而是会开启一个异步更新队列,如果我们重复修改某个数据,异步更新队列还会进行去重操作;对于同一个组件内的多个数据变化,它们会被合并成一次更新;同一事件循环
中的所有数据变化完成之后,再进行一次批量更新,创建新的虚拟dom,与旧的虚拟dom比较,得出最小变更,再把这个最小变更应用到实际dom。
使用方法:Vue.nextTick(()=>{})
或者this.$nextTick(()=>{})
nextTick是异步api,会返回一个promise对象,传入的回调函数会在dom更新后
执行,也可以使用async/await来操作dom。
1 | <div id="app"> {{ message }} </div> |
1 | this.message = '修改后的值' |
1 | this.message = '修改后的值' |
面试官:Vue中的$nextTick有什么作用? | web前端面试 - 面试官系列
说说你对vue中key的理解
key是给每一个虚拟dom(或者说vnode)的唯一id。在diff过程中,根据key值,可以更准确, 更快的找到待比较的虚拟dom,从而优化diff算法,提高dom的复用率。
如果不设置key,那key值默认就都是undefined,将会按顺序进行新旧虚拟dom的比较。
详细可参考禹神的vue视频:030_尚硅谷Vue技术_key作用与原理_哔哩哔哩_bilibili
什么是虚拟dom?有什么作用?如何实现?
是什么
虚拟dom本质就是一个用来描述真实dom的js对象,是对真实dom的高度抽象。
有什么作用
- 操作
实际dom
是非常消耗性能的,频繁操作dom可能导致页面卡顿。使用diff算法进行,进行新旧虚拟dom之间的比较,能帮助我得出最小的变更,再把这个变更应用到实际dom中,从而减少dom的操作次数。 - 抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI。从
React
到Vue
,虚拟DOM
为这两个框架都带来了跨平台的能力(React-Native
和Weex
)
在vue中如何创建虚拟dom
createElement
创建 VNode
的过程,每个 VNode
有 children
,children
每个元素也是一个VNode
,这样就形成了一个虚拟树结构,用于描述真实的DOM
树结构。
一个典型的 vnode
对象可能包含以下字段:
tag
: 元素类型(例如'div'
、'span'
等)data
: 包含元素的属性、样式、事件处理器等元数据children
: 子节点数组,可以是其他vnode
或文本字符串text
: 如果是文本节点,则包含文本内容el
: 引用对应的真实DOM节点(仅在某些实现中存在)
diff算法
说说你对keep-alive的理解
keep-alive是vue中的内置组件,包裹动态组件(router-view)时,会缓存不活动的组件实例,而不是销毁它们,防止重复渲染DOM。
被缓存的组件会额外多出两个生命周期activated
和deactivated
keep-alive可以使用一些属性
来更精细的控制组件缓存。
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存max
- 数字。最多可以缓存多少组件实例,超出这个数字之后,则删除第一个被缓存的组件。
1 | <keep-alive include="a,b"> |
注意vue3中的keep-alive的语法不同于vue2,可以参考官方文档
组件名称匹配,组件名称指的到底是是什么呢?
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),匿名组件不能被匹配。
组件被缓存了,如何获取数据呢?
借助beforeRouteEnter
或则activated
生命周期函数
1 | beforeRouteEnter(to, from, next){ |
1 | activated(){ |
Mixin
mixin本质就是一个js对象,包含了vue组件任意功能选项,如data
、components
、methods
、created
、computed
等等
,被用来分发 Vue
组件中的可复用功能。
可分为全局混入和局部混入
1 | Vue.mixin({ |
1 | export default { |
如果混入组件的时候出现了功能选项冲突,一般以组件功能选项为准。
面试官:说说你对vue的mixin的理解,有什么应用场景? | web前端面试 - 面试官系列
跨域是什么?Vue项目中你是如何解决跨域的呢?
是什么
跨域本质是浏览器
基于同源策略的一种安全手段
,它是浏览器最核心也最基本的安全功能,服务器间通信不会有跨域的问题。
所谓同源(即指在同一个域)具有以下三个相同点
- 协议相同(protocol)
- 主机相同(host)
- 端口相同(port)
反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域(非同源产生跨域)
举个例子,我们直接打开 HTML 文件使用的是file:///
协议加载,如果文档内部请求了其他网络资源
,因为HTTP 请求使用的是 http://
或 https://
协议,协议不同,就发生了跨域。
如何解决
- JSONP
- CORS
- Proxy
JSONP
利用了script
标签可以跨域加载脚本
其实还有其他标签可以跨域加载资源,貌似大部分标签都可以跨域加载资源…..
媒体资源
标签 | 作用 |
---|---|
img标签 | 可以跨域加载图像资源 |
audio和video标签 | 可以跨域加载视频,音频 |
前端基础三大文件
标签 | 作用 |
---|---|
link标签 | 可以跨域加载CSS文件 |
iframe标签 | 可以跨域加载HTML页面。 |
script标签 | 可以跨域加载脚本 |
jsonp请求有个明显的缺点只能发送get
请求
Proxy
代理(Proxy)也称网络代理
,是一种特殊的网络服务
,允许一个(一般为客户端)通过代理
与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击。
代理的方式也可以有多种:
在脚手架中配置
在开发过程中,我们可以在
脚手架
中配置代理。我们可以通过webpack(或者vite)
为我们开起一个本地服务器
(devServer,域名一般是localhost:8080
),作为请求的代理对象
,所以说,这个本地服务器不仅能部署
我们开发打包
的资源,还能起到代理
作用。通过该服务器
转发
请求至目标服务器,得到结果再转发给前端,因为服务器之间通信不存在跨域问题,所以能解决跨域问题。打包之后的项目文件,因为脱离了代理服务器,所以说这种方式只能在
开发环境
使用。1
2
3
4
5
6
7
8
9
10//vue.config.js 即vue-cli脚手架(基于webpack)开发的vue项目
devServer: {
proxy: {
'/api': {//匹配所有以/api开头的请求路径
target: 'http://localhost:3000', // 后端服务器地址
changeOrigin: true, // 改变请求源,让浏览器认为请求是来自本地
pathRewrite: { '^/api': '' }, // 重写路径,移除前缀
}
}
}1
2
3
4
5
6
7
8
9//vite.config.js 即vue-create脚手架(基于vite)开发的vue项目
server: {
proxy: {
'/api': {//匹配所有以/api开头的请求路径
target: 'http://localhost:3000', // 后端服务器地址
changeOrigin: true, // 改变请求源,让浏览器认为请求是来自本地
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径,移除前缀
}
}可以看到,我们要使用代理,在编写接口时,就不能书写完整的路径,比如就不能直接把请求url写成
https://www.sanye.blog/books
,这样必然跨域,我们应该把请求写为/books
,部署到本地服务器后加载网页,发起这个请求前,会先自动与域名
拼接,实际的请求就变为http://localhost:8080/books
,这样就没跨域,不过确实,这么操作的话,就是在请求本地服务器中的books
资源,而不是目标服务器中的,如果我们本地服务器中有这个资源(vue-cli中是public目录下有books文件,无后缀),那么本地服务器就会把这个资源返回给浏览器,无论我们是否开启了代理,所以我们实际还要添加/api
类似的多余的前缀,确保我们访问的不是
本地服务器中的资源,然后本地服务器会帮我们按照配置的规则进行路径重写
,得到正确的请求URL,再向目标服务器请求资源。在服务端开启代理
其实也不是打包后就不能通过代理来解决跨域问题,如果我们把
打包后的前端资源
部署到本地的服务器
,比如使用基于node.js
的express
框架搭建的本地服务器
,我们也可以通过配置代理来解决跨域问题。1
2
3
4
5
6
7
8
9
10
11
12const express = require( 'express ')
const app = express()
//其实webpack-dev-server开启代理功能的核心也是这个中间件
const { createProxyMiddleware } = require( 'http-proxy-middleware ');
app.use(express.static( . /public))//引入静态资源
app.use( '/api' ,createProxyMiddleware({
target: ' https:// www.toutiao.com',
changeOrigin:true,
pathRewrite:{
'^/api ' : ''
}
}))总之想要配置代理,就离不开一台允许你配置代理的
服务器
,把打包后的前端资源托管到其他平台
,我们也无法来配置代理,也就无法解决跨域问题。
CORS
CORS (Cross-Origin Resource Sharing),即跨域资源共享,意思就是虽然你在跨域请求我的资源,但是我还是选择性的
共享资源给你,浏览器根据响应头
中的特定字段
来决定是否拦截跨域请求
返回的数据。
因为需要在响应头
上做文章,所以这个工作主要是前后端协调后由后端负责,至于前后端如何协调,参考简单请求
和复杂请求
部分。
如何理解简单请求和复杂请求
简单请求 | 复杂请求 | |
---|---|---|
请求方法(携带在请求行中) | get,post,head | 除get,post,head外的请求方法 |
请求头 | 满足cors安全规范(一般不修改请求头就是安全的)Content-Type 的值仅限于以下三种之一: application/x-www-form-urlencoded multipart/form-data text/plain | 设置了自定义的 HTTP 头,或者 Content-Type 不是上述三种之一时 |
在非跨域情况下,区分二者并没有什么意义,但是在跨域情况下,发送复杂请求前,会先发送一次预检请求
,请求方法为options
,
在请求头中携带Origin
,Access-Control-Request-Method
,Access-Control-Request-Headers
字段,询问服务器是否接受来自xxx源,请求方法为xxx,请求头为xxx的跨域复杂请求
,如果接受则再发送这样的复杂请求
。
服务端处理代码(以express框架为例)
1 | app.options( '/students ',( req,res)=>{ |
这样处理起来明显比较繁琐,实际上我们借助CORS中间件
就能统一处理简单请求和复杂请求(包括预检请求)
的跨域问题。
vue项目如何部署?有遇到布署服务器后刷新404问题吗
如何部署
前后端分离开发模式下,前后端是独立布署
的,前端只需要将最后的构建物上传至目标服务器的web
容器指定的静态目录
下即可,我们知道vue
项目在构建后,是生成一系列的静态文件。
404问题
HTTP 404 错误意味着链接指向的资源不存在,问题在于为什么不存在?且为什么只有history
模式下会出现这个问题,而hash模式下不会有?
history模式,刷新页面,前端路由部分
会被当作请求URL
的一部分发送给服务器,然而服务器并没有相关配置
,所以响应404
。
而hash模式,前端路由在URL的#
后面,不会被当作请求URL的一部分。
要解决使用history路由的项目,必须和后端
沟通,当请求的页面不存在时,返回index.html
,把页面控制权全交给前端路由
,
但是这样有个问题,就是后端服务器不会再响应404
错误了,当找不到请求的资源总是会返回index.html,即便请求的资源在前后端中都不存在(即把页面控制权交给前端路由,也没有对应的页面),所以为了避免这种情况,应该在 Vue应用里面覆盖所有的路由情况
,最后给出一个 404 页面
(虽然说是404页面,但是响应状态码是200,因为返回了index.html
)
hash路由和history路由的实现原理,二者有什么区别?
哈希路由
(Hash-based Routing)和 History 路由
(History API-based Routing)是前端路由的两种常见实现方式,它们用于在单页面应用程序 (SPA) 中模拟多页面体验,而无需重新加载整个页面。
hash路由
是什么
前端路由
被放到url
的hash
部分,即url中#
后面的部分。哈希值
改变也不会触发页面重新加载
,浏览器也不会将哈希值
发送到服务器,因此无论哈希值
如何变化,服务器只会返回同一个初始 HTML 文件。
优缺点
- 不需要服务器配置支持,因为哈希值不会被发送给服务器。
兼容性好
,几乎所有浏览器都支持哈希变化事件。- URL 中包含显眼的
#
符号,可能影响美观。 - 前端路由部分十分明确,方便部署,可以部署在服务器的
任何位置
。
如何做
可以直接设置 window.location.hash
属性来改变
URL 中的哈希部分,改变 window.location.hash
不会触发页面刷新
,但它会添加一个新的历史记录条目
。
前端 JavaScript 监听 hashchange
事件来检测
哈希的变化,并根据新的哈希值更新页面内容。
1 | window.addEventListener('hashchange', function () { |
history路由
是什么
使用标准的路径形式,例如 http://example.com/page1
,前端路由
被放到url
中的资源路径
部分
优缺点
没有显眼的
#
号,更为美观需要后端支持,否则会出现
404
问题,因为前端路由会被发送到后端,而后端并未做对应配置。对较老版本的浏览器兼容性较差(IE10+ 支持 History API)
history路由的项目一般部署在
服务器根目录
,域名后面的路径就是前端路径
,否则需要在前端路由库
(比如VueRouter)中做额外配置,确保浏览器能从url中提取出文件路径。1
2
3
4
5
6
7const router = new VueRouter({
mode: 'history',
base: '/app/', // 设置基础路径
routes: [
// 你的路由配置
]
});例如,如果用户的 URL 是
http://example.com/app/user/profile
,那么前端路由库会将/user/profile
视为实际的路由路径,而/app/
则被视为基础路径。
如何做
使用 HTML5
的 History API (history.pushState()
和 history.replaceState()
) 来修改
URL,而不会触发页面刷新。
history.pushState(stateObj, title, url)
- 功能:向浏览器的
历史栈
中添加一个新的记录,并更新当前 URL
,但不重新加载页面。 - 参数
stateObj
: 一个对象,用于存储与该状态相关联的数据,可以通过popstate
事件访问。title
: 通常被忽略或设为空字符串(大多数浏览器不支持)。url
: 新的 URL,可以是相对路径或绝对路径,但不能改变域名
。
history.replaceState(stateObj, title, url)
- 功能:
替换
当前的历史记录条目,而不是添加新的条目。它同样更新当前 URL
但不刷新页面。 - 参数:与
pushState
相同。
监听 popstate
事件来响应浏览器的前进/后退按钮操作。
1 | // History 路由监听 |
说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢 ?
在划分项目结构的时候,需要遵循一些基本的原则:
- 文件夹和文件夹内部文件的语义一致性
- 单一入口/出口
- 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
- 公共的文件应该以绝对路径的方式从根目录引用
/src
外的文件不应该被引入
文件夹和文件夹内部文件的语义一致性
我们的目录结构都会有一个文件夹是按照路由模块
来划分的,如pages
文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅
应该包含路由模块,而不应该有别的其他的非路由模块的文件夹
这样做的好处在于一眼就从 pages
文件夹看出这个项目的路由有哪些
单一入口/出口
举个例子,在pages
文件夹里面存在一个seller
文件夹,这时候seller
文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js
应该作为外部引入 seller 模块的唯一入口
1 | // 错误用法 |
就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
使用相对路径
可以保证模块内部的独立性
1 | // 正确用法 |
举个例子
假设我们现在的 seller 目录是在 src/pages/seller
,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller
。
如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller
文件夹内部不需要做任何变更。
但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import
的路径做修改
总之就是要体会到相对路径的好处,移动文件也不需要修改路径,只要相对位置没变就好。
/src 外的文件不应该被引入
vue-cli
脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src
文件夹,里面放着所有的项目需要的资源,js
, css
, png
, svg
等等。src
外会放一些项目配置,依赖,环境等文件
这样的好处是方便划分项目代码文件和配置文件
Vue如何做权限管理
权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源
而前端权限归根结底是请求的发起权
,请求的发起可能有下面两种形式触发
- 页面加载触发
- 页面上的按钮点击触发
如何做
前端权限控制可以分为四个方面:
- 接口权限
- 按钮权限
- 菜单权限
- 路由权限
接口权限
接口权限目前一般采用jwt
的形式来验证,没有通过的话一般返回401
(用户不存在),跳转到登录页面重新进行登录
登录完拿到token
,将token
存起来,通过axios
请求拦截器进行拦截,每次请求的时候头部携带token
1 | axios.interceptors.request.use(config => { |
vue3有了解过吗?能说说跟vue2的区别吗?
背景
关于vue3
的重构背景,尤大是这样说的:
「Vue 新版本的理念成型于 2018 年末,当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久,但在这段时期,前端世界已经今昔非比了
在我们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题」
简要就是:
- 利用新的语言特性(es6)
- 解决架构问题
新增特性
Vue 3 中需要关注的一些新功能包括:
fragment
在 Vue 2 中,组件必须有一个根元素。而在 Vue 3 中,组件可以有多个根节点,它们会自动被
fragment
标签包裹,它不参与渲染。Teleport
Teleport
组件的内部结构
将会被“传送”到指定的目标容器中组件标签的
to
属性,指定目标容器的选择器
或DOM
节点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>composition Api
即组合式api,把逻辑紧密联系的代码放到一起,提高了代码的可维护性。
Vue3
vue3做了哪些优化?
这是一个很大的话题,这里只做简要介绍
,后续对每个部分都有详细解释
。
更小
Vue3移除了一些不常用的 API,引入
tree-shaking
,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了。更快
主要体现在编译方面:
- diff算法优化
- 静态提升
- 事件监听缓存
- SSR优化
更友好
vue3
在兼顾vue2
的options API
的同时还推出了composition API
,大大增加了代码的逻辑组织和代码复用能力。
优化方案
vue3
从很多层面都做了优化,可以分成三个方面:
源码
源码可以从两个层面展开:
源码管理
vue3
整个源码是通过monorepo
的方式维护的,根据功能将不同的模块拆分到packages
目录下面不同的子目录中这样使得
模块拆分
更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。TypeScript
Vue3
是基于typeScript
编写的,提供了更好的类型检查,能支持复杂的类型推断
性能
体积优化
编译优化
数据劫持优化
在
vue2
中,数据劫持是通过Object.defineProperty
,这个 API 有一些缺陷,并不能检测对象属性的添加和删除1
2
3
4
5
6
7
8Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})尽管
Vue
为了解决这个问题提供了set
和delete
实例方法,但是对于用户来说,还是增加了一定的心智负担。相比之下,
vue3
是通过proxy
监听整个对象,那么对于删除还是监听当然也能监听到同时
Proxy
并不能监听到内部深层次的对象变化,而Vue3
的处理方式是在getter
中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归。
语法API
这里当然说的就是composition API
,其两大显著的优化:
- 优化逻辑组织
- 优化逻辑复用
在vue2
中,我们是通过mixin
实现功能复用,如果多个mixin
混合,会存在两个非常明显的问题:命名冲突
和数据来源不清晰
而在组合式api中,我们可以将一些可复用的代码抽离出来作为一个函数
并导出,在需要使用的地方导入后直接调用即可。这个种模块化的方式既解决了命名冲突
的问题,也解决了数据来源不清晰
的问题。
1 | // mouse/index.js |
在组件中使用
1 | //导入 |
Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?
defineProperty
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改对象的现有属性,并返回此对象。
1 | function observe(obj) { |
1 | function defineReactive(obj, key, val) { |
1 | const arrData = [1,2,3,4,5]; |
缺点小结
Object.defineProperty
无法监听到数组方法对数组元素的修改- 需要遍历对象每个属性
逐个添加监听
,而且无法监听到对象属性
的添加
与删除
,如果属性值是嵌套对象,还深层监听,造成性能问题。
Proxy
Proxy
的监听是整个对象
,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了
定义一个响应式方法reactive
,这个reactive
方法就是vue3中的reactive
方法的简化版
1 | function reactive(obj) { |
测试一下简单数据的操作,发现都能劫持
1 | const state = reactive({ |
再测试嵌套对象情况,这时候发现就不那么 OK 了
1 | const state = reactive({ |
如果要解决,需要在get
之上再进行一层代理
1 | const observed = new Proxy(obj, { |
修改后输出的结果:
1 | 获取bar:[object Object] |
总结
Object.defineProperty
这个方法存在许多缺点,比如必须遍历对象
的所有属性逐个添加监听
,而且无法监听对象属性的增加与删除,如果属性的值是引用类型
还需要深度监听
,造成性能问题
。
对于数组,Object.defineProperty
方法无法监听到数组方法对数组元素的修改,需要重写数组方法。
而Proxy能监听整个对象的变化,也能监听到数组方法对数组元素的修改。
说说Vue 3.0中Treeshaking特性?
是什么
Tree shaking
是一种通过清除多余代码方式来优化项目打包体积
的技术。
如何做
Tree shaking
是基于ES6
模块语法(import
与exports
),主要是借助ES6
模块的静态编译
思想,在编译时
就能确定模块的依赖关系,以及输入和输出的变量。
Tree shaking
无非就是做了两件事:
- 编译阶段利用
ES6 Module
判断哪些模块已经加载 - 判断那些模块和变量未被使用或者引用,进而删除对应代码
相比之下,CommonJS 模块语法导入导出的始终是整个对象,只有在运行时才能确定具体的依赖关系,是动态加载的。这使得在编译阶段
难以准确地判断哪些代码是真正必要的,因此不适合用于 tree shaking
。
Composition Api 与 Options Api 有什么不同?
代码组织方式
选项式api按照代码的类型
来组织代码;而组合式api按照代码的逻辑
来组织代码,逻辑紧密关联的代码会被放到一起。
代码复用方式
在选项式api这,我们使用mixin
来实现代码复用,使用单个mixin
似乎问题不大,但是当我们一个组件混入大量不同的 mixins
的时候
就存在两个非常明显的问题:
- 命名冲突
- 数据来源不清晰
而在组合式api中,我们可以将一些可复用的代码抽离出来作为一个函数
并导出,在需要在使用的地方导入后直接调用即可。这个种模块化
的方式,既解决了命名冲突
的问题,也解决了数据来源不清晰
的问题。