Appearance
Vue2篇
1. 说说你对vue的理解
1.1 vue是什么
Vue.js(/vjuː/,或简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。
1.2 vue的核心特性
- 数据驱动(MVVM)
MVVM表示的是 Model-View-ViewModel
Model:模型层,负责处理业务逻辑以及和服务器端进行交互。
View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面。
ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁。
- 组件化
什么是组件化一句话来说就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件。
组件化的优势:
降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现。
调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单。
提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级。
- 指令系统
解释:指令 (Directives) 是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
常用的指令:
条件渲染指令
v-if列表渲染指令
v-for属性绑定指令
v-bind事件绑定指令
v-on双向数据绑定指令
v-model
2. v-show和v-if有什么区别?使用场景分别是什么?
2.1 v-show与v-if的共同点
我们都知道在 vue 中 v-show 与 v-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示
在用法上也是相同的
vue
<Model v-show="isShow" />
<Model v-if="isShow" />当表达式为
true的时候,都会占据页面的位置当表达式都为
false时,都不会占据页面位置
2.2 v-show与v-if的区别
控制手段不同
编译过程不同
编译条件不同
控制手段:v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
v-show由false变为true的时候不会触发组件的生命周期v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法
性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
2.3 v-show与v-if的使用场景
v-if 与 v-show 都能控制dom元素在页面的显示v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)
如果需要非常频繁地切换,则使用 v-show 较好
如果在运行时条件很少改变,则使用 v-if 较好
3. 请描述下你对vue生命周期的理解?在created和mounted这两个生命周期中请求数据有什么区别呢?
3.1 生命周期是什么
生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程在Vue中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程我们可以把组件比喻成工厂里面的一条流水线,每个工人(生命周期)站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作PS:在Vue生命周期钩子会自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())
3.2 生命周期有哪些
Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期
| 生命周期 | 描述 |
|---|---|
| beforeCreate | 组件实例被创建之初 |
| created | 组件实例已经完全创建 |
| beforeMount | 组件挂载之前 |
| mounted | 组件挂载到实例上去之后 |
| beforeUpdate | 组件数据发生变化,更新之前 |
| updated | 组件数据更新之后 |
| beforeDestroy | 组件实例销毁之前 |
| destroyed | 组件实例销毁之后 |
| activated | keep-alive 缓存的组件激活时 |
| deactivated | keep-alive 缓存的组件停用时调用 |
| errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
3.3 生命周期整体流程
Vue生命周期流程图

