Skip to content

Vue2篇

1. 说说你对vue的理解

1.1 vue是什么

Vue.js(/vjuː/,或简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。

1.2 vue的核心特性

  1. 数据驱动(MVVM)

MVVM表示的是 Model-View-ViewModel

  • Model:模型层,负责处理业务逻辑以及和服务器端进行交互。

  • View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面。

  • ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁。

  1. 组件化

什么是组件化一句话来说就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件。

组件化的优势:

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现。

  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单。

  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级。

  1. 指令系统

解释:指令 (Directives) 是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。

常用的指令:

  • 条件渲染指令 v-if

  • 列表渲染指令v-for

  • 属性绑定指令v-bind

  • 事件绑定指令v-on

  • 双向数据绑定指令v-model

2. v-show和v-if有什么区别?使用场景分别是什么?

2.1 v-show与v-if的共同点

我们都知道在 vuev-showv-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示

在用法上也是相同的

vue
<Model v-show="isShow" />

<Model v-if="isShow" />
  • 当表达式为true的时候,都会占据页面的位置

  • 当表达式都为false时,都不会占据页面位置

2.2 v-show与v-if的区别

  • 控制手段不同

  • 编译过程不同

  • 编译条件不同

控制手段:v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

  • v-showfalse变为true的时候不会触发组件的生命周期

  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

2.3 v-show与v-if的使用场景

v-ifv-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组件实例销毁之后
activatedkeep-alive 缓存的组件激活时
deactivatedkeep-alive 缓存的组件停用时调用
errorCaptured捕获一个来自子孙组件的错误时被调用

3.3 生命周期整体流程

Vue生命周期流程图

具体分析

beforeCreate -> created

  • 初始化vue实例,进行数据观测

created

  • 完成数据观测,属性与方法的运算,watchevent事件回调的配置

  • 可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computedwatch完成数据计算

  • 此时vm.$el 并没有被创建

created -> beforeMount

  • 判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译

  • 优先级:render > template > outerHTML

  • vm.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

  • 更新的数据必须是被渲染在模板上的(eltemplaterender之一)

  • 此时view层还未更新

  • 若在beforeUpdate中再次修改数据,不会再次触发更新方法

updated

  • 完成view层的更新

  • 若在updated中再次修改数据,会再次触发更新方法(beforeUpdateupdated

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-ifv-for都是vue模板系统中的指令在vue模板编译的时候,会将指令系统转化成可执行的render函数

示例

编写一个p标签,同时使用v-ifv-for

vue
<div id="app">
    <p v-if="isShow" v-for="item in items">
        {{ item.title }}
    </p>
</div>

创建vue实例,存放isShowitems数据

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) }
}

_lvue的列表渲染函数,函数内部都会进行一次if判断初步得到结论:v-for优先级是比v-if高再将v-forv-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-forv-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 注意事项

  1. 永远不要把 v-ifv-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)

  2. 如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环

vue
<template v-if="isShow">
  <p v-for="item in items" /></p>
</template>
  1. 如果条件出现在循环内部,可通过计算属性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) // 0

vue组件可能会有很多个实例,采用函数返回一个全新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种常规的通信方案

  1. 通过 props 传递

  2. 通过 $emit 触发自定义事件

  3. 使用 ref

  4. EventBus

  5. $parent 或$root

  6. attrs 与 listeners

  7. Provide 与 Inject

  8. 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进行传递

  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject

  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

8. 双向数据绑定是什么

8.1 什么是双向绑定

我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了ViewModel的数据也自动被更新了,这种情况就是双向绑定举个栗子

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

8.2 双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑

  • 视图层(View):应用的展示效果,各类UI组件

  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图

  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听

  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

8.3 实现双向绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe

  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile

  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher

  5. 将来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统⼀通知

实现思路

  1. defineReactive时为每⼀个key创建⼀个Dep实例

  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1

  3. 由于触发name1getter方法,便将watcher1添加到name1对应的Dep中

  4. 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是什么

开始之前,我们先还原两个实际工作场景

  1. 当我们在使用v-for时,需要给单元加上key
vue
<ul>
    <li v-for="item in items" :key="item.id">...</li>
</ul>
  1. +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操作
  • 比较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,那么keyundefined,这时候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]
        }
    }
    ...
}