likes
comments
collection
share

校招前端vue面试题集锦

作者站长头像
站长
· 阅读数 23

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();
pop();
shift();
unshift();
splice();
sort();
reverse();

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

Vuex 为什么要分模块并且加命名空间

  • 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块
  • 命名空间 :默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名

Vue.js的template编译

简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点),详细步骤如下:

首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。

然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)

Vue 子组件和父组件执行顺序

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed

写过自定义指令吗 原理是什么

指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。

自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind

1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。

4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。

5. unbind:只调用一次,指令与元素解绑时调用。

原理

1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性

2.通过 genDirectives 生成指令代码

3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子

4.当执行指令对应钩子函数时,调用对应指令定义的方法

Vue模版编译原理知道吗,能简单说一下吗?

简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:

  • 生成AST树
  • 优化
  • codegen

首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最后一步是将优化后的AST树转换为可执行的代码

参考 前端进阶面试题详细解答

描述下Vue自定义指令

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。一般需要对DOM元素进行底层操作时使用,尽量只用来操作 DOM展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定v-model的值也不会同步更新;如必须修改可以在自定义指令中使用keydown事件,在vue组件中使用 change事件,回调中修改vue数据;

(1)自定义指令基本内容

  • 全局定义:Vue.directive("focus",{})
  • 局部定义:directives:{focus:{}}
  • 钩子函数:指令定义对象提供钩子函数

    o bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

    o inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。

    o update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。

    o ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用。

    o unbind:只调用一次,指令与元素解绑时调用。

  • 钩子函数参数o el:绑定元素

    o bing: 指令核心对象,描述指令全部信息属性

    o name

    o value

    o oldValue

    o expression

    o arg

    o modifers

    o vnode 虚拟节点

    o oldVnode:上一个虚拟节点(更新钩子函数中才有用)

(2)使用场景

  • 普通DOM元素进行底层操作的时候,可以使用自定义指令
  • 自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。

(3)使用案例

初级应用:

  • 鼠标聚焦
  • 下拉菜单
  • 相对时间转换
  • 滚动动画

高级应用:

  • 自定义指令实现图片懒加载
  • 自定义指令集成第三方插件

Vue的生命周期方法有哪些

  1. Vue 实例有一个完整的生命周期,也就是从开始创建初始化数据编译模版挂载Dom -> 渲染更新 -> 渲染卸载等一系列过程,我们称这是Vue的生命周期
  2. Vue生命周期总共分为8个阶段创建前/后载入前/后更新前/后销毁前/后
beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activated deactivated
生命周期vue2生命周期vue3描述
beforeCreatebeforeCreate在实例初始化之后,数据观测(data observer) 之前被调用。
createdcreated实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMountbeforeMount在挂载开始之前被调用:相关的 render 函数首次被调用
mountedmountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdatebeforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
updatedupdated由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
beforeDestroybeforeUnmount实例销毁之前调用。在这一步,实例仍然完全可用
destroyedunmounted实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

其他几个生命周期

生命周期vue2生命周期vue3描述
activatedactivatedkeep-alive专属,组件被激活时调用
deactivateddeactivatedkeep-alive专属,组件被销毁时调用
errorCapturederrorCaptured捕获一个来自子孙组件的错误时被调用
-renderTracked调试钩子,响应式依赖被收集时调用
-renderTriggered调试钩子,响应式依赖被触发时调用
-serverPrefetchssr only,组件实例在服务器上被渲染前调用
  1. 要掌握每个生命周期内部可以做什么事
  2. beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
  3. created 组件初始化完毕,可以访问各种数据,获取接口数据等
  4. beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
  5. mounted 实例已经挂载完成,可以进行一些DOM操作
  6. beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  7. updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
  8. destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
  9. vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
  10. vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

