使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对对象添加属性或者通过下标方式修改数组时,Object.defineproperty无法监测到,Vue底层对数组的监测是通过建立一个对象,在这个对象里重写push,pop,shift,unshift,sort,reverse,splice七种方法,在这七种方法里对数组元素添加响应,并让这个对象指向array.prototype。
在Vue3中已经采用proxy方法,可以监听到所有属性,但由于是ES6的语法,有兼容性问题。
MVVM和MVC
MVC模式分离了Model,View和Controller层,其中View负责页面的显示逻辑,Model层负责存储页面的数据以及进行对数据的操作,View与Model层应用了观察者模式,当Model层发生变化会通知View层更新页面。Controller层是View层与Model层的纽带,当用户与页面进行交互,Controller层中的事件触发器开始工作,通过调用Model层,完成对Model的修改,然后Model层再通知view层更新。
MVVM模式分为Model,View和ViewModel层:
- Model代表数据模型,数据和业务逻辑都在Model层定义
- View层代表视图,负责展示数据
- ViewModel层监听Model中数据的改变并控制视图的更新,处理用户交互 Model与ViewModel层之间存在着双向数据绑定,因此虽然Model与View层无直接关联,但通过ViewModel层,Model层的数据变化会引起View层视图刷新,View层用户交互改变的数据也会在Model层同步。
整理语言版
Vue中的MVVM模式包括model层数据层,view层视图层和viewModel层,viewModel层与view层、model层实现了双向数据绑定。ViewModel层做了两件事,一是通过DataBindings完成了Model到View层的映射,无需手动update view,二是通过DOM listeners完成view到Model的监听,这样model就会随view触发事件而改变。
computed 和 watch 的区别
- 功能上:computed是计算属性,watch是监听一个值的变化,然后执行相应的回调。
- 是否调用缓存:computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch在每次监听的值发生变化时都会执行回调。
- 是否调用return:computed中的函数必须要用return返回,watch不用
- computed默认第一次加载时就开始监听,而watch默认第一次加载不进行监听,如果需要,要加immediate:true
- 使用场景:computed---当一个属性收到多个属性影响时,如计算购物车商品总价。watch----当一条数据影响到多条数据时,如搜索框。
computed 和 methods 的区别
可以将同一函数定义为methods或者一个计算属性,于结果而言两者相同,不过对于逻辑较为繁杂的一般使用Methods
不同点:computed会基于他的依赖进行缓存,只有当它的依赖发生变化时,computed才会重新求值,否则会从缓存中调用函数。而methods调用时会始终执行该函数。
slot插槽
slot分为三类,默认插槽、具名插槽和作用域插槽。插槽slot是子组件的一个模板标签元素,这一标签是否显示、如何显示由父组件指定。
- 默认插槽:又名匿名插槽,当slot没有指定name属性时就为默认插槽,一个组件内只能有一个默认插槽(多个默认插槽无法分辨是哪个)
- 具名插槽,具有name属性的插槽,一个组件内可以有多个具名插槽。
- 作用域插槽,可以是默认插槽,也可以是具名插槽。作用域插槽的特点是子组件在渲染作用域插槽时,可以将子组件内部的数据传递给父组件,父组件就可以在模板内接收数组并决定如何渲染这个插槽
在Vue2.6.0中,具名插槽和作用域插槽的写法统一为v-slot,缩写为#
<span>
<slot>{{ user.lastName }}</slot>
</span>
//current-user组件
//父组件
<current-user>
{{ user.firstName }}
</current-user>
父组件是无法获取到user的,于是使用作用域插槽
<span>
<slot v-bind:user="user"> <!--这个被称为插槽props!-->
{{ user.lastName }}
</slot>
</span>
//父组件用v-slot绑定,我们选择将包含所有插槽 prop 的对象命名为 slotProps
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签内的内容,存放在vm.slot.default,具名插槽为vm.slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
过滤器的作用,如何实现过滤器
Vue中filters不会修改数据,而是会过滤数据,改变用户看到的输出。
使用场景:
- 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。
- 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用
fliters过滤器来处理数据。
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 {{ }} 和 v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。
例如,在显示金额,给商品价格添加单位:
<li>商品价格:{{item.price | filterPrice}}</li>
filters: {
filterPrice (price) {
return price ? ('¥' + price) : '--'
}
}
如何保存页面当前的状态
用keep-alive标签,当组件在keep-alive内被切换时组件的activated,deactivated这两个生命周期钩子函数会被执行,被包裹在keep-alive标签中的组件的状态会被保留
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
<input v-model="sth" />
// 等同于
<input
v-bind:value="message"
v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;
</kepp-alive>
v-if v-show v-html原理
- v-if:v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,所以不会被渲染。
- v-show:v-show会生成vnode,也会生成真实dom插入dom树中,但会被修改display属性。
- v-html:v-html就是将innerHTML设置为v-html的值。
- v-pre:不进行解析
为什么不建议v-if v-for连用
- v-for优先级高于v-if,每次都会进行v-if的判断
- v-if是局部编译卸载,与v-for配合使用性能消耗大。
常见的事件修饰符及其应用
- .stop: 等同于event.stopProppagation(),防止事件冒泡
- .prevent 等同于event.preventDefault(),阻止浏览器默认行为
- .capture 与事件冒泡方向相反,事件捕获由外到内
- .self 只触发自己范围内元素,不包含子元素
- .once 只触发一次 事件冒泡的应用在于给父元素添加事件,从而在子元素触发事件时通过事件冒泡调用到父元素身上的事件。
v-if v-show 区别
- 手段:v-if是动态的往DOM树内插入删除dom节点,v-show是控制dom元素的display样式来控制显隐。
- 编译过程:v-if切换有一个局部编译、卸载的过程。
- 编译条件:v-if是惰性的,如果初始条件为假,那么什么都不做,只有在条件第一次变为真时才开始局部编译;而v-show无论首次条件是否为真都会被编译,然后被缓存,而且dom元素保留
v-model是如何实现的,语法糖实际是什么
- 作用在表单元素上,动态绑定input的value指向message变量,并且在触发input事件时动态的将message设置为目标值。
<input v-model="message" />
// 等同于
<input
v-bind:value="message"
v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;
- 作用在组件上 在自定义组件中,v-model会默认利用名为value的prop和名为input的事件
本质是用prop和$emit实现
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
<base-checkbox v-model="lovingVue"></base-checkbox>
这两个组件具体交互是怎么样的呢:
- 首先将父组件转化为如下格式
<base-checkbox v-bind="lovingVue" v-on:input="lovingVue=$event"></base-checkbox>
父组件将lovingVue变量传入base-checkbox组件,如果不使用model选项使用的prop名为value(默认),一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
- base-checkbox子组件接收props,将其动态绑定到input的checked上,并向父组件传出名为change的事件,父组件将接收到的值赋值给lovingVue
总结来讲,v-model作用在组件上时,默认prop时value,event是input,model选项可以修改这个默认。
data为什么是一个函数而不是对象
Javascript中的对象是引用数据类型,当多个实例引用一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
而在Vue中,我们编写组件需要让每个组件的数据独立,即组件的复用性,我们需要让同一个组件在不同地方引用时有私有的数据空间,所以要写成函数返回值的模式。
对keepalive的理解
- keepalive组件能够缓存子组件,使其不经历销毁流程(即beforeDestroy, Destroyed),而是经历(activated, deactivate)。
- keepalive有三个配置项,分别是include,exclude和max,分别是缓存名单,不缓存名单和最大缓存个数。配合路由的meta进行使用。
- keepalive在各个生命周期内做的事:
created---初始化一个cache和keys,前者用来存放缓存的虚拟节点集合,后者用来缓存组件的key集合;mounted--- 检测include和exclude变化进行相应操作;destroyed:清除所有缓存相关的东西 - keepalive自身组件不会渲染到页面上。
$nextTick原理及作用
Vue中DOM更新是异步的,只要观察到数据变化,Vue将开启一个队列,并缓冲同一个事件循环中发生的所有数据改变并去重,然后在下一次事件循环中,Vue刷新队列并执行工作。
在Vue中,DOM的更新一般是在一轮事件循环中的最后执行,但当我们执行一个同步操作(如获取DOM)时,同步任务栈并未完成,异步任务队列也就没有被放进执行栈,这时我们就可以使用nextTick当中的回调函数会被放在下一次事件循环中执行,这样就保证了这个回调一定是在Dom更新完成之后执行的
使用场景:
- 当数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构时,这个操作的方法要在nextTick中。
<div id="app">
<p ref="myWidth" v-if="showMe">{{ message }}</p>
<button @click="getMyWidth">获取p元素宽度</button>
</div>
getMyWidth() {
this.showMe = true;
//this.message = this.$refs.myWidth.offsetWidth;
//报错 TypeError: this.$refs.myWidth is undefined
this.$nextTick(()=>{
//dom元素更新后执行,此时能拿到p元素的属性
this.message = this.$refs.myWidth.offsetWidth;
})
}
- 在vue生命周期中,如何要在created钩子里获取dom,需要放在nextTick里,watch同理。
- 立即更改某个数据后,获取DOM元素发现其数据并没有更改,此时是因为更新DOM的操作被放在了一个异步队列中,直到同一事件循环中所有数据变化完成,才会进行异步队列的处理。所以这本质上是一种优化策略。$nexttick返回的是一个Promise对象。
Vue中给data中的对象属性添加一个新属性会发生什么,如何解决
<template>
<div>
<ul>
<li v-for="value in obj" :key="value"> {{value}} </li>
</ul>
<button @click="addObjB">添加 obj.b</button>
</div>
</template>
<script>
export default {
data () {
return {
obj: {
a: 'obj.a'
}
}
},
methods: {
addObjB () {
this.obj.b = 'obj.b'
console.log(this.obj)
}
}
}
</script>
addObjB方法给obj添加了一个b属性,值为obj.b,这个添加显然是会生效的,但视图却不会刷新,因为在vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式属性,也就不会触发视图的更新,这时就需要用$set方法
addObjB(){
this.$set(this.obj,"b","obj.b")
Vue2响应式原理对于数组的处理
Vue2响应式原理的核心就是Object.defineProperty,而对于数组而言,如果把它当作对象,key为下标,value为元素来监听的话,在splice、reverse等方法就会多次触发getter和setter,这在数据量大时的性能消耗是巨大的。
于是Vue2重写了数组的七种方法,建立了一个对象,这个对象指向Array.prototype,首先这个对象可以通过原型链调用原始方法更改数组,其次这个对象会在调用这些方法的同时提示watcher更新,并对push unshift splice这些新增元素的方法进行判断,即对新增的元素进行响应式的处理。
单页面与多页面对比
概念:
- SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。
Vue data 中某一个属性值发生变化,视图会立即同步执行渲染吗
不会,因为Vue对DOM的更新是异步的,Vue会维护一个队列,然后监听到数据变化就将同一事件循环的这些数据进行缓冲,同时,如果同一个watcher被重复触发即数据被多次更改,只有最后一次更改会被推入缓冲队列中,然后在下一个事件循环tick中,Vue刷新队列并执行工作。
mixin extends 留坑
留坑
Vue自定义指令
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
一般使用自定义指令只用来操作DOM展示,不修改内部的值。下面举一个输入框的例子,当页面加载时,该元素将获得焦点 (注意:autofocus 在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
当然也可以局部注册
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
然后你可以在模板中任何元素上使用新的 v-focus property,如下:
<input v-focus>
子组件可以直接改变父组件的数据吗
不可以,因为Vue提倡单向数据流。每次父组件更新时,子组件的所有prop都会刷新为最新的值。只能通过$emit派发一个自定义事件,父组件收到后由父组件更改
assets和static的区别
相同点:assets和static两个都是存放静态资源文件。项目中所需要的资源图片文件,图标等都可以放在这两个文件夹下。
不同点:assets中存放的静态资源文件会经过打包上传,而static不会进行打包。
Vue项目的性能优化
(1) 编码
- 减少data中的数据,data中的数据都是响应式的,也就意味着data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不连用。v-if的条件变化会导致整个v-for内的dom插入删除。
- 采用keep-alive缓存组件
- 路由懒加载,异步加载组件
- 防抖、节流
- 按需引入第三方模块
- 长列表滚动到可使界面动态加载
- 图片懒加载
(2) 打包 - 生产环境下删除console.log,用uglifyjs-webpack-plugin
- 使用CDN加载第三方模块
留坑why - thread-load多线程打包
- splitChunks抽离公共文件 ?留坑
对SPA的理解
SPA就是单页面应用,他会web页面初始化的时候加载对应的html,css,js,当页面加载完成,SPA不会因为用户操作而重新加载资源,而是用路由机制实现HTML的变化。
hash路由
hash路由的本质是锚点(#)定位,可以总结出以下几个特点
- hash路由会触发网页跳转,即浏览器的前进后退
- hash可以改变url,但不会触发页面的重新加载(hash的改变记录在window.history)中,即不会刷新页面,不会重新加载,自然就不算是http请求。
- 锚点后面(#)的内容是不会提交到server端的。
history路由
history路由使用了h5提供的pushState(),replaceState方法实现浏览器的前进后退,通过history.state将任意类型的数据添加到记录中。用户在history模式下操作切换页面,虽然url会被改变,但浏览器不会刷新页面也不会往服务器发送请求,但会触发代码里的监听事件从而改变页面内容,所以无需向服务端发送请求。但如果此时用户刷新页面,浏览器发送给服务端的就是新的url,所以服务端要进行配置使得这些url返回同一个index.html
生命周期
生命周期
Vue实例有一个完整的生命周期,从开始创建,初始化数据,编译模板,挂载DOM到渲染,然后更新DOM,渲染,卸载等一系列流程。
beforeCreate: 数据观测和初始化事件还未开始,此时还未对data进行响应式化,watcher也就没有依赖数据,所以在这个data、computed等都不可访问。created: 实例创建完成,但DOM树未挂载,无法获取DOM元素,但data、methods、computed等都已经配置完成。beforeMount:在挂载前执行,实例已经完成编译模板,把data中的数据和模板生成html。此时还没有挂载html到页面上,render函数首次被调用生成虚拟dom。mounted:挂载完成,此时虚拟dom已经被转化为真实dom并插入到DOM树上。beforeUpdate:数据有更新时被调用updated:已经用diff算法用最小dom开支完成重新渲染dombeforeDestroy:实例销毁之前调用,此时实例仍然可用destroyed:实例销毁后调用,所有事件监听器会被移除,子实例也被销毁
另外keep-alive也有独特的生命周期,为activated和deactivated,用keep-alive包裹的组件在切换时不会销毁,而是缓存到内存中并执行deactivated钩子函数,命中缓存渲染后会执行activated钩子函数
生命周期详细版
- 首先我们把创建Vue实例到created这部分称为初始化阶段
- 判断是根组件还是子组件,如果是根组件就合并全局配置到vm.options上
- 初始化实例属性,如
$children,$refs,初始化事件监听、渲染函数等。 - 调用beforeCreate
- 初始化data, methods, computed, watch等
- 调用created函数
- 进入模板编译阶段
- 模板编译阶段在created和beforeMount之间
- 模板解析:将模板字符串通过正则转化为抽象语法书AST
- 优化:遍历AST,将其中的静态节点打上标记
- 将AST转换为渲染函数
- 进入挂载阶段
- 挂载阶段在beforeMount 和 Mounted间
- 调用beforeMount钩子
- 生成虚拟DOM,进行数据劫持,依赖收集。
- 将虚拟DOM渲染为真实DOM挂载到页面
- 调用Mounted函数
- 销毁阶段
- beforeDestroy
- 移除依赖,销毁子组件
- 销毁组件
Vue子组件和父组件执行顺序
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate created beforeMount mounted
- 父组件 mounted
- 父组件 beforeUpdate
- 子组件 beforeUpdate updated
- 父组件 updated
在哪个生命周期发送ajax请求
在created发送异步请求,因为在created发送异步请求能更快获取到服务端数据,减少页面加载时间。个人理解时beforeMount要生成虚拟dom,而且beforeMount之后要将虚拟dom转为真实dom插入到dom树上,所以排除beforeMount;而mounted要在子组件挂载完之后才能调用,所以排除mounted,而且mounted发送异步请求,由于此时DOM已经挂载,可能会造成抖动。
组件通信
props/ $emit
父组件向子组件传值用props,子组件向父组件传值用$emit绑定事件
// 父组件
<template>
<div class="section">
<com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
<p>{{currentIndex}}</p>
</div>
</template>
<script>
import comArticle from './test/article.vue'
export default {
name: 'comArticle',
components: { comArticle },
data() {
return {
currentIndex: -1,
articleList: ['红楼梦', '西游记', '三国演义']
}
},
methods: {
onEmitIndex(idx) {
this.currentIndex = idx
}
}
}
</script>
//子组件
<template>
<div>
<div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
</div>
</template>
<script>
export default {
props: ['articles'],
methods: {
emitIndex(index) {
this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
}
}
}
</script>
全局事件总线(on)
eventbus事件总线适合父子兄弟组件间通信,相当于将传输的数据存储在事件总线中,其他组件通过引入事件总线进行传输。
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
data(){
return{
num:0
}
},
methods:{
add(){
EventBus.$emit('addition', {
num:this.num++
})
}
}
}
</script>
<template>
<div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
}
}
</script>
依赖注入(provide/ inject)
适用于祖孙组件通信,即多层次的父子组件通信,无需多层props
provide/ inject是vue提供的两个钩子,与data,methods同级
provide用来发送数据或方法inject用来接收数据或方法
provide() {
return {
num: this.num
};
}
provide() {
return {
app: this
};
}
data() {
return {
num: 1
};
}
inject: ['app']
console.log(this.app.num)
也可以这样写,来访问父组件的所有属性
provide() {
return {
app: this
};
}
data() {
return {
num: 1
};
}
inject: ['app']
console.log(this.app.num)
ref/$refs
这个用在子组件上就可以访问子组件的实例
<template>
<child ref="child"></component-a>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
mounted () {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
}
</script>
$$parent$children
- 使用
$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法) - 使用
$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。
.sync v-model
两者作用相同,都是父子组件v-bind $emit的语法糖,.sync默认子组件emit的事件名为update:传递数据名,v-model默认子组件是接受value的prop,emit的是input,不过可以通过model属性配置。
vuex
$root
获取app.vue的数据
slot
作用域插槽
$attrs, $listeners
绑定在子组件的非prop属性会作为子组件根元素的css属性,可以通过inheritAttrs=false阻止这种继承,同时通过$attrs接收这些属性。它也可以绑定v-bind:"$attrs"在孙子组件上,孙子组件上就可以通过props接收。
组件通信总结
- 父子组件
- 父组件v-bind动态绑定数据给子组件传输,子组件用
props接收;子组件用this.$emit传递事件名和参数,父组件用v-on绑定事件接收 - 深层次的可以用
provide,inject, 祖先组件用provide,写法类似data,子孙组件用inject,写法类似props - $parent和$$children
- 父组件用$refs访问子组件实例
- 兄弟组件
eventbus,本质相当于创建了一个空的vue实例,其他组件引入这个实例,在这个实例上传递和监听事件。
为什么动态绑定的图片地址需要用require
因为不使用require,地址不会进行编译。首先我们知道,如果使用静态指定图片地址,进行编译后,图片指定的地址会被改变。
// vue文件中静态的引入一张图片
<template>
<div class="home">
<!-- 直接引入图片静态地址, 不再使用v-bind -->
<img src="../assets/logo.png" alt="logo">
</div>
</template>
//最终编译的结果
//这张图片是可以被正确打开的
<img src="/img/logo.6c137b82.png" alt="logo">
为什么data要是函数
当组件实例化时,如果data是对象,那么所有组件的实例就会指向同一个对象,那么一个实例数据的更改就会污染其他实例。data如果是函数,那么每次组件实例化都会调用这个函数返回新的对象。