«
Vue面试

时间:2022-6   


前端面试题手册:Vue

整合原讨论区帖子并更新内容

基础知识

Vue的理解

Vue是一个构建用户界面的渐进式框架,典型的 MVVM 框架。只关心图层;不关心具体是如何实现的。

Vue的优点主要有:

为什么说Vue是一个渐进式框架

渐进式,通俗点讲就是,你想用啥你就用啥,咱也不强求你。你想用component就用,不用也行,你想用vuex就用,不用也可以。

组件的设计原则

(1)页面上每个独立的可视/可交互区域视为一个组件(比如页面的头部,尾部,可复用的区块)

(2)每个组件对应一个工程目录,组件所需要的各种资源在这个目录下就近维护(组件的就近维护思想体现了前端的工程化思想,为前端开发提供了很好的分治策略,在vue.js中,通过.vue文件将组件依赖的模板,js,样式写在一个文件中)
(每个开发者清楚开发维护的功能单元,它的代码必然存在在对应的组件目录中,在该目录下,可以找到功能单元所有的内部逻辑)

(3)页面不过是组件的容器,组件可以嵌套自由组合成完整的页面

vue.js 的两个核心是什么

数据驱动和组件化思想

MVVM、MVC、MVP的区别

MVC

MVVM

MVVM 由 Model、View、ViewModel 三部分构成Model 代表数据模型,也可以在 Model 中定义数据修改和业务逻辑;View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来;ViewModel 是一个同步 View 和 Model 的对象;

MVP

MVVM的优缺点?

优点:

缺点:

v-model 双向绑定的原理是什么

v-model实际上是语法糖,下面就是语法糖的构造过程。

而v-model自定义指令下包裹的语法是input的value属性、input事件,整个过程是:

<input v-modle="inputV" />
// 等同于
<input :value="inputV" @input="inputV = $event.target.value"/>

这就完成了v-model的数据双向绑定。

我们会发现elementUI的所有自定义组件都适用v-model这一语法糖,除了input之外,select、textarea也用到这一语法糖。

比如checkbox:

// 看似执行了v-model一个指令
<input type="checkbox" v-model="checkedNames">
// 实际上
<input
  type="checkbox" 
  :value="checkedNames" 
  @change="checkedNames = $event.target.value" 
/>

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 与父组件的交互如下:

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

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

讲一下Vue 2.0 响应式数据的原理

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

(其他)使用 Object.defineProperty() 来进行数据劫持有什么缺点

Vue3.0 和 2.0 的响应式原理区别

Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

但是这样做有以下问题:

Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于 Object.defineProperty(),其有以下特点:

Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

Vue监控不了数组变化,有什么解决办法

当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。那么解决方式主要有几种方式:

this.$set(this.arr, 0, "OBKoro1"); // 改变数组
this.$set(this.obj, "c", "OBKoro1"); // 改变对象
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()

vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用splice方***比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作

vm.$set 的实现原理是:

(其他)delete和Vue.delete删除数组的区别

Vue.set()的原理

因为响应式数据 我们给对象和数组本身都增加了ob属性,代表的是 Observer 实例。当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪 然后会触发对象ob的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组。

相关代码如下:

export function set(target: Array | Object, key: any, val: any): any {
  // 如果是数组 调用我们重写的splice方法 (这样可以更新视图)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  // 如果是对象本身的属性,则直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  const ob = (target: any).__ob__;

  // 如果不是响应式的也不需要将其定义成响应式属性
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 将属性定义成响应式的
  defineReactive(ob.value, key, val);
  // 通知视图更新
  ob.dep.notify();
  return val;
}

Vue 的单向数据流吗?

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告

如果实在要改变父组件的 prop 值 可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改。

常见的事件修饰符及其作用

(其他)data为什么是一个函数而不是对象

$nextTick 原理及作用

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理

nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶

Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。

由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。

this.$nextTick(() => {    // 获取数据的操作...})

所以,在以下情况下,会用到nextTick:

因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。

Vue是如何收集依赖的?

在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶

function defieneReactive (obj, key, val){
  const dep = new Dep();
  ...
  Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {
      if(Dep.target){
        dep.depend();
        ...
      }
      return val
    }
    ...
  })
}