<div id="app">{{name}}</div>
<script>
    const vm = new Vue({
        data(){
            return {name:'poetries'}
        },
        el: '#app',
        beforeCreate(){
            // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。
            console.log('beforeCreate');
        },
        created(){
            // 属性和方法的运算, watch/event 事件回调。这里没有$el
            console.log('created')
        },
        beforeMount(){
            // 相关的 render 函数首次被调用。
            console.log('beforeMount')
        },
        mounted(){
            // 被新创建的 vm.$el 替换
            console.log('mounted')
        },
        beforeUpdate(){
            //  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
            console.log('beforeUpdate')
        },
        updated(){
            //  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
            console.log('updated')
        },
        beforeDestroy(){
            // 实例销毁之前调用 实例仍然完全可用
            console.log('beforeDestroy')
        },
        destroyed(){ 
            // 所有东西都会解绑定,所有的事件监听器会被移除
            console.log('destroyed')
        }
    });
    setTimeout(() => {
        vm.name = 'poetry';
        setTimeout(() => {
            vm.$destroy()  
        }, 1000);
    }, 1000);
</script>
  1. 组合式API生命周期钩子

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup() 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreate不需要*
created不需要*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写
export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

setupcreated谁先执行?

  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好
  • setup:在beforeCreatecreated之间执行
  • created:组件被创建出来,组件的methodsdata已经初始化好了
由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined
import { ref } from "vue"
export default {
  // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去
  setup(){
    let count = ref(1)
    function myFn(){
      count.value +=1
    }
    return {count,myFn}
  },

}
  1. 其他问题
  2. 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
  • vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
  • vue生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。
  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted
  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created
  • DOM 渲染在哪个周期中就已经完成?mounted中,

    • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
    mounted: function () {
        this.$nextTick(function () {
            // Code that will run only after the
            // entire view has been rendered
        })
      }
### keep-alive 中的生命周期哪些

keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。

当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。



### 什么是 MVVM?

Model–View–ViewModel (MVVM) 是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表

MVVM 源自于经典的 Model–View–Controller(MVC)模式  ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用

(1)ViewView 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。

(2)Model 层

Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。

(3)ViewModel 层

ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。

MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:

(1)View

<div id="app">

<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>

</div>

(2)ViewModel 层

var app = new Vue({

el: '#app',
data: {  // 用于描述视图状态   
    message: 'Hello Vue!', 
},
methods: {  // 用于描述视图行为  
    showMessage(){
        let vm = this;
        alert(vm.message);
    }
},
created(){
    let vm = this;
    // Ajax 获取 Model 层的数据
    ajax({
        url: '/your/server/data/api',
        success(res){
            vm.message = res;
        }
    });
}

})

3Model

{

"url": "/your/server/data/api",
"res": {
    "success": true,
    "name": "IoveC",
    "domain": "www.cnblogs.com"
}

}


## Vue组件data为什么必须是个函数?

* **根实例对象`data`可以是对象也可以是函数** (根实例是单例),不会产生数据污染情况
* **组件实例对象`data`必须为函数** 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果`data`是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间`data`不冲突,`data`必须是一个函数,

**简版理解**

// 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类// Vue.extend 根据用户定义产生一个新的类function Vue() {}function Sub() { // 会将data存起来

this.data = this.constructor.options.data();

}Vue.extend = function(options) {

Sub.options = options; // 静态属性
return Sub;

}let Child = Vue.extend({

data:()=>( { name: 'zf' })

});

// 两个组件就是两个实例, 希望数据互不感染let child1 = new Child();let child2 = new Child();

console.log(child1.data.name);child1.data.name = 'poetry';console.log(child2.data.name);

// 根不需要 任何的合并操作 根才有vm属性 所以他可以是函数和对象 但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数

**相关源码**

// 源码位置 src/core/global-api/extend.jsexport function initExtend (Vue: GlobalAPI) { Vue.extend = function (extendOptions: Object): Function {

extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
  return cachedCtors[SuperId]
}

const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
  validateComponentName(name)
}

const Sub = function VueComponent (options) {
  this._init(options)
}
// 子类继承大Vue父类的原型
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
  Super.options,
  extendOptions
)
Sub['super'] = Super

// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
  initProps(Sub)
}
if (Sub.options.computed) {
  initComputed(Sub)
}

// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use

// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
  Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) { 
  Sub.options.components[name] = Sub // 记录自己 在组件中递归自己  -> jsx
}

// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)

// cache constructor
cachedCtors[SuperId] = Sub
return Sub

}}


### v-model 可以被用在自定义组件上吗?如果可以,如何使用?

