写点什么

滴滴前端高频 vue 面试题(边面边更)

作者:bb_xiaxia1998
  • 2023-03-13
    浙江
  • 本文字数:20996 字

    阅读完需:约 69 分钟

Vue-router 路由模式有几种

vue-router3 种路由模式:hashhistoryabstract,对应的源码如下所示


switch (mode) {    case 'history':    this.history = new HTML5History(this, options.base)    break    case 'hash':    this.history = new HashHistory(this, options.base, this.fallback)      break    case 'abstract':    this.history = new AbstractHistory(this, options.base)      break  default:    if (process.env.NODE_ENV !== 'production') {      assert(false, `invalid mode: ${mode}`)    }}
复制代码


其中,3 种路由模式的说明如下:


  • hash: 使用 URL hash 值来作路由,支持所有浏览器

  • history : 依赖 HTML5 History API 和服务器配置

  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

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

  • Vuex 中所有的状态更新的唯一途径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。

  • 每个 mutation 执行完成后都会对应到一个新的状态变更,这样 devtools 就可以打个快照存下来,然后就可以实现 time-travel 了。如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。

Vue 组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信


组件传参的各种方式



组件通信常用方式有以下几种


  • props / $emit 适用 父子组件通信

  • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的

  • ref$parent / $children(vue3废弃) 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法

  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

  • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件

  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

  • provide / inject 适用于 隔代组件通信

  • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系

  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用

  • Vuex 适用于 父子、隔代、兄弟组件通信

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。


根据组件之间关系讨论组件通信最为清晰有效


  • 父子组件:props/$emit/$parent/ref

  • 兄弟组件:$parent/eventbus/vuex

  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root


下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯


1. 父子组件通信


使用props,父组件可以使用props向子组件传递数据。


父组件vue模板father.vue:


<template>  <child :msg="message"></child></template>
<script>import child from './child.vue';export default { components: { child }, data () { return { message: 'father message'; } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <div>{{msg}}</div></template>
<script>export default { props: { msg: { type: String, required: true } }}</script>
复制代码


回调函数(callBack)


父传子:将父组件里定义的method作为props传入子组件


// 父组件Parent.vue:<Child :changeMsgFn="changeMessage">methods: {    changeMessage(){        this.message = 'test'    }}
复制代码


// 子组件Child.vue:<button @click="changeMsgFn">props:['changeMsgFn']
复制代码


子组件向父组件通信


父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件


父组件vue模板father.vue:


<template>    <child @msgFunc="func"></child></template>
<script>import child from './child.vue';export default { components: { child }, methods: { func (msg) { console.log(msg); } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <button @click="handleClick">点我</button></template>
<script>export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } }}</script>
复制代码


2. provide / inject 跨级访问祖先组件的数据


父组件通过使用provide(){return{}}提供需要传递的数据


export default {  data() {    return {      title: '我是父组件',      name: 'poetry'    }  },  methods: {    say() {      alert(1)    }  },  // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法  provide() {    return {      message: '我是祖先组件提供的数据',      name: this.name, // 传递属性      say: this.say    }  }}
复制代码


子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数


<template>  <p>曾孙组件</p>  <p>{{message}}</p></template><script>export default {  // inject 注入/接收祖先组件传递的所需要的数据即可   //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}  inject: [ "message","say"],  mounted() {    this.say();  },};</script>
复制代码


3. children 获取父组件实例和子组件实例的集合


  • this.$parent 可以直接访问该组件的父实例或组件

  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的


<!-- parent.vue --><template><div>  <child1></child1>     <child2></child2>   <button @click="clickChild">$children方式获取子组件值</button></div></template><script>import child1 from './child1'import child2 from './child2'export default {  data(){    return {      total: 108    }  },  components: {    child1,    child2    },  methods: {    funa(e){      console.log("index",e)    },    clickChild(){      console.log(this.$children[0].msg);      console.log(this.$children[1].msg);    }  }}</script>
复制代码


<!-- child1.vue --><template>  <div>    <button @click="parentClick">点击访问父组件</button>  </div></template><script>export default {  data(){    return {      msg:"child1"    }  },  methods: {    // 访问父组件数据    parentClick(){      this.$parent.funa("xx")      console.log(this.$parent.total);    }  }}</script>
复制代码


<!-- child2.vue --><template>  <div>    child2  </div></template><script>export default {  data(){    return {     msg: 'child2'    }  }}</script>
复制代码


4. listeners 多级组件通信


$attrs 包含了从父组件传过来的所有props属性


// 父组件Parent.vue:<Child :name="name" :age="age"/>
// 子组件Child.vue:<GrandChild v-bind="$attrs" />
// 孙子组件GrandChild<p>姓名:{{$attrs.name}}</p><p>年龄:{{$attrs.age}}</p>
复制代码


$listeners包含了父组件监听的所有事件


// 父组件Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件Child.vue:<button @click="$listeners.changeNameFn"></button>
复制代码


5. ref 父子组件通信


// 父组件Parent.vue:<Child ref="childComp"/><button @click="changeName"></button>changeName(){    console.log(this.$refs.childComp.age);    this.$refs.childComp.changeAge()}
// 子组件Child.vue:data(){ return{ age:20 }},methods(){ changeAge(){ this.age=15 }}
复制代码


6. 非父子, 兄弟组件之间通信


vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js可以是这样:


// Bus.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的功能
复制代码


<template>    <button @click="toBus">子组件传给兄弟组件</button></template>
<script>export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } }}</script>
复制代码


另一个组件也在钩子函数中监听on事件


export default {  data() {    return {      message: ''    }  },  mounted() {    this.$bus.$on('foo', (msg) => {      this.message = msg    })  }}
复制代码


7. $root 访问根组件中的属性或方法


  • 作用:访问根组件中的属性或方法

  • 注意:是根组件,不是父组件。$root只对根组件有用


var vm = new Vue({  el: "#app",  data() {    return {      rootInfo:"我是根元素的属性"    }  },  methods: {    alerts() {      alert(111)    }  },  components: {    com1: {      data() {        return {          info: "组件1"        }      },      template: "<p>{{ info }} <com2></com2></p>",      components: {        com2: {          template: "<p>我是组件1的子组件</p>",          created() {            this.$root.alerts()// 根组件方法            console.log(this.$root.rootInfo)// 我是根元素的属性          }        }      }    }  }});
复制代码


8. vuex


  • 适用场景: 复杂关系的组件数据传递

  • Vuex 作用相当于一个用来存储共享变量的容器



  • state用来存放共享变量的地方

  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值

  • mutations用来存放修改state的方法。

  • actions也是用来存放修改 state 的方法,不过action是在mutations的基础上进行。常用来做一些异步操作


小结


  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref

  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递

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

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

Vue 中组件和插件有什么区别

1. 组件是什么


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


组件的优势


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

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

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


2. 插件是什么


插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:


  • 添加全局方法或者属性。如: vue-custom-element

  • 添加全局资源:指令/过滤器/过渡等。如 vue-touch

  • 通过全局混入来添加一些组件选项。如vue-router

  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。

  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如vue-router


3. 两者的区别


两者的区别主要表现在以下几个方面:


  • 编写形式

  • 注册形式

  • 使用场景


3.1 编写形式


编写组件


编写一个组件,可以有很多方式,我们最常见的就是 vue 单文件的这种格式,每一个.vue文件我们都可以看成是一个组件


vue 文件标准格式


<template></template><script>export default{     ...}</script><style></style>
复制代码


我们还可以通过template属性来编写一个组件,如果组件内容多,我们可以在外部定义template组件内容,如果组件内容并不多,我们可直接写在template属性上


<template id="testComponent">     // 组件显示的内容    <div>component!</div>   </template>
Vue.component('componentA',{ template: '#testComponent' template: `<div>component</div>` // 组件内容少可以通过这种形式})
复制代码


编写插件


vue插件的实现应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象


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) { // 逻辑... }}
复制代码


3.2 注册形式


组件注册


vue 组件注册主要分为全局注册局部注册


全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项


Vue.component('my-component-name', { /* ... */ })
复制代码


局部注册只需在用到的地方通过components属性注册一个组件


const component1 = {...} // 定义一个组件
export default { components:{ component1 // 局部注册 }}
复制代码


插件注册


插件的注册通过Vue.use()的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项


Vue.use(插件名字,{ /* ... */} )
复制代码


注意的是:


注册插件的时候,需要在调用 new Vue() 启动应用之前完成


Vue.use会自动阻止多次注册相同插件,只会注册一次


4. 使用场景


  • 组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue

  • 插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身


简单来说,插件就是指对Vue的功能的增强或补充

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新


源码相关


get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
复制代码

Vue 中 diff 算法原理

DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用 diff 算法


vuediff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)的方式进行比较。


简单来说,Diff 算法有以下过程


  • 同级比较,再比较子节点(根据keytag标签名判断)

  • 先判断一方有子节点和一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)

  • 比较都有子节点的情况(核心diff)

  • 递归比较子节点

  • 正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以VueDiff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

  • Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比ReactDiff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅

  • 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升



vue3 中采用最长递增子序列来实现diff优化


回答范例


思路


  • diff算法是干什么的

  • 它的必要性

  • 它何时执行

  • 具体执行方式

  • 拔高:说一下vue3中的优化


回答范例


  1. Vue中的diff算法称为patching算法,它由Snabbdom 修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换

  2. 最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOMpatching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新

  3. vuediff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch 函数,并传入新旧两次虚拟 DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作

  4. patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3patch为例


  • 首先判断两个节点是否为相同同类节点,不同则删除重新创建

  • 如果双方都是文本则更新文本内容

  • 如果双方都是元素节点则递归更新子元素,同时更新元素属性

  • 更新子节点时又分了几种情况

  • 新的子节点是文本,老的子节点是数组则清空,并设置文本;

  • 新的子节点是文本,老的子节点是文本则直接更新文本;

  • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;

  • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节 blabla

  • vue3中引入的更新策略:静态节点标记等


vdom 中 diff 算法的简易实现


以下代码只是帮助大家理解diff算法的原理和流程


  1. vdom转化为真实dom


const createElement = (vnode) => {  let tag = vnode.tag;  let attrs = vnode.attrs || {};  let children = vnode.children || [];  if(!tag) {    return null;  }  //创建元素  let elem = document.createElement(tag);  //属性  let attrName;  for (attrName in attrs) {    if(attrs.hasOwnProperty(attrName)) {      elem.setAttribute(attrName, attrs[attrName]);    }  }  //子元素  children.forEach(childVnode => {    //给elem添加子元素    elem.appendChild(createElement(childVnode));  })
//返回真实的dom元素 return elem;}
复制代码


  1. 用简易diff算法做更新操作


function updateChildren(vnode, newVnode) {  let children = vnode.children || [];  let newChildren = newVnode.children || [];
children.forEach((childVnode, index) => { let newChildVNode = newChildren[index]; if(childVnode.tag === newChildVNode.tag) { //深层次对比, 递归过程 updateChildren(childVnode, newChildVNode); } else { //替换 replaceNode(childVnode, newChildVNode); } })}
复制代码


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

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。

  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。


components:{   AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }
复制代码


原理


export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {     // async component     let asyncFactory     if (isUndef(Ctor.cid)) {         asyncFactory = Ctor         Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend         // 第二次渲染时Ctor不为undefined         if (Ctor === undefined) {             return createAsyncPlaceholder( // 渲染占位符 空虚拟节点                 asyncFactory,                 data,                 context,                 children,                 tag             )         }     } }function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {     if (isDef(factory.resolved)) {         // 3.在次渲染时可以拿到获取的最新组件         return factory.resolved     }    const resolve = once((res: Object | Class<Component>) => {         factory.resolved = ensureCtor(res, baseCtor)         if (!sync) {             forceRender(true) //2. 强制更新视图重新渲染         } else {             owners.length = 0         }     })    const reject = once(reason => {         if (isDef(factory.errorComp)) {             factory.error = true forceRender(true)         }     })    const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后     sync = false     return factory.resolved }
复制代码

从 0 到 1 自己构架一个 vue 项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织

综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可


思路


  • 构建项目,创建项目基本结构

  • 引入必要的插件:

  • 代码规范:prettiereslint

  • 提交规范:husky,lint-staged`

  • 其他常用:svg-loadervueusenprogress

  • 常见目录结构


回答范例


  1. 0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件

  2. 目前vue3项目我会用vite或者create-vue创建项目

  3. 接下来引入必要插件:路由插件vue-router、状态管理vuex/piniaui库我比较喜欢element-plus 和antd-vuehttp工具我会选axios

  4. 其他比较常用的库有vueusenprogress,图标可以使用vite-svg-loader

  5. 下面是代码规范:结合prettiereslint即可

  6. 最后是提交规范,可以使用huskylint-stagedcommitlint

  7. 目录结构我有如下习惯: .vscode:用来放项目中的 vscode 配置


  • plugins:用来放 vite 插件的 plugin 配置

  • public:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下

  • src:用来放项目代码文件

  • api:用来放http的一些接口配置

  • assets:用来放一些 CSS 之类的静态资源

  • components:用来放项目通用组件

  • layout:用来放项目的布局

  • router:用来放项目的路由配置

  • store:用来放状态管理Pinia的配置

  • utils:用来放项目中的工具方法类

  • views:用来放项目的页面文件

keep-alive 使用场景和原理

  • keep-aliveVue 内置的一个组件, 可以实现组件缓存 ,当组件切换时不会对当前组件进行卸载。 一般结合路由和动态组件一起使用 ,用于缓存组件

  • 提供 includeexclude 属性, 允许组件有条件的进行缓存 。两者都支持字符串或正则表达式,include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include

  • 对应两个钩子函数 activateddeactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated

  • keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰


  • <keep-alive></keep-alive> 包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染

  • 比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染


关于 keep-alive 的基本用法


<keep-alive>  <component :is="view"></component></keep-alive>
复制代码


使用includesexclude


<keep-alive include="a,b">  <component :is="view"></component></keep-alive>
<!-- 正则表达式 (使用 `v-bind`) --><keep-alive :include="/a|b/"> <component :is="view"></component></keep-alive>
<!-- 数组 (使用 `v-bind`) --><keep-alive :include="['a', 'b']"> <component :is="view"></component></keep-alive>
复制代码


匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配


设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):


  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated

  • 再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated


使用场景


使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive


举个栗子:


当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive


首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive


在路由中设置keepAlive属性判断是否需要缓存


{  path: 'list',  name: 'itemList', // 列表页  component (resolve) {    require(['@/pages/item/list'], resolve) }, meta: {  keepAlive: true,  title: '列表页' }}
复制代码


使用<keep-alive>


<div id="app" class='wrapper'>    <keep-alive>        <!-- 需要缓存的视图组件 -->         <router-view v-if="$route.meta.keepAlive"></router-view>     </keep-alive>      <!-- 不需要缓存的视图组件 -->     <router-view v-if="!$route.meta.keepAlive"></router-view></div>
复制代码


思考题:缓存后如何获取数据


解决方案可以有以下两种:


  • beforeRouteEnter:每次组件渲染的时候,都会执行beforeRouteEnter


beforeRouteEnter(to, from, next){    next(vm=>{        console.log(vm)        // 每次进入路由执行        vm.getData()  // 获取数据    })},
复制代码


  • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子


// 注意:服务器端渲染期间avtived不被调用activated(){  this.getData() // 获取数据},
复制代码


扩展补充:LRU 算法是什么?



LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件


相关代码


keep-alivevue中内置的一个组件


源码位置:src/core/components/keep-alive.js


export default {  name: "keep-alive",  abstract: true, //抽象组件
props: { include: patternTypes, //要缓存的组件 exclude: patternTypes, //要排除的组件 max: [String, Number], //最大缓存数 },
created() { this.cache = Object.create(null); //缓存对象 {a:vNode,b:vNode} this.keys = []; //缓存组件的key集合 [a,b] },
destroyed() { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys); } },
mounted() { //动态监听include exclude this.$watch("include", (val) => { pruneCache(this, (name) => matches(val, name)); }); this.$watch("exclude", (val) => { pruneCache(this, (name) => !matches(val, name)); }); },
render() { const slot = this.$slots.default; //获取包裹的插槽默认值 获取默认插槽中的第一个组件节点 const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件 // 获取该组件节点的componentOptions const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions; if (componentOptions) { // 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag const name: ?string = getComponentName(componentOptions); const { include, exclude } = this; // 不走缓存 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode if ( // not included 不包含 (include && (!name || !matches(include, name))) || // excluded 排除里面 (exclude && name && matches(exclude, name)) ) { //返回虚拟节点 return vnode; }
const { cache, keys } = this; // 获取组件的key值 const key: ?string = vnode.key == null ? // same constructor may get registered as different local components // so cid alone is not enough (#3269) componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key; // 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 if (cache[key]) { //通过key 找到缓存 获取实例 vnode.componentInstance = cache[key].componentInstance; // make current key freshest remove(keys, key); //通过LRU算法把数组里面的key删掉 keys.push(key); //把它放在数组末尾 } else { cache[key] = vnode; //没找到就换存下来 keys.push(key); //把它放在数组末尾 // prune oldest entry //如果超过最大值就把数组第0项删掉 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode); } }
vnode.data.keepAlive = true; //标记虚拟节点已经被缓存 } // 返回虚拟节点 return vnode || (slot && slot[0]); },};
复制代码


可以看到该组件没有template,而是用了render,在组件渲染的时候会自动执行render函数


this.cache是一个对象,用来存储需要缓存的组件,它将以如下形式存储:


this.cache = {  'key1':'组件1',  'key2':'组件2',  // ...}
复制代码


在组件销毁的时候执行pruneCacheEntry函数


function pruneCacheEntry (  cache: VNodeCache,  key: string,  keys: Array<string>,  current?: VNode) {  const cached = cache[key]  /* 判断当前没有处于被渲染状态的组件,将其销毁*/  if (cached && (!current || cached.tag !== current.tag)) {    cached.componentInstance.$destroy()  }  cache[key] = null  remove(keys, key)}
复制代码


mounted钩子函数中观测 includeexclude 的变化,如下:


mounted () {  this.$watch('include', val => {      pruneCache(this, name => matches(val, name))  })  this.$watch('exclude', val => {      pruneCache(this, name => !matches(val, name))  })}
复制代码


如果includeexclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下


function pruneCache (keepAliveInstance, filter) {  const { cache, keys, _vnode } = keepAliveInstance  for (const key in cache) {    const cachedNode = cache[key]    if (cachedNode) {      const name = getComponentName(cachedNode.componentOptions)      if (name && !filter(name)) {        pruneCacheEntry(cache, key, keys, _vnode)      }    }  }}
复制代码


在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可


关于keep-alive的最强大缓存功能是在render函数中实现


首先获取组件的key值:


const key = vnode.key == null? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : ''): vnode.key
复制代码


拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:


/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */if (cache[key]) {    vnode.componentInstance = cache[key].componentInstance    /* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */    remove(keys, key)    keys.push(key)} 
复制代码


直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个


this.cache对象中没有该key值的情况,如下:


/* 如果没有命中缓存,则将其设置进缓存 */else {    cache[key] = vnode    keys.push(key)    /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */    if (this.max && keys.length > parseInt(this.max)) {        pruneCacheEntry(cache, keys[0], keys, this._vnode)    }}
复制代码


表明该组件还没有被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys


此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉

vue-router 动态路由是什么

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果


const User = {  template: "<div>User</div>",};
const router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头 { path: "/user/:id", component: User }, ],});
复制代码


问题: vue-router 组件复用导致路由参数失效怎么办?


解决方法:


  1. 通过 watch 监听路由参数再发请求


watch: { //通过watch来监听路由变化 "$route": function(){  this.getData(this.$route.params.xxx); }}
复制代码


  1. :key 来阻止“复用”


<router-view :key="$route.fullPath" />
复制代码


回答范例


  1. 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由

  2. 例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数

  3. 路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。

  4. 参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query$route.hash

diff 算法

时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue 进行优化转化成 O(n)


理解:


  • 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点

  • 扩展 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加 key 只会移动减少操作 DOM。

  • 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。

  • 只进行同层比较,不会进行跨层比较。


diff 算法的优化策略:四种命中查找,四个指针


  1. 旧前与新前(先比开头,后插入和删除节点的这种情况)

  2. 旧后与新后(比结尾,前插入或删除的情况)

  3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)

  4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)

Class 与 Style 如何动态绑定

Class 可以通过对象语法和数组语法进行动态绑定


对象语法:


<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: { isActive: true, hasError: false}
复制代码


数组语法:


<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: { activeClass: 'active', errorClass: 'text-danger'}
复制代码


Style 也可以通过对象语法和数组语法进行动态绑定


对象语法:


<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: { activeColor: 'red', fontSize: 30}
复制代码


数组语法:


<div v-bind:style="[styleColor, styleSize]"></div>
data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' }}
复制代码

Vue template 到 render 的过程

vue 的模版编译过程主要如下:template -> ast -> render 函数


vue 在模版编译版本的码中会执行 compileToFunctions 将 template 转化为 render 函数:


// 将模板编译为render函数const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
复制代码


CompileToFunctions 中的主要逻辑如下∶ (1)调用 parse 方法将 template 转化为 ast(抽象语法树)


constast = parse(template.trim(), options)
复制代码


  • parse 的目标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。

  • 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造 AST 树的目的。


AST 元素节点总共三种类型:type 为 1 表示普通元素、2 为表达式、3 为纯文本


(2)对静态节点做优化


optimize(ast,options)
复制代码


这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化


深度遍历 AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用。


(3)生成代码


const code = generate(ast, options)
复制代码


generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``) 生成 render 函数。

什么是 mixin ?

  • Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。

  • 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。

  • 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

Vue2.x 响应式数据原理

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


对象内部通过 defineReactive 方法,使用 Object.defineProperty 来劫持各个属性的 settergetter(只会劫持已经存在的属性),数组则是通过重写数组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)
复制代码


源码分析



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为例说明,通过数据响应式加上虚拟DOMpatch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的 DOM 操作,从而大大提升开发效率,降低开发难度

  • vue2中的数据响应式会根据数据类型来做不同处理,如果是 对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的 7 个变更方法 ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的MapSet这些数据结构不支持等问题

  • 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了

vue 和 react 的区别

=> 相同点:


1. 数据驱动页面,提供响应式的试图组件2. 都有virtual DOM,组件化的开发,通过props参数进行父子之间组件传递数据,都实现了webComponents规范3. 数据流动单向,都支持服务器的渲染SSR4. 都有支持native的方法,react有React native, vue有wexx
复制代码


=> 不同点:


 1.数据绑定:Vue实现了双向的数据绑定,react数据流动是单向的 2.数据渲染:大规模的数据渲染,react更快 3.使用场景:React配合Redux架构适合大规模多人协作复杂项目,Vue适合小快的项目 4.开发风格:react推荐做法jsx + inline style把html和css都写在js了      vue是采用webpack + vue-loader单文件组件格式,html, js, css同一个文件
复制代码

Vue-router 路由有哪些模式?

一般有两种模式: (1)hash 模式:后面的 hash 值的变化,浏览器既不会向服务器发出请求,浏览器也不会刷新,每次 hash 值的变化会触发 hashchange 事件。 (2)history 模式:利用了 HTML5 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。


其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并


  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。

  • Vue.component你可以创建 ,也可以取组件。


相关代码如下


export default function initExtend(Vue) {  let cid = 0; //组件的唯一标识  // 创建子类继承Vue父类 便于属性扩展  Vue.extend = function (extendOptions) {    // 创建子类的构造函数 并且调用初始化方法    const Sub = function VueComponent(options) {      this._init(options); //调用Vue初始化方法    };    Sub.cid = cid++;    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类    Sub.prototype.constructor = Sub; //constructor指向自己    Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options    return Sub;  };}
复制代码

v-once 的使用场景有哪些

分析


v-onceVue中内置指令,很有用的API,在优化方面经常会用到


体验


仅渲染元素和组件一次,并且跳过未来更新


<!-- single element --><span v-once>This will never change: {{msg}}</span><!-- the element have children --><div v-once>  <h1>comment</h1>  <p>{{msg}}</p></div><!-- component --><my-component v-once :comment="msg"></my-component><!-- `v-for` directive --><ul>  <li v-for="i in list" v-once>{{i}}</li></ul>
复制代码


回答范例


  • v-oncevue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新

  • 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段

  • 我们只需要作用的组件或元素上加上v-once即可

  • vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了

  • 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算


原理


下面例子使用了v-once


<script setup>import { ref } from 'vue'const msg = ref('Hello World!')</script><template>  <h1 v-once>{{ msg }}</h1>  <input v-model="msg"></template>
复制代码


我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:


// ...return (_ctx, _cache) => {  return (_openBlock(), _createElementBlock(_Fragment, null, [    // 从缓存获取vnode    _cache[0] || (      _setBlockTracking(-1),      _cache[0] = _createElementVNode("h1", null, [        _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)      ]),      _setBlockTracking(1),      _cache[0]    ),// ...
复制代码

Vue 组件间通信有哪几种方式?

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。


(1)props / $emit 适用 父子组件通信 这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。


(2)ref 与 $parent / $children 适用 父子组件通信


  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问父 / 子实例


(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。


(4)$attrs/$listeners 适用于 隔代组件通信


  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on事件监听器。它可以通过 v-on="$listeners" 传入内部组件


(5)provide / inject 适用于 隔代组件通信 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。 (6)Vuex 适用于 父子、隔代、兄弟组件通信 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。


  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
滴滴前端高频vue面试题(边面边更)_Vue_bb_xiaxia1998_InfoQ写作社区