前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2023前端二面高频vue面试题集锦1

2023前端二面高频vue面试题集锦1

原创
作者头像
bb_xiaxia1998
发布2023-01-02 10:37:48
1.2K0
发布2023-01-02 10:37:48
举报
文章被收录于专栏:前端开发面试手册

vuex是什么?怎么使用?哪种功能场景使用它?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性

  • vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 使用Vuex解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State 中能有效解决多层级组件嵌套的跨组件通信问题

vuexState 在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State 中呢? 目前主要有两种数据会使用 vuex 进行管理:

  • 组件之间全局共享的数据
  • 通过后端异步请求的数据

包括以下几个模块

  • stateVuex 使用单一状态树,即每个应用将仅仅包含一个store 实例。里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性
  • mutations:更改Vuexstore中的状态的唯一方法是提交mutation
  • gettersgetter 可以对 state 进行计算操作,它就是 store 的计算属性虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
  • actionaction 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态action 可以包含任意异步操作
  • modules:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuexstore对象分割成模块(modules)

modules:项目特别复杂的时候,可以让每一个模块拥有自己的statemutationactiongetters,使得结构非常清晰,方便管理

回答范例

思路

  • 给定义
  • 必要性阐述
  • 何时使用
  • 拓展:一些个人思考、实践经验等

回答范例

  1. Vuex 是一个专为 Vue.js 应用开发的 状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念
  3. Vuex 解决状态管理的同时引入了不少概念:例如statemutationaction等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。
  4. 我在使用vuex过程中感受到一些等

可能的追问

  1. vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?
  2. 刷新浏览器,vuex中的state会重新变为初始状态。解决方案-插件 vuex-persistedstate
  3. actionmutation的区别是什么?为什么要区分它们?

action中处理异步,mutation不可以 mutation做原子操作 action可以整合多个mutation的集合 mutation 是同步更新数据(内部会进行是否为异步方式更新数据的检测) $watch 严格模式下会报错 action 异步操作,可以获取数据后调佣 mutation 提交最终数据

  • 流程顺序:“相应视图—>修改State”拆分成两部分,视图触发ActionAction再触发Mutation`。
  • 基于流程顺序,二者扮演不同的角色:Mutation:专注于修改State,理论上是修改State的唯一途径。Action:业务代码、异步请求
  • 角色不同,二者有不同的限制:Mutation:必须同步执行。Action:可以异步,但不能直接操作State

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

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

源码相关

代码语言:javascript
复制
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() 
}

Vue3速度快的原因

Vue3.0 性能提升体现在哪些方面

  • 代码层面性能优化主要体现在全新响应式API,基于Proxy实现,初始化时间和内存占用均大幅改进;
  • 编译层面做了更多编译优化处理,比如静态标记pachFlagdiff算法增加了一个静态标记,只对比有标记的dom元素)、事件增加缓存静态提升(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff过程;
  • 打包时更好的支持tree-shaking,因此整体体积更小,加载更快
  • ssr渲染以字符串方式渲染

一、编译阶段

试想一下,一个组件结构如下图

代码语言:html
复制
<template>
    <div id="content">
        <p class="text">静态文本</p>
        <p class="text">静态文本</p>
        <p class="text">{ message }</p>
        <p class="text">静态文本</p>
        ...
        <p class="text">静态文本</p>
    </div>
</template>

可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费

因此,Vue3在编译阶段,做了进一步优化。主要有如下:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

1. diff 算法优化

  • Vue 2x 中的虚拟 dom 是进行全量的对比。
  • Vue 3x 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容化

Vue2.x的diff算法

vue2.xdiff算法叫做全量比较,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom对比,即使有些内容是永恒固定不变的

Vue3.0的diff算法

vue3.0diff算法有个叫静态标记(PatchFlag)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了

已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高

代码语言:javascript
复制
export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
                        //上面这个1就是静态标记
 ]))
}

关于静态类型枚举如下

代码语言:javascript
复制
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
// 指示在diff算法中退出优化模式
BALL = -2

2. hoistStatic 静态提升

  • Vue 2x : 无论元素是否参与更新,每次都会重新创建。
  • Vue 3x : 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
代码语言:html
复制
<p>HelloWorld</p>
<p>HelloWorld</p>

<p>{ message }</p>

开启静态提升前

代码语言:javascript
复制
export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
 ]))
}

开启静态提升后编译结果

代码语言:javascript
复制
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _hoisted_1,
  _hoisted_2,
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
 ]))
}

可以看到开启了静态提升后,直接将那两个内容为helloworldp标签声明在外面了,直接就拿来用了。同时 _hoisted_1_hoisted_2 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

3. cacheHandlers 事件监听缓存

  • 默认情况下 绑定事件会被视为动态绑定 ,所以每次都会去追踪它的变化
  • 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
代码语言:html
复制
<div>
 <button @click = 'onClick'>点我</button>
</div>

开启事件侦听器缓存之前:

代码语言:javascript
复制
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
                       // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
 ]))
})

这里有一个8,表示着这个节点有了静态标记,有静态标记就会进行diff算法对比差异,所以会浪费时间

开启事件侦听器缓存之后:

代码语言:javascript
复制
export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("button", {
   onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
  }, "点我")
 ]))
}

上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用

4. SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

代码语言:html
复制
<div>
    <div>
        <span>你好</span>
    </div>
    ...  // 很多个静态属性
    <div>
        <span>{{ message }}</span>
    </div>
</div>

编译后

代码语言:javascript
复制
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><div><span>你好</span>...<div><span>你好</span><div><span>${
    _ssrInterpolate(_ctx.message)
  }</span></div></div>`)
}