可以。v-model 实际上是一个语法糖,如:

<input v-model="searchText">

实际上相当于:

<input v-bind:value="searchText" v-on:input="searchText = $event.target.value"

用在自定义组件上也是同理:

<custom-input v-model="searchText">

相当于:

<custom-input v-bind:value="searchText" v-on:input="searchText = $event"

</custom-input>
显然,custom-input 与父组件的交互如下:

1. 父组件将`searchText`变量传入custom-input 组件,使用的 prop 名为`value`;
2. custom-input 组件向父组件传出名为`input`的事件,父组件将接收到的值赋值给`searchText`;

所以,custom-input 组件的实现应该类似于这样:

Vue.component('custom-input', { props: ['value'], template: <input v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > })


### 组件通信

组件通信的方式如下:

### (1) props  /   $emit

父组件通过`props`向子组件传递数据,子组件通过`$emit`和父组件通信

##### 1. 父组件向子组件传值

- `props`只能是父组件向子组件进行传值,`props`使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
- `props` 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
- `props`属性名规则:若在`props`中使用驼峰形式,模板中需要使用短横线的形式

// 父组件<template> <div id="father">

<son :msg="msgData" :fn="myFunction"></son>

</div></template>

<script>import son from "./son.vue";export default { name: father, data() {

msgData: "父组件数据";

}, methods: {

myFunction() {
  console.log("vue");
},

}, components: { son },};</script>

// 子组件<template> <div id="son">

<p>{{ msg }}</p>
<button @click="fn">按钮</button>

</div></template><script>export default { name: "son", props: ["msg", "fn"] };</script>

##### 2. 子组件向父组件传值

- `$emit`绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过`v-on`监听并接收参数。

// 父组件<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>

### (2)eventBus事件总线(`$emit / $on`)

`eventBus`事件总线适用于**父子组件****非父子组件**等之间的通信,使用步骤如下: **(1)创建事件中心管理组件之间的通信**

// event-bus.js

import Vue from 'vue'export const EventBus = new Vue()

**(2)发送事件** 假设有两个兄弟组件`firstCom`和`secondCom`:

<template> <div>

<first-com></first-com>
<second-com></second-com>

</div></template>

<script>import firstCom from "./firstCom.vue";import secondCom from "./secondCom.vue";export default { components: { firstCom, secondCom } };</script>

在`firstCom`组件中发送事件:

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

**(3)接收事件** 在`secondCom`组件中发送事件:

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

在上述代码中,这就相当于将`num`值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

### (3)依赖注入(provide / inject)

这种方式就是Vue中的**依赖注入**,该方法用于**父子组件之间的通信**。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。

`provide / inject`是Vue提供的两个钩子,和`data`、`methods`是同级的。并且`provide`的书写形式和`data`一样。

- `provide` 钩子用来发送数据或方法
- `inject`钩子用来接收数据或方法

在父组件中:

provide() {

return {     
    num: this.num  
};

}

在子组件中:

inject: ['num']

还可以这样写,这样写就可以访问父组件中的所有属性:

provide() { return {

app: this

};}data() { return {

num: 1

};}

inject: ['app']console.log(this.app.num)

**注意:** 依赖注入所提供的属性是**非响应式**的。

### (3)ref / $refs

这种方式也是实现**父子组件**之间的通信。

`ref`: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。

在子组件中:

export default { data () {

return {
  name: 'JavaScript'
}

}, methods: {

sayHello () {
  console.log('hello')
}

}}

在父组件中:

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

### (4)`$parent / $children`

- 使用`$parent`可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
- 使用`$children`可以让组件访问子组件的实例,但是,`$children`并不能保证顺序,并且访问的数据也不是响应式的。

在子组件中:

<template> <div>

<span>{{ message }}</span>
<p>获取父组件的值为: {{ parentVal }}</p>

</div></template>

<script>export default { data() {

return { message: "Vue" };

}, computed: {

parentVal() {
  return this.$parent.msg;
},

},};</script>

在父组件中:

// 父组件中<template> <div class="hello_world">

<div>{{ msg }}</div>
<child></child>
<button @click="change">点击改变子组件值</button>

</div></template>