具体分析
beforeCreate -> created
- 初始化
vue实例,进行数据观测
created
完成数据观测,属性与方法的运算,
watch、event事件回调的配置可调用
methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computed和watch完成数据计算此时
vm.$el并没有被创建
created -> beforeMount
判断是否存在
el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译优先级:
render>template>outerHTMLvm.el获取到的是挂载DOM的
beforeMount
在此阶段可获取到
vm.el此阶段
vm.el虽已完成DOM初始化,但并未挂载在el选项上
beforeMount -> mounted
- 此阶段
vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM
mounted
vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM
beforeUpdate
更新的数据必须是被渲染在模板上的(
el、template、render之一)此时
view层还未更新若在
beforeUpdate中再次修改数据,不会再次触发更新方法
updated
完成
view层的更新若在
updated中再次修改数据,会再次触发更新方法(beforeUpdate、updated)
beforeDestroy
- 实例被销毁前调用,此时实例属性与方法仍可访问
destroyed
完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
并不能清除DOM,仅仅销毁实例
使用场景分析
| 生命周期 | 描述 |
|---|---|
| beforeCreate | 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务 |
| created | 组件初始化完毕,各种数据可以使用,常用于异步数据获取 |
| beforeMount | 未执行渲染、更新,dom未创建 |
| mounted | 初始化结束,dom已创建,可用于获取访问数据和dom元素 |
| beforeUpdate | 更新前,可用于获取更新前各种状态 |
| updated | 更新后,所有状态已是最新 |
| beforeDestroy | 销毁前,可用于一些定时器或订阅的取消 |
| destroyed | 组件已销毁,作用同上 |
3.4 题外话:数据请求在created和mouted的区别
created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成mounted是在页面dom节点渲染完毕之后就立刻执行的触发时机上created是比mounted要更早的。 两者相同点:都能拿到实例对象的属性和方法讨论这个问题本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况建议:放在create生命周期当中
4. v-if和v-for的优先级是什么?
4.1 作用
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名在 v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化
两者在用法上
vue
<Modal v-if="isShow" />
<li v-for="item in items" :key="item.id">
{{ item.label }}
</li>4.2 优先级
v-if与v-for都是vue模板系统中的指令在vue模板编译的时候,会将指令系统转化成可执行的render函数
示例
编写一个p标签,同时使用v-if与 v-for
vue
<div id="app">
<p v-if="isShow" v-for="item in items">
{{ item.title }}
</p>
</div>创建vue实例,存放isShow与items数据
js
const app = new Vue({
el: '#app',
data() {
return {
items: [
{ title: 'foo' },
{ title: 'baz' }
]
}
},
computed: {
isShow() {
return this.items && this.items.length > 0
}
}
})模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数
js
ƒ anonymous() {
with (this) { return
_c('div', { attrs: { "id": "app" } },
_l((items), function (item)
{ return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) }
}_l是vue的列表渲染函数,函数内部都会进行一次if判断初步得到结论:v-for优先级是比v-if高再将v-for与v-if置于不同标签
vue
<div id="app">
<template v-if="isShow">
<p v-for="item in items">{{item.title}}</p>
</template>
</div>再输出下render函数
js
ƒ anonymous() {
with(this){return
_c('div',{attrs:{"id":"app"}},
[(isShow)?[_v("\n"),
_l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}这时候我们可以看到,v-for与v-if作用在不同标签时候,是先进行判断,再进行列表的渲染我们再在查看下vue源码源码位置:\vue-dev\src\compiler\codegen\index.js
js
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
...
}在进行if判断的时候,v-for是比v-if先进行判断最终结论:v-for优先级比v-if高
4.3 注意事项
永远不要把
v-if和v-for同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)如果避免出现这种情况,则在外层嵌套
template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
vue
<template v-if="isShow">
<p v-for="item in items" /></p>
</template>- 如果条件出现在循环内部,可通过计算属性
computed提前过滤掉那些不需要显示的项
js
computed: {
items: function() {
return this.list.filter(function (item) {
return item.isShow
})
}
}5. 为什么data属性是一个函数而不是一个对象?
5.1 实例和组件定义data的区别
vue实例的时候定义data属性既可以是一个对象,也可以是一个函数
js
const app = new Vue({
el: '#app',
// 对象格式
data() {
return {
foo: 'foo'
}
},
// 函数格式
data() {
return {
foo: 'foo'
}
}
})组件中定义data属性,只能是一个函数如果为组件data直接定义为一个对象
js
Vue.component('Component1', {
data() {
return {
foo: 'foo'
}
},
template: '<div>组件</div>'
})则会得到警告信息

警告说明:返回的data应该是一个函数在每一个组件实例中
5.2 组件data定义函数与对象的区别
上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例这里我们模仿组件构造函数,定义data属性,采用对象的形式
js
function Component() {
}
Component.prototype.data = {
count: 0
}创建两个组件实例
js
const componentA = new Component()
const componentB = new Component()修改componentA组件data属性的值,componentB中的值也发生了改变
js
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 1产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影响
如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)
js
function Component() {
this.data = this.data()
}
Component.prototype.data = function () {
return {
count: 0
}
}修改componentA组件data属性的值,componentB中的值不受影响
js
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 0vue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染
5.3 原理分析
首先可以看看vue初始化data的代码,data的定义可以是函数也可以是对象源码位置:/vue-dev/src/core/instance/state.js
js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
}data既能是object也能是function,那为什么还会出现上文警告呢?
别急,继续看下文
组件在创建的时候,会进行选项的合并
源码位置:/vue-dev/src/core/util/options.js自定义组件会进入mergeOptions进行选项合并
js
Vue.prototype._init = function (options?: Object) {
...
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
}定义data会进行数据校验源码位置:/vue-dev/src/core/instance/init.js这时候vm实例为undefined,进入if判断,若data类型不是function,则出现警告提示
js
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
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)
}5.4 结论
根实例对象
data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况组件实例对象
data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
6. Vue中组件和插件有什么区别?
6.1 组件是什么
回顾以前对组件的定义:
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件
组件的优势
降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
6.2 插件是什么
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
添加全局方法或者属性。如:
vue-custom-element添加全局资源:指令/过滤器/过渡等。如
vue-touch通过全局混入来添加一些组件选项。如
vue-router添加
Vue实例方法,通过把它们添加到Vue.prototype上实现。一个库,提供自己的
API,同时提供上面提到的一个或多个功能。如vue-router
6.3 两者的区别
两者的区别主要表现在以下几个方面:
编写形式
注册形式
使用场景
编写形式
编写组件
编写一个组件,可以有很多方式,我们最常见的就是vue单文件的这种格式,每一个.vue文件我们都可以看成是一个组件vue文件标准格式
vue
<template>
</template>
<script>
export default{
...
}
</script>
<style>
</style>我们还可以通过template属性来编写一个组件,如果组件内容多,我们可以在外部定义template组件内容,如果组件内容并不多,我们可直接写在template属性上
js
<template id="testComponent"> // 组件显示的内容
<div>component!</div>
</template>
Vue.component('componentA',{
template: '#testComponent'
template: `<div>component</div>` // 组件内容少可以通过这种形式
})编写插件
vue插件的实现应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象
js
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}注册形式
组件注册
vue组件注册主要分为全局注册与局部注册全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项
js
Vue.component('MyComponentName', { /* ... */ })局部注册只需在用到的地方通过components属性注册一个组件
js
const component1 = {...} // 定义一个组件
export default {
components:{
component1 // 局部注册
}
}插件注册
插件的注册通过Vue.use()的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项
js
Vue.use(插件名字, { /* ... */})注意的是:
注册插件的时候,需要在调用 new Vue() 启动应用之前完成Vue.use会自动阻止多次注册相同插件,只会注册一次
使用场景
具体的其实在插件是什么章节已经表述了,这里在总结一下
组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身简单来说,插件就是指对Vue的功能的增强或补充
7. Vue组件之间的通信方式都有哪些?
7.1 组件间通信的概念
开始之前,我们把组件间通信这个词进行拆分
组件
通信
都知道组件是vue最强大的功能之一,vue中每一个.vue我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信组件间通信即指组件(.vue)通过某种方式来传递信息以达到某个目的举个栗子我们在使用UI框架中的table组件,可能会往table组件中传入某些数据,这个本质就形成了组件之间的通信
7.2 组件间通信解决了什么
在古代,人们通过驿站、飞鸽传书、烽火报警、符号、语言、眼神、触碰等方式进行信息传递,到了今天,随着科技水平的飞速发展,通信基本完全利用有线或无线电完成,相继出现了有线电话、固定电话、无线电话、手机、互联网甚至视频电话等各种通信方式从上面这段话,我们可以看到通信的本质是信息同步,共享回到vue中,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统
7.3 组件间通信的分类
组件间通信的分类可以分成以下
父子组件之间的通信
兄弟组件之间的通信
祖孙与后代组件之间的通信
非关系组件间之间的通信
关系图:

7.4 组件间通信的方案
整理vue中8种常规的通信方案
通过 props 传递
通过 $emit 触发自定义事件
使用 ref
EventBus
$parent 或$root
attrs 与 listeners
Provide 与 Inject
Vuex
props传递数据

适用场景:父组件传递数据给子组件
子组件设置
props属性,定义接收父组件传递过来的参数父组件在使用子组件标签中通过字面量来传递值
Children.vue
js
props:{
// 字符串形式
name:String // 接收的类型参数
// 对象形式
age:{
type:Number, // 接收的类型为数值
defaule:18, // 默认值为18
require:true // age属性必须传递
}
}Father.vue组件
vue
<Children name="jack" age=18 />$emit 触发自定义事件
适用场景:子组件传递数据给父组件
子组件通过
$emit触发自定义事件,$emit第二个参数为传递的数值父组件绑定监听器获取到子组件传递过来的参数
Chilfen.vue
js
this.$emit('add', good)Father.vue
vue
<Children @add="cartAdd($event)" />ref
父组件在使用子组件的时候设置
ref父组件通过设置子组件
ref来获取数据
父组件
vue
<Children ref="foo" />
this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据EventBus
使用场景:兄弟组件传值
创建一个中央事件总线
EventBus兄弟组件通过
$emit触发自定义事件,$emit第二个参数为传递的数值另一个兄弟组件通过
$on监听自定义事件
Bus.js
js
// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {} // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
$emit(name, args) {
if (this.callbacks[name])
this.callbacks[name].forEach(cb => cb(args))
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能Children1.vue
js
this.$bus.$emit('foo')Children2.vue
js
this.$bus.$on('foo', this.handle)$parent 或$ root
- 通过共同祖辈
$parent或者$root搭建通信桥连
兄弟组件
this.$parent.on('add',this.add)
另一个兄弟组件
this.$parent.emit('add')
$attrs 与$ listeners
适用场景:祖先传递数据给子孙
设置批量向下传属性
$attrs和$listeners包含了父级作用域中不作为
prop被识别 (且获取) 的特性绑定 ( class 和 style 除外)。可以通过
v-bind="$attrs"传⼊内部组件
vue
// child:并未在props中声明foo
<p>
{{$attrs.foo}}
</p>
// parent
<HelloWorld foo="foo" />vue
// 给Grandson隔代传值,communication/index.vue
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2>
// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>
// Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">
{{msg}}
</div>provide 与 inject
在祖先组件定义
provide属性,返回传递的值在后代组件通过
inject接收组件传递过来的值
祖先组件
js
provide(){
return {
foo:'foo'
}
}后代组件
js
inject:['foo'] // 获取到祖先组件传递过来的值vuex
适用场景: 复杂关系的组件数据传递
Vuex作用相当于一个用来存储共享变量的容器state用来存放共享变量的地方getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值mutations用来存放修改state的方法。actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作
小结
父子关系的组件数据传递选择
props与$emit进行传递,也可选择ref兄弟关系的组件数据传递可选择
$bus,其次可以选择$parent进行传递祖先与后代组件数据传递可选择
attrs与listeners或者Provide与Inject复杂关系的组件数据传递可以通过
vuex存放共享的变量
8. 双向数据绑定是什么
8.1 什么是双向绑定
我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定举个栗子

当用户填写表单时,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定关系图如下

8.2 双向绑定的原理是什么
我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成
数据层(Model):应用的数据及业务逻辑
视图层(View):应用的展示效果,各类UI组件
业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解ViewModel
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
监听器(Observer):对所有数据的属性进行监听
解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
8.3 实现双向绑定
我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的
new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中同时对模板执行编译,找到其中动态绑定的数据,从
data中获取并初始化视图,这个过程发生在Compile中同时定义⼀个更新函数和
Watcher,将来对应数据变化时Watcher会调用更新函数由于
data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher将来data中数据⼀旦发生变化,会首先找到对应的
Dep,通知所有Watcher执行更新函数
流程图如下:

实现
先来一个构造函数:执行初始化,对data执行响应化处理
js
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data
// 对data选项做响应式处理
observe(this.$data)
// 代理data到vm上
proxy(this)
// 执行编译
new Compile(options.el, this)
}
}对data选项执行响应化具体操作
js
function observe(obj) {
if (typeof obj !== 'object' || obj == null)
return
new Observer(obj)
}
class Observer {
constructor(value) {
this.value = value
this.walk(value)
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
}编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

js
class Compile {
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el) // 获取dom
if (this.$el)
this.compile(this.$el)
}
compile(el) {
const childNodes = el.childNodes
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log(`编译元素${node.nodeName}`)
}
else if (this.isInterpolation(node)) {
console.log(`编译插值⽂本${node.textContent}`) // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node) // 对子元素进行递归遍历
}
})
}
isElement(node) {
return node.nodeType == 1
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
}依赖收集
视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知

