1. Vue的优点?Vue的缺点?
优点:渐进式,组件化,轻量级,虚拟dom,响应式,单页面路由,数据与视图分开(MVVM模型)
缺点:单页面不利于seo,不支持IE8以下,首屏加载时间长
2. MVVM是什么?和MVC有何区别呢?
MVC
- Model(模型):负责从数据库中取数据
- View(视图):负责展示数据的地方
- Controller(控制器):用户交互的地方,例如
点击事件
等等 - 思想:Controller将Model的数据展示在View上
MVVM
- VM:也就是View-Model,做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
- 思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
区别
整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素
。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性
Vue是不是MVVM框架?
Vue是MVVM框架,但是不是严格符合MVVM,因为MVVM规定Model和View不能直接通信,而Vue的ref
可以做到这点
3. Vue中使用过哪些Vue的修饰符呢?
//前端分页插件 Pagination 的开发
<el-pagination
:total="total"
:page-size.sync="pageSize"
v-bind="$attrs"/>
.sync (vue中已弃用,用v-model代替)
.sync修饰符可以实现子组件与父组件的双向绑定,并且可以实现子组件同步修改父组件的值。
<body>
<div id="app">
{{dataApp}}
<hr>
//引用多个组件的时候要使用双标签,</> 这种形式只能显示一个
<Child :money.sync="dataApp"> </Child>
<!-- 两句作用等同 -->
<Child :money="dataApp" v-on:update:money="dataApp = $event"> </Child>
</div>
</body>
<script>
Vue.component('Child', {
props: ['money'],
template: `
<div class="child">
{{ money }}
<button @click="$emit('update:money', money-100)"> 点击
</button>
</div >`
})
var vm = new Vue({
el: '#app',
data: {
dataApp: 10000
},
})
</script>
//这里注意我们的事件名称被换成了update:money
//update:是被固定的,是vue约定好的名称部分
Vue中的‘:’和‘@’
- :src="url" 是 v-bind:src = "url"的缩写
- @click = "something" 是 v-on:click = "something" 的缩写
v-model也可实现双向绑定
例如官方文档给出:
<input v-model="something">
这不过是以下示例的语法糖:
<input v-bind:value="something" v-on:input="something = $event.target.value">
这就解释了为什么在input元素使用v-model绑定数据后,可以实现双向绑定,因为输入的时候会触发元素的input事件。
而通过这个语法糖,我们可以轻易的实现子组件和父组件数据的双向绑定,下面用代码演示。
父组件
<template>
<div id="app">
<img src="./assets/logo.png">
<!--<router-view/>-->
<car v-model="index"></car>
<div>{{index}}</div>
</div>
</template>
<script>
import Car from "./car.vue"
export default {
data(){
return{
index:0
}
},
name: 'App',
components:{Car}
}
</script>
子组件
<template>
<div @click="$emit('input',value+1)">汽车</div>
</template>
<script>
export default{
props:["value"]
}
</script>
父组件通过v-model绑定index到子组件上,而通过v-model绑定的数据在子组件中默认的key是value,所以在props上用value取值,最后通过点击事件执行emit上触发的事件是input
前面我们说过v-model绑定的事件是input,从而在子组件上触发了父组件的input事件,而在触发事件的时候可以传值,所以就实现了父子组件数据双向绑定,如果用的是v-bind,还需要自己去定义事情,所以使用v-model可以减少代码量。
和sync修饰符一样可以实现双向绑定,在原理上相差无几
4. 使用过哪些Vue的内部指令呢?
5. 组件之间的传值方式有哪些?
常见使用场景可以分为三类:
- 父子组件通信:
props
;$parent
/$children
;provide
/inject
;ref
;$attrs
/$listeners
/ .sync - 兄弟组件通信:
eventBus
(事件总线) ; vuex - 跨级通信:
eventBus
;Vuex;provide
/inject
、$attrs
/$listeners
跨级通信时 通过$attrs 可以将用不到的属性 传给后代 下例中的v2,v3
事件监听的透传可以通过 v-on="$listeners" 实现
<body>
<div id="app">
<father :v1="'value1'" :v2="'value2'" :v3="'value3'"></father>
</div>
</body>
<script>
Vue.component('father', {
// inheritAttrs: false,
props: ['v1'],
template: `
<div>
<p>v1 is {{v1}}</p>
<son v-bind='$attrs'></son>
</div>
`
})
Vue.component('son', {
props: ['v2'],
template: "<div ><p>v2 is {{v2}}</p><grandSon v-bind='$attrs'></grandSon></div>"
})
Vue.component('grandSon', {
props: ['v3'],
template: "<p>v3 is {{v3}}</p>"
})
let vm = new Vue({
el: '#app',
data: {
},
})
</script>
页面展示:
v1 is value1
v2 is value2
v3 is value3
6. Vue 中的 key 到底有什么用?
key 是给每一个 vnode 的唯一 id,依靠 key凭借diff算法可以更准确的更快速的更新dom操作,
当数据改变时,会触发setter
,并且通过Dep.notify
去通知所有订阅者Watcher
,订阅者们就会调用patch方法
为什么需要虚拟dom?
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。例如,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。
举个例子,请看以下真实DOM
:
<ul id="list">
<li class="item">哈哈</li>
<li class="item">呵呵</li>
<li class="item">嘿嘿</li>
</ul>
复制代码
对应的虚拟DOM
为:
let oldVDOM = { // 旧虚拟DOM
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, children: ['哈哈']
},
{
tagName: 'li', props: { class: 'item' }, children: ['呵呵']
},
{
tagName: 'li', props: { class: 'item' }, children: ['嘿嘿']
},
]
}
注意:平常v-for循环渲染的时候,不建议用index作为循环项的key,会导致不需要更新的节点更新
Diff 算法:
仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新(广度优先算法)
7. 谈一谈 nextTick 的原理?
Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。
举个例子
当设置vm.message = 'changed',虽然视图看上去是即时更新,但实际上从代码角度获取变更后的值的话,该组件的值并不会立即更新。
这里就涉及到Vue
中对DOM
的更新策略了,Vue
在更新 DOM
时是异步执行的。只要侦听到数据变化,Vue
将开启一个事件队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher
被多次触发,只会被推入到事件队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM
操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue
刷新事件队列并执行实际 (已去重的) 工作。
在上面这个例子中,当我们通过 vm.message = ‘new message‘
更新数据时,此时该组件不会立即重新渲染。当刷新事件队列时,组件会在下一个事件循环“tick”中重新渲染。所以当我们更新完数据后,此时又想基于更新后的 DOM
状态来做点什么,此时我们就需要使用Vue.nextTick(callback)
,把基于更新后的DOM
状态所需要的操作放入回调函数callback
中,这样回调函数将在 DOM
更新完成后被调用。
总结一下就是 $nextTick
将回调函数放到微任务或者宏任务当中以延迟它地执行顺序
OK,现在大家应该对nextTick
是什么,那么问题又来了,Vue
为什么要这么设计?为什么要异步更新DOM
?这就涉及到另外一个知识:JS
的运行机制
8. 谈一谈Vue-router 中hash模式和history模式的区别?
vue-router
默认 hash 模式,在通过vue-cli创建项目的时候,会出现 Use history mode for router? 也就是再问你:是不是用history模式来创建路由
hash模式和history模式的不同
最直观的区别就是在url中 hash
带了一个很丑的 # ,而history
是没有#的
对于vue这类渐进式前端开发框架,为了构建 SPA(单页面应用),需要引入前端路由系统,这也就是 Vue-Router 存在的意义。前端路由的核心,就在于 —— 改变视图的同时不会向后端发出请求。
为了达到这一目的,浏览器当前提供了以下两种支持:
- hash —— 即地址栏 URL 中的 # 符号(此 hash 不是密码学里的散列运算)。比如这个 URL:www.abc.com/#/hello hash 的值为 #/hello。
它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
- history —— 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
因此可以说,hash 模式和 history 模式都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由。
使用场景
一般场景下,hash 和 history 都可以,除非你更在意颜值,# 符号夹杂在 URL 里看起来确实有些不太美丽。
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成URL 跳转而无须重新加载页面。
另外,根据 Mozilla Develop Network 的介绍,调用 history.pushState() 相比于直接修改 hash,存在以下优势:
-
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
-
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
-
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
-
- pushState() 可额外设置 title 属性供后续使用。
当然啦,history 也不是样样都好。SPA 虽然在浏览器里游刃有余,但真要通过 URL 向后端发起 HTTP 请求时,两者的差异就来了。尤其在用户手动输入 URL 后回车,或者刷新(重启)浏览器的时候。
总结
1 hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 www.abc.com 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
2 history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如www.abc.com/book/id 如果后端缺少对 /book/id 的路由处理,将返回 404 错误。Vue-Router,官网里如此描述:
“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖 所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个 页面就是你 app 依赖的页面。”
后端用nginx
配置的例子:
location / {
try_files $uri $uri/ /index.html;
}
nginx配置详解:www.cnblogs.com/jedi1995/p/…
3 结合自身例子,对于一般的 Vue + Vue-Router + Webpack + XXX 形式的 Web 开发场景,用 history 模式即可,只需在后端(Apache 或 Nginx)进行简单的路由配置,同时搭配前端路由的 404 页面支持。
9. 在vue中如何更新数组?
数组原生方法
Array.prototype.splice
被称为数组最强大的方法,具有删除、增加、替换功能,可以用 splice来更新
为什么 splice 可以触发更新?
Vue将被侦听的数组(这里就是list)的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
- splice()
不再是数组原生方法了,而是 Vue重写过的方法
官方 API Vue.set()
Vue.set是官方提供的全局 API,别名 vm.$set,用来主动触发响应
其实set方法本质上还是调用的splice方法来触发响应,
vm.$forceUpdate()
强制使 Vue实例重新渲染,调用 vm.$forceUpdate() 可以强制渲染
通常你应该避免使用这个方法,而是通过数据驱动的正常方法来操作
当你无路可走的时候,可以试试这个方法,但此方法不可滥用,想想你只是想更改某个数组项,但是却可能更新了整个组件
正如官网所说的:
如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。
深拷贝
一般粗暴的都是通过 序列化然后反序列化 回来 来实现
可能你还会封装自己的 cloneDeep方法,虽然也能触发响应,但是仅仅是更新某一项就要用到深拷贝,确实有点别扭
map()
map是数组的原生方法,用来做数组映射,类似的非变更方法(不会改变原数组)还有 slice、concat、filter这些,它们不会变更原始数组,而总是返回一个新数组那么在 Vue中我们直接替换数组也是可以实现更新的
你可能认为这将导致 Vue丢弃现有 DOM并重新渲染整个列表。幸运的是,Vue做的够多。
Vue为了使得 DOM元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。还记得在模板中使用 v-for必须要提供 key吗,Vue根据这个 key值能够更高效的找出差异,精准定位并更新。
实际上,这种基于源数据生成新数据(同时不影响外部)的方式符合函数式编程的思想,如果你用过 redux,那么在写 reducer时会经常用到 map、filter这些
数组项为对象的情况
在开发中可能会遇到 this.list[1].name = 'jerry'这种情况,如果数组项是对象,那么是可以通过下标直接更新这个对象的属性其实是 Vue在初始化数组的时候做了处理,对于数组项是非对象会直接返回,不做操作,如果是对象,那么会用 Observer类初始化它,给对象属性加上 getter、setter监听器
Observer类初始化时,如果是数组,一般情况下会调用protoAugment()、observeArray()。protoAugment会将value(数组) 的__proto__指向arrayMethods,这里着重看observeArray,它会在每个数组项调用observe(),observe
小结
可以看到,如果数组项不是对象,会直接返回;数组项为对象,会对该对象继续进行
Observer初始化,进而调用 walk(),对每个属性调用 defineReactive(),defineReactive会通过Object.defineProperty给属性加上 getter、setter监听器,所以给数组项重新赋值就会触发响应了关于为什么 Vue没有将 arr[index] = val变成响应式,网上有很多讨论,作者也有回答,大体来说,就是 性能代价和获得的用户体验不成正比。 arr[index] = val虽然不是响应式,但也有提供的官方 API来操作,作为一个框架,Vue已经做的够多了。当然Vue3将Object.defineProperty换成了 Proxy,那么这个问题也就不复存在了。
详细参考:baijiahao.baidu.com/s?id=168350…
10.Vue 组件 data 为什么必须是函数?
new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。
所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
11.谈谈 Vue 事件机制,手写$on
,$off
,$emit
,$once
?
Vue 事件机制 本质上就是 一个 观察者模式
模式的实现。
class Vue {
constructor() {
// 事件通道调度中心
this._events = Object.create(null);
}
$on(event, fn) {
if (Array.isArray(event)) {
event.map(item => {
this.$on(item, fn);
});
} else {
(this._events[event] || (this._events[event] = [])).push(fn);
}
return this;
}
$once(event, fn) {
function on() {
this.$off(event, on);
fn.apply(this, arguments);
}
on.fn = fn;
this.$on(event, on);
return this;
}
$off(event, fn) {
if (!arguments.length) {
this._events = Object.create(null);
return this;
}
if (Array.isArray(event)) {
event.map(item => {
this.$off(item, fn);
});
return this;
}
const cbs = this._events[event];
if (!cbs) {
return this;
}
if (!fn) {
this._events[event] = null;
return this;
}
let cb;
let i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break;
}
}
return this;
}
$emit(event) {
let cbs = this._events[event];
if (cbs) {
const args = [].slice.call(arguments, 1);
cbs.map(item => {
args ? item.apply(this, args) : item.call(this);
});
}
return this;
}
}
12.Vue项目 Tab切换以及缓存页面的处理
vue通过 router-link(默认渲染成带有正确链接的 <a>
标签)的方式,实现Tab切换
<template>
<menu class="left"/>//menu代码部分如上
<div class="right">
<tab-list>
<tab-item v-for="(item,index) in tabList" :key="index">
<router-link :to="item.path">{{item.name}}</router-link>
<icon class="delete" @click="deleteTab"></icon>
</tab-item>
</tab-list>
<page-view>
<router-view></router-view>//这里是页面展示
</page-view>
</div>
</template>
ps:<router-view>
组件是一个 functional 组件,渲染路径匹配到的视图组件。<router-view>
渲染的组件还可以内嵌自己的 <router-view>
,根据嵌套路径,渲染嵌套组件。
其他属性 (非 router-view 使用的属性) 都直接传给渲染的组件, 很多时候,每个路由的数据都是包含在路由参数中。
因为它也是个组件,所以可以配合 <transition>
和 <keep-alive>
使用。如果两个结合一起用,要确保在内层使用 <keep-alive>
:
<transition>
<keep-alive>
<router-view></router-view>
</keep-alive>
</transition>
相同的路由组件如何重新渲染?
开发人员经常遇到的情况是,多个路由解析为同一个Vue组件。问题是,Vue出于性能原因,默认情况下共享组件将不会重新渲染,如果你尝试在使用相同组件的路由之间进行切换,则不会发生任何变化。
const routes = [
{
path: /a,
component: MyComponent
},
{
path: /b,
component: MyComponent
},
];
如果依然想重新渲染,怎么办呢?可以使用
key
<template>
<router-view :key=$route.path></router-view>
</template>
仅仅是做tab切换,远远是不够的,毕竟大家想要tab页就是要来回切换操作,我们需要保存他在不同tab里操作的进度,比如说填写的表单信息,或者已经查询好的数据列表等。
那么我们要怎么缓存组件呢?
只需要用到vue中的keep-alive组件
keep-alive
- 是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
- 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
- 与 相似,只是一个抽象组件,它不会在DOM树中渲染(真实或者虚拟都不会),也不在父组件链中存在,比如:你永远在 this.$parent 中找不到 keep-alive 。
注:不能使用keep-alive来缓存固定组件,会无效
//无效
<keep-alive>
<my-component></my-component>
</keep-alive>
使用老版本vue 2.1之前的使用
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
需要在路由信息里面设置router的元信息meta
export default new Router({
routes: [
{
path: '/a',
name: 'A',
component: A,
meta: {
keepAlive: false // 不需要缓存
}
},
{
path: '/b',
name: 'B',
component: B,
meta: {
keepAlive: true // 需要被缓存
}
}
]
})
比较新而且简单的用法
- 直接缓存所有组件/路由
<keep-alive>
<router-view/>
</keep-alive>
<keep-alive>
<component :is="view"></component>
</keep-alive>
- 使用include来处理需要缓存的组件/路由
include有几种用法,可以是数组,字符串用标点隔开,也可以是正则
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
</transition>
- 使用exclude来排除不需要缓存的路由
跟include正好相反,在exclude里的组件不会被缓存。用法类似,不作赘述
一种比较奇怪的情况
当页面跳转方式有A->C和B->C两种,但是我们从A到C的时候,不需要缓存,从B到C的时候需要缓存。这时候就要用到路由的钩子结合老版本用法来实现了。
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
to.meta.keepAlive = false; // 让下一页不缓存
next();
}
};
export default {
data() {
return {};
},
methods: {},
//组件的生命周期,不同于beforeEach钩子这类属于全局路由的钩子函数(所有路由开始的时候最先执行)
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = true; //下一页缓存
next();
}
};
缓存组件的生命周期函数
缓存组件第一次打开的时候,和普通组件一样,也需要执行created, mounted等函数。
但是在被再次激活和被停用时,这几个普通组件的生命周期函数都不会执行,会执行两个比较独特的生命周期函数。
- activated
这个会在缓存的组件重新激活时调用
- deactivated
这个会在缓存的组件停用时调用
13.Vue跟React的区别?
相同点:
- 1.都使用了虚拟dom
- 2.组件化开发
- 3.都是单向数据流(父子组件之间,不建议子修改父传下来的数据)
- 4.都支持服务端渲染
不同点:
- 1.React的JSX,Vue的template
- 2.数据变化,React手动(setState),Vue自动(初始化已响应式处理,Object.defineProperty)
- 3.React的Redux,Vue的Vuex
ps:对于视图的描述这件事 react 和 vue 用了不同的方案,react 是给 js 扩展了 jsx 的语法,由 babel 实现,可以在描述视图的时候直接用 js 来写逻辑,没啥新语法。而 vue 是实现了一套 template 的 DSL,引入了插值、指令、过滤器等模版语法,相对于 jsx 来说更简洁,template 的编译器由 vue 实现。
但是 vue template 也不全是好处,因为和 js 上下文割裂开来,引入 typescript 做类型推导的时候就比较困难,需要单独把所有 prop、method、data 的类型声明一遍才行。而 react 的 jsx 本来就是和 js 同一个上下文,结合 typescript 就很自然。
14. computed和watch有何区别?
- 1.
computed
是依赖已有的变量来计算一个目标变量,大多数情况都是多个变量
凑在一起计算出一个变量
,并且computed
具有缓存机制
,依赖值不变的情况下其会直接读取缓存进行复用,computed
不能进行异步操作
(因为computed 需要 return,而 watch 不需要。根据事件循环机制,return属于同步执行) - 2.
watch
是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量
的变化决定多个变量
的变化,watch
可以进行异步操作
- 3.简单记就是:一般情况下
computed
是多对一
,watch
是一对多
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
},
watch: {
firstName(newval,oldval) {
console.log(newval)
console.log(oldval)
}
}
在Vue的 template模板内({{}})是可以写一些简单的js表达式的很便利,如上直接计算 {{this.firstName + ’ ’ + this.lastName}},因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷
也正是用于解决此类问题。
watche 更像是一个 data 的数据监听回调,当依赖的 data 的数据变化,执行回调,在方法中会传入 newVal 和 oldVal。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。如果你需要在某个数据变化时做一些事情,使用watch。 如果一个数据依赖于其他数据,那么把这个数据设计为computed 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化
ps:computed如何实现传参?
// html
<div>{{ total(3) }}
// js
computed: {
total() {
return function(n) {
return n * this.num
}
},
}
计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
template
<p>{{ calculateBooksMessage() }}</p>
js
// 组件中
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter 函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now()
并不是一个响应式依赖:
js
computed: {
now() {
return Date.now()
}
}
相比之下,方法调用总是会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list
,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list
。没有缓存的话,我们会重复执行非常多次 list
的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
15. vue生命周期
activated:指keep-alive缓存的组件激活时,deactiveted同理
父子组件生命周期顺序
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
Vue 获取初始化数据是放在created还是mounted
官方文档:
也就是说Vue实例中的data和methods已经可以使用了。但是还没有挂载到页面上。可以在created里面去后端获取数据。
当进入mounted这个生命周期函数。此时页面已经渲染出来,可以进行dom操作。
总而言之。created生命周期函数,可以去后端异步获取数据,并保存到data里面。
mounted的话,如果需要在页面上操作dom,就要在这个函数执行。但是注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,那就需要使用到vm.$nextTick
Vue中在哪个生命周期内调用异步请求?(直观)
一般来说,可以在,created,mounted中都可以发送数据请求,但是,大部分时候,会在created发送请求。
Created的使用场景:如果页面首次渲染的就来自后端数据。因为,此时data已经挂载到vue实例了。
在 created(如果希望首次选的数据来自于后端,就在此处发请求)(只发了异步请求,渲染是在后端响应之后才进行的)、beforeMount、mounted(在mounted中发请求会进行二次渲染) 这三个钩子函数中进行调用。
因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是最常用的是在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求
有两个优点:
第一点:能更快获取到服务端数据,减少页面 loading 时间;
第二点:放在 created 中有助于一致性,因为ssr 不支持 beforeMount 、mounted 钩子函数。
Vue中的computed是在生命周期的哪个阶段执行的
在new Vue()
的时候,vue\src\core\instance\index.js
里面的_init()
初始化各个功能
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)
}
在_init()
中有这样的一个执行顺序:其中initState()
是在beforeCreate
和created
之间
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) //初始化
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
在initState()做了这些事情:
if (opts.props) initProps(vm, opts.props)//初始化Props
if (opts.methods) initMethods(vm, opts.methods)//初始化methods
if (opts.data) {
initData(vm)} else {
observe(vm._data = {}, true /* asRootData */)}//初始化data
if (opts.computed) initComputed(vm, opts.computed)//初始化computed
所以Props
,methods
,data
和computed
的初始化都是在beforeCreated
和created
之间完成的。
16. 为什么v-if和v-for不建议用在同一标签?
在Vue2中,v-for
优先级是高于v-if
的,咱们来看例子
<div v-for=item in [1, 2, 3, 4, 5, 6, 7] v-if=item !== 3>
{{item}}
</div>
上面的写法是v-for
和v-if
同时存在,会先把7个元素都遍历出来,然后再一个个判断是否为3,并把3给隐藏掉,这样的坏处就是,渲染了无用的3节点,增加无用的dom操作,建议使用computed来解决这个问题:
<div v-for=item in list>
{{item}}
</div>
computed() {
list() {
return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
}
}
17.如何将获取data中某一个数据的初始状态?
在开发中,有时候需要拿初始状态去计算。例如
data() {
return {
num: 10
},
mounted() {
this.num = 1000
},
methods: {
howMuch() {
// 计算出num增加了多少,那就是1000 - 初始值
// 可以通过this.$options.data().xxx来获取初始值
console.log(1000 - this.$options.data().num)
}
}
官方:用于当前 Vue 实例的初始化选项。需要在选项中包含自定义 property 时会有用处
new Vue({
customOption: 'foo',
created: function () {
console.log(this.$options.customOption) // => 'foo'
}
})
18.Vue的el属性和 $mount优先级?
比如下面这种情况,Vue会渲染到哪个节点上
new Vue({
router,
store,
el: '#app',
render: h => h(App)
}).$mount('#king')
这是官方的一张图,可以看出
el
和$mount
同时存在时,el优先级
>$mount
19.vue的hook的使用
从语法上来看,就是组件内的生命周期函数在执行结束后会 $emit 一个 hook + 生命周期名字 的自定义事件。
这样的话,我们就可以在一个生命周期方法里监听其余生命周期的发生。一些情况下确实可以达到优化或简化代码的效果。比如到多个生命周期的逻辑都非常简单,简单到只有一句时,这个时候就可以考虑将这些代码放在 created 或者 mounted 里通过这种方式来监听并执行那些简单逻辑。上面的是在组件内的使用。在组件外的使用同样简单。
同一组件中使用
这是我们常用的使用定时器的方式
export default{
data(){
timer:null
},
mounted(){
this.timer = setInterval(()=>{
//具体执行内容
console.log('1');
},1000);
}
beforeDestory(){
clearInterval(this.timer);
this.timer = null;
}
}
上面做法不好的地方在于:得全局多定义一个timer变量,可以使用hook这么做:
export default{
methods:{
fn(){
const timer = setInterval(()=>{
//具体执行代码
console.log('1');
},1000);
this.$once('hook:beforeDestroy',()=>{
clearInterval(timer);
timer = null;
})
}
}
}
父子组件使用
如果子组件需要在mounted时触发父组件的某一个函数,平时都会这么写:
//父组件
<rl-child @childMounted=childMountedHandle/>
method () {
childMountedHandle() {
// do something...
}
},
// 子组件
mounted () {
this.$emit('childMounted')
},
使用hook的话可以更方便:
//父组件
<rl-child @hook:mounted=childMountedHandle/>
method () {
childMountedHandle() {
// do something...
}
},
20.不需要响应式的数据应该怎么处理?
方法一:将数据定义在data之外
data () {
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}
方法二:Object.freeze()
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
}
}
21.vuex的有哪些属性?用处是什么?
- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
- Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
vuex 中dispatch 和 commit 的用法和区别
dispatch:含有异步操作,例如向后台提交数据,写法: this.$store.dispatch('action方法名',值)
commit:同步操作,写法:this.$store.commit('mutations方法名',值)
区别:
1.Action提交的是mutation,而不是直接变更状态,可以包含任意的异步操作。
2.dispatch推送一个action。
1.dispatch 异步操作 this.store.dispatch('action的方法',arg),调用actions里的方法。
2.commit同步操作this.store.commit('mutations的方法', arg),调用mutations里的方法。
双向绑定和 vuex 是否冲突?
会存在冲突
当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用 v-model 会比较棘手:
假设这里的 obj 是在计算属性中返回的一个属于 Vuex store 的对象,在用户输入时, v-model 会试图
直接修改 obj.message 。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一
个错误。
所以不要使用v-model 使用绑定事件的方式调用commit去解决
22.Vue slot 和 slot-scope的作用?
默认插槽和具名插槽 slot:
插槽本质上是为了动态生成HTML。 如果只是数据是动态的,用普通的数据绑定就可以, 如果不仅数据是动态的,HTML也是动态的,数据绑定就不行了,因为HTML语法会混在JS语法里面,导致语法错误。 用插槽其实就是解决这个问题。
默认插槽
<template>
<div>
<h3>这是父组件</h3>
<son>实践slot</son>
</div>
</template>
<template>
<div>
<h4>这是子组件</h4>
<input type="text" placeholder="请输入">
<slot></slot>
</div>
</template>
具名插槽
<template>
<div>
<h3>这是父组件</h3>
<son><span>实践slot</span></son>
<son>
<template slot="myslot">
<div>实践具名slot</div>
</template>
</son>
</div>
</template>
<template>
<div>
<h4>这是子组件</h4>
<input type="text" placeholder="请输入">
<slot></slot>
<slot name="myslot"></slot>
</div>
</template>
作用域插槽 slot-scope:
官网中有一句特别强调的话:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。简单的所,就是父组件中不能直接用自组件中定义的data数据。而slot-scope的出现就是解决了这样的问题。如下代码
父组件
<template lang="">
<div>
<h3>这是父组件</h3>
<son>
<template slot="myslot" slot-scope="scope">
<ul>
<li v-for="item in scope.data">{{item}}</li>
</ul>
</template>
</son>
</div>
</template>
子组件
<template>
<div>
<h4>这是子组件</h4>
<input type="text" placeholder="请输入">
<slot name="myslot" :data='list'></slot>
</div>
</template>
<script>
export default {
name:'Son',
data(){
return{
list:[
{name:"Tom",age:15},
{name:"Jim",age:25},
{name:"Tony",age:13}
]
}
}
}
</script>
在子组件中的插槽上有一句data="list",而在父组件中也有slot-scope="scope",slot-scope就是取data的值,slot-scope的值是自定义的,可以取任何名称,但data的值传过来时是以对象形式传输的,所以scope.data才是list的值。\
效果如图: