Vue八股文
Vue八股文
你别睡这么晚Vue 的设计模式
介绍一下 MVVM 模式,和 MVC 模式有什么区别?
MVVM 即 Model-View-ViewModel 的简写,即模型-视图-视图模型。模型(Model)指的是后端传递的数据。视图(View)指的是所看到的页面。视图模型(ViewModel)是 MVVM 模式的核心,它是连接 View 和 Model 的桥梁。
视图模型有两个方向的作用:
1.将模型(Model)转化成视图(View),即将后端传递的数据转化成所看到的页面,实现的方式是:数据绑定。
2.将视图(View)转化成模型(Model),即将所看到的页面转化成后端的数据,实现的方式是:DOM 事件监听。当这两个方向的数据转换都实现时,我们称之为数据的双向绑定。
MVVM模型图解:
MVC 是 Model-View-Controller 的简写,即模型-视图-控制器。M 和 V 指的意思和 MVVM 中的 M 和 V 意思一样。C 即 Controller 指的是页面业务逻辑。使用 MVC 的目的就是将 M 和 V 的代码分离。MVC 是单向通信,也就是 View 跟 Model ,必须通过 Controller 来承上启下。
MVC模型图解:
MVVM 与 MVC 最大的区别就是:MVVM 实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(双向绑定)。
生命周期
Vue2 的生命周期有哪些
生命周期可以理解为三部分,第一部分为初始化 Vue 实例的过程,第二部分为数据渲染,更新 Dom 的过程,第三部分为销毁实例的过程,每一个部分都有对应的钩子函数来完成对应的部分过程。
初始化阶段的钩子函数:
1 | beforeCreate()//实例创建前:模板和数据均未获取到 |
更新阶段的钩子函数:
1 | beforeUpdate()//模板更新前:data改变后,更新数据模板前调用 |
销毁阶段的钩子函数:
1 | beforeDestroy() //实例销毁前 |
keep-alive 中的生命周期有哪些
keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存,在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。 如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。 当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期; 当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。
父子组件生命周期执行顺序
1.挂载阶段
该过程主要涉及 beforeCreate、created、beforeMount、mounted 4 个钩子函数。执行顺序为:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
一定得等子组件挂载完毕后,父组件才能挂在完毕,所以父组件的 mounted 在最后。
2.更新阶段
该过程主要涉及 beforeUpdate、updated 2 个钩子函数。注意,当父子组件有数据传递时,才有这个更新阶段执行顺序的比较。执行顺序为:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
3.销毁阶段
该过程主要涉及beforeDestroy、destroyed 2 个钩子函数。执行顺序为:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
总结:
1.当父组件执行完 beforeMount 挂载开始后,会依次执行子组件中的钩子,直到全部子组件 mounted 挂载到实例上,父组件才会进入 mounted 钩子
2.子级触发事件,会先触发父级 beforeUpdate 钩子,再去触发子级 beforeUpdate 钩子,下面又是先执行子级 updated 钩子,后执行父级 updated 钩子
平时发送异步请求在哪个生命周期,并解释原因
我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:能更快获取到服务端数据,减少页面加载时间,用户体验更好;SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
【延伸问答】created 和 mouted 区别
created 生命周期钩子函数会在组件实例被创建之后立即调用,但是此时组件的模板还没有被渲染成 HTML,因此在这个时候不能访问组件中的 DOM 元素。
mounted 生命周期钩子函数则会在组件的模板被渲染成 HTML 之后调用,这个时候可以访问组件中的 DOM 元素。通常在这个钩子函数中进行异步请求数据、初始化插件、订阅事件等操作。
总之,created 钩子函数适用于组件初始化时需要进行一些操作,而 mounted 钩子函数适用于需要操作组件的 DOM 元素时。
vue2 和 vue3 的区别
一、性能提升
- 基于Proxy的新响应式系统(数据劫持优化)
由原来的Object.defineProperty的getter 和setter,改成了ES6 Proxy 作为其观察机制(准确说是 Proxy 配合 Reflect,Reflect提供了一些操作Object对象的方法),初始化时无需递归遍历数据,初始化效率更高,而且也可以监控数组。速度加倍,节省了一半内存开销。
- 虚拟DOM重写
虚拟DOM静态属性缓存,避免重复patch。内存换时间。
- Diff算法优化
增加了静态标记flag。标记和提升所有静态根节点,diff 的时候只⽐较动态节点内容。
- 静态提升(hoistStatic)
Vue2中无论元素是否参与更新,每次都会重新创建(createVNode),然后再渲染(Vue2 中的虚拟dom节点是进⾏全量的更新)。
在Vue3中使用了静态提升后,对于静态不需要发生变化的元素,只会被创建一次,静态节点都被提升到 render ⽅法之外,在渲染时直接复用即可。(静态提升避免了静态元素节点频繁重复创建)
- 体积更小
vue3整个源码体积相对减少,优化了打包方法,引入tree-shaking,按需编译,避免打包无用模块(例如只打包用到的ref,reactive,components等),使得打包后的bundle的体积更小,提升了运行效率。(通过摇树优化核⼼库体积,减少不必要的代码量)
二、开发体验提升
- 用 Ts 完全重写
基于typescipt编写,可以享受到类型提示。对Ts支持更好。
编辑器可提供强有力的类型检查和错误提示。
- Compositon Api 的支持
compositon Api 可以解决业务分离问题,使代码有更好的复用性。
同时,也方便后续的维护和管理,setup() 的出现,使得相关的业务代码得以集中起来,方便查找和维护。我们可以把不同的业务代码进行逻辑抽离,比如使用hooks形式,更容易维护。而Vue2不同的业务代码都混杂在options中,不便管理。
都说 Composition API 和 React Hook 很像,请问他们的区别是什么?
从 React Hook 从实现的角度来看,React 是通过链表去实现 hooks 的调用的。需要确保每次更新时 hooks 的调用顺序一致,这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
所以有以下几个限制:
- 不在循环中、条件语句中、函数嵌套中调用 Hook
- 你必须确保它总是在 React Top level 调用函数 Hook
- 使用效果、依赖关系必须手动确定
和 Composition API 是基于 Vue 的响应系统,和React Hook 相比:
- 在 setup() 函数中,一个组件实例只执行一次,而React Hook 每次重新渲染时,都需要调用 Hook,给 React 带来的 GC 比 Vue 更大的压力,性能也相对 Vue 来说比较慢
- Compositon API 不必担心调用的顺序,它也可以在循环中、条件、在嵌套函数中任意位置使用 响应式系统自动实现依赖关系收集,而且组件的性能优化是由 Vue 内部完成的,而 React Hook 的依赖关系需要手动传递,并且依赖关系的顺序必须得到保证,尤其是使用 useEffect、useMemo 等 Hook 时,否则组件性能会因为依赖关系不正确而下降。
- reactive + ref 属于响应式数据,⽐ react 的useState,要更难理解
虽然Compoliton API区别于React Hook,但它的设计思路也是来自React Hook的参考。
ref 和 reactive 的区别
- ref 支持基础类型
- 监听方式不同
watch 可以直接监听 ref 的基础类型
watch 监听 ref 的对象类型,需要.value
或者加个 deep
watch 监听 reactive
默认会加上deep
- ref 需要
.value
使用
vue3在哪些方面提升了性能
通过响应式系统的重写
、编译优化
、源码体积的优化(按需加载)
三个方面提升了性能。
1. 响应式系统提升
vue2在初始化的时候,通过Object.defineProperty对data的每个属性进行访问和修改的拦截,getter进行依赖收集、setter派发更新。在属性值是对象的时候还需要递归调用defineproperty。看下大致实现的代码:
1 | function observe(target) { |
而如果属性是数组,还需要覆盖数组的七个方法(会改变原数组的七个方法)进行变更的通知:
1 | const arrayProto = Array.prototype |
从这几段代码可以看出Object.defineProperty的几个缺点:
- 初始化时需要遍历对象所有key,层级多的情况下,性能有一定影响
- 动态新增、删除对象属性无法拦截,只能用set/delete api代替
- 不支持新的Map、Set等数据结构
- 无法监控到数组下标的变化(监听的性能代价太大)
所以在vue3中用了proxy全面代替Object.defineProperty的响应式系统。proxy是比较新的浏览器特性,拦截的是整个对象而不是对象的属性,可以拦截多种方法,包括属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性,并且是懒执行的特性,也就是在访问到的时候才会触发,当访问到对象属性的时候才会递归代理这个对象属性,所以性能比vue2有明显的优势。
总结下proxy的优势:
- 可以监听多种操作方法,包括动态新增的属性和删除属性、has、apply等操作
- 可以监听数组的索引和 length 等属性
- 懒执行,不需要初始化的时候递归遍历
- 浏览器新标准,性能更好,并且有持续优化的可能
看下大致实现拦截对象的方法。
1 | exportfunction reactive(target: object) { |
2. 编译优化(虚拟dom优化)
编译优化主要是通过重写虚拟dom。优化的点包括编译模板的静态标记
、静态提升
、事件缓存
- 静态标记(PatchFlag)
根据尤大直播所说,更新的性能提升1.32倍,ssr提升23倍。 在对更新的节点进行对比的时候,只会去对比带有静态标记的节点。并且 PatchFlag 枚举定义了十几种类型,用以更精确的定位需要对比节点的类型。
看这段代码
1 | <div id="app"> |
vue2编译后的渲染函数:
1 | function render() { |
这个render函数会返回vnode,后面更新的时候vue2会调patch
函数比旧vnode进行diff算法更新(在我的上篇文章有解析过),这时候对比是整个vnode,包括里面的静态节点<p>前端好好玩</p>
,这样就会有一定的性能损耗。
vue3编译后的渲染函数:
1 | import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from"vue" |
只有**_createVNode** 这个函数带有第四个参数的才是非静态节点,也就是需要后续diff的节点。第四个参数是这个节点具体包含需要被diff的类型,比如是text
节点,只有{}这种模板变量的绑定,后续只需要对比这个text即可,看下源码中定义了哪些枚举的元素类型:
1 | TEXT = 1,// 动态的文本节点 |
//
位运算,有符号右移运算符,不了解的可以看我掘金的第一篇文章juejin.cn/post/688518…[1]
- 静态提升
静态提升的意思就是把函数里的某些变量放到外面来,这样再次执行这个函数的时候就不会重新声明。vue3在编译阶段做了这个优化。还是上面那段代码,分别看下vue2和vue3编译后的不同
vue2:
1 | function render() { |
vue3:
1 | import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue" |
可以看到vue3将不变的节点声明放到了外面去执行,后面再渲染的时候直接去_hoited变量就行,而vue2每次render都需要执行_c生成新的节点。这里还有一个点,_hoisted_2的_createVNode第四个参数-1,标记这个节点永远不需要diff。
- 事件缓存
默认情况下事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化。但是正常情况下,我们的 @click 事件在视图渲染前和渲染后,都是同一个事件,基本上不需要去追踪它的变化,所以 Vue 3.0 对此作出了相应的优化叫事件监听缓存
1 | <div id="app"> |
vue3编译后:
1 | import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue" |
可以看到onClick有一个_cache判断缓存赋值的操作,从而变成静态节点
3. 源码体积的优化
vue3通过重构全局api和内部api,支持了tree shaking,任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
1. Vue面试题
1.1 v-model 作用?
参考答案:
v-model本质上不过是语法糖,可以用 v-model 指令在表单及元素上创建双向数据绑定。
- 它会根据控件类型自动选取正确的方法来更新元素
- 它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理
- v-model会忽略所有表单元素的value、checked、selected特性的初始值,而总是将 Vue 实例的数据作为数据来源,因此我们应该通过 JavaScript 在组件的data选项中声明初始值
扩展:
v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用value属性和input事件;
- checkbox 和 radio 使用checked属性和change事件;
- select 字段将value作为 prop 并将change作为事件。
1.2 v-model 实现原理?
参考答案:
v-model只不过是一个语法糖而已,真正的实现靠的还是
- v-bind:绑定响应式数据
- 触发oninput 事件并传递数据
1 | <input v-model="sth" /> |
1.3 Vue2.0 双向绑定的缺陷?
参考答案:
Vue2.0的数据响应是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty () 来劫持各个属性的setter、getter,但是它并不算是实现数据的响应式的完美方案,某些情况下需要对其进行修补或者hack这也是它的缺陷,主要表现在两个方面:
- vue 实例创建后,无法检测到对象属性的新增或删除,只能追踪到数据是否被修改
- 不能监听数组的变化
解析:
vue 实例创建后,无法检测到对象属性的新增或删除,只能追踪到数据是否被修改(Object.defineProperty只能劫持对象的属性)。
当创建一个Vue实例时,将遍历所有DOM对象,并为每个数据属性添加了get和set。get和set 允许Vue观察数据的更改并触发更新。但是,如果你在Vue实例化后添加(或删除)一个属性,这个属性不会被vue处理,改变get和set。
解决方案:
1
2
3
4
5
6
7Vue.set(obj, propertName/index, value)
// 响应式对象的子对象新增属性,可以给子响应式对象重新赋值
data.location = {
x: 100,
y: 100
}
data.location = {...data, z: 100}不能监听数组的变化
vue在实现数组的响应式时,它使用了一些hack,把无法监听数组的情况通过重写数组的部分方法来实现响应式,这也只限制在数组的push/pop/shift/unshift/splice/sort/reverse七个方法,其他数组方法及数组的使用则无法检测到,例如如下两种使用方式
1
2vm.items[index] = newValue;
vm.items.lengthvue实现数组响应式的方法
通过重写数组的Array.prototype对应的方法,具体来说就是重新指定要操作数组的prototype,并重新该prototype中对应上面的7个数组方法,通过下面代码简单了解下实现原理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const methods = ['pop','shift','unshift','sort','reverse','splice', 'push'];
// 复制Array.prototype,并将其prototype指向Array.prototype
let proto = Object.create(Array.prototype);
methods.forEach(method => {
proto[method] = function () { // 重写proto中的数组方法
Array.prototype[method].call(this, ...arguments);
viewRender() // 视图更新
function observe(obj) {
if (Array.isArray(obj)) { // 数组实现响应式
obj.__proto__ = proto; // 改变传入数组的prototype
return;
}
if (typeof obj === 'object') {
... // 对象的响应式实现
}
}
}
})
1.4 Vue3.0 实现数据双向绑定的方法
参考答案:
vue3.0 实现数据双向绑定是通过Proxy
Proxy是 ES6 中新增的一个特性,翻译过来意思是”代理”,用在这里表示由它来“代理”某些操作。 Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。
扩展:
使用proxy实现,双向数据绑定,相比2.0的Object.defineProperty ()优势:
- 可以劫持整个对象,并返回一个新对象
- 有13种劫持操作
1.5 Vuex是什么,每个属性是干嘛的,如何使用
参考答案:
Vuex是什么?
Vuex是专门为Vuejs应用程序设计的状态管理工具。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
具体工作:vuex是一种状态管理机制,将全局组件的共享状态抽取出来为一个store,以一个单例模式存在,应用任何一个组件中都可以使用,vuex更改state的唯一途径是通过mutation,mutation需要commit触发, action实际触发是mutation,其中mutation处理同步任务,action处理异步任务。
Vuex每个属性是干嘛的?
Vuex的属性包含以下6个:
1)state
state是存储的单一状态,是存储的基本数据。
2)Getters
getters是store的计算属性,对state的加工,是派生出来的数据。就像computed计算属性一样,getter返回的值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变才会被重新计算。
3)Mutations
mutations提交更改数据,使用store.commit方法更改state存储的状态。(mutations同步函数)
4)Actions
actions像一个装饰器,提交mutation,而不是直接变更状态。(actions可以包含任何异步操作)
5)Module
Module是store分割的模块,每个模块拥有自己的state、getters、mutations、actions。
1 | const moduleA = { |
6)辅助函数
Vuex提供了mapState、MapGetters、MapActions、mapMutations等辅助函数给开发在vm中处理store。
Vuex的使用方法?
1 | import Vuex from 'vuex'; |
1.6 Vuex实现原理
参考答案:
通过以下三个方面来阐述vuex的实现原理:
- store是怎么注册的?
- mutation,commit 是怎么实现的?
- 辅助函数是怎么实现的?
store是怎么注册的?
我们看到Vuex在vue 的生命周期中的初始化钩子前插入一段 Vuex 初始化代码。给 Vue 的实例注入一个
$store的属性,这也就是为什么我们在 Vue 的组件中可以通过this.$store.xxx, 访问到 Vuex 的各种数据和状态
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
36export default function (Vue) {
// 获取当前 Vue 的版本
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 2.x 通过 hook 的方式注入
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 兼容 1.x
// 使用自定义的 _init 方法并替换 Vue 对象原型的_init方法,实现注入
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store 注入
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 子组件从其父组件引用 $store 属性
this.$store = options.parent.$store
}
}
}mutations,commit 是怎么实现的
1
2
3
4
5
6
7
8
9function registerMutation (store, type, handler, local) {
// 获取 type(module.mutations 的 key) 对应的 mutations, 没有就创建一个空数组
const entry = store._mutations[type] || (store._mutations[type] = [])
// push 处理过的 mutation handler
entry.push(function wrappedMutationHandler (payload) {
// 调用用户定义的 hanler, 并传入 state 和 payload 参数
handler.call(store, local.state, payload)
})
}registerMutation 是对 store 的 mutation 的初始化,它接受 4 个参数,store为当前 Store 实例,type为 mutation 的 key,handler 为 mutation 执行的回调函数,path 为当前模块的路径。
mutation 的作用就是同步修改当前模块的 state ,函数首先通过 type 拿到对应的 mutation 对象数组, 然后把一个 mutation 的包装函数 push 到这个数组中,这个函数接收一个参数 payload,这个就是我们在定义 mutation 的时候接收的额外参数。这个函数执行的时候会调用 mutation 的回调函数,并通过 getNestedState(store.state, path) 方法得到当前模块的 state,和 playload 一起作为回调函数的参数。
我们知道mutation是通过commit来触发的,这里我们也来看一下commit的定义
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
37commit (_type, _payload, _options) {
// 解析参数
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
// 根据 type 获取所有对应的处理过的 mutation 函数集合
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 执行 mutation 函数
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 执行所有的订阅者函数
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}commit 支持 3 个参数,type 表示 mutation 的类型,payload 表示额外的参数,根据 type 去查找对应的 mutation,如果找不到,则输出一条错误信息,否则遍历这个 type 对应的 mutation 对象数组,执行 handler(payload) 方法,这个方法就是之前定义的 wrappedMutationHandler(handler),执行它就相当于执行了 registerMutation 注册的回调函数。
辅助函数
辅助函数的实现都差不太多,在这里了解一下mapState
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
26export const mapGetters = normalizeNamespace((namespace, getters) => {
// 返回结果
const res = {}
// 遍历规范化参数后的对象
// getters 就是传递给 mapGetters 的 map 对象或者数组
normalizeMap(getters).forEach(({ key, val }) => {
val = namespace + val
res[key] = function mappedGetter () {
// 一般不会传入 namespace 参数
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
// 如果 getter 不存在则报错
if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
// 返回 getter 值, store.getters 可见上文 resetStoreVM 的分析
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})mapState在调用了 normalizeMap 函数后,把传入的 states 转换成由 {key, val} 对象构成的数组,接着调用 forEach 方法遍历这个数组,构造一个新的对象,这个新对象每个元素都返回一个新的函数 mappedState,函数对 val 的类型判断,如果 val 是一个函数,则直接调用这个 val 函数,把当前 store 上的 state 和 getters 作为参数,返回值作为 mappedState 的返回值;否则直接把 this.$store.state[val]作为 mappedState 的返回值。为了更直观的理解,我们看下最终mapState的效果
1
2
3
4
5
6
7computed: mapState({
name: state => state.name,
})
// 等同于
computed: {
name: this.$store.state.name
}
1.7 mutation和action有什么区别?
参考答案:
mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于件: 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进 行状态更改的地方,并且它会接受 state 作为第一个参数
1 | const store = new Vuex.Store({ |
不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:
1 | store.commit('increment') |
Action: Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
让我们来注册一个简单的 action:
1 | const store = new Vuex.Store({ |
扩展:事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutation 就行。异步竞态怎么处理那是用户自己的事情。
vuex 真正限制你的只有 mutation 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)。同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。
1.8 修改ElementUI 样式的几种方式?
参考答案:
修改ElementUI 样式的方式有四种:
新建全局样式表
新建 global.css 文件,并在 main.js 中引入。 global.css 文件一般都放在 src->assets 静态资源文件夹下的 style 文件夹下,在 main.js 的引用写法如下:
1
import "./assets/style/global.css"
在 global.css 文件中写的样式,无论在哪一个 vue 单页面都会覆盖 ElementUI 默认的样式。
在当前-vue-单页面中添加一个新的style标签
在当前的vue单页面的style标签后,添加一对新的style标签,新的style标签中不要添加scoped属性。在有写scoped的style标签中书写的样式不会覆盖 ElementUI 默认的样式。
使用/deep/深度修改标签样式
找到需要修改的 ElementUI 标签的类名,然后在类名前加上/deep/,可以强制修改默认样式。这种方式可以直接用到有scoped属性的 style 标签中。
1
2
3
4// 修改级联选择框的默认宽度
/deep/ .el-cascader {
width: 100%;
}通过内联样式 或者 绑定类样式覆盖默认样式
通过内联样式 style ,绑定类样式的方式,可以在某些标签中可以直接覆盖默认样式,不是很通用。具体实例如下:
1 | <el-button :style="selfstyle">默认按钮</el-button> |
通过绑定修改样式方式修改:
1 | <el-button :class="[selfbutton]">默认按钮</el-button> |
扩展:
第一种全局引入css文件的方式,适合于对elementUI整体的修改,比如整体配色的修改;
第二种添加一个style标签的形式,也能够实现修改默认样式的效果,但实际上因为是修改了全局的样式,因此 在不同的vue组件中修改同一个样式有可能会有冲突。
第三种方式通过 /deep/ 的方式可以很方便的在vue组件中修改默认样式,也不会于其他页面有冲突。
第四种方式局限性比较大,可以使用,但不推荐使用。
1.9 elementui 有什么用?
参考答案:
Element-UI:是一套采用 Vue 2.0 作为基础框架实现的组件库,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的组件库,提供了配套设计资源,帮助网站快速成型
扩展:
Element-UI特点:
一致性 Consistency
- 与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;
- 在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。
反馈 Feedback
- 控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;
- 页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。
效率 Efficiency
- 简化流程:设计简洁直观的操作流程;
- 清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;
- 帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。
可控 Controllability
- 用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;
- 结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。
1.10 导航守卫
参考答案:
导航守卫主要用来通过跳转或取消的方式守卫导航。
简单的说,导航守卫就是路由跳转过程中的一些钩子函数。路由跳转是一个大的过程,这个大的过程分为跳转前中后等等细小的过程,在每一个过程中都有一函数,这个函数能让你操作一些其他的事儿的时机,这就是导航守卫。
解析:
路由守卫的具体方法:
全局前置守卫
你可以使用 router.beforeEach 注册一个全局前置守卫:
1
2
3
4const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})当一个导航开始时,全局前置守卫按照注册顺序调用。守卫是异步链式调用的,导航在最后的一层当中。
1
2
3
4
5
6
7
8
9new Promise((resolve, reject) => {
resolve('第一个全局前置守卫')
}.then(() => {
return '第二个全局前置守卫'
}.then(() => {
...
}.then(() => {
console.log('导航终于开始了') // 导航在最后一层中
})
每个守卫方法接收三个参数(往后的守卫都大同小异):
\1. to: Route: 即将要进入的目标 路由对象
\2. from: Route: 当前导航正要离开的路由
\3. next: Function: 一定要调用该方法将控制权交给下一个守卫,执行效果依赖 next 方法的参数。
next(): 进入下一个守卫。如果全部守卫执行完了。则导航的状态就是 confirmed (确认的)。
next(false): 中断当前的导航(把小明腿打断了)。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器 后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
next(‘/‘) 或者 next({ path: ‘/‘ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航(小 明被打断腿并且送回家了)。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。
next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递router.
onError() 注册过的回调。
注意:永远不要使用两次next,这会产生一些误会。
全局解析守卫
这和 router.beforeEach 类似,但他总是被放在最后一个执行。
全局后置钩子
导航已经确认了的,小明已经到了外婆家了,你打断他的腿他也是在外婆家了。
1
2
3
4router.afterEach((to, from) => {
// 你并不能调用next
// ...
})路由独享的守卫
在路由内写的守卫
1
2
3
4
5
6
7
8
9
10
11const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})组件内的守卫
5.1 beforeRouteEnter
5.2 beforeRouteUpdate (2.2 新增)
5.3 beforeRouteLeave
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 路由被 confirm 前调用
// 组件还未渲染出来,不能获取组件实例 `this`
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`,一般用来数据获取。
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
扩展:
导航全过程
- 导航被触发。
- 在准备离开的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。(如果你的组件是重用的)
- 在路由配置里调用 beforeEnter。
- 解析即将抵达的组件。
- 在即将抵达的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
1.11 路由守卫进行判断登录
参考答案:
在vue项目中,切换路由时肯定会碰到需要登录的路由,其原理就是在切换路径之前进行判断,你不可能进入页面再去判断有无登录重新定向到login,那样的话会导致页面已经渲染以及它的各种请求已经发出。
如需要登录的路由可在main.js中统一处理(全局前置守卫)
我们可以在入口文件man.js里面进行配置,使用router.beforeEach方法,不懂得可以打印to,from的参数就ok,requireAuth可以随意换名的,只要man.js里面跟配置路由的routes里面的字段保持一致:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import router from './router'
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)){ // 判断该路由是否需要登录权限
if(!sessionStorage.getItem('token') && !localStorage.getItem('token')){
next({
path: '/login',
query: {redirect: to.fullPath} // 将跳转的路由path作为参数,登录成功后跳转到该路由
})
}else{
next();
}
}else {
next();
}
});
new Vue({
el: '#app',
router,
render: h => h(App)
})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
27export default new Router({
routes: [
{
path: '/',
name: 'home',
redirect: '/home'
},
{
path: '/home',
component: Home,
meta: {
title: '',
requireAuth: true, // 添加该字段,表示进入这个路由是需要登录的
}
},
{
path:'/login',
name:'login',
component:Login
},
{
path:'/register',
name:'register',
component:Register
}
]
})全局后置守卫
1
2
3router.afterEach((to, from) => {
// ...
})单独路由独享守卫(与全局一致,可单独对某个路由进行配置)
1
2
3
4
5
6
7
8
9
10
11const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})组件内部路由守卫(可写在与生命周期同级位置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
1.12 vue-router 实现懒加载
参考答案:
懒加载:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
实现:结合 Vue 的异步组件和 Webpack 的代码分割功能,可以实现路由组件的懒加载
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
1
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):
1
import('./Foo.vue') // 返回 Promise
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
1
const Foo = () => import('./Foo.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用Foo:
1
2
3
4
5const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
1.13 js是如何监听HistoryRouter的变化的
参考答案:
通过浏览器的地址栏来改变切换页面,前端实现主要有两种方式:
通过hash改变,利用window.onhashchange 监听。
HistoryRouter:通过history的改变,进行js操作加载页面,然而history并不像hash那样简单,因为history的改变,除了浏览器的几个前进后退(使用 history.back(), history.forward()和 history.go() 方法来完成在用户历史记录中向后和向前的跳转。)等操作会主动触发popstate 事件,pushState,replaceState 并不会触发popstate事件,要解决history监听的问题,方法是:
首先完成一个订阅-发布模式,然后重写history.pushState, history.replaceState,并添加消息通知,这样一来只要history的无法实现监听函数就被我们加上了事件通知,只不过这里用的不是浏览器原生事件,而是通过我们创建的event-bus 来实现通知,然后触发事件订阅函数的执行。
具体操作如下:
- 订阅-发布模式示例
1 | class Dep { // 订阅池 |
- 重写history方法,并添加window.addHistoryListener事件机制。
1 | var addHistoryMethod = (function(){ |
1.14 HashRouter 和 HistoryRouter的区别和原理
参考答案:
vue-router是Vue官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。vue-router默认 hash 模式,还有一种是history模式。
原理:
hash路由:hash模式的工作原理是hashchange事件,可以在window监听hash的变化。我们在url后面随便添加一个#xx触发这个事件。vue-router默认的是hash模式—使用URL的hash来模拟一个完整的URL,于是当URL改变的时候,页面不会重新加载,也就是单页应用了,当#后面的hash发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,并且会触发hasChange这个事件,通过监听hash值的变化来实现更新页面部分内容的操作
对于hash模式会创建hashHistory对象,在访问不同的路由的时候,会发生两件事:
HashHistory.push()将新的路由添加到浏览器访问的历史的栈顶,和HasHistory.replace()替换到当前栈顶的路由history路由:
主要使用HTML5的pushState()和replaceState()这两个api结合window.popstate事件(监听浏览器前进后退)来实现的,pushState()可以改变url地址且不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器记录进行修改
区别:
- hash模式较丑,history模式较优雅
- pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL
- pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中
- pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串
- pushState可额外设置title属性供后续使用
- hash兼容IE8以上,history兼容IE10以上
- history模式需要后端配合将所有访问都指向index.html,否则用户刷新页面,会导致404错误
使用方法:
1 | <script> |
1.15 Vue router 原理, 哪个模式不会请求服务器
参考答案:
Vue router 的两种方法,hash模式不会请求服务器
解析:
- url的hash,就是通常所说的锚点#,javascript通过hashChange事件来监听url的变化,IE7以下需要轮询。比如这个 URL:http://www.abc.com/#/hello,hash 的值为#/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
- HTML5的History模式,它使url看起来像普通网站那样,以“/”分割,没有#,单页面并没有跳转。不过使用这种模式需要服务端支持,服务端在接收到所有请求后,都只想同一个html文件,不然会出现404。因此单页面应用只有一个html,整个网站的内容都在这一个html里,通过js来处理。
1.16 组件通信的方式
参考答案:
组件通信的方式的方式有以下8种方法:
props和$emit
这是最最常用的父子组件通信方式,父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的
listeners
第一种方式处理父子组件之间的数据传输有一个问题:如果多层嵌套,父组件A下面有子组件B,组件B下面有组件C,这时如果组件A想传递数据给组件C怎么办呢?
如果采用第一种方法,我们必须让组件A通过prop传递消息给组件B,组件B在通过prop传递消息给组件C;要是组件A和组件C之间有更多的组件,那采用这种方式就很复杂了。从Vue 2.4开始,提供了listeners来解决这个问题,能够让组件A之间传递消息给组件C。
v-model
父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,在子组件中通过this.$emit(‘input’,val)自动修改v-model绑定的值
provide和inject
父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。
中央事件总线
上面方式都是处理的父子组件之间的数据传递,那如果两个组件不是父子关系呢?也就是兄弟组件如何通信?
这种情况下可以使用中央事件总线的方式。新建一个Vue事件bus对象,然后通过bus.on监听触发的事件。
parent和children
boradcast和dispatch
vue1.0中提供了这种方式,但vue2.0中没有,但很多开源软件都自己封装了这种方式,比如min ui、element ui和iview等。 比如如下代码,一般都作为一个mixins去使用, broadcast是向特定的父组件,触发事件,dispatch是向特定的子组件触发事件,本质上这种方式还是on和on和emit的封装,但在一些基础组件中却很实用
vuex处理组件之间的数据交互
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候才有上面这一些方法可能不利于项目的维护,vuex的做法就是将这一些公共的数据抽离出来,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的
1.17 vue组件间传值, attrs和listeners 了解过吗?
参考答案:
listeners的作用:解决多层嵌套情况下,父组件A下面有子组件B,组件B下面有组件C,组件A传递数据给组件B的问题,这个方法是在Vue 2.4提出的。
listeners解决问题的过程:
C组件
1 | Vue.component('C',{ |
B组件
1 | Vue.component('B',{ |
A组件
1 | Vue.component('A',{ |
解析:
- C组件中能直接触发getCData的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性
- 通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)
1.18 组建传值,事件总线是怎么用的
参考答案:
中央事件总线主要用来解决兄弟组件通信的问题。
实现方式:新建一个Vue事件bus对象,然后通过bus.on监听触发的事件。
1 | Vue.component('brother1',{ |
1.19 vue生命周期中异步加载在mouted还是create里实现
参考答案:
最常用的是在 created 钩子函数中调用异步请求
解析:
一般来说,可以在,created,mounted中都可以发送数据请求,但是,大部分时候,会在created发送请求。
Created的使用场景:如果页面首次渲染的就来自后端数据。因为,此时data已经挂载到vue实例了。
在 created(如果希望首次选的数据来自于后端,就在此处发请求)(只发了异步请求,渲染是在后端响应之后才进行的)、beforeMount、mounted(在mounted中发请求会进行二次渲染) 这三个钩子函数中进行调用。
因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是最常用的是在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有两个优点:
第一点:能更快获取到服务端数据,减少页面 loading 时间;
第二点:放在 created 中有助于一致性,因为ssr 不支持 beforeMount 、mounted 钩子函数。
1.20 vue钩子函数(重点问了keep-alive)
参考答案:
Vue生命周期经历哪些阶段:
- 总体来说:初始化、运行中、销毁
- 详细来说:开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程
生命周期经历的阶段和钩子函数:
实例化vue(组件)对象:new Vue()
初始化事件和生命周期 init events 和 init cycle
beforeCreate函数:
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
即此时vue(组件)对象被创建了,但是vue对象的属性还没有绑定,如data属性,computed属性还没有绑定,即没有值。
此时还没有数据和真实DOM。
即:属性还没有赋值,也没有动态创建template属性对应的HTML元素(二阶段的createUI函数还没有执行)
挂载数据(属性赋值)
包括 属性和computed的运算
Created函数:
vue对象的属性有值了,但是DOM还没有生成,$el属性还不存在。
此时有数据了,但是还没有真实的DOM
即:data,computed都执行了。属性已经赋值,但没有动态创建template属性对应的HTML元素,所以,此时如果更改数据不会触发updated函数
如果:数据的初始值就来自于后端,可以发送ajax,或者fetch请求获取数据,但是,此时不会触发updated函数
- 检查
6.1 检查是否有el属性
检查vue配置,即new Vue{}里面的el项是否存在,有就继续检查template项。没有则等到手动绑定调用 vm.el的绑定。
6.2 检查是否有template属性
检查配置中的template项,如果没有template进行填充被绑定区域,则被绑定区域的el对outerHTML(即 整个#app DOM对象,包括
和
标签)都作为被填充对象替换掉填充区域。即: 如果vue对象中有 template属性,那么,template后面的HTML会替换$el对应的内容。如果有render属 性,那么render就会替换template。 即:优先关系时: render > template > el
beforeMount函数:
模板编译(template)、数据挂载(把数据显示在模板里)之前执行的钩子函数
此时 this.$el有值,但是数据还没有挂载到页面上。即此时页面中的
{{}}
里的变量还没有被数据替换模板编译:用vue对象的数据(属性)替换模板中的内容
Mounted函数:
模板编译完成,数据挂载完毕
即:此时已经把数据挂载到了页面上,所以,页面上能够看到正确的数据了。
一般来说,我们在此处发送异步请求(ajax,fetch,axios等),获取服务器上的数据,显示在DOM里。
beforeUpdate函数:
组件更新之前执行的函数,只有数据更新后,才能调用(触发)beforeUpdate,注意:此数据一定是在模板上出现的数据,否则,不会,也没有必要触发组件更新(因为数据不出现在模板里,就没有必要再次渲染)
数据更新了,但是,vue(组件)对象对应的dom中的内部(innerHTML)没有变,所以叫作组件更新前
updated函数:
组件更新之后执行的函数
vue(组件)对象对应的dom中的内部(innerHTML)改变了,所以,叫作组件更新之后
activated函数:keep-alive组件激活时调用
activated函数:keep-alive组件停用时调用
beforeDestroy:vue(组件)对象销毁之前
destroyed:vue组件销毁后
keep-alive
解析: 比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用
1.21 vue keep-alive
参考答案:
keep-alive:keep-alive可以实现组件缓存,是Vue.js的一个内置组件。
作用:
- 它能够把不活动的组件实例保存在内存中,而不是直接将其销毁
- 它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中
使用方式:
- 常用的两个属性include/exclude,允许组件有条件的进行缓存。
- 两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。
- keep-alive的中还运用了LRU(Least Recently Used)算法。
原理:Vue 的缓存机制并不是直接存储 DOM 结构,而是将 DOM 节点抽象成了一个个 VNode节点,所以,keep- alive的缓存也是基于VNode节点的而不是直接存储DOM结构。
其实就是将需要缓存的VNode节点保存在this.cache中/在render时,如果VNode的name符合在缓存条件(可以用include以及exclude控制),则会从this.cache中取出之前缓存的VNode实例进行渲染。
1.22 既然函数是引用类型,为什么 vue 的 data 还是可以用函数
参考答案:
JavaScript只有函数构成作用域(注意理解作用域,只有函数{}构成作用域,对象的{}以及if(){}都不构成作用域),data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。
1.23 vue 中 $nextTick 作用与原理
参考答案:
作用:是为了可以获取更新后的DOM 。
由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了 Vue.nextTick(),就是在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
原理:
在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用
- Promise
- MutationObserver
- setImmediate
- 如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
1.24 vue的特性
参考答案:
- 表单操作
- 自定义指令
- 计算属性
- 过滤器
- 侦听器
- 生命周期
1.25 v-if v-show区别
参考答案:
v-show和v-if都是用来显示隐藏元素,v-if还有一个v-else配合使用,两者达到的效果都一样,但是v-if更消耗性能的,因为v-if在显示隐藏过程中有DOM的添加和删除,v-show就简单多了,只是操作css。
解析:
v-show
v-show不管条件是真还是假,第一次渲染的时候都会编译出来,也就是标签都会添加到DOM中。之后切换的时候,通过display: none;样式来显示隐藏元素。可以说只是改变css的样式,几乎不会影响什么性能。
v-if
在首次渲染的时候,如果条件为假,什么也不操作,页面当作没有这些元素。当条件为真的时候,开始局部编译,动态的向DOM元素里面添加元素。当条件从真变为假的时候,开始局部编译,卸载这些元素,也就是删除。
1.26 Vue 列表为什么加 key
参考答案:
vue中列表循环需加:key=”唯一标识” 唯一标识且最好是静态的,因为vue组件高度复用增加Key可以标识组件的唯一性,为了更好地区别各个组件 key的作用主要是为了高效的更新虚拟DOM
解析:
vue和react的虚拟DOM的Diff算法大致相同,其核心是基于两个简单的假设
首先讲一下diff算法的处理方法,对操作前后的dom树同一层的节点进行对比,一层一层对比,
当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。
比如一下这个情况:
可以在B和C之间加一个F,Diff算法默认执行起来是这样的:
即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?
所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
1.27 jquery 和 vue相比
参考答案:
- jquery:轻量级的js库
- vue:前端js库,是一个精简的MVVM,它专注于MVVM模型的viewModel层,通过双向数据绑定把view和model层连接起来,通过对数据的操作就可以完成对页面视图的渲染。
Vue | jQuery |
---|---|
数据驱动视图(MVVM思想:数据视图完全分离;数据驱动、双向绑定;) | 直接操作DOM(获取、修改、赋值、事件绑定) |
操作简单 | 操作麻烦 |
模块化 | x |
实现单页面 | x |
组件复用 | x |
性能高:使用的虚拟DOM,减少 dom的操作 | x |
扩展:
- vue适用的场景:复杂数据操作的后台页面,表单填写页面
- jquery适用的场景:比如说一些html5的动画页面,一些需要js来操作页面样式的页面
- 二者也是可以结合起来一起使用的,vue侧重数据绑定,jquery侧重样式操作,动画效果等,则会更加高效率的完成业务需求
1.28 为什么选择用vue做页面展示
参考答案:
MVVM 框架:
Vue 正是使用了这种 MVVM 的框架形式,并且通过声明式渲染和响应式数据绑定的方式来帮助我们完全避免了对 DOM 的操作。
单页面应用程序
Vue 配合生态圈中的 Vue-Router 就可以非常方便的开发复杂的单页应用
轻量化与易学习
Vue 的生产版本只有 30.90KB 的大小,几乎不会对我们的网页加载速度产生影响。同时因为 Vue 只专注于视图层,单独的 Vue 就像一个库一样,所以使我们的学习成本变得非常低
渐进式与兼容性
Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。Vue 只做界面,而把其他的一切都交给了它的周边生态(axios(Vue 官方推荐)、Loadsh.js、Velocity.js 等)来做处理,这就要求 Vue 必须要对其他的框架拥有最大程度的兼容性
视图组件化
Vue 允许通过组件来去拼装一个页面,每个组件都是一个可复用的 Vue 实例,组件里面可以包含自己的数据,视图和代码逻辑。方便复用
虚拟 DOM(Virtual DOM)
Vue 之所以可以完全避免对 DOM 的操作,就是因为 Vue 采用了虚拟 DOM 的方式,不但避免了我们对 DOM 的复杂操作,并且大大的加快了我们应用的运行速度。
社区支持
得益于 Vue 的本土化身份(Vue 的作者为国人尤雨溪),再加上 Vue 本身的强大,所以涌现出了特别多的国内社区,这种情况在其他的框架身上是没有出现过的,这使得我们在学习或者使用 Vue 的时候,可以获得更多的帮助
未来的 Vue 走向
Vue 是由国人尤雨溪在 Google 工作的时候,为了方便自己的工作而开发出来的一个库,而在 Vue 被使用的过程中,突然发现越来越多的人喜欢上了它。所以尤雨溪就进入了一个边工作、边维护的状态,在这种情况下 Vue 依然迅速的发展。
而现在尤雨溪已经正式辞去了 Google 的工作,开始专职维护 Vue,同时加入进来的还有几十位优秀的开发者,他们致力于把 Vue 打造为最受欢迎的前端框架。事实证明 Vue 确实在往越来越好的方向发展了(从 Angular、React、Vue 的对比图中可以看出 Vue 的势头)。所以我觉得完全不需要担心未来 Vue 的发展,至少在没有新的颠覆性创新出来之前,Vue 都会越做越好。
1.29 vue/angular区别
参考答案:
体积和性能
相较于vue,angular显得比较臃肿,比如一个包含了 Vuex + Vue Router 的 Vue 项目 (gzip 之后 30kB) ,而 angular-cli 生成的默认项目尺寸 (~65KB) 还是要小得多。
在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢。Vue.js使用基于依赖追踪的观察并且使用异步队列更新。所有的数据都是独立触发的。 对于庞大的应用来说,这个优化差异还是比较明显的
Virtual DOM vs Incremental DOM
在底层渲染方面,vue 使用的虚拟dom,而angular 使用的是Incremental DOM,Incremental DOM的优势在于低内开销
Vue 相比于 Angular 更加灵活,可以按照不同的需要去组织项目的应用代码。比如,甚至可以直接像引用jquery那样在HTML中引用vue,然后仅仅当成一个前端的模板引擎来用。
es6支持
es6是新一代的javascript标准,对JavaScript进行了大量的改进,使用es6开发已是基本需求。虽然有部分十分老旧的浏览器不支持es6,但是可以利用现代开发工具将es6编译成es5。在对es6的支持上两者都做得很好,(TS本身就是es6的超集)
学习曲线
针对前端而言,angular的学习曲线相对较大,vue学习起来更容易一些。不过对java和c的使用者而言,angular的静态检查、依赖注入的特性,以及面向对象的编程风格,使得angular都要更亲切一些。
使用热度
在使用热度上,vue具有更大优势,主要原因是更受数量庞大的中国开发者欢迎。较低的上手难度,易懂的开发文档,以及国人主导开发的光环,都使得vue更为流行
1.30 双向数据绑定原理
参考答案:
目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。
实现数据绑定的做法有大致如下几种:
发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是vm.set(‘property’, value)
这种方式现在毕竟太low了,我们更希望通过vm.property = value这种方式更新数据,同时自动更新视图,于是有了下面两种方式
脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过setInterval()定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:
- DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
- XHR响应事件 ( $http )
- 浏览器Location变更事件 ( $location )
- Timer事件( interval )
- 执行 digest()或apply()
数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
1.31 既然 Vue 通过数据劫持可以精准探测数据在具体dom上的变化,为什么还需要虚拟 DOM diff 呢?
参考答案:
前置知识: 依赖收集、虚拟 DOM、响应式系统
现代前端框架有两种方式侦测变化,一种是 pull ,一种是 push
pull: 其代表为React,我们可以回忆一下React是如何侦测到变化的,我们通常会用setStateAPI显式更新,然后React会进行一层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的Diff操作查找「哪发生变化了」,另外一个代表就是Angular的脏检查操作。
push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知。因此Vue是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher
一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的。
1.32 简单聊聊 new Vue 以后发生的事情
参考答案:
- new Vue会调用 Vue 原型链上的_init方法对 Vue 实例进行初始化;
- 首先是initLifecycle初始化生命周期,对 Vue 实例内部的一些属性(如 children、parent、isMounted)进行初始化;
- initEvents,初始化当前实例上的一些自定义事件(Vue.$on);
- initRender,解析slots绑定在 Vue 实例上,绑定createElement方法在实例上;
- 完成对生命周期、自定义事件等一系列属性的初始化后,触发生命周期钩子beforeCreate;
- initInjections,在初始化data和props之前完成依赖注入(类似于 React.Context);
- initState,完成对data和props的初始化,同时对属性完成数据劫持内部,启用监听者对数据进行监听(更改);
- initProvide,对依赖注入进行解析;
- 完成对数据(state 状态)的初始化后,触发生命周期钩子created;
- 进入挂载阶段,将 vue 模板语法通过vue-loader解析成虚拟 DOM 树,虚拟 DOM 树与数据完成双向绑定,触发生命周期钩子beforeMount;
- 将解析好的虚拟 DOM 树通过 vue 渲染成真实 DOM,触发生命周期钩子mounted;
1.33 v-for中的key的理解?
参考答案:
需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点。主要是为了高效的更新虚拟DOM。
1.34 vue首屏白屏如何解决?
参考答案:
- 路由懒加载
- vue-cli开启打包压缩 和后台配合 gzip访问
- 进行cdn加速
- 开启vue服务渲染模式
- 用webpack的externals属性把不需要打包的库文件分离出去,减少打包后文件的大小
- 在生产环境中删除掉不必要的console.log
1 | plugins: [ |
- 开启nginx的gzip ,在nginx.conf配置文件中配置
1 | http { //在 http中配置如下代码, |
- 添加loading效果,给用户一种进度感受
1.35 vue单页面和传统的多页面区别?
参考答案:
单页面应用(SPA)
通俗一点说就是指只有一个主页面的应用,浏览器一开始要加载所有必须的 html, js, css。所有的页面内容都包含在这个所谓的主页面中。但在写的时候,还是会分开写(页面片段),然后在交互的时候由路由程序动态载入,单页面的页面跳转,仅刷新局部资源。多应用于pc端。
多页面(MPA)
指一个应用中有多个页面,页面跳转时是整页刷新
单页面的优点:
用户体验好,快,内容的改变不需要重新加载整个页面,基于这一点spa对服务器压力较小;前后端分离;页面效果会比较炫酷(比如切换页面内容时的专场动画)。
单页面缺点:
不利于seo;导航不可用,如果一定要导航需要自行实现前进、后退。(由于是单页面不能用浏览器的前进后退功能,所以需要自己建立堆栈管理);初次加载时耗时多;页面复杂度提高很多。
1.36 refs、$parent的使用?
参考答案:
$root
可以用来获取vue的根实例,比如在简单的项目中将公共数据放再vue根实例上(可以理解为一个全局 store ),因此可以代替vuex实现状态管理;
$refs
在子组件上使用ref特性后,this.属性可以直接访问该子组件。可以代替事件emit 和refs.testId获取指定元素。注意:refs。
$parent
$parent属性可以用来从一个子组件访问父组件的实例,可以替代将数据以 prop 的方式传入子组件的方式;当变更父级组件的数据的时候,容易造成调试和理解难度增加;
1.36 路由跳转和location.href的区别?
参考答案:
使用location.href=’/url’来跳转,简单方便,但是刷新了页面;
使用路由方式跳转,无刷新页面,静态跳转;
1.37 scss是什么?在vue.cli中的安装使用步骤是?有哪几大特性?
参考答案:
css的预编译。
使用步骤:
- 先装css-loader、node-loader、sass-loader等加载器模块
- 在build目录找到webpack.base.config.js,在那个extends属性中加一个拓展.scss
- 在同一个文件,配置一个module属性
- 然后在组件的style标签加上lang属性 ,例如:lang=”scss”
特性:
可以用变量,例如($变量名称=值);
可以用混合器,例如()
可以嵌套
1.38 delete与vue.delete区别?
delte会删除数组的值,但是它依然会在内存中占位置
而vue.delete会删除数组在内存中的占位
1 | let arr1 = [1,2,3] |
1.39 computed和watch的区别
参考答案:
computed
计算结果并返回,只有当被计算的属性发生改变时才会触发(即:计算属性的结果会被缓存,除非依赖的响应属性变化才会重新及孙)
watch
监听某一个值,当被监听的值发生变化时,执行相关操作。
与computed的区别是,watch更加适用于监听某一个值得变化,并做对应操作,比如请求后太接口等。而computed适用于计算已有的值并返回结果。 监听简单数据类型:
1 | data(){ |
1.40 Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
参考答案:
1)Vue为什么要用vm.$set() 解决对象新增属性不能响应的问题
- Vue使用了Object.defineProperty实现双向数据绑定
- 在初始化实例时对属性执行 getter/setter 转化
- 属性必须在data对象上存在才能让Vue将它转换为响应式的(这也就造成了Vue无法检测到对象属性的添加或删除)
所以Vue提供了Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
1 | export function set (target: Array<any> | Object, key: any, val: any): any { |
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,
- 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理