一、MVVM原理
在Vue2官方文档中没有找到Vue是MVVM的直接证据,但文档有提到:虽然没有完全遵循MVVM模型,但是 Vue 的设计也受到了它的启发,因此在文档中经常会使用vm(ViewModel 的缩写) 这个变量名表示 Vue 实例。
为了感受MVVM模型的启发,我简单列举下其概念。
MVVM是Model-View-ViewModel的简写,由三部分构成:
Model: 模型持有所有的数据、状态和程序逻辑
View: 负责界面的布局和显示
ViewModel:负责模型和界面之间的交互,是Model和View的桥梁
大厂面试题分享 面试题库
前端后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
二、SPA单页面应用
单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的Web应用程序。我们开发的Vue项目大多是借助个官方的CLI脚手架,快速搭建项目,直接通过new Vue构建一个实例,并将el:'#app'挂载参数传入,最后通过npm run build的方式打包后生成一个index.html,称这种只有一个HTML的页面为单页面应用。
当然,vue也可以像jq一样引入,作为多页面应用的基础框架。
三、Vue的特点
清晰的官方文档和好用的api,比较容易上手。
是一套用于构建用户界面的渐进式框架,将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。
使用 Virtual DOM。
提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件。
四、Vue的构建入口
vue使用过程中可以采用以下两种方式:
在vue脚手架中直接使用,参考文档:https://cn.vuejs.org/v2/guide/installation.html
或者在html文件的头部通过静态文件的方式引入: <script src="" target="_blank">https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
那么问题来了,使用的或者引入的到底是什么?
答:引入的是已经打包好的vue.js文件,通过rollup构建打包所得。
构建入口在哪里?
答:在vue源码的package.json文件中:
"scripts":{// ..."build":"node scripts/build.js","build:ssr":"npm run build -- web-runtime-cjs,web-server-renderer","build:weex":"npm run build -- weex",// ...},复制代码
通过执行npm run build的时候,会进行scripts/build.js文件的执行,npm run build:ssr和npm run build:weex的时候,将ssr和weex作为参数传入,按照参数构建出不一样的vue.js打包文件。
所以说,vue中的package.json文件就是构建的入口,具体构建流程可以参考vue2入口:构建入口。
五、对import Vue from "vue"的理解
在使用脚手架开发项目时,会有一行代码import Vue from "vue",那么这个Vue指的是什么。
答:一个构造函数。
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
复制代码
我们开发中引入的Vue其实就是这个构造函数,而且这个构造函数只能通过new Vue的方式进行使用,否则会在控制台打印警告信息。定义完后,还会通过initMixin(Vue)、stateMixin(Vue)、eventsMixin(Vue)、lifecycleMixin(Vue)和renderMixin(Vue)的方式为Vue原型中混入方法。我们通过import Vue from "Vue"引入的本质上就是一个原型上挂在了好多方法的构造函数。
六、对new Vue的理解
// main.js文件importVuefrom"vue";
var app = newVue({
el: '#app',
data() {
return {
msg: 'hello Vue~'
}
},
template: `<div>{{msg}}</div>`,
})
console.log(app);
复制代码
new Vue就是对构造函数Vue进行实例化,执行结果如下:
可以看出实例化后的实例中包含了很多属性,用来对当前app进行描述,当然复杂的Vue项目这个app将会是一个树结构,通过$parent和$children维护父子关系。
new Vue的过程中还会执行this._init方法进行初始化处理。
七、编译
虚拟DOM的生成必须通过render函数实现,render函数的产生是在编译阶段完成,核心代码如下:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
复制代码
主要完成的功能是:
通过const ast = parse(template.trim(), options)将template转换成ast树
通过optimize(ast, options)对ast进行优化
通过const code = generate(ast, options)将优化后的ast转换成包含render字符串的code对象,最终render字符串通过new Function转换为可执行的render函数
八、虚拟DOM
先看浏览器对HTML的理解:
<div><h1>My title</h1>
Some text content
<!-- TODO: Add tagline --></div>复制代码
当浏览器读到这些代码时,它会建立一个DOM树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。 上述 HTML 对应的 DOM 节点树如下图所示:
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
再看Vue对HTML template的理解
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
简言之,浏览器对HTML的理解是DOM树,Vue对HTML的理解是虚拟DOM,最后在patch阶段通过DOM操作的api将其渲染成真实的DOM节点。
九、模板或者组件渲染
Vue中的编译会执行到逻辑vm._update(vm._render(), hydrating),其中的vm._render执行会获取到vNode,vm._update就会对vNode进行patch的处理,又分为模板渲染和组件渲染。
十、数据响应式处理
Vue的数据响应式处理的核心是Object.defineProperty,在递归响应式处理对象的过程中,为每一个属性定义了一个发布者dep,当进行_render函数执行时会访问到当前值,在get中通过dep.depend进行当前Watcher的收集,当数据发生变化时会在set中通过dep.notify进行Watcher的更新。
十一、this.$set
const app = new Vue({
el: "#app",
data() {
return {
obj: {
name: "name-1"
}
};
},
template: `<div @click="change">{{obj.name}}的年龄是{{obj.age}}</div>`,
methods: {
change() {
this.obj.name = 'name-2';this.obj.age = 30;
}
}
});复制代码
以上例子执行的结果是:
name-1的年龄是
当点击后依然是:
name-2的年龄是
可以看出点击后,obj的name属性变化得到了视图更新,而age属性并未进行变化。
name属性响应式的过程中锁定了一个发布者dep,在当前视图渲染时在发布者dep的subs中做了记录,一旦其发生改变,就会触发set方法中的dep.notify,继而执行视图的重新渲染。然而,age属性并未进行响应式的处理,当其改变时就不能进行视图渲染。
此时就需要通过this.$set的方式对其进行手动响应式的处理。
十二、组件注册
组件的使用是先注册后使用,又分为:
全局注册:可以直接在页面中使用
局部注册:使用时需要通过import xxx from xxx的方式引入,并且在当前组件的选项components中增加局部组件的名称。
十三、异步组件
Vue单页面应用中一个页面只有一个<div id="app"></div>承载所有节点,因此复杂项目可能会出现首屏加载白屏等问题,Vue异步组件就很好的处理了这问题。
十四、this.$nextTick
因为通过new实例化构造函数Vue的时候会执行初始化方法this._init,其中涉及到的方法大多都是同步执行。nextTick在vue中是一个很重要的方法,在new Vue实例化的同步过程中将一些需要异步处理的函数推到异步队列中去,可以等new Vue所有的同步任务执行完后,再执行异步队列中的函数。
十五、keep-alive内置组件
vue中支持组件化,并且也有用于缓存的内置组件keep-alive可直接使用,使用场景为路由组件和动态组件。
activated表示进入组件的生命周期,deactivated表示离开组件的生命周期
include表示匹配到的才缓存,exclude表示匹配到的都不缓存
max表示最多可以缓存多少组件
十六、生命周期
vue中的生命周期有哪些?
答案:11个,分别为beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated、deactivated、beforeDestroy、destroyed和errorCaptured。
十七、v-show和v-if的区别
先看v-if和v-show的使用场景:
(1)v-if更多的使用在需要考虑白屏时间或者切换次数很少的场景
(2)v-show更多使用在transition控制的动画或者需要非常频繁地切换的场景
再从底层实现思路上分析:
(1)v-if条件为false时,会生成空的占位注释节点,那么在考虑首页白屏时间时,选用v-if比较合适。条件从false变化为true的话会从空的注释节点变成真实节点,条件再变为false时真实节点又会变成注释节点,如果切换次数比较多,那么开销会比较大,频繁切换场景不建议使用v-if。
(2)v-show条件为false时,会生成真实的节点,只是为当前节点增加了display:none来控制其隐藏,相比v-if生成空的注释节点其首次渲染开销是比较大的,所以不建议用在考虑首屏白屏时间的场景。如果我们频繁切换v-show的值,从display:none到display:block之间的切换比起空的注释节点和真实节点的开销要小很多,这种场景就建议使用v-show。
十八、v-for中key的作用
在v-for进行循环展示过程中,当数据发生变化进行渲染的过程中,会进行新旧节点列表的比对。首先新旧vnode列表首先通过首首、尾尾、首尾和尾首的方式进行比对,如果key相同则采取原地复用的策略进行节点的移动。
如果首尾两两比对的方式找不到对应关系,继续通过key和vnode的对应关系进行寻找。
如果key和vnode对应关系中找不到,继续通过sameVnode的方式在未比对的节点中进行寻找。
如果都找不到,则将其按照新vnode进行createElm的方式进行创建,这种方式是比节点移动的方式计算量更大。
最后将旧的vnode列表中没有进行匹配的vnode中的vnode.elm在父节点中移除。
简单总结就是,新的vnode列表在旧的vnode列表中去寻找具有相同的key的节点进行原地复用,如果找不到则通过创建的方式createElm去创建一个,如果旧的vnode列表中没有进行匹配则在父节点中移除其vnode.elm。这就是原地复用逻辑的大体实现。
十九、v-for和v-if能同时使用吗
答案是:用了也能出来预期的效果,但是会有性能浪费。
同时包含v-for和v-if的template模板在编辑阶段会执行v-for比v-if优先级更高的编译流程;在生成vnode的阶段,会包含属性isComment为true的空白占位vnode;在patch阶段,会生成真实的占位节点。虽然一个空的占位节点无妨,但是如果数据量比较大的话,也是一个性能问题。
当然,可以在获取到数据(一般是在beforeCreate或者created阶段)时进行过滤处理,也可以通过计算属性对其进行处理。
二十、vue中的data为什么是函数
答案是:是不是一定是函数,得看场景。并且,也无需担心什么时候该将data写为函数还是对象,因为vue内部已经做了处理,并在控制台输出错误信息。
场景一:new Vue({data: ...})
这种场景主要为项目入口或者多个html页面各实例化一个Vue时,这里的data即可用对象的形式,也可用工厂函数返回对象的形式。因为,这里的data只会出现一次,不存在重复引用而引起的数据污染问题。
场景二:组件场景中的选项
在生成组件vnode的过程中,组件会在生成构造函数的过程中执行合并策略:
// data合并策略
strats.data = function (
parentVal,
childVal,
vm
) {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
);
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};
复制代码
如果合并过程中发现子组件的数据不是函数,即typeof childVal !== 'function'成立,进而在开发环境会在控制台输出警告并且直接返回parentVal,说明这里压根就没有把childVal中的任何data信息合并到options中去。
二十一、this.$watch
使用场景:用来监听数据的变化,当数据发生变化的时候,可以做一些业务逻辑的处理。
配置参数:
deep:监听数据的深层变化
immediate:立即触发回调函数
实现思路: Vue构造函数定义完成以后,在执行stateMixin(Vue)时为Vue.prototype上定义$watch。该方法通过const watcher = new Watcher(vm, expOrFn, cb, options)进行Watcher的实例化,将options中的user属性设置为true。并且,$watch逻辑结束的会返回函数function unwatchFn () { watcher.teardown() },用来取消侦听的函数。
二十二、计算属性和侦听属性的区别
相同点: 两者都是Watcher实例化过程中的产物
计算属性:
使用场景:模板内的表达式主要用于简单运算,对于复杂的计算逻辑可以用计算属性
计算属性是基于它们的响应式依赖进行缓存的,当依赖的数据未发生变化时,多次调用无需重复执行函数
计算属性计算结果依赖于data中的值
同步操作,不支持异步
侦听属性:
使用场景:当需要在数据变化时执行异步或开销较大的操作时,可以用侦听属性
可配置参数:可以通过配置immediate和deep来控制立即执行和深度监听的行为
侦听属性侦听的是data中定义的
二十三、v-model
// main.js
new Vue({
el: "#app",
data() {
return {
msg: ""
};
},
template: `<div>
<input v-model="msg" placeholder="edit me">
<p>msg is: {{ msg }}</p>
</div>`
});
复制代码
普通input:input中的v-model,最终通过target.addEventListener处理成在节点上监听input事件function($event){msg=$event.target.value}}的形式,当input值变化时msg也跟着改变。
// main.js
const inputBox = {
template: `<input @input="$emit('input', $event.target.value)">`,
};
new Vue({
el: "#app",
template: `<div>
<input-box v-model="msg"></input-box>
<p>{{msg}}</p>
</div>`,
components: {
inputBox
},
data() {
return {
msg: 'hello world!'
};
},
});复制代码
组件:v-model在组件中则通过给点击事件绑定原生事件,当触发到$emit的时候,再进行回调函数ƒunction input($$v) {msg=$$v}的执行,进而达到子组件修改父组件中数据msg的目的。
二十四、v-slot
v-slot产生的主要目的是,在组件的使用过程中可以让父组件有修改子组件内容的能力,就像在子组件里面放了个插槽,让父组件往插槽内塞入父组件中的楔子;并且,父组件在子组件中嵌入的楔子也可以访问子组件中的数据。v-slot的产生让组件的应用更加灵活。
1、具名插槽
let baseLayout = {
template: `<divclass="container"><header><slotname="header"></slot></header><main><slot></slot></main><footer><slotname="footer"></slot></footer></div>`,
data() {
return {
url: ""
};
}
};
new Vue({
el: "#app",
template: `<base-layout><templatev-slot:header><h1>title-txt</h1></template><p>paragraph-1-txt</p><p>paragraph-2-txt</p><templatev-slot:footer><p>foot-txt</p></template></base-layout>`,
components: {
baseLayout
}
});
复制代码
引入的组件baseLayout中的template被添加了属性v-slot:header和v-slot:footer,子组件中定义了对应的插槽被添加了属性name="header"和name="footer",未被进行插槽标识的内容被插入到了匿名的<slot></slot>中。
2、作用域插槽
let currentUser = {
template: `<span>
<slot name="user" v-bind:userData="childData">{{childData.firstName}}</slot>
</span>`,
data() {
return {
childData: {
firstName: "first",
lastName: "last"
}
};
}
};
new Vue({
el: "#app",
template: `<current-user>
<template v-slot:user="slotProps">{{slotProps.userData.lastName}}</template>
</current-user>`,
components: {
currentUser
}
});复制代码
当前例子中作用域插槽通过v-bind:userData="childData"的方式,将childData作为参数,父组件中通过v-slot:user="slotProps"的方式进行接收,为父组件使用子组件中的数据提供了可能。
二十五、Vue.filters
filters类似于管道流可以将上一个过滤函数的结果作为下一个过滤函数的第一个参数,又可以在其中传递参数让过滤器更灵活。
// main.js文件import Vue from "vue";
Vue.filter("filterEmpty", function(val) {
returnval || "";
});
Vue.filter("filterA", function(val) {
returnval + "平时周末的";
});
Vue.filter("filterB", function(val, info, fn) {
returnval + info + fn;
});
new Vue({
el: "#app",
template: `<div>{{msg | filterEmpty | filterA | filterB('爱好是', transformHobby('chess'))}}</div>`,
data() {
return {
msg: "张三"
};
},
methods: {
transformHobby(type) {
const map = {
bike: "骑行",
chess: "象棋",
game: "游戏",
swimming: "游泳"
};
return map[type] || "未知";
}
}
});
复制代码
其中我们对msg通过filterEmpty、filterA和filterB('爱好是', transformHobby('chess'))}进行三层过滤。
二十六、Vue.use
作用:Vue.use被用来安装Vue.js插件,例如vue-router、vuex、element-ui。
install方法:如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为install方法。install方法调用时,会将Vue作为参数传入。
调用时机:该方法需要在调用 new Vue() 之前被调用。
特点:当 install 方法被同一个插件多次调用,插件将只会被安装一次。
二十七、Vue.extend和选项extends
1、Vue.extend
Vue.extend使用基础Vue构造器创建一个“子类”,参数是一个包含组件选项的对象,实例化的过程中可以修改其中的选项,为实现功能的继承提供了思路。
newVue({
el: "#app",
template: `<div><div id="person1"></div><div id="person2"></div></div>`,
mounted() {
// 定义子类构造函数varProfile = Vue.extend({
template: '<p @click="showInfo">{{name}} 喜欢 {{fruit}}</p>',
data: function () {
return {
name: '张三',
fruit: '苹果'
}
},
methods: {
showInfo() {
console.log(`${this.name}喜欢${this.fruit}`)
}
}
})
// 实例化1,挂载到`#person1`上newProfile().$mount('#person1')
// 实例化2,并修改其`data`选项,挂载到`#person2`上newProfile({
data: function () {
return {
name: '李四',
fruit: '香蕉'
}
},
}).$mount('#person2')
},
});
复制代码
在当前例子中,通过Vue.extend构建了子类构造函数Profile,可以通过new Profile的方式实例化无数个vm实例。我们定义初始的template、data和methods供vm进行使用,如果有变化,在实例的过程中传入新的选项参数即可,比如例子中实例化第二个vm的时候就对data进行了调整。
2、选项extends
extends允许声明扩展另一个组件 (可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件,以实现组件继承的目的。
const common = {
template: `<div>{{name}}</div>`,
data() {
return {
name: '表单'
}
}
}
const create = {
extends: common,
data() {
return {
name: '新增表单'
}
}
}
const edit = {
extends: common,
data() {
return {
name: '编辑表单'
}
}
}
newVue({
el: "#app",
template: `<div>
<create></create>
<edit></edit>
</div>`,
components: {
create,
edit,
}
});
复制代码
当前极简demo中定义了公共的表单common,然后又在新增表单组件create和编辑表单组件edit中扩展了common。
二十八、Vue.mixin和选项mixins
全局混入和局部混入视情况而定,主要区别在全局混入是通过Vue.mixin的方式将选项混入到了Vue.options中,在所有获取子组件构建函数的时候都将其进行了合并,是一种影响全部组件的混入策略。
而局部混入是将选项通过配置mixins选项的方式合并到当前的子组件中,只有配置了mixins选项的组件才会受到混入影响,是一种局部的混入策略。
二十九、Vue.directive和directives
1、使用场景
主要用于对于DOM的操作,比如:文本框聚焦,节点位置控制、防抖节流、权限管理、复制操作等功能
2、钩子函数
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
3、钩子函数参数
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 钩子中可用。
4、动态指令参数
指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
三十、vue中的原生事件
vue中可以通过@或者v-on的方式绑定事件,也可为其添加修饰符。
newVue({
el: '#app',
template: `<div @click='divClick'><a @clickt='aClick' href=''>点击</a></div>`,
methods: {
divClick() {
console.log('divClick')
},
aClick() {
console.log('aClick')
},
}
})
复制代码
以上例子如果点击a会触发其默认行为,如果href不为空还会进行跳转。除此之外,点击还会继续触发div上绑定的点击事件。
如果通过@click.stop.prevent='aClick'的方式为a标签的点击事件添加修饰符stop和prevent,那么就不会触发其a的默认行为,即使href不为空也不会进行跳转,同时,div上的点击事件也不会进行触发。
模板的渲染一般分为编译生成render函数、render函数执行生成vNode和patch进行渲染。下面按照这步骤进行简单分析。
1、render
通过编译生成的render函数:
with(this) {
return _c('div', {
on: {
"click": divClick
}
}, [_c('a', {
attrs: {
"href": "http://www.baidu.com"
},
on: {
"click": function ($event) {
$event.stopPropagation();
$event.preventDefault();
return aClick($event)
}
}
}, [_v("点击")])])
}
复制代码
其中div的on作为div事件描述。a标签的attrs作为属性描述,on作为事件描述,在描述中.stop被编译成了$event.stopPropagation()来阻止事件冒泡,.prevent被编译成了$event.preventDefault()用来阻止a标签的默认行为。
2、vNode
通过执行Vue.prototype._render将render函数转换成vNode。
3、patch
patch的过程中,当完成$el节点的渲染后会执行invokeCreateHooks(vnode, insertedVnodeQueue)逻辑,其中,针对attrs会将其设置为$el的真实属性,当前例子中会为a标签设置herf属性。针对on会通过target.addEventListener的方式将其处理过的事件绑定到$el上,当前例子中会分别对div和a中的click进行处理,再通过addEventListener的方式进行绑定。
小结
vue中的事件,从编译生成render再通过Vue.prototype._render函数执行render到生成vNode,主要是通过on作为描述。在patch渲染阶段,将on描述的事件进行处理再通过addEventListener的方式绑定到$el上。
三十一、常用修饰符
1、表单修饰符
(1).lazy
在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 ,可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步:
<input v-model.lazy="msg">
复制代码
(2).number
如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:
<input v-model.number="age" type="number">
复制代码
(3).trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:
<input v-model.trim="msg">
复制代码
2、事件修饰符
(1).stop
阻止单击事件继续传播。
<!--这里只会触发a--><div @click="divClick"><av-on:click.stop="aClick">点击</a></div>复制代码
(2).prevent
阻止标签的默认行为。
<a href="http://www.baidu.com" v-on:click.prevent="aClick">点击</a>
复制代码
(3).capture
事件先在有.capture修饰符的节点上触发,然后在其包裹的内部节点中触发。
<!--这里先执行divClick事件,然后再执行aClick事件--><div @click="divClick"><av-on:click="aClick">点击</a></div>复制代码
(4).self
只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。
<!--在a标签上点击时只会触发aClick事件,只有点击phrase的时候才会触发divClick事件--><div @click.self="divClick">phrase<av-on:click="aClick">点击</a></div>复制代码
(5).once
不像其它只能对原生的 DOM 事件起作用的修饰符,.once 修饰符还能被用到自定义的组件事件上,表示当前事件只触发一次。
<a v-on:click.once="aClick">点击</a>
复制代码
(6).passive
.passive 修饰符尤其能够提升移动端的性能
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --><!-- 而不会等待 `onScroll` 完成 --><!-- 这其中包含 `event.preventDefault()` 的情况 --><divv-on:scroll.passive="onScroll">...</div>复制代码
3、其他修饰符
除了表单和事件的修饰符,Vue还提供了很多其他修饰符,在使用的时候可以查阅文档。
小结
Vue中提供了很多好用的功能和api,那么修饰符的出现就为功能和api提供了更为丰富的扩展属性和更大的灵活度。
三十二、vue-router
vue路由是单页面中视图切换的方案,有三种mode:
hash,#后的仅仅作为参数,不属于url部分
history,路径作为请求url请求资源链接,如果找不到会出现404错误
abstract,服务端渲染场景 hash场景下,会出现url链接,再修改其view-router中对应的值。
三十三、vuex
vuex是状态管理仓库,一般使用的场景为:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态。其管理的状态是响应式的,修改也只能显式提交mutation的方式修改。vuex有state、getter、mutation、action和module五个核心,并且通过module实现了vuex树的管理。
三十四、eventBus
使用场景:兄弟组件传参
const eventBus = newVue();
const A = {
template: `<div @click="send">component-a</div>`,
methods: {
send() {
eventBus.$emit('sendData', 'data from A')
}
},
}
const B = {
template: `<div>component-b</div>`,
created() {
eventBus.$on('sendData', (args) => {
console.log(args)
})
},
}
newVue({
el: '#app',
components: {
A,
B,
},
template: `<div><A></A><B></B></div>`,
})
复制代码
在当前例子中,A组件和B组件称为兄弟组件,A组件通过事件总线eventBus中的$emit分发事件,B组件则通过$on来监听事件。
实现原理:eventsMixin
exportfunctioneventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
constvm: Component = thisif (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration// instead of a hash lookupif (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$once = function (event: string, fn: Function): Component {
constvm: Component = thisfunctionon () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
constvm: Component = this// allif (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of eventsif (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific eventconst cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = nullreturn vm
}
// specific handlerlet cb
let i = cbs.lengthwhile (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
constvm: Component = thisif (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
复制代码
在Vue构造函数定义完执行的eventsMixin函数中,在Vue.prototype上分别定义了$on、$emit、$off和$once的方法易实现对事件的绑定、分发、取消和只执行一次的方法。eventBus就是利用了当new Vue实例化后实例上的$on、$emit、$off和$once进行数据传递。
三十五、ref
使用场景: 父组件获取子组件数据或者执行子组件方法
const A = {
template: `<div>{{childData.age}}</div>`,
data() {
return {
childData: {
name: 'qb',
age: 30
},
}
},
methods: {
increaseAge() {
this.childData.age++;
}
}
}
newVue({
el: '#app',
components: {
A,
},
template: `<A ref='childRef' @click.native='changeChildData'></A>`,
methods: {
changeChildData() {
// 执行子组件的方法this.$refs.childRef.increaseAge()
// 获取子组件的数据console.log(this.$refs.childRef.childData);
},
}
})
复制代码
在当前例子中,通过ref='childRef'的方式在当前组件中定义一个ref,可以通过this.$refs.childRef的方式获取到子组件A。可以通过this.$refs.childRef.increaseAge()的方式执行子组件中age增加的方法,也可以通过this.$refs.childRef.childData的方式获取到子组件中的数据。
三十六、props
使用场景: 父子传参
const A = {
template: `<div @click='emitData'>{{childData}}</div>`,
props: ['childData'],
methods: {
emitData() {
this.$emit('emitChildData', 'data from child')
}
},
}
newVue({
el: '#app',
components: {
A
},
template: `<A :childData='parentData' @emitChildData='getChildData'></A>`,
data() {
return {
parentData: 'data from parent'
}
},
methods: {
getChildData(v) {
console.log(v);
}
}
})
复制代码
从当前例子中可以看出,数据父传子是通过:childData='parentData'的方式,数据子传父是通过this.$emit('emitChildData', 'data from child')的方式,然后,父组件通过@emitChildData='getChildData'的方式进行获取。
大厂面试题分享 面试题库
前端后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
1、父组件render函数
new Vue中传入的模板template经过遍历生成的render函数如下:
with(this) {
return _c('A', {
attrs: {
"childData": parentData
},
on: {
"emitChildData": getChildData
}
})
}
复制代码
其中data部分有attrs和on来描述属性和方法。
在通过createComponent创建组件vnode的过程中,会通过const propsData = extractPropsFromVNodeData(data, Ctor, tag)的方式获取props,通过const listeners = data.on的方式获取listeners,最后将其作为参数通过new VNode(options)的方式实例化组件vnode。
2、子组件渲染
在通过const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance )创建组件实例的过程中,会执行到组件继承自Vue的._init方法,通过initEvents将事件处理后存储到vm._events中,通过initProps将childData赋值到子组件A的vm实例上,并进行响应式处理,让其可以通过vm.childData的方式访问,并且数据发生变化时视图也可以发生改变。
组件模板编译后对应的render函数是:
with(this) {
return _c('div', {
on: {
"click": emitData
}
}, [_v(_s(childData))])
}
复制代码
在createElm完成节点的创建后,在invokeCreateHooks(vnode, insertedVnodeQueue)阶段,给DOM原生节点节点绑定emitData。
3、this.$emit
在点击执行this.$emit时,会通过var cbs = vm._events[event]取出_events中的事件进行执行。
至此,父组件中的传递的数据就在子组件中可以通过this.xxx的方式获得,也可以通过this.$emit的方式将子组件中的数据传递给父组件。
三十七、$attrs和$listeners
使用场景: 父子组件非props属性和非native方法传递
// main.js文件importVuefrom"vue";
const B = {
template: `<div @click="emitData">{{ formParentData }}</div>`,
data() {
return {
formParentData: ''
}
},
inheritAttrs: false,
created() {
this.formParentData = this.$attrs;
console.log(this.$attrs, '--------------a-component-$attrs')
console.log(this.$listeners, '--------------b-component-$listeners')
},
methods: {
emitData() {
this.$emit('onFun', 'form B component')
}
},
}
const A = {
template: `<B v-bind='$attrs' v-on='$listeners'></B>`,
components: {
B,
},
props: ['propData'],
inheritAttrs: false,
created() {
console.log(this.$attrs, '--------------b-component-$attrs')
console.log(this.$listeners, '--------------b-component-$listeners')
}
}
newVue({
el: '#app',
components: {
A,
},
template: `<A :attrData='parentData' :propData='parentData' @click.native="nativeFun" @onFun="onFun"></A>`,
data() {
return {
parentData: 'msg'
}
},
methods: {
nativeFun() {
console.log('方法A');
},
onFun(v) {
console.log('方法B', v);
},
}
})
复制代码
当前例子中,new Vue的template模板中有attrData、propData、click.native和onFun在进行传递。实际运行后,在A组件中this.$attrs为{attrData: 'msg'},this.$listeners为{onFun:f(...)}。在A组件中通过v-bind='$attrs'和v-on='$listeners'的方式继续进行属性和方法的传递,在B组件中就可以获取到A组件中传入的$attrs和$listeners。
当前例子中完成了非props属性和非native方法的传递,并且通过v-bind='$attrs'和v-on='$listeners'的方式实现了属性和方法的跨层级传递。
同时通过this.$emit的方法触发了根节点中onFun事件。
关于例子中的inheritAttrs: false,默认情况下父作用域的不被认作props的attribute绑定将会“回退”且作为普通的HTML属性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置inheritAttrs到false,这些默认行为将会被去掉。
三十八、$parent和$children
使用场景: 利用父子关系进行数据的获取或者方法的调用
const A = {
template: `<div @click="changeParentData">{{childRandom}}</div>`,
data() {
return {
childRandom: Math.random()
}
},
mounted() {
console.log(this.$parent.parentCount, '--child-created--'); // 获取父组件中的parentCount
},
methods: {
changeParentData() {
console.log(this.$parent); // 打印当前实例的$parentthis.$parent.changeParentData(); // 调用当前父级中的方法`changeParentData`
},
changeChildData() {
this.childRandom = Math.random();
}
}
}
const B = {
template: `<div>b-component</div>`,
}
newVue({
el: '#app',
components: {
A,
B,
},
template: `<div><A></A><B></B><p>{{parentCount}}</p><button @click="changeChildrenData">修改子组件数据</button></div>`,
data() {
return {
parentCount: 1
}
},
mounted() {
console.log(this.$children[0].childRandom, '--parent-created--'); // 获取第一个子组件中的childRandom
},
methods: {
changeParentData() {
this.parentCount++;
},
changeChildrenData() {
console.log(this.$children); // 此时有两个子组件this.$children[0].changeChildData(); // 调起第一个子组件中的'changeChildData'方法
}
}
})
复制代码
在当前例子中,父组件可以通过this.$children获取所有的子组件,这里有A组件和B组件,可以通过this.$children[0].childRandom的方式获取子组件A中的数据,也可以通过this.$children[0].changeChildData()的方式调起子组件A中的方法。
子组件可以通过this.$parent的方式获取父组件,可以通过this.$parent.parentCount获取父组件中的数据,也可以通过this.$parent.changeParentData()的方式修改父组件中的数据。
Vue中$parent和$children父子关系的底层构建请参考杂谈:parent/parent/children的底层逻辑
三十九、inject和provide
使用场景:嵌套组件多层级传参
const B = {
template: `<div>{{parentData1}}{{parentData2}}</div>`,
inject: ['parentData1', 'parentData2'],
}
const A = {
template: `<B></B>`,
components: {
B,
},
}
new Vue({
el: '#app',
components: {
A,
},
template: `<A></A>`,
provide: {
parentData1: {
name: 'name-2',
age: 30
},
parentData2: {
name: 'name-2',
age: 29
},
}
})
复制代码
例子中在new Vue的时候通过provide提供了两个数据来源parentData1和parentData2,然后跨了一个A组件在B组件中通过inject注入了这两个数据。
1、initProvide
在执行组件内部的this._init初始化方法时,会执行到initProvide逻辑:
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
复制代码
如果在当前vm.$options中存在provide,会将其执行结果赋值给vm._provided。
2、initInjections
functioninitInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
functionresolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cachedconst result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...if (key === '__ob__') continueconst provideKey = inject[key].fromlet source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default'in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} elseif (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
复制代码
如果当前组件中有选项inject,会以while循环的方式不断在source = source.$parent中寻找_provided,然后获取到祖先组件中提供的数据源,这是实现祖先组件向所有子孙后代注入依赖的核心。
四十、Vue项目能做的性能优化
1、v-if和v-show
频繁切换时使用v-show,利用其缓存特性
首屏渲染时使用v-if,如果为false则不进行渲染
2、v-for的key
列表变化时,循环时使用唯一不变的key,借助其本地复用策略
列表只进行一次渲染时,key可以采用循环的index
3、侦听器和计算属性
侦听器watch用于数据变化时引起其他行为
多使用compouter计算属性顾名思义就是新计算而来的属性,如果依赖的数据未发生变化,不会触发重新计算
4、合理使用生命周期
在destroyed阶段进行绑定事件或者定时器的销毁
使用动态组件的时候通过keep-alive包裹进行缓存处理,相关的操作可以在actived阶段激活
5、数据响应式处理
不需要响应式处理的数据可以通过Object.freeze处理,或者直接通过this.xxx = xxx的方式进行定义
需要响应式处理的属性可以通过this.$set的方式处理,而不是JSON.parse(JSON.stringify(XXX))的方式
6、路由加载方式
页面组件可以采用异步加载的方式
7、插件引入
第三方插件可以采用按需加载的方式,比如element-ui。
8、减少代码量
采用mixin的方式抽离公共方法
抽离公共组件
定义公共方法至公共js中
抽离公共css
9、编译方式
如果线上需要template的编译,可以采用完成版vue.esm.js
如果线上无需template的编译,可采用运行时版本vue.runtime.esm.js,相比完整版体积要小大约30%
10、渲染方式
服务端渲染,如果是需要SEO的网站可以采用服务端渲染的方式
前端渲染,一些企业内部使用的后端管理系统可以采用前端渲染的方式
11、字体图标的使用
有些图片图标尽可能使用字体图标
四十一、Vue项目白屏问题
1、开启gzip压缩减小文件体积。
2、webpack设置productionSourceMap:false,不在线上环境打包.map文件。
3、路由懒加载
4、异步组件的使用
5、静态资源使用cdn链接引入
6、采用ssr服务端渲染方案
7、骨架屏或者loading效果填充空白间隙
8、首次不渲染的隐藏采用v-if
9、注重代码规范:抽取公共组件,公共js,公共css样式,减小代码体积。删除无用代码,减少非必要注释。防止写出死循环等等
10、删除辅助开发的console.log
11、非Vue角度思考:非重要文件采用异步加载方式、css样式采用媒体查询、采用域名分片技术、http1升级成http2、如果是SSR项目考虑服务端渲染有没有可优化的点、请求头是否带了多余信息等思路
内容有些多,大体可以归类为从服务端拿到资源的速度、资源的体积和渲染是否阻塞的角度去作答。
四十二、从0到1构建一个Vue项目需要注意什么
架子:选用合适的初始化脚手架(vue-cli2.0或者vue-cli3.0)
请求:数据axios请求的配置
登录:登录注册系统
路由:路由管理页面
数据:vuex全局数据管理
权限:权限管理系统
埋点:埋点系统
插件:第三方插件的选取以及引入方式
错误:错误页面
入口:前端资源直接当静态资源,或者服务端模板拉取
SEO:如果考虑SEO建议采用SSR方案
组件:基础组件/业务组件
样式:样式预处理起,公共样式抽取
方法:公共方法抽离
四十三、SSR
1、什么是服务端渲染(SSR)?
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
2、为什么使用服务端渲染(SSR)?
与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
3、使用服务器端渲染 (SSR) 时需要考虑的问题?
使用服务器端渲染 (SSR) 时还需要有一些权衡之处
开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。
四十四、scoped
在Vue项目开发的项目中如果样式中未使用scoped,组件间的样式会出现覆盖的问题。
反例:
// app.vue文件
<template><div><h3class="title">app-txt</h3><child></child></div></template><script>import child from"@/components/child";
exportdefault {
components: { child },
};
</script><style>.title {
color: red;
}
</style>复制代码
// child.vue文件
<template><h3class="title">child-txt</h3></template><style>.title {
color: green;
}
</style>复制代码
父组件和子组件的样式颜色都为green,子组件中的样式覆盖了父组件的样式。
正例:
<template><h3class="title">child-txt</h3></template><stylescoped>.title {
color: green;
}
</style>复制代码
此时,父组件中颜色为red,子组件中颜色为green。
主要原因:
例子中的DOM节点和CSS层叠样式中都被添加了data-v-xxx来表示唯一,所以scoped是给当前组件的节点和样式唯一标识为data-v-xxx,避免了样式覆盖。
大厂面试题分享 面试题库
后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库