以上只保留了关键代码,主要就是 const dep = new Dep()实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend() 进行依赖收集。

Dep是整个依赖收集的核心,其关键代码如下:

class Dep {
  static target;
  subs;

  constructor () {
    ...
    this.subs = [];
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  removeSub (sub) {
    remove(this.sub, sub)
  }
  depend () {
    if(Dep.target){
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subds.slice();
    for(let i = 0;i < subs.length; i++){
      subs[i].update()
    }
  }
}

Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶

class Watcher {
  getter;
  ...
  constructor (vm, expression){
    ...
    this.getter = expression;
    this.get();
  }
  get () {
    pushTarget(this);
    value = this.getter.call(vm, vm)
    ...
    return value
  }
  addDep (dep){
        ...
    dep.addSub(this)
  }
  ...
}
function pushTarget (_target) {
  Dep.target = _target
}

Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。

在实例化 Vue 时,依赖收集的相关过程如下∶ 初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。 初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,

updateComponent = () => {
  vm._update(vm._render())
}
new Watcher(vm, updateComponent)

get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。

this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。

Vue 单页应用与多页应用的区别

概念:

对比项/模式 SPA MPA
结构 一个主页面+许多模块的组件 许多完整的页面
体验 页面切换快,体验佳;当初次加载文件过多时,需要做相关调优 页面切换慢,网速慢的时候,体验尤其不好
资源文件 组件公用的资源字需要加载一次 每个页面都要自己加载公用的资源
适用场景 对体验度和流畅度有较高要求的应用,不利于SEO(可借助SSR优化SEO) 适用于对SEO要求较高的应用
过渡动画 Vue提供了transition的封装组件,容易实现 很难实现
内容更新 相关组件的切换,即局部更新 整体HTML的切换,费钱(重复HTTP请求)
路由模式 可以使用hash,也可以使用history 普通链接跳转
数据传递 因为单页面,使用全局变量就好(Vuex) cookie、localStorage等缓存方案,URL参数,调用接口保存等
相关成本 前期开发成本较高,后期维护较为容易 前期开发成本低,后期维护就比较麻烦,因为可能一个功能需要改很多地方

什么是 mixin ?

mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。

Vue.mixin({    
    beforeCreate() {        // ...逻辑        // 这种方式会影响到每个组件的 beforeCreate 钩子函数    }})

虽然文档不建议在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。

mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。 另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

组件通信(重点)

组件通信的方式如下:

1、props / $emit

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

父组件向子组件传值

// 父组件
<template>
    <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
    </div>
</template>

//JavaScript代码
import son from "./son.vue";
export default {
    name: father,
    data() {
        msgData: "父组件数据";
    },
    methods: {
        myFunction() {
            console.log("vue");
        }
    },
    components: {
        son
    }
};

// 子组件
<template>
    <div id="son">
        <p>{{msg}}</p>
        <button @click="fn">按钮</button>
    </div>
</template>

//JavaScript代码
export default {
    name: "son",
    props: ["msg", "fn"]
};

子组件向父组件传值

// 父组件
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

//JavaScript代码
import comArticle from './test/article.vue'
export default {
  name: 'comArticle',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx
    }
  }
}

//子组件
<template>
  <div>
    <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
  </div>
</template>

//JavaScript代码
export default {
  props: ['articles'],
  methods: {
    emitIndex(index) {
      this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
    }
  }
}

2、eventBus事件总线($emit / $on

eventBus事件总线适用于父子组件非父子组件等之间的通信,使用步骤如下:

(1)创建事件中心管理组件之间的通信

// event-bus.js

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

(2)发送事件: 假设有两个兄弟组件firstComsecondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

//JavaScript代码
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}

firstCom组件中发送事件:

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>

//JavaScript代码
import {EventBus} from './event-bus.js' // 引入事件中心

export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}

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

<template>
  <div>求和: {{count}}</div>
</template>

//JavaScript代码
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}

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

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

3、依赖注入(provide / inject)

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

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

在父组件中:

provide() { 
    return {     
        num: this.num  
    };
}

在子组件中:

inject: ['num']

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

provide() {
 return {
    app: this
  };
}
data() {
 return {
    num: 1
  };
}

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

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

4、ref / $refs

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

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

在子组件中:

export default {
  data () {
    return {
      name: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>

//JavaScript代码
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }

5、parent / $children

在子组件中:

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

//JavaScript代码
export default {
  data() {
    return {
      message: 'Vue'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}

在父组件中:

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

//JavaScript代码
import child from './child.vue'
export default {
  components: { child },
  data() {
    return {
      msg: 'Welcome'
    }
  },
  methods: {
    change() {
      // 获取到子组件
      this.$children[0].message = 'JavaScript'
    }
  }
}

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

6、attrs / $listeners

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

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

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

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

A组件(APP.vue):

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

//JavaScript代码
import Child1 from './Child1.vue';
export default {
    components: { Child1 },
    methods: {
        onTest1() {
            console.log('test1 running');
        },
        onTest2() {
            console.log('test2 running');
        }
    }
};

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>

//JavaScript代码
import Child2 from './Child2.vue';
export default {
    props: ['pChild1'],
    components: { Child2 },
    inheritAttrs: false,
    mounted() {
        this.$emit('test1'); // 触发APP.vue中的test1方法
    }
};

C 组件 (Child2.vue):

<template>
    <div class="child-2">
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{$attrs}}</p>
    </div>
</template>

//JavaScript代码
export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
        this.$emit('test2');// 触发APP.vue中的test2方法
    }
};

在上述代码中:

总结

(1)父子组件间通信

(2)兄弟组件间通信

(3)任意组件之间

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

Vue生命周期

Vue2 与Vue3的生命周期对比

Vue2 Vue3
beforeCreate(组件创建之前) setup(组件创建之前)
created(组件创建完成) setup(组件创建完成)
beforeMount(组件挂载之前) onBeforeMount(组件挂载之前)
mounted(组件挂载完成) onMounted(组件挂载完成)
beforeUpdate(数据更新,虚拟DOM打补丁之前) onBeforeUpdate(数据更新,虚拟DOM打补丁之前)
updated(数据更新,虚拟DOM渲染完成) onUpdated(数据更新,虚拟DOM渲染完成)
beforeDestroy(组件销毁之前) onBeforeUnmount(组件销毁之前)
destroyed(组件销毁之后) onUnmounted(组件销毁之后)

生命周期钩子函数

状态 说明
beforeCreate(创建前) 组件实例更被创建,组件属性计算之前,数据对象 data 都为 undefined,未初始化。
created(创建后) 组件实例创建完成,属性已经绑定,数据对象 data 已存在,但 dom 未生成,$el 未存在
beforeMount(挂载前) vue 实例的$el 和 data 都已初始化,挂载之前为虚拟的 dom 节点,data.message 未替换
mounted(挂载后) vue 实例挂载完成,data.message 成功渲染。
beforeUpdate(更新前) 当 data 变化时,会触发 beforeUpdate 方法
updated(更新后) 当 data 变化时,会触发 updated 方法
beforeDestroy(销毁前) 组件销毁之前调用
destroyed(销毁后) 组件销毁之后调用,对 data 的改变不会再触发周期函数,vue 实例已解除事件监听和 dom 绑定,但 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

异步请求放在哪个生命周期中

我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

keep-alive 中的生命周期哪些

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

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

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

简单介绍keep-alive

keep-alive 原理

//keep-alive 内部声明周期函数
  created () {
    this.cache = Object.create(null)
    this.keys = []
  }

拓展

//力扣解题思路
1、创建map来保存数据
2、get:访问某key,访问完要将其放在最后的。若key存在,先保存value值,删除key,再添加key,最后返回保存的value值。若key不存在,返回-1
3、put:新加一个key,要将其放在最后的。所以,若key已经存在,先删除,再添加。如果容量超出范围了,将map中的头部删除。
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.map = new Map();
    }
    get(key) {
        if (this.map.has(key)) {
            // get表示访问该值
            // 所以在访问的同时,要将其调整位置,放置在最后
            const temp = this.map.get(key);
            // 先删除,再添加
            this.map.delete(key);
            this.map.set(key, temp);
            // 返回访问的值
            return temp;
        } else {
            // 不存在,返回-1
            return -1;
        }
    }
    put(key, value) {
        // 要将其放在最后,所以若存在key,先删除
        if (this.map.has(key)) this.map.delete(key);
        // 设置key、value
        this.map.set(key, value);
        if (this.map.size > this.capacity) {
            // 若超出范围,将map中头部的删除
            // map.keys()返回一个迭代器
            // 迭代器调用next()方法,返回包含迭代器返回的下一个值,在value中
            this.map.delete(this.map.keys().next().value);
        }
    }
}

(其他)created和mounted的区别

Vue路由

vue-router有多少种模式

// 监听hash变化,点击浏览器的前进后退会触发
window.addEventListener('hashchange', function(event){ 
    let newURL = event.newURL; // hash 改变后的新 url
    let oldURL = event.oldURL; // hash 改变前的旧 url
},false)

分析:当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据

location / {
    try_files  $uri $uri/ @router index index.html;
}
location @router {
    rewrite ^.*$ /index.html last;
}

分析:利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。pushState()方法可以改变URL地址且不会发送请求,replaceState()方法可以读取历史记录栈,还可以对浏览器记录进行修改。 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。

hash和history模式实现vue-router跳转api的区别

api hash history
push window.location.assign window.history.pushState
replace window.location.replace window.history.replaceState
go window.history.go window.history.go
back window.history.go(-1) window.history.go(-1)
forward window.history.go(1) window.history.go(1)

了解过动态路由吗

- 传参数 获取参数 url中形式 参数问题
this.$route.query this.$router.push({path: '/index',query:{id:id}}) this.$route.query.id http://127.0.0.1:8080/#/index?id=1 刷新路由跳转页面参数不消失
this.$route.params this.$router.push({name: 'index',params:{id:id} }) this.$route.params.id http://127.0.0.1:8080/#/index 刷新路由跳转页面参数消失

route和router的区别

1、router是VueRouter的一个对象,通过Vue.use(VueRouter)和Vue构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由,包含了许多关键的对象和属性。


2、route是一个跳转的路由对象,每一个路由都会有一个$route对象,是一个局部的对象,可以获取对应的name,path,params,query等

从这两者不同的结构可以看出两者的区别,他们的一些属性是不同的

$route.path 字符串,等于当前路由对象的路径,会被解析为绝对路径,如/home/index

$route.params 对象,含路有种的动态片段和全匹配片段的键值对,不会拼接到路由的url后面

$route.query 对象,包含路由中查询参数的键值对。会拼接到路由url后面

$route.router 路由规则所属的路由器

$route.matchd 数组,包含当前匹配的路径中所包含的所有片段所对象的配置参数对象

$route.name 当前路由的名字,如果没有使用具体路径,则名字为空

Vue-Router 的懒加载是如何实现的

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
    routes: [
        {
            path: '/',
            name: 'HelloWorld',
            component:HelloWorld
        }
    ]
})
import Vue from 'vue'
import Router from 'vue-router'
/* 此处省去之前导入的HelloWorld模块 */
Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      //此处对原代码进行修改
      component: resolve=>(require(["@/components/HelloWorld"],resolve))
    }
  ]
})
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      //此处对原代码进行修改
      component: ()=>import("@/components/HelloWorld")
    }
  ]
})

(拓展)组件懒加载

<template>
  <div>
    <One></One>
  </div>
</template>

//JavaScript代码
import One from './one'
export default {
  components:{
    "One":One
  },
  data () {
    return {
      msg: 'This is a component'
    }
  }
}
<template>
  <div>
    <One></One>
  </div>
</template>

//JavaScript代码
export default {
  components:{
  //原代码修改
    "One":resolve=>(['./one'],resolve)
  },
  data () {
    return {
      msg: 'This is a component'
    }
  }
}
<template>
  <div>
    <One></One>
  </div>
</template>

//JavaScript代码
export default {
  components:{
    //原代码修改
    "One": ()=>import("./one");
  },
  data () {
    return {
      msg: 'This is a component'
    }
  }
}

路由导航守卫有哪些

