目录
一、Vue是什么
Vue.js(简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。(一个构建数据驱动的渐进性框架),通过API实现响应数据绑定和视图更新。
二、Vue核心特性
数据驱动(MVVM)
这里的详细解释参考之前的文章:Vue 前端面试题第一篇(MVVM、MVC、MVP设计模式)_低保和光头哪个先来的博客-CSDN博客
Model:模型层,负责处理业务逻辑以及和服务器端进行交互
View:视图层:负责将数据模型转化为UI展示出来,可以理解为HTML页面
ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁
双向绑定
此处双向绑定也是考点,参考:Vue 前端面试题第二篇(双向绑定与 v-model)_低保和光头哪个先来的博客-CSDN博客
组件化
把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue
中每一个.vue
文件都可以视为一个组件
组件化优势
1.降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
2.调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
3.提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
指令系统
指令是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
常用的指令:
条件渲染指令 v-if
列表渲染指令 v-for
属性绑定指令 v-bind
---单向绑定
事件绑定指令 v-on
---事件监听
双向数据绑定指令 v-model
三、Vue跟传统开发的区别
Vue所有的界面事件,都是只去操作数据的,Jquery操作DOM
Vue所有界面的变动,都是根据数据自动绑定出来的,Jquery操作DOM
四、SPA是什么
即单页应用。SPA
是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换,打断用户体验。在单页应用中,所有必要的代码(HTML
、JavaScript
和CSS
)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面,页面在任何时间点都不会重新加载,也不会将控制转移到其他页面
五、SPA和MPA的区别
在MPA
(多页应用)中,每个页面都是一个独立的主页面。当访问另一个页面时,都需要重新加载html
、css
、js
文件
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 (Hash) | 历史模式 (History) |
SEO(搜索引擎优化) | 难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
解释SEO和SSR
SEO: 利用搜索引擎的规则,提高网站在有关搜索引擎内的自然排名
SSR:服务器渲染
单页应用优缺点
优点
-
具有桌面应用的即时性、网站的可移植性和可访问性
-
用户体验好、快,内容的改变不需要重新加载整个页面
-
良好的前后端分离,分工更明确
缺点
-
不利于搜索引擎的抓取
-
首次渲染速度相对较慢
六、实现一个SPA
步骤
1.显示当前界面,点击界面按钮,或浏览器回退/前进按钮触发 hash 变化
2.检测 hash 或者 pushstate 变化
3.以当前 hash 为索引,加载对应资源
4.等待资源加载完毕,隐藏之前的界面,执行回调
hash 模式
通过监听url
中的hash
来进行路由跳转
history 模式
借用 HTML5 history api
,api
提供了丰富的 router
相关属性
history 相关的api
history.pushState: 浏览器历史记录添加记录
history.replaceState: 修改浏览器历史纪录中当前记录
history.popState: 当 history 发生变化时触发
七、如何给SPA做SEO
SEO: 利用搜索引擎的规则,提高网站在有关搜索引擎内的自然排名
1.SSR服务端渲染
将组件或页面通过服务器生成html,再返回给浏览器
2.静态化
目前主流的静态化主要有两种:
(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面实际存在于服务器的硬盘中
(2)另外一种是通过WEB服务器的 URL Rewrite
的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。
3.使用Phantomjs针对爬虫处理
通过Nginx
配置,判断访问来源是否为爬虫,如果是,则搜索引擎的爬虫请求会转发到一个node server
,再通过PhantomJS
来解析完整的HTML
,返回给爬虫。
八、v-show与v-if
共同点
能控制元素在页面是否显示
-
当表达式为
true
时,都会占据页面的位置 -
当表达式都为
false
时,都不会占据页面位置
区别
控制手段不同
v-show
控制显示与隐藏:为该元素添加display:none
,dom
元素仍存在
v-if
控制显示与隐藏:是将dom
元素整个添加或删除
编译过程不同
v-show
只是基于css切换
v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件
编译条件不同
v-show
由false
变为true
的时候不会触发组件的生命周期
v-if
是真正的条件渲染,它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。渲染条件为假时不做操作,直到为真才渲染。由false
变为true
的时候,触发组件的beforeCreate
、created
、beforeMount
、mounted
钩子。由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法
性能消耗不同
v-show
有更高的初始渲染消耗
v-if
有更高的切换消耗
使用场景不同
v-if
与 v-show
都能控制dom
元素在页面的显示
v-if
相比 v-show
开销更大的(直接操作dom
节点增加与删除)
如果需要频繁地切换,使用 v-show 较好
如果在运行时条件很少改变,使用 v-if 较好
九、v-for与v-if
1.v-for
优先级比v-if
高
2.v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true
值时被渲染
3.v-for
指令基于一个数组来渲染一个列表,使用 item in items
形式的特殊语法,其中 items
是源数据数组或对象, item
是被迭代的数组元素
在使用 v-for
的时候,建议设置唯一的key
值,便于diff
算法进行优化
<a v-if="isShow"/>
<p v-for="item in items" :key="id"/>
一般不会同时使用两者,如果确实需要同时使用,则不编写在同一元素身上,可以在外层嵌套template
标签,在template 层进行 v-if 判断,在内部进行 v-for 循环
<template v-if="isShow">
<p v-for="item in items" :key="id"/>
</template>
如果 v-if 判断 出现在 v-for 循环 内部,可通过计算属性computed
提前过滤掉那些不需要显示的项
computed: {
items: function() {
return this.list.filter(function (item) {
return item.isShow
})
}
}
十、Vue实例挂载的过程
new Vue
会调用_init_
方法
-
定义
$set
、$get
、$delete
、$watch
等方法 -
定义
$on
、$off
、$emit
、$off
等事件 -
定义
_update
、$forceUpdate
、$destroy
生命周期 -
调用
$mount
进行页面的挂载 -
挂载的时候主要是通过
mountComponent
方法 -
定义
updateComponent
更新函数 -
执行
render
生成虚拟DOM
-
_update
将虚拟DOM
生成真实DOM
结构,并且渲染到页面中
十一、Vue的生命周期
生命周期是什么
在Vue
中,实例从创建到销毁的过程就是生命周期(创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等过程)
使用场景
生命周期 | 描述 |
---|---|
beforeCreate | 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务 |
created | 组件初始化完毕,各种数据可以使用,常用于异步数据获取 |
beforeMount | 未执行渲染、更新,dom未创建 |
mounted | 初始化结束,dom已创建,可用于获取访问数据和dom元素 |
beforeUpdate | 更新前,可用于获取更新前各种状态 |
updated | 更新后,所有状态已是最新 |
beforeDestroy | 销毁前,可用于一些定时器或订阅的取消 |
destroyed | 组件已销毁,作用同上组件实例完成销毁 |
activated | keep-alive 缓存的组件激活时 |
deactivated | keep-alive 缓存的组件停用时 |
errorCaptured | 捕获来自子孙组件的错误时 |
为什么一般在created进行异步数据请求
在钩子函数 created、beforeMount、mounted 中都可以进行异步数据请求,因为在这三个钩子函数中,data 已经创建,可以将服务端返回的数据进行赋值。
但一般在 created 中调用异步请求,有以下优点:
· 能更快获取到服务端数据,减少页面加载时间,用户体验更好
· SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性
十二、如何解决SPA首屏加载速度慢
首屏时间是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间。此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容。
加载慢的原因
1.网络延时问题
2.资源文件体积是否过大
3.资源是否重复发送请求去加载
4.加载脚本时,渲染内容堵塞
SPA首屏优化方式
1.减小入口文件体积
通过路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
2.静态资源本地缓存
前端合理利用localStorage
后端返回资源问题:
-
采用
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头 -
采用
Service Worker
离线缓存
3.UI框架按需加载
不要直接引用整个UI库
import ElementUI from 'element-ui'
而是按需引用,减小文件缓存体积
import { TableColumn, MessageBox } from 'element-ui'
4.组件重复打包
假设A.js
文件是一个常用的库,现在有多个路由使用了A.js
文件,这就造成了重复下载
解决方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks: 3 // 表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
5.图片资源的压缩
6.开启GZip压缩
拆完包之后,用gzip
做一下压缩,安装compression-webpack-plugin
cnmp i compression-webpack-plugin -D
在vue.congig.js
中引入并修改webpack
配置
const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}
在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip
,就发送给它gzip
格式的文件 我的服务器是用express
框架搭建的 只要安装一下compression
就能使用
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用
7.使用SSR
SSR(服务端渲染),组件或页面通过服务器生成html字符串,再发送到浏览器。使用Nuxt.js
实现服务端渲染
十三、为什么data属性是一个函数而不是一个对象
根实例对象data
可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
组件实例对象data
必须为函数,目的是为了防止多个组件实例对象之间共用一个data
,产生数据污染。采用函数的形式,initData
时会将其作为工厂函数都会返回全新data
对象
十四、动态给vue的data添加一个新的属性
定义一个p标签用于显示data属性中的数据,点击按钮实现增加新数据的效果(同时改变vue中的data属性以及UI界面的显示)
<p v-for="(value,key) in item" :key="key">
{{ value }}
</p>
<button @click="addProperty">动态添加新属性</button>
我们在vue
实例中,定义data
属性和methods
方法,然后通过绑定点击事件输出data中的数据,点击按钮,数据更新了(console
打印出了新属性),但页面没有更新,仍然只显示 ‘旧属性’
const app = new Vue({
el:"#app",
data:()=>{
item:{
oldProperty:"旧属性"
}
},
methods:{
addProperty(){
this.items.newProperty = "新属性" // 为items添加新属性
console.log(this.items) // 输出带有newProperty的items
}
}
})
在vue2中,因为初始设置的数据可以触发setter
与getter
,从而变成响应式数据,而按钮点击新增的数据并不会通过Object.defineProperty 变成响应式数据,不会自动触发setter
与getter
解决方案
-
如果为对象添加少量的新属性,可以直接采用
Vue.set()
-
如果需要为新对象添加大量的新属性,则通过
Object.assign()
创建新对象
vue3
是用过proxy
实现数据响应式的,直接动态添加新属性仍可以实现数据响应式
十五、Vue中的组件和插件
Vue 核心特性中介绍了组件的定义,而插件是用来为 Vue
添加全局功能
插件的功能范围没有严格的限制——一般有下面几种:
-
添加全局方法或者属性。如:
vue-custom-element
-
添加全局资源:指令/过滤器/过渡等。如
vue-touch
-
通过全局混入来添加一些组件选项。如
vue-router
-
添加
Vue
实例方法,通过把它们添加到Vue.prototype
上实现。 -
一个库,提供自己的
API
,同时提供上面提到的一个或多个功能。如vue-router
区别:
编写形式不同
每一个.vue
文件都可以看成是一个组件
`vue`文件标准格式
<template></template>
<script>
export default{
...
}
</script>
<style></style>
还可以通过template
属性来编写组件,如果组件内容多,可以在外部定义template
组件内容
<template id="testComponent"> // 组件显示的内容
<div>component!</div>
</template>
Vue.component('componentA',{
template: '#testComponent'
template: `<div>component</div>` // 组件内容少可以通过这种形式
})
vue
插件的实现应该暴露一个 install
方法。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
注册形式不同
vue
组件注册主要分为全局注册与局部注册
全局注册通过Vue.component
方法,第一个参数为组件的名称,第二个参数为传入的配置项
Vue.component('my-component', { /* ... */ })
局部注册只需在用到的地方通过components
属性注册一个组件
const component1 = {...} // 定义一个组件
export default {
components:{
component1 // 局部注册
}
}
插件的注册通过Vue.use()
的方式进行注册,第一个参数为插件的名字,第二个参数是可选择的配置项
Vue.use('my-plugin', { /* ... */} )
注册插件的时候,需要在调用 new Vue()
启动应用之前完成
Vue.use
会自动阻止多次注册相同插件,只会注册一次
使用场景不同
组件 (Component)
是用来构成 App
的业务模块,它的目标是 App.vue
插件 (Plugin)
是对Vue
的功能的增强或补充,它的目标是 Vue
本身,
十六、组件间通信
每个组件之间的都有独自的作用域,组件间通信是为了组件间数据共享
组件间通信的分类
父子组件之间的通信
兄弟组件之间的通信
祖孙与后代组件之间的通信
非关系组件间之间的通信
通信方式
父子关系的组件数据传递选择 props
与 $emit
进行传递,也可选择ref
props传递数据(父组件传递数据给子组件)
子组件设置props
属性,定义接收父组件传递过来的参数
// Children.vue
props:{
// 字符串形式
name:String, // 接收的类型参数
// 对象形式
age:{
type:Number, // 接收的类型为数值
defaule:18, // 默认值为18
require:true // age属性必须传递
}
}
父组件在使用子组件标签中通过字面量来传递值
// Father.vue
<Children :name="jack" :age=18/>
$emit 触发自定义事件(子组件传递数据给父组件)
子组件通过$emit触发
自定义事件,$emit
第二个参数为传递的数值
// Children.vue
this.$emit('add', good)
父组件绑定监听器获取到子组件传递过来的参数
// Father.vue
<Children @add="cartAdd($event)" />
ref(子组件传递数据给父组件)
父组件在使用子组件的时候设置ref
,父组件通过设置子组件ref
来获取数据
// Father.vue
<Children ref="foo" />
// Children.vue
this.$refs.foo // 获取子组件实例,通过子组件实例能拿到对应的数据
兄弟关系的组件数据传递可选择$bus
,或者选择$parent
进行传递
EventBus(兄弟组件传值)
创建一个全局事件总线EventBus
// 创建一个全局事件总线
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的功能
兄弟组件通过$emit
触发自定义事件,$emit
第二个参数为传递的数值
// Brother1.vue
this.$bus.$emit('foo')
另一个兄弟组件通过$on
监听自定义事件
// Brother2.vue
this.$bus.$on('foo', this.handle)
共同祖辈$parent
或者$root
(兄弟组件传值)
// Brother1.vue
this.$parent.on('add',this.add)
// Brother2.vue
this.$parent.emit('add')
祖先与后代组件数据传递可选择attrs
与listeners
或者 Provide
与 Inject
$attrs 与$ listeners(祖先传递数据给子孙)
设置批量向下传属性$attrs
和 $listeners
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo"/>
包含了父级作用域中不作为 prop
被识别 (且获取) 的特性绑定 ( class 和 style 除外),通过 v-bind="$attrs"
传⼊内部组件
// 给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
属性,返回传递的值
// GrandParent
provide(){
return {
foo:'foo'
}
}
在后代组件通过inject
接收组件传递过来的值
// Grandson
inject:['foo'] // 获取到祖先组件传递过来的值
vuex
存放共享的变量的容器(复杂关系的组件数据传递)
1.state:存放数据
2.mutations:存放操作数据的方法
3.actions:异步操作(也会有同步操作),actions不能直接修改state数据,而是通过提交mutations去更新state中数据
4.getters:存放基于state计算出来的一些值,类似与vue.js中的计算属性computed,当状态值发生改变是才会调用它的方法
5.modules:模块化管理,当需要管理的状态繁多时,可采用模块化管理,更易维护和阅读。
注意:如果采用模块化管理,默认mutations,actions,getters是注册到全局的,一般会开启命名空间:namespaced:true
大致流程:
客户端经过某种操作去更改了state中的状态值,然后把新的状态通知给组件,同时组件也可通过异步操作去发起状态更改,这个异步操作中可以是请求数据也可以是其他异步程序。
十七、NextTick
NextTick是什么
是一种优化策略。在下次 DOM 更新循环结束之后,执行延迟回调。即修改数据和更新数据是异步执行,在修改数据之后立即使用这个方法,获取更新后的 DOM
为什么要用NextTick
如果不适用 nextTick
更新机制,那么 num
每次更新值,都会触发视图更新
{{num}}
for(let i=0; i<100000; i++){
num = i
}
有了nextTick
机制,只需要等更新循环结束之后,更新一次
怎么用NextTick
在修改数据后,想立刻得到更新后的DOM
结构,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM
结构)
第二个参数为:执行函数上下文
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {
// DOM 更新了
console.log(vm.$el.textContent) // 修改后的值
})
组件内使用 vm.$nextTick()
实例方法只需要通过this.$nextTick()
,并且回调函数中的 this
将自动绑定到当前的 Vue
实例上
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '修改后的值'
})
$nextTick()
会返回一个 Promise
对象,可以是用async/await
完成相同作用的事情
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'
十八、vue中的mixin
mixin是什么
mixin
(混入),提供了一种非常灵活的方式,来分发 Vue
组件中的可复用功能,本质是一个js
对象,可以包含组件中任意功能选项,如data
、components
等,将共用的功能以对象的方式传入 mixins
选项中,当组件使用 mixins
对象时,所有mixins
对象的选项都将被混入该组件本身的选项中来
minin的分类
局部混入
定义一个mixin
对象
var Mixin = {
methods: {
hello: function () {
console.log('hello mixin!')
}
}
}
组件通过mixins
属性调用mixin
对象
Vue.component('componentA',{
mixins: [Mixin]
})
该组件在使用的时候,混合了mixin
里面的方法,执行hello
方法
全局混入
通过Vue.mixin()
进行全局的混入,常用于插件的编写
Vue.mixin({
created: function () {
console.log("全局混入")
}
})
使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)
什么时候用mixin
当组件A和组件B都要实现hello这个function时,为了减少代码重复,通过Vue
的mixin
功能hello这个方法提出来,在两个组件中都调用mixin即可
const hello = {
methods: {
Hello() {
console.log('hello!')
}
}
}
const A = {
template: '#A',
mixins: [hello]
};
const B = {
template: '#B',
mixins: [hello]
}