<script>import child from "./child.vue";export default { components: { child }, data() {

return { msg: "Welcome" };

}, methods: {

change() {
  // 获取到子组件
  this.$children[0].message = "JavaScript";
},

},};</script>

在上面的代码中,子组件获取到了父组件的`parentVal`值,父组件改变了子组件中`message`的值。 **需要注意:**

- 通过`$parent`访问到的是上一级父组件的实例,可以使用`$root`来访问根组件的实例
- 在组件中使用`$children`拿到的是所有的子组件的实例,它是一个数组,并且是无序的
- 在根组件`#app`上拿`$parent`得到的是`new Vue()`的实例,在这实例上再拿`$parent`得到的是`undefined`,而在最底层的子组件拿`$children`是个空数组
- `$children` 的值是**数组**,而`$parent`是个**对象**

### (5)`$attrs / $listeners`

考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?

如果是用`props/$emit`来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。

针对上述情况,Vue引入了`$attrs / $listeners`,实现组件之间的跨代通信。

先来看一下`inheritAttrs`,它的默认值true,继承所有的父组件属性除`props`之外的所有属性;`inheritAttrs:false` 只继承class属性 。

- `$attrs`:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
- `$listeners`:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 `v-on="$listeners"` 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

A组件(`APP.vue`):

<template> <div id="app">

//此处监听了两个事件,可以在B组件或者C组件中直接触发
<child1
  :p-child1="child1"
  :p-child2="child2"
  @test1="onTest1"
  @test2="onTest2"
></child1>

</div></template><script>import Child1 from "./Child1.vue";export default { components: { Child1 }, methods: {

onTest1() {
  console.log("test1 running");
},
onTest2() {
  console.log("test2 running");
},

},};</script>

B组件(`Child1.vue`):

<template> <div class="child-1">

<p>props: {{ pChild1 }}</p>
<p>$attrs: {{ $attrs }}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>

</div></template><script>import Child2 from "./Child2.vue";export default { props: ["pChild1"], components: { Child2 }, inheritAttrs: false, mounted() {

this.$emit("test1"); // 触发APP.vue中的test1方法

},};</script>

C 组件 (`Child2.vue`):

<template> <div class="child-2">

<p>props: {{ pChild2 }}</p>
<p>$attrs: {{ $attrs }}</p>

</div></template><script>export default { props: ["pChild2"], inheritAttrs: false, mounted() {

this.$emit("test2"); // 触发APP.vue中的test2方法

},};</script>

在上述代码中:

- C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了`$listeners` 属性
- 在B组件中通过v-bind 绑定`$attrs`属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

### (6)总结

**(1)父子组件间通信**

- 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
- 通过 ref 属性给子组件设置一个名字。父组件通过 `$refs` 组件名来获得子组件,子组件通过 `$parent` 获得父组件,这样也可以实现通信。
- 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。

**(2)兄弟组件间通信**

- 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
- 通过 `$parent/$refs` 来获取到兄弟组件,也可以进行通信。

**(3)任意组件之间**

- 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。

如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。



### Vue模版编译原理知道吗,能简单说一下吗?

简单说,Vue的编译过程就是将`template`转化为`render`函数的过程。会经历以下阶段:

- 生成AST树
- 优化
- codegen

首先解析模版,生成`AST语法树`(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以`跳过对它们的比对`,对运行时的模板起到很大的优化作用。

编译的最后一步是`将优化后的AST树转换为可执行的代码`### Vue生命周期钩子是如何实现的

* `vue`的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
* 内部会对钩子函数进行处理,将钩子函数维护成数组的形式

> `Vue` 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)

<script>

// Vue.options 中会存放所有全局属性

// 会用自身的 + Vue.options 中的属性进行合并
// Vue.mixin({
//     beforeCreate() {
//         console.log('before 0')
//     },
// })
debugger;
const vm = new Vue({
    el: '#app',
    beforeCreate: [
        function() {
            console.log('before 1')
        },
        function() {
            console.log('before 2')
        }
    ]
});
console.log(vm);

</script>

相关代码如下

export function callHook(vm, hook) { // 依次执行生命周期对应的方法 const handlers = vm.$options[hook]; if (handlers) {

for (let i = 0; i < handlers.length; i++) {
  handlers[i].call(vm); //生命周期里面的this指向当前实例
}

}}