二、源码体积

相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking

任何一个函数,如refreactivecomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

代码语言:javascript
复制
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
  setup(props, context) {
    const age = ref(18)

    let state = reactive({
      name: 'test'
    })

    const readOnlyAge = computed(() => age.value++) // 19

    return {
        age,
        state,
        readOnlyAge
    }
  }
});

三、响应式系统

vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式

vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历

  • 可以监听动态属性的添加
  • 可以监听到数组的索引和数组length属性
  • 可以监听删除属性

什么是递归组件?举个例子说明下?

分析

递归组件我们用的比较少,但是在TreeMenu这类组件中会被用到。

体验

组件通过组件名称引用它自己,这种情况就是递归组件

代码语言:html
复制
<template>
  <li>
    <div> {{ model.name }}</div>
    <ul v-show="isOpen" v-if="isFolder">
      <!-- 注意这里:组件递归渲染了它自己 -->
      <TreeItem
        class="item"
        v-for="model in model.children"
        :model="model">
      </TreeItem>
    </ul>
  </li>
<script>
export default {
  name: 'TreeItem',
  // ...
}
</script>

回答范例

  1. 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
  2. 实际开发中类似TreeMenu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
  3. 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。
  4. 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent,这样实际获取的组件就是当前组件本身

原理

递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)

代码语言:javascript
复制
const _component_Comp = _resolveComponent("Comp", true)

就是在传递maybeSelfReference

代码语言:javascript
复制
export function resolveComponent(
  name: string,
  maybeSelfReference?: boolean
): ConcreteComponent | string {
  return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}

resolveAsset中最终返回的是组件自身:

代码语言:javascript
复制
if (!res && maybeSelfReference) {
    // fallback to implicit self-reference
    return Component
}

怎样理解 Vue 的单向数据流

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

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

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

有两种常见的试图改变一个 prop 的情形 :

  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop用作其初始值
代码语言:javascript
复制
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  1. 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
代码语言:javascript
复制
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

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

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

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

怎么缓存当前的组件?缓存后怎么更新

缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3keep-alive有比较大的更新,能说的点比较多

思路

  • 缓存用keep-alive,它的作用与用法
  • 使用细节,例如缓存指定/排除、结合routertransition
  • 组件缓存后更新可以利用activated或者beforeRouteEnter
  • 原理阐述

回答范例

  1. 开发中缓存组件使用keep-alive组件,keep-alivevue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
代码语言:html
复制
<keep-alive>
  <component :is="view"></component>
</keep-alive>
  1. 结合属性includeexclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive
代码语言:html
复制
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. 缓存后如果要获取数据,解决方案可以有以下两种
  2. beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
代码语言:javascript
复制
beforeRouteEnter(to, from, next){
  next(vm=>{
    console.log(vm)
    // 每次进入路由执行
    vm.getData()  // 获取数据
  })
},
  • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子
代码语言:javascript
复制
activated(){
    this.getData() // 获取数据
},
  1. keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于componentis属性是个响应式数据,因此只要它变化,keep-aliverender函数就会重新执行

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文件标准格式