实现思路
defineReactive时为每⼀个key创建⼀个Dep实例初始化视图时读取某个
key,例如name1,创建⼀个watcher1由于触发
name1的getter方法,便将watcher1添加到name1对应的Dep中当
name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
js
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}声明Dep
js
class Dep {
constructor() {
this.deps = [] // 依赖管理
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}创建watcher时触发getter
js
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this
this.vm[this.key]
Dep.target = null
}
}依赖收集,创建Dep实例
js
function defineReactive(obj, key, val) {
this.observe(val)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)// Dep.target也就是Watcher实例
return val
},
set(newVal) {
if (newVal === val)
return
dep.notify() // 通知dep执行更新方法
},
})
}9. 说说你对slot的理解?slot使用场景有哪些?
9.1 slot是什么
在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充
举个栗子
vue
<template id="element-details-template">
<slot name="element-name">
Slot template
</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,
js
customElements.define('element-details', class extends HTMLElement {
constructor() {
super()
const template = document
.getElementById('element-details-template')
.content
const shadowRoot = this.attachShadow({ mode: 'open' })
.appendChild(template.cloneNode(true))
}
})在Vue中的概念也是如此Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口
可以将其类比为插卡式的FC游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容)
放张图感受一下

9.2 使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
9.3 分类
slot可以分来以下三种:
默认插槽
具名插槽
作用域插槽
默认插槽
子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面
父组件在使用的时候,直接在子组件的标签内写入内容即可
子组件Child.vue
vue
<template>
<slot>
<p>插槽后备的内容</p>
</slot>
</template>父组件
vue
<Child>
<div>默认插槽</div>
</Child>具名插槽
子组件用name属性来表示插槽的名字,不传为默认插槽父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值子组件Child.vue
vue
<template>
<slot>插槽后备的内容</slot>
<slot name="content">
插槽后备的内容
</slot>
</template>父组件
vue
<child>
<template v-slot:default>具名插槽</template>
<!-- 具名插槽⽤插槽名做参数 -->
<template v-slot:content>内容...</template>
</child>作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用子组件Child.vue
vue
<template>
<slot name="footer" test-props="子组件的值">
<h3>没传footer插槽</h3>
</slot>
</template>父组件
vue
<child>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
<template #default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
</child>小结:
v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用默认插槽名为
default,可以省略default直接写v-slot缩写为
#时不能不写参数,写成#default可以通过解构获取
v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"
10. 你知道vue中key的原理吗?说说你对它的理解
10.1 Key是什么
开始之前,我们先还原两个实际工作场景
- 当我们在使用
v-for时,需要给单元加上key
vue
<ul>
<li v-for="item in items" :key="item.id">...</li>
</ul>- 用
+new Date()生成的时间戳作为key,手动强制触发重新渲染
vue
<Comp :key="+new Date()" />那么这背后的逻辑是什么,key的作用又是什么?
一句话来讲
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点
场景背后的逻辑
当我们在使用v-for时,需要给单元加上key
如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed
用+new Date()生成的时间戳作为key,手动强制触发重新渲染
- 当拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧key Comp会被移除,新key Comp触发渲染
10.2 设置key与不设置key区别
举个例子:
创建一个实例,2秒后往items数组插入数据
jsx
<body>
<div id="demo">
<p v-for="item in items" :key="item">{{item}}</p>
</div>
<script src="../../dist/vue.js"></script>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: { items: ['a', 'b', 'c', 'd', 'e'] },
mounted () {
setTimeout(() => {
this.items.splice(2, 0, 'f') //
}, 2000);
},
});
</script>
</body>在不使用key的情况,vue会进行这样的操作:

分析下整体流程:
比较A,A,相同类型的节点,进行
patch,但数据相同,不发生dom操作比较B,B,相同类型的节点,进行
patch,但数据相同,不发生dom操作比较C,F,相同类型的节点,进行
patch,数据不同,发生dom操作比较D,C,相同类型的节点,进行
patch,数据不同,发生dom操作比较E,D,相同类型的节点,进行
patch,数据不同,发生dom操作循环结束,将E插入到
DOM中
一共发生了3次更新,1次插入操作
在使用key的情况:vue会进行这样的操作:
比较A,A,相同类型的节点,进行
patch,但数据相同,不发生dom操作比较B,B,相同类型的节点,进行
patch,但数据相同,不发生dom操作比较C,F,不相同类型的节点
- 比较E、E,相同类型的节点,进行
patch,但数据相同,不发生dom操作
- 比较E、E,相同类型的节点,进行
比较D、D,相同类型的节点,进行
patch,但数据相同,不发生dom操作比较C、C,相同类型的节点,进行
patch,但数据相同,不发生dom操作循环结束,将F插入到C之前
一共发生了0次更新,1次插入操作
通过上面两个小例子,可见设置key能够大大减少对页面的DOM操作,提高了diff效率
设置key值一定能提高diff效率吗?
其实不然,文档中也明确表示
当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出
建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升
10.3 原理分析
源码位置:core/vdom/patch.js
这里判断是否为同一个key,首先判断的是key值是否相等如果没有设置key,那么key为undefined,这时候undefined是恒等于undefined
js
function sameVnode(a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag
&& a.isComment === b.isComment
&& isDef(a.data) === isDef(b.data)
&& sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder)
&& a.asyncFactory === b.asyncFactory
&& isUndef(b.asyncFactory.error)
)
)
)
}updateChildren方法中会对新旧vnode进行diff,然后将比对出的结果用来更新真实的DOM
js
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
...
} else if (isUndef(oldEndVnode)) {
...
} else if (sameVnode(oldStartVnode, newStartVnode)) {
...
} else if (sameVnode(oldEndVnode, newEndVnode)) {
...
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
...
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
...
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
...
}