路由守卫 名称 作用
全局守卫 beforeEach(to,from,next) 路由跳转前触发,常用于登录验证。
beforeResolve(to,from,next) 在 beforeEach 和 组件内 beforeRouteEnter 之后,afterEach 之前调用。
afterEach(to,from) 发生在 beforeEach 和 beforeResolve 之后,beforeRouteEnter 之前。路由在触发后执行。
路由独享守卫 beforeEnter 在 beforeEach 之后执行,和它功能一样 ,不怎么常用
组件内的守卫 beforeRouteEnter 路由进入之前调用。不能获取组件 this 实例 ,因为路由在进入组件之前,组件实例还没有被创建。
beforeRouteUpdate 在当前路由改变时,并且该组件被复用时调用,可以通过 this 访问实例。当前路由 query 变更时,该守卫会被调用。
beforeRouteLeave 导航离开该组件的对应路由时调用,可以访问组件实例 this。

导航守卫的三个参数

to:即将要进入的目标 路由对象。

from:当前导航正要离开的路由对象。

next:函数,必须调用,不然路由跳转不过去。

触发钩子的完整顺序

说说你对router-link的了解

<router-link>是Vue-Router的内置组件,在具有路由功能的应用中作为声明式的导航使用。

<router-link>有8个props,其作用是:

props 作用
to 必填,表示目标路由的链接。User
replace 默认值为false,若设置的话,当点击时,会调用router.replace()
append 设置 append 属性后,则在当前 (相对) 路径前添加基路径。
tag 渲染成tag设置的标签,如tag:'li',渲染结果为
  • foo
  • active-class 默认值为router-link-active,设置链接激活时使用的 CSS 类名。
    exact-active-class 默认值为router-link-exact-active,设置链接被精确匹配的时候应该激活的 class。
    exact 是否精确匹配,默认为false。
    event 声明可以用来触发导航的事件。

    Vuex(重点)

    vuex 的个人理解

    vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)

    主要包括以下几个模块:

    Vuex中action和mutation的区别

    Redux 和 Vuex的区别

    (1)Redux 和 Vuex区别

    通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;

    (2)共同思想

    本质上:redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案; 形式上:vuex借鉴了redux,将store作为全局的数据中心,进行mode管理;

    为什么要使用 Vuex 或者 Redux

    由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。

    所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。

    另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。

    为什么 Vuex 的 mutation 中不能做异步操作?

    Vuex 页面刷新数据丢失怎么解决?

    需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件

    推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。(Redux同理)

    Vuex 中 action 通常是异步的,那么如何知道 action 什么时候结束呢?

    在 action 函数中返回 Promise,然后再提交时候用 then 处理。

    在模块中,getter 和 mutation 和 action 中怎么访问全局的 state 和 getter?

    Vue指令

    平时有用过哪些指令

    v-if、v-show、v-html 的原理

    v-if 和 v-show 的区别

    Vue指令 v-if v-show
    共同点 动态显示DOM元素 动态设置DOM元素
    手段 动态的向DOM树内添加或者删除DOM元素 设置DOM元素的display样式属性控制显示和隐藏
    编译过程 有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件 简单的基于CSS切换
    编译条件 如果初始条件为假,则不进行操作;只有在条件第一次变为真时才开始局部编译 在任何条件下都会被编译,然后被缓存,而且DOM元素保留
    性能消耗 v-if有更高的切换消耗 v-show有更高的初始渲染消耗
    使用场景 v-if适合条件不太可能改变,也就是不需要频繁切换条件的场景 v-show适合频繁切换的场景

    v-if和v-for中key的作用

    vue 中 key 值的作用可以分为两种情况来考虑,

    题外话:当然,在开发过程并非一定要使用key,如果只是为了简单展示数据,其实也可以index来标识,视情况而定就好啦。

    v-for 与 v-if 的优先级

    1、v-for优先于v-if被解析;

    2、如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能;

    3、要避免出现这种情况,则在外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环;

    4、如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项。

    为什么不建议用index作为key?

    使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。

    为什么避免 v-if 和 v-for 同时使用

    vue2.x 中v-for优先级高于v-if,vue3.x 相反。所以2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用,造成性能浪费;3.x 版本中 v-if 总是优先于 v-for 生效,导致v-if访问不了v-for中的变量。

    解析:

    一般我们在两种常见的情况下会倾向于这样做:

    当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级,所以这个模板:

    <ul>
      <li
        v-for="user in users"
        v-if="user.isActive"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>

    将会经过如下运算:

    this.users.map(function (user) {
      if (user.isActive) {
        return user.name
      }
    })

    因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。

    通过将其更换为在如下的一个计算属性上遍历:

    computed: {
      activeUsers: function () {
        return this.users.filter(function (user) {
          return user.isActive
        })
      }
    }
    <ul>
      <li
        v-for="user in activeUsers"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>

    我们将会获得如下好处:

    为了获得同样的好处,我们也可以把:

    <ul>
      <li
        v-for="user in users"
        v-if="shouldShowUsers"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>

    更新为:

    <ul v-if="shouldShowUsers">
      <li
        v-for="user in users"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>

    通过将 v-if 移动到容器元素,我们不会再对列表中的每个用户检查 shouldShowUsers。取而代之的是,我们只检查它一次,且不会在 shouldShowUsers 为否的时候运算 v-for。

    反例:

    <ul>
      <li
        v-for="user in users"
        v-if="user.isActive"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>
    
    <ul>
      <li
        v-for="user in users"
        v-if="shouldShowUsers"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>

    正确例子

    <ul>
      <li
        v-for="user in activeUsers"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>
    
    <ul v-if="shouldShowUsers">
      <li
        v-for="user in users"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>

    v-on可以绑定多个方法吗

    <p v-on="{click:one,mousemove:two}">v-on绑定多个方法</p>//这里绑定一个点击事件和鼠标移动事件

    Vue属性

    methods watch和compute的区别

    - computed watch methods
    作用机制 自动调用,完成我们希望完成的作用 自动调用,完成我们希望完成的作用 主动调用
    性质 计算属性,事实上和data对象里的数据属性是相同的 类似于监听机制跟事件机制 定义的是函数,使用时跟函数调用一样
    缓存 支持缓存,只有依赖的数据发生了变化,才会重新计算 不支持缓存,数据变化时,它就会触发相应的操作
    是否支持异步 不支持异步,当 Computed 中有异步操作时,无法监听数据的变化 支持异步监听 支持异步处理
    场景 一个数据受多个数据影响 一个数据影响多个数据 提供可调用的函数

    watch

    computed

    虚拟DOM(重点/加分项)

    讲一下Virtual DOM

    由于在浏览器中操作 DOM 是很昂贵的。频繁操作 DOM,会产生一定性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

    优点:

    缺点:

    了解diff算法吗

    拓展

    patchVnode函数做了哪些操作

    updateChildren方法

    另外,如果一开始oldL在newNode的指针找不到时,新列表的第一个节点b去旧列表进行遍历比较,这里会有两种情况,找到相同节点没找到相同节点

    找到的情况,在旧节点中找到相同节点b,将节点b移动到首位,然后重新开始进行双端的步骤对比。如果在旧节点找不到,则在头部直接添加新节点,并将newL指针指向下一位,再继续进行对比。

    拓展

    (其他)Vue 3.0

    Vue3.0有什么更新

    (1)监测机制的改变

    (2)只能监测属性,不能监测对象

    (3)模板

    (4)对象式的组件声明方式

    (5)其它方面的更改

    Vue3.0 为什么要用 proxy?

    在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶

    Proxy 实现的响应式原理与 Vue2的实现原理相同,实现方式大同小异∶

    了解过Vue插槽吗,有几种?

    slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。

    实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在 vm.slot中,默认插槽为 vm.slot.default,具名插槽为 vm.slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

    (其他)对 React 和 Vue 的理解,它们的异同

    相似之处:

    不同之处 :

    1)数据流

    Vue默认支持数据双向绑定,而React一直提倡单向数据流

    2)虚拟DOM

    Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。

    3)组件化

    React与Vue最大的不同是模板的编写。

    具体来讲:React中render函数是支持闭包特性的,所以import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。

    4)监听数据变化的实现原理不同

    5)高阶组件

    react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。

    高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不能采用HOC来实现。

    6)构建工具

    两者都有自己的构建工具:

    7)跨平台

    有做过哪些性能优化?

    服务端渲染了解过吗?

    大致流程就是将 Source(源码)通过 webpack 打包出两个 bundle,其中 Server Bundle 是给服务端用的,服务端通过渲染器 bundleRenderer 将 bundle 生成 html 给浏览器用;另一个 Client Bundle 是给浏览器用的,别忘了服务端只是生成前期首屏页面所需的 html ,后期的交互和数据处理还是需要能支持浏览器脚本的 Client Bundle 来完成。

    一个小栗子

    // 第 1 步:创建一个 Vue 实例
    const Vue = require('vue')
    const app = new Vue({
      template: `<div>Hello World</div>`
    })
    // 第 2 步:创建一个 renderer
    const renderer = require('vue-server-renderer').createRenderer()
    // 第 3 步:将 Vue 实例渲染为 HTML
    renderer.renderToString(app, (err, html) => {
      if (err) throw err
      console.log(html)
      // => <div data-server-rendered="true">Hello World</div>
    })

    上面例子利用 vue-server-renderer npm 包将一个vue示例最后渲染出了一段 html。将这段html发送给客户端就轻松的实现了服务器渲染了。

    const server = require('express')()
    server.get('*', (req, res) => {
      // ... 生成 html
      res.end(html)
    })
    server.listen(8080)

    服务端渲染和客户端渲染的区别

    - 客户端渲染 服务端渲染
    html的生成原理 由js生成html 由后台语言通过一些模板引擎生成
    优点 前端做视图和交互 响应快,用户体验好
    后端提供接口,数据 搜索引擎友好,有seo优化
    前后端分离 nodejs层服务器渲染,前端性能优化更顺手
    前端做路由 可操作空间更大
    服务器计算压力变轻
    缺点 用户等待时间变长,尤其是请求数多且有一定先后顺序的时候 增加服务器计算压力;如果不是增加node中间层,前后端分工不明,不能很好的并行并发
    耗时比较 数据请求:客户端在不同网络环境进行数据请求,外网http请求开销大,导致时间差 数据请求:服务端在内网请求,数据响应速度快
    步骤:客户端需要等待js代码下载,加载完成在请求数据,渲染 步骤:服务端是先请求数据再渲染可视化部分,即服务端不需要等待js代码下载,并会返回一个已经有内容的页面
    渲染内容:客户端渲染,是经历一个从无到有完整的渲染步骤 渲染内容:服务端先渲染可视化部分,客户端再做二次渲染
    适合场景 单页面应用,如Vue 用户体验比较高的比如首屏加载,重复较多的公共页面可以使用服务器渲染,减少ajax请求,提高用户体验

    讲讲图片懒加载

    <img v-lazy="/static/img/01.png"/>

    gzip 压缩了解多少

    gizp压缩是一种http请求优化方式,通过减少文件体积来提高加载速度。html、js、css文件甚至json数据都可以用它压缩,可以减小60%以上的体积。前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。

    牛富贵:命令行执行:npm i compression-webpack-plugin -D

    牛富贵:在webpack的dev开发配置文件中加入如下代码:

    const CompressionWebpackPlugin = require('compression-webpack-plugin')
    plugins: [
       new CompressionWebpackPlugin()
    ]

    启用gzip压缩打包之后,会自动生成gz包。目前大部分主流浏览器客户端都是支持gzip的,不支持gzip格式文件的会默认访问源文件的,故不要配置清除源文件。配置好之后,打开浏览器访问线上,F12查看控制台,如果该文件资源的响应头里显示有Content-Encoding: gzip,表示浏览器支持并且启用了Gzip压缩的资源。

    实现一个axios拦截器

    新建 request.js 文件,导入 axios
    const request = axios.create({
      baseURL: xxx,
      // baseURL: '项目基地址'
      timeout: 5000 // 设置 5 秒延时关闭请求
    }) 
    // request.interceptors.request.use() // 请求拦截器
    request.interceptors.request.use(config => {
    
      config.headers.Authorization = `Bearer ${token}` // 设置请求头携带 token
      return config 
    }, error => {
      console.log(error) // 发生错误打印
      return error
    })
    // request.interceptors.response.use() // 响应拦截器
    request.interceptors.response.use(config => {
      return config // 成功直接返回
    }, error => {
      if (error.response.status === 401) { //如果发生错误,查看错误码是多少 401 为权限不够,token 过期
        alert('token 请求超时!请重新登录!')
        // 进行操作,如删除 vuex 中过期用户数据等一系列操作
        router.push('/login') // 强行返回到登录页
      }
      return error
    })
    export default request

    如何中断axios请求

    官方提供了两种方法

    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    
    axios.get('/user/12345', {
      cancelToken: source.token
    }).catch(function(thrown) {
      if (axios.isCancel(thrown)) {
        console.log('Request canceled', thrown.message);
      } else {
         // 处理错误
      }
    });
    
    axios.post('/user/12345', {
      name: 'new name'
    }, {
      cancelToken: source.token
    })
    
    // 取消请求(message 参数是可选的)
    source.cancel('Operation canceled by the user.');
    const CancelToken = axios.CancelToken;
    let cancel;
    
    axios.get('/user/12345', {
      cancelToken: new CancelToken(function executor(c) {
        // executor 函数接收一个 cancel 函数作为参数
        cancel = c;
      })
    });
    
    // cancel the request
    cancel();

    Vue渲染大量数据时应该怎么优化

    对于长列表,通常分两种情况来优化。

    export default {
      data: () => {
        return {
          users: [
            /* a long static list */
          ]
        };
      },
      async create() {
        const users = await axios.get("/users");
        //数据冻结
        this.users = Object.freeze(users);
      }
    };

    Object.freeze() 方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。

    //举个栗子
    let arr = [0];
    Object.freeze(a); // 数组中数据不能被修改了.
    
    arr[0] = 1; // fails silently
    arr.push(2); // fails silently

    1、假设有 1 万条记录需要同时渲染,我们屏幕的可见区域的高度为 500px,而列表项的高度为 50px,则此时我们在屏幕中最多只能看到 10 个列表项,那么在首次渲染的时候,我们只需加载 10 条即可。

    2、当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域应该显示的列表项。

    3、假设滚动发生,滚动条距顶部的位置为 150px,则我们可得知在可见区域内的列表项为第 4 项至第 13 项。

    4、实现

    虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

    5、页面结构

    //可视化区域容器
    <div class="list-container">
        //真实数据容器,以便生成滚动条
        <div class="list-phantom"></div>
        //渲染区域
        <div class="list">
          <!-- item(1) -->
          <!-- item(2) -->
          <!-- ...... -->
          <!-- item(3) -->
        </div>
    </div>

    6、接着,监听 list-containerscroll事件,获取滚动位置 scrollTop

    推算出:

    7、完整版

    <template>
      <div ref="list" class="list-container" @scroll="scrollEvent($event)">
        <div class="list-phantom" :style="{ height: listHeight + 'px' }"></div>
        <div class="list" :style="{ transform: getTransform }">
          <div ref="items"
            class="list-item"
            v-for="item in visibleData"
            :key="item.id"
            :style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
          >{{ item.value }}</div>
        </div>
      </div>
    </template>
    export default {
      name:'VirtualList',
      props: {
        //所有列表数据
        listData:{
          type:Array,
          default:()=>[]
        },
        //每项高度
        itemSize: {
          type: Number,
          default:200
        }
      },
      computed:{
        //列表总高度
        listHeight(){
          return this.listData.length * this.itemSize;
        },
        //可显示的列表项数
        visibleCount(){
          return Math.ceil(this.screenHeight / this.itemSize)
        },
        //偏移量对应的 style
        getTransform(){
          return `translate3d(0,${this.startOffset}px,0)`;
        },
        //获取真实显示列表数据
        visibleData(){
          return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
        }
      },
      mounted() {
        this.screenHeight = this.$el.clientHeight;
        this.start = 0;
        this.end = this.start + this.visibleCount;
      },
      data() {
        return {
          //可视区域高度
          screenHeight:0,
          //偏移量
          startOffset:0,
          //起始索引
          start:0,
          //结束索引
          end:null,
        };
      },
      methods: {
        scrollEvent() {
          //当前滚动位置
          let scrollTop = this.$refs.list.scrollTop;
          //此时的开始索引
          this.start = Math.floor(scrollTop / this.itemSize);
          //此时的结束索引
          this.end = this.start + this.visibleCount;
          //此时的偏移量
          this.startOffset = scrollTop - (scrollTop % this.itemSize);
        }
      }
    };