代码语言:html
复制
<template>
</template>
<script>
export default{ 
    ...
}
</script>
<style>
</style>

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

代码语言:html
复制
<template id="testComponent">     // 组件显示的内容
    <div>component!</div>   
</template>

Vue.component('componentA',{ 
    template: '#testComponent'  
    template: `<div>component</div>`  // 组件内容少可以通过这种形式
})

编写插件

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

代码语言:javascript
复制
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方法,第一个参数为组件的名称,第二个参数为传入的配置项

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

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

代码语言:javascript
复制
const component1 = {...} // 定义一个组件

export default {
    components:{
        component1   // 局部注册
    }
}

插件注册

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

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

注意的是:

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

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

4. 使用场景

  • 组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue
  • 插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身

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

Composition API 与 Options API 有什么不同

分析

Vue3最重要更新之一就是Composition API,它具有一些列优点,其中不少是针对Options API暴露的一些问题量身打造。是Vue3推荐的写法,因此掌握好Composition API应用对掌握好Vue3至关重要

What is Composition API?(opens new window)

  • Composition API出现就是为了解决Options API导致相同功能代码分散的现象

体验

Composition API能更好的组织代码,下面用composition api可以提取为useCount(),用于组合、复用

compositon api提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

回答范例

  1. Composition API是一组API,包括:Reactivity API生命周期钩子依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件
  2. Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options APImixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对ts支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixinsprovide/inject
  3. Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益

可能的追问

  1. Composition API能否和Options API一起使用?

可以在同一个组件中使用两个script标签,一个使用vue3,一个使用vue2写法,一起使用没有问题

代码语言:html
复制
<!-- vue3 -->
<script setup>
  // vue3写法
</script>

<!-- 降级vue2 -->
<script>
  export default {
    data() {},
    methods: {}
  }
</script>

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。
  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。
代码语言:javascript
复制
components:{ 
  AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) 
}

原理

代码语言:javascript
复制
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 
}

子组件可以直接改变父组件的数据么,说明原因

这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题

思路

  • 讲讲单项数据流原则,表明为何不能这么做
  • 举几个常见场景的例子说说解决方案
  • 结合实践讲讲如果需要修改父组件状态应该如何做

回答范例

  1. 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告
代码语言:javascript
复制
const props = defineProps(['foo'])
// ❌ 下面行为会被警告, props是只读的!
props.foo = 'bar'
  1. 实际开发过程中有两个场景会想要修改一个属性:

这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:

代码语言:javascript
复制
const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)

这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

代码语言:javascript
复制
const props = defineProps(['size'])
// prop变化,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
  1. 实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性

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:

代码语言:html
复制
<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:

代码语言:html
复制
<template>
    <div>{{msg}}</div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true
    }
  }
}
</script>

回调函数(callBack)

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

代码语言:javascript
复制
// 父组件Parent.vue:
<Child :changeMsgFn="changeMessage">
methods: {
    changeMessage(){
        this.message = 'test'
    }
}
代码语言:javascript
复制
// 子组件Child.vue:
<button @click="changeMsgFn">
props:['changeMsgFn']

子组件向父组件通信

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

父组件vue模板father.vue:

代码语言:html
复制
<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:

代码语言:html
复制
<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{}}提供需要传递的数据

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

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

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

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

  • this.$parent 可以直接访问该组件的父实例或组件
  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的
代码语言:html
复制
<!-- 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>
代码语言:html
复制
<!-- 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>
代码语言:html
复制
<!-- child2.vue -->
<template>
  <div>
    child2
  </div>
</template>
<script>
export default {
  data(){
    return {
     msg: 'child2'
    }
  }
}
</script>

4. $attrs + $listeners多级组件通信

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

代码语言:javascript
复制
// 父组件Parent.vue:
<Child :name="name" :age="age"/>

// 子组件Child.vue:
<GrandChild v-bind="$attrs" />

// 孙子组件GrandChild
<p>姓名:{{$attrs.name}}</p>
<p>年龄:{{$attrs.age}}</p>

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

代码语言:javascript
复制
// 父组件Parent.vue:
<Child :name="name" :age="age" @changeNameFn="changeName"/>

// 子组件Child.vue:
<button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