// 调用的时候Vue.prototype._init = function (options) { const vm = this; vm.$options = mergeOptions(vm.constructor.options, options); callHook(vm, "beforeCreate"); //初始化数据之前 // 初始化状态 initState(vm); callHook(vm, "created"); //初始化数据之后 if (vm.$options.el) {

vm.$mount(vm.$options.el);

}};

// 销毁实例实现Vue.prototype.$destory = function() {

 // 触发钩子
callHook(vm, 'beforeDestory')
// 自身及子节点
remove() 
// 删除依赖
watcher.teardown() 
// 删除监听
vm.$off() 
// 触发钩子
callHook(vm, 'destoryed')

}

**原理流程图**

![](https://img-blog.csdnimg.cn/img_convert/0de63c1d02a9bc77d428c5c4a66d20de.png)



### Computed 和 Watch 的区别

**对于Computed:**

- 它支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当Computed中有异步操作时,无法监听数据的变化
- computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
- 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
- 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。

**对于Watch:**

- 它不支持缓存,数据变化时,它就会触发相应的操作
- 支持异步监听
- 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
- 当一个属性发生变化时,就需要执行相应的操作
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
  - immediate:组件加载立即触发回调函数
  - deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

**总结:**

- computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
- watch 侦听器 : 更多的是**观察**的作用,**无缓存性**,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

**运用场景:**

- 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
- 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。



### Vue模版编译原理知道吗,能简单说一下吗?

简单说,Vue的编译过程就是将`template`转化为`render`函数的过程。会经历以下阶段:

- 生成AST树
- 优化
- codegen

首先解析模版,生成`AST语法树`(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以`跳过对它们的比对`,对运行时的模板起到很大的优化作用。

编译的最后一步是`将优化后的AST树转换为可执行的代码`。



### assets和static的区别

**相同点:** `assets` 和 `static` 两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点

**不相同点:**`assets` 中存放的静态资源文件在项目打包时,也就是运行 `npm run build` 时会将 `assets` 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 `static` 文件中跟着 `index.html` 一同上传至服务器。`static` 中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 `static` 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 `assets` 中打包后的文件提交较大点。在服务器中就会占据更大的空间。

**建议:** 将项目中 `template`需要的样式文件js文件等都可以放置在 `assets` 中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如`iconfoont.css` 等文件可以放置在 `static` 中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。

## 谈谈你对MVVM的理解

为什么要有这些模式,目的:职责划分、分层(将`Model`层、`View`层进行分类)借鉴后端思想,对于前端而已,就是如何将数据同步到页面上

**MVC模式** 代表:`Backbone` + `underscore` + `jquery`

![](https://img-blog.csdnimg.cn/img_convert/7be9ea159299032da61ddf4239ebe343.png)

* 传统的 `MVC` 指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染
* `MVVM`:传统的前端会将数据手动渲染到页面上, `MVVM` 模式不需要用户收到操作 `dom` 元素,将数据绑定到 `viewModel` 层上,会自动将数据渲染到页面中,视图变化会通知 `viewModel`层 更新数据。`ViewModel` 就是我们 `MVVM` 模式中的桥梁

**MVVM模式** 映射关系的简化,隐藏了`controller`

![](https://img-blog.csdnimg.cn/img_convert/3162a144e02f7d3b32fc4be403e0ef2c.png)

> `MVVM`是`Model-View-ViewModel`缩写,也就是把`MVC`中的`Controller`演变成`ViewModel`。`Model`层代表数据模型,`View`代表UI组件,`ViewModel`是`View`和`Model`层的桥梁,数据会绑定到`viewModel`层并自动将数据渲染到页面中,视图变化的时候会通知`viewModel`层更新数据。

* `Model`: 代表数据模型,也可以在`Model`中定义数据修改和操作的业务逻辑。我们可以把`Model`称为数据层,因为它仅仅关注数据本身,不关心任何行为
* `View`: 用户操作界面。当`ViewModel`对`Model`进行更新的时候,会通过数据绑定更新到`View`
* `ViewModel`: 业务逻辑层,`View`需要什么数据,`ViewModel`要提供这个数据;`View`有某些操作,`ViewModel`就要响应这些操作,所以可以说它是`Model for View`.

**总结** : `MVVM`模式简化了界面与业务的依赖,解决了数据频繁更新。`MVVM` 在使用当中,利用双向绑定技术,使得 `Model` 变化时,`ViewModel` 会自动更新,而 `ViewModel` 变化时,`View` 也会自动变化。

我们以下通过一个 `Vue` 实例来说明 `MVVM` 的具体实现

<!-- View 层 -->

<div id="app">

<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>

</div>

// ViewModel 层

var app = new Vue({

el: '#app',
data: {  // 用于描述视图状态   
    message: 'Hello Vue!', 
},
methods: {  // 用于描述视图行为  
    showMessage(){
        let vm = this;
        alert(vm.message);
    }
},
created(){
    let vm = this;
    // Ajax 获取 Model 层的数据
    ajax({
        url: '/your/server/data/api',
        success(res){
            vm.message = res;
        }
    });
}

})

// Model 层

{

"url": "/your/server/data/api",
"res": {
    "success": true,
    "name": "test",
    "domain": "www.baidu.com"
}

}


## Vue2.x 响应式数据原理

整体思路是数据劫持+观察者模式

对象内部通过 `defineReactive` 方法,使用 `Object.defineProperty` 来劫持各个属性的 `setter`、`getter`(只会劫持已经存在的属性),数组则是通过`重写数组7个方法`来实现。当页面使用对应属性时,每个属性都拥有自己的 `dep` 属性,存放他所依赖的 `watcher`(依赖收集),当属性变化后会通知自己对应的 `watcher` 去更新(派发更新)

**Object.defineProperty基本使用**

function observer(value) { // proxy reflect

if (typeof value === 'object' && typeof value !== null)
for (let key in value) {
    defineReactive(value, key, value[key]);
}

}

function defineReactive(obj, key, value) {

observer(value);
Object.defineProperty(obj, key, {
    get() { // 收集对应的key 在哪个方法(组件)中被使用
        return value;
    },
    set(newValue) {
        if (newValue !== value) {
            observer(newValue);
            value = newValue; // 让key对应的方法(组件重新渲染)重新执行
        }
    }
})

}let obj1 = { school: { name: 'poetry', age: 20 } };observer(obj1);console.log(obj1)

**源码分析**

![](https://img-blog.csdnimg.cn/img_convert/e7a7b704996fdd0393aca3dc1d9552dd.png)

class Observer { // 观测值 constructor(value) {

this.walk(value);

} walk(data) {

// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
  let key = keys[i];
  let value = data[key];
  defineReactive(data, key, value);
}

}}// Object.defineProperty数据劫持核心 兼容性在ie9以及以上function defineReactive(data, key, value) { observe(value); // 递归关键 // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止 // 思考?如果Vue数据嵌套层级过深 >>性能会受影响 Object.defineProperty(data, key, {

get() {
  console.log("获取值");

  //需要做依赖收集过程 这里代码没写出来
  return value;
},
set(newValue) {
  if (newValue === value) return;
  console.log("设置值");
  //需要做派发更新过程 这里代码没写出来
  value = newValue;
},

});}export function observe(value) { // 如果传过来的是对象或者数组 进行属性劫持 if (

Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)

) {

return new Observer(value);

}}

说一说你对vue响应式理解回答范例

* 所谓数据响应式就是**能够使数据变化可以被检测并对这种变化做出响应的机制**
* `MVVM`框架中要解决的一个核心问题是连接数据层和视图层,通过**数据驱动**应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理
* 以`vue`为例说明,通过数据响应式加上虚拟`DOM`和`patch`算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度
* `vue2`中的数据响应式会根据数据类型来做不同处理,如果是 **对象则采用`Object.defineProperty()`的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法** ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用`Vue.set/delete`这样特殊的`api`才能生效;对于`es6`中新产生的`Map`、`Set`这些数据结构不支持等问题
* 为了解决这些问题,`vue3`重新编写了这一部分的实现:利用`ES6`的`Proxy`代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊`api`,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的`reactivity`包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了