代码语言:javascript
复制
// 父组件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可以是这样:

代码语言:javascript
复制
// 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的功能  
代码语言:html
复制
<template>
    <button @click="toBus">子组件传给兄弟组件</button>
</template>

<script>
export default{
    methods: {
    toBus () {
      this.$bus.$emit('foo', '来自兄弟组件')
    }
  }
}
</script>

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

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

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

  • 作用:访问根组件中的属性或方法
  • 注意:是根组件,不是父组件。$root只对根组件有用
代码语言:javascript
复制
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存放共享的变量

怎么监听vuex数据的变化

分析

  • vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
  • 既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()

回答范例

  1. 我知道几种方法:
  2. 可以通过watch选项或者watch方法监听状态
  3. 可以使用vuex提供的API:store.subscribe()
  4. watch选项方式,可以以字符串形式监听$store.state.xxsubscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。
  5. watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中

实践

watch方式

代码语言:javascript
复制
const app = createApp({
    watch: {
      '$store.state.counter'() {
        console.log('counter change!');
      }
    }
})

subscribe方式:

代码语言:javascript
复制
store.subscribe((mutation, state) => {
    if (mutation.type === 'add') {
      console.log('counter change in subscribe()!');
    }
})

vue3.2 自定义全局指令、局部指令

代码语言:javascript
复制
// 在src目录下新建一个directive文件,在此文件夹下新建一个index.js文件夹,接着输入如下内容
const directives =  (app) => {
  //这里是给元素取得名字,虽然是focus,但是实际引用的时候必须以v开头
  app.directive('focus',{
    //这里的el就是获取的元素
    mounted(el) {
      el.focus() 
     }
  })
}

//默认导出 directives
export default directives
代码语言:javascript
复制
// 在全局注册directive
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import directives from './directives'

const app = createApp(App)
directives(app)

app.use(store).use(router).mount('#app')
代码语言:html
复制
<!-- 在你需要的页面进行自定义指令的使用 -->
<template>
  <div class="container">
    <div class="content">
      <input type="text"  v-focus>
      内容
    </div>
  </div>
</template>

<script setup>
import { reactive, ref } from 'vue'
// const vMove:Directive = () =>{

// }
</script>

vue3.2 setup语法糖模式下,自定义指令变得及其简单

代码语言:html
复制
<input type="text" v-model="value" v-focus>

<script setup>
//直接写,但是必须是v开头
const vFocus = {
  mounted(el) {
    // 获取input,并调用其focus()方法
    el.focus()
  }
}
</script>
代码语言:html
复制
<!-- demo 进去页面自动获取焦点,然后让盒子的颜色根据你input框输入的内容变色,并且作防抖处理 -->

<template>
  <div class="container">
    <div class="content" v-move="{ background: value }">
      内容
      <input type="text" v-model="value" v-focus @keyup="see">
    </div>
  </div>
</template>

<script setup>
import { reactive, ref } from 'vue'
const value = ref('')

const vFocus = {
  mounted(el) {
    // 获取input,并调用其focus()方法
    el.focus()
  }
}

let timer = null

const vMove = (el, binding) => {
  if (timer !== null) {
    clearTimeout(timer)
  }
  timer = setTimeout(() => {
    el.style.background = binding.value.background
    console.log(el);
  }, 1000);
}

</script>

<style lang="scss" scoped>
.container {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;

  .content {
    border-top: 5px solid black;
    width: 200px;
    height: 200px;
    cursor: pointer;
    border-left: 1px solid #ccc;
    border-right: 1px solid #ccc;
    border-bottom: 1px solid #ccc;
  }
}
</style>

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;
  • 属性改变后,通知计算属性重新计算

实现时,主要如下

  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。
  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

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为例
  5. 首先判断两个节点是否为相同同类节点,不同则删除重新创建
  6. 如果双方都是文本则更新文本内容
  7. 如果双方都是元素节点则递归更新子元素,同时更新元素属性
  8. 更新子节点时又分了几种情况
    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则直接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
  9. vue3中引入的更新策略:静态节点标记等

vdom中diff算法的简易实现

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

  1. vdom转化为真实dom
代码语言:javascript
复制
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算法做更新操作
代码语言:javascript
复制
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);
    }
  })
}

</details>

动态给vue的data添加一个新的属性时会发生什么?怎样解决?

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()
Vue.set()
代码语言:javascript
复制
Vue.set( target, propertyName/index, value )

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过Vue.set向响应式对象中添加一个property,并确保这个新 property同样是响应式的,且触发视图更新

关于Vue.set源码(省略了很多与本节不相关的代码)

源码位置:src\core\observer\index.js

代码语言:javascript
复制
function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用defineReactive方法,实现新增属性的响应式

关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦截

大致代码如下:

代码语言:javascript
复制
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                console.log(`set ${key}:${newVal}`);
                val = newVal
            }
        }
    })
}
Object.assign()

直接使用Object.assign()添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

代码语言:javascript
复制
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
$forceUpdate

如果你发现你自己需要在 Vue中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate迫使Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结
  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

PS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

v-if和v-show区别

  • v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
  • 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
  • v-showfalse变为true的时候不会触发组件的生命周期
  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗

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

  • v-ifv-show 都能控制dom元素在页面的显示
  • v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)
  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行时条件很少改变,则使用 v-if 较好

v-show与v-if原理分析

  1. v-show原理

不管初始条件是什么,元素总是会被渲染

我们看一下在vue中是如何实现的

代码很好理解,有transition就执行transition,没有就直接设置display属性

代码语言:javascript
复制
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
  beforeMount(el, { value }, { transition }) {
    el._vod = el.style.display === 'none' ? '' : el.style.display
    if (transition && value) {
      transition.beforeEnter(el)
    } else {
      setDisplay(el, value)
    }
  },
  mounted(el, { value }, { transition }) {
    if (transition && value) {
      transition.enter(el)
    }
  },
  updated(el, { value, oldValue }, { transition }) {
    // ...
  },
  beforeUnmount(el, { value }) {
    setDisplay(el, value)
  }
}
  1. v-if原理

v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分

返回一个node节点,render函数通过表达式的值来决定是否生成DOM

代码语言:javascript
复制
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // ...
      return () => {
        if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            key,
            context
          ) as IfConditionalExpression
        } else {
          // attach this branch's codegen node to the v-if root.
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            key + ifNode.branches.length - 1,
            context
          )
        }
      }
    })
  }
)

了解history有哪些方法吗?说下它们的区别

history 这个对象在html5的时候新加入两个api history.pushState()history.repalceState() 这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

从参数上来说:

代码语言:javascript
复制
window.history.pushState(state,title,url)
//state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
//title:标题,基本没用,一般传null
//url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。
//如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,
//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/

window.history.replaceState(state,title,url)
//与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录

另外还有:

  • window.history.back() 后退
  • window.history.forward()前进
  • window.history.go(1) 前进或者后退几步

从触发事件的监听上来说:

  • pushState()replaceState()不能被popstate事件所监听
  • 而后面三者可以,且用户点击浏览器前进后退键时也可以

从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 配置
  8. plugins:用来放 vite 插件的 plugin 配置
  9. public:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下
  10. src:用来放项目代码文件
  11. api:用来放http的一些接口配置
  12. assets:用来放一些 CSS 之类的静态资源
  13. components:用来放项目通用组件
  14. layout:用来放项目的布局
  15. router:用来放项目的路由配置
  16. store:用来放状态管理Pinia的配置
  17. utils:用来放项目中的工具方法类
  18. views:用来放项目的页面文件

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • vuex是什么?怎么使用?哪种功能场景使用它?
  • Watch中的deep:true是如何实现的
  • Vue3速度快的原因
  • 什么是递归组件?举个例子说明下?
  • 怎样理解 Vue 的单向数据流
  • 为什么不建议用index作为key?
  • 怎么缓存当前的组件?缓存后怎么更新
  • Vue中组件和插件有什么区别
  • Composition API 与 Options API 有什么不同
  • 为什么要使用异步组件
  • 子组件可以直接改变父组件的数据么,说明原因
  • Vue组件之间通信方式有哪些
  • 怎么监听vuex数据的变化
  • vue3.2 自定义全局指令、局部指令
  • Vue computed 实现
  • Vue中diff算法原理
  • 动态给vue的data添加一个新的属性时会发生什么?怎样解决?
    • Vue.set()
      • Object.assign()
        • $forceUpdate
          • 小结
          • v-if和v-show区别
            • 了解history有哪些方法吗?说下它们的区别
              • 从0到1自己构架一个vue项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档