前端vue面试题

1,234 阅读19分钟

未经允许,禁止转载 持续更新中......

本文整理总结了vue常见面试题,以便前端开发爱好者学习,更好的理解vue框架,拿到满意的offer。如有不对之处,欢迎指正

1. 说一下MVVM和MVC的区别

MVVM.png

  • MVVM是一种软件架构的模式。

    • M 代表 model数据模型(发送请求获取到的数据)

    • V 代表 View视图(页面)

    • VM 代表 ViewModel 视图模型 MVVM通过数据绑定和事件监听的方式,实现了数据的双向绑定。一是将模型转换为视图,也就是将获取到的数据转换为页面,实现的方式是数据绑定。二是将视图转换为模型,也就是将页面转换为数据,实现的方式是数据监听

  • MVC是Model-View-Controller的简写。即模型-视图-控制器。C指的是Controller页面业务逻辑,负责从视图读取数据,控制用户输入,并向模型发送数据。MVC是单向通信,也就是M和V必须通过Controller来承上启下

    MVVM和MVC区别在于MVVM实现了数据和视图的自动同步,不需要我们手动的操作DOM,当model改变时,view会自动改变,反之亦然。

2. Vue响应式的原理是什么

Vue 的响应式原理是核心是通过 ES5 的 Object.defindeProperty 进行数据劫持,然后利用 get 和 set 方法进行获取和设置,data 中声明的属性都被添加到了get和set中,当读取 data 中的数据时自动调用 get 方法,当修改 data 中的数据时,自动调用 set 方法,检测到数据的变化,会通知观察者 Wacher,观察者 Wacher自动触发重新render 当前组件(子组件不会重新渲染),生成新的虚拟 DOM 树,Vue 框架会遍历并对比新虚拟 DOM 树和旧虚拟 DOM 树中每个节点的差别,并记录下来,最后,加载操作,将所有记录的不同点,局部修改到真实 DOM 树上。

data.png

let data = {
    name:"lis",
    age: 20,
    sex: "男"
}
// vue2.0实现  使用Object.defineProperty进行数据劫持
// Object.defineProperty接收三个参数 参数1:对象  参数2:对象的某个属性  参数3: 描述信息 对象

for(let key in data){
    /** 当数据被劫持后 我们再访问对象的这个属性会访问不到 出现undefined 
    所以我们要把当前对象的属性赋值给一个临时变量,通过访问这个变量达到目的**/
    let temp = data[key]
    Object.defineProperty(data, key, {
        get(){
            return temp
        },
        //set函数数据变化时会自动调用,接收一个参数value,拿到的就是变化的值
        set(value){
            temp = value
        }
    })
}
// vue3.0实现 使用Proxy 进行数据的代理
let newData = new Proxy(data, {
    get(target, key){
        return target[key]
    },
    set(target, key, value){
        target[key] = value
    }
})

3. v-if和v-show的区别

  • v-if实质是动态的创建和销毁DOM元素
  • v-show 实质是控制元素的css属性display显示和隐藏 使用场景: 需要频繁切换的时候使用v-show,不需要频繁切换的时候使用v-for。这样可以节省性能开销,使用v-if频繁切换会增加性能消耗。

4. v-for和v-if为什么不能一起使用

如果同时出现v-for和v-if,无论判断条件是否成立,都会执行一遍v-for循环,这样浪费性能,所以要尽可能的避免两者一起使用。

5. vue中的修饰符都有哪些

  • 事件修饰符:

    • .prevent 阻止事件默认行为

    • .stop 阻止事件冒泡

    • .capture 设置事件的捕获机制

    • .self 只有点击元素自身才会触发

    • .once 事件只触发一次

    • .native element-ui的修饰符,使用组件注册事件注册不上的时候使用

  • 按键修饰符 .tab.enter.esc.space.delete.up.down.left.right

  • v-model修饰符:

    • .trim去除首尾空格

    • .lazy只在输入框失去焦点或按回车键时更新内容,不是实时更新

    • .number将数据转换成number类型(原本是字符串类型)

    • .sync父子组件传值,子组件想更新这个值,可以使用此修饰符简化

6. v-for为什么要加key

key的主要作用是高效的更新DOM,提高渲染性能。同时key属性可以避免数据混乱的情况出现。

原理: vue能够实现数据和视图实时更新,使我们不操作DOM可以直接操作数据,渲染页面,其原理是虚拟DOM和高效的diff算法。当页面数据发生变化的时候,diff算法会比较同一层级的节点。如果节点类型不同,直接删除前面的节点,再创建插入新的节点,而不会再去比较这个节点后面的子节点;如果节点类型相同,则会重新的设置该节点的属性,从而实现节点更新。使用key给每个节点做一个唯一的标识,diff算法就可以正确的识别此节点,就地更新找到正确的位置插入新的节点。

不加key的情况:如果不加key 在对比新旧虚拟dom的时候,diff算法对比同一层节点相同就复用,不同就重新设置,此时复用的时候就会出现数据混乱的情况

7. 说一下虚拟DOM是什么

用JavaScript的Object对象模拟真实DOM节点,对真实DOM进行抽象,通过特定的render方法将其渲染成真实的DOM节点。由于真实的DOM元素比较复杂,属性相对较多,虚拟DOM只提取了一些和渲染相关的属性。频繁的操作真实DOM会产生性能问题。

8. 为什么组件中的data是一个函数,new vue实例中data可以是一个对象

组件是用来复用的,组件中的data写成一个函数,数据以函数返回值形式定义,函数有独立的作用域,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,由于对象是引用类型,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。因为new vue里面的代码是不存在复用的情况,所以可以写成对象形式

9. computed和watch的区别是什么

  • 计算属性computed:

    • 支持缓存,只有依赖数据发生改变,才会重新进行计算

    • 不支持异步,当computed内有异步操作时无效,无法监听数据的变化

    • 如果computed需要对数据修改,需要写get和set两个方法,当数据变化时,调用set方法。

    • computed擅长处理的场景:一个数据受多个数据影响,例如购物车计算总价

小知识点: 计算属性函数放在methods和computed的区别: 两者都能实现效果,但是放在methods中需要调用这个函数,用一次就会调用一次造成性能浪费,而放在computed中会缓存计算的结果,只有数据变化才会重新计算,不然都是直接调用的缓存结果

  • 监听属性Watch:

    • 不支持缓存,数据变,直接会触发相应的操作

    • watch支持异步;监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值

    • immediate:组件加载立即触发回调函数执行

    • deep:true的意思就是深入监听,任何修改obj里面任何一个属性都会触发这个监听器里的 handler方法来处理逻辑

    • watch擅长处理的场景:一个数据影响多个数据,例如搜索框

10. 说一下vue的生命周期

lifecycle.png

  • 初始化阶段 : 创建vue实例,准备数据,准备模版,生成虚拟DOM,渲染成真实DOM

  • 更新阶段 : 数据变化的时候,会进行新旧虚拟DOM的对比,找到差异化的部分,一次性更新真实DOM

  • 销毁阶段 : 当调用vm.destroy()的时候,vue实例就会被销毁,释放掉相关资源,此时适合清除定时器

11. vue勾子函数有哪些

  • beforeCreate在data数据注入到vm实例之前,此时vm实例上并没有数据。适合做loading的一些渲染

  • Created 在data数据注入到vm实例之后,此时vm身上已经有了数据。适合发送ajax请求

  • beforeMount 生成的结构替换视图之前,此时DOM还没更新

  • mounted 生成的结构替换视图之后,此时DOM树已经完成渲染到页面。适合进行一些DOM的操作

  • beforeUpdate 数据变化了,DOM更新之前

  • updated 数据变化了,DOM更新之后。适合监听数据的一些变化

  • beforeDestroy 实例销毁之前,此时还可以访问到实例的数据。 适合销毁非vue资源,防止内存泄露,比如清除定时器。

  • destroyed 实例销毁之后,此时组件已经销毁。

  • activated 被keep-alive缓存的组件激活时调用。当我们运用了组件缓存时,如果想每次切换都发送一次请求的话,需要把请求函数写在activated中,而写在created或mounted中其只会在首次加载该组件的时候起作用。

  • deactivated 被keep-alive缓存的组件失活时调用

  • errorCaptured 组件报错的时候执行

12. 说一说vue中的keep-alive

  • keep-alive的话,就是我们在开发的过程中,有一些组件我们不希望频繁的重新加载,keep-alive可以保存组件的状态,也就是缓存组件,下次展示的时候,不用重新渲染,节约性能。

  • keep-alive提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高

场景: 使用vue在开发单页面应用的时候,我们通常会使用Vue-Router进行页面导航,在路由切换的时候,页面是会重新加载的,比如当用户浏览第几页对应的数据详情页面,查看返回之后,如果不加任何处理,列表页面此时会重新渲染,默认显示第一页的数据而不是用户之前浏览到的位置,这样给用户不好的体验,这个时候我们就可以使用keep-alive缓存组件的状态,这样组件就不会销毁和重建。我们就可以在activated和deactivated两个勾子函数中进行一些操作。

13. 有使用过vue中的$nextTick吗,说下它的原理

Vue修改数据后,视图不会立即更新,而是等同一个事件循环中所有的数据变化完成后,统一的更新视图,此时 DOM还没有更新,我们进行DOM的一些操作的时候就会找不到DOM元素报错。但我们又需要操作DOM,Vue中提供了一个$nextTick的方法,此方法会等待组件的DOM刷新之后,执行callback函数,从而保证了callback函数可以立即操作到更新后的DOM。只要DOM一渲染完成,立马执行回调函数。

$nextTick同时也支持Promise,如果没有给此方法传入回调函数,就会返回一个Promise,此时我们可以使用async和await等待dom更新完成

场景: 1、点击按钮显示输入框并获取输入框焦点

场景: 2、点击获取元素高度

14. v-model的原理

  • v-model其实是一个语法糖,它主要提供了两个功能,view层输入值影响data的属性值,属性值变化会更新view层的值。我们可以给元素绑定v-bind指令并触发input事件来实现v-model,组件中我们可以给父组件一个v-model绑定值,子组件接收一个名字为value的属性值,并触发一个名字为input的事件来实现v-model
// 父组件
<Children v-model="isShow"> -- 此处相当于注册了一个input事件和v-bind绑定了名为value的值

//子组件接收
props:{
    value: Boolean
},
// 子组件触发input事件
methods: {
   show() {
       this.$emit('input',false)
   }
}

15. vue组件与组件之间是如何传数据的

  • 父传子
// 父组件
<Children :list="list">
// 子组件 props接收
<script>
export default {
    props: {
        list: {
          type: Array,
          required: true
        },
    }
}
</script>
  • 子传父
// 在父组件中给子组件绑定一个自定义的事件,子组件通过$emit()触发该事件并传值
// 父组件
<Children @showDialog="showFn">
// 子组件 
<script>
export default {
    methods: {
        show() {
            this.$emit('showDialog',false)
        }
    }
}
</script>
  • 非父子组件
// event bus又叫事件总线,通用的组件通讯解决方案,可以实现任意两个组件的通讯

// 1、在main.js中创建bus对象,并挂载到原型上
const bus = new Vue()
Vue.prototype.bus = bus

// 2、提供数据的组件发布事件
this.bus.$emit('getData',value)

// 3、接收数据的组件订阅事件
this.bus.$on('getData',(value)=>{value就是接收的数据})
  • 组件可以通过$parent$children获取父组件和子组件实例,进而获取数据

  • 使用$refs获取组件实例,进而获取数据

  • $attrs$listeners,对一些组件进行二次封装时,方便传值

// $attrs 获取父组件传递的所有属性,但不包含class和style
// $listeners 获取父组件传递的所有事件,但不包含.native修饰符的
/** 
    总结: `$attrs`和`$listeners` 可以通过v-bind="$attrs"和v-on="$listeners"
    向子组件传递父组件的属性和事件
*/
<Children @changeTab="changeTab" :color="color" :size="small"></Children>
  • 使用provideinject,依赖注入,提供数据的组件provide,使用数据的组件inject

  • 再有的话就不属于vue了,local也可以传递参数

16. 路由传参方式的区别

路由传参.png

  • params传参,必须用name跳转,因为path跳转会忽略掉parmars,需要提供路由的name 或手写完整的带有参数的path
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user

17. Vue 的父组件和子组件生命周期钩子函数执行顺序

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程:

    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程:

    父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程

    父 beforeUpdate -> 父 updated

  • 销毁过程

    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

18. Vue-Router路由有哪些方式

  • hash模式: 后面的 hash 值的变化,浏览器既不会向服务器发出请求,浏览器也不会刷新,每次 hash 值的变化会触发 hashchange 事件

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

19. Vue中style标签加scoped属性的作用和原理

作用: 实现组件的私有化,防止全部同名css污染
原理: scoped会在DOM结构和css样式上添加唯一的标识 data-v-xxxx 以达到类似限制作用域的效果

Snipaste_2021-07-02_19-42-27.png

20. 说一说vue的优缺点

  • 优点:
    • 数据驱动
    • 组件化
    • 轻量级
    • SPA单页面
    • 3.0的界面化管理工具比较好用
    • 容易入门
    • 中文社区比较强大
  • 缺点
    • 不支持IE8以下的浏览器
    • 比较耗内存:每个组件都会实例化一个Vue实例,实例的属性和方法很多
    • 定义在data里面的对象,实例化时,都会递归的遍历转成响应式数据,然而有的响应式数据我们并不会用到,造成性能上的浪费

21. vue2.0和3.0响应式的区别

  • Object.defineProperty

    • 用于监听对象的数据变化

    • 无法监听到数组变化(下标、长度)

    • 只能劫持对象的自身属性,动态添加的劫持不到

//vue2中对数组和动态属性劫持不到的解决办法
//1.动态属性
this.$set() //接收三个参数:需要新增属性的对象、新增的属性名、新增的属性值
this.$delete() //删除对象的某个属性
//2.数组 vue重写了数组的方法 push pop shift unshift splice sort reverse
//数组也可以使用this.$set方法,第二个参数改为下标就可以

  • Proxy MDN文档

    • Proxy 可以直接监听对象而非属性

    • Proxy 可以直接监听数组的变化

    • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是Object.defineProperty 不具备的

    • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而Obejct.defineProperty 只能遍历对象属性直接修改

    • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

    • Object.defineProperty 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写

22. Vue组件中的定时器要怎么销毁

  • 单个定时器
const timer = setInterval(() => {}, 500);
this.$once('hook:beforeDestroy',() => {
    clearInterval(timer)
}
  • 多个定时器

    页面中多个定时器,我们可以在data选项中创建一个timer对象,给每个定时器取个名字一一映射在对象timer中,在beforeDestroy构造函数中 遍历timer对象,清除每个定时器

data() {
    timer: {
        ...n个定时器
    }
}
beforeDestroy() {
    for(let k in this.timer) {
        clearInterval(k)
    }
}

23. 有使用过插槽吗? 简单说一下

当我们在封装组件的时候,有一些内容我们不希望写死,可以自定义组件的内容,我们就可以用插槽。插槽会将所携带的内容插入到指定的某个位置,具有模块化和更大的重用性。而插槽显不显示是由父组件控制的,插槽在哪里显示由子组件来控制

  • 默认插槽 子组件写入slot,slot所在的位置就是父组件要显示的内容

  • 具名插槽 slot添加name属性 父组件通过给template 添加对应的插槽名字

  • 作用域插槽 子组件slot标签上绑定需要的值<slot :data="user"></slot>;父组件使用v-slot="user"接收传递过来的值

24. vue中有哪些指令,能说出它们分别有什么作用吗

  • v-bind 用于设置动态属性

    • 对class增强 动态控制class属性 值可以是一个数组或者对象

    • 对style增强 动态控制style属性 值可以是一个数组或者对象

  • v-on 用于注册事件

  • v-ifv-show 控制元素的显示和隐藏

  • v-elsev-else-if 配合v-if使用

  • v-model 数据双向绑定

  • v-text 等同于innerText 标签不生效

  • v-html 等同于innerHtml 识别标签

  • v-for 遍历

25. 你了解过vue3吗?相比vue2 vue3有哪些改变

  • 数据响应式数据重新实现(Es6的proxy替代es5的Object.defineProperty)

  • 源码使用typescript重写

  • 虚拟Dom新算法

  • 提供了compositon api,为了更好的逻辑复用和代码组织

  • 自定义了渲染器(可以根据自己需求自定义各种各样的渲染器)

  • Fragment,模版可以有多个根元素 等等

26. vue模版编译过程

  • 简单说,Vue的编译过程就是将template转化为render函数的过程。

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

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

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

27. vue的优化做过哪些

  • v-if和v-for不能一起使用

  • 页面采用keep-alive缓存组件

  • 区分v-if和v-show使用场景

  • v-for必须加key且保证key的唯一性

  • 使用路由懒加载、异步组件、组件封装

  • 防抖 节流

  • 第三方模块的按需引入

  • 图片路由懒加载

  • 精灵图

  • 代码压缩

28. 计算属性中的函数名可以和data数据中的名字相同吗

不可以,因为在初始化vue实例的时候,不管是计算属性还是data都会被挂载到实例上,而计算属性中的函数名和data重复,会覆盖掉data

29. methods方法中的函数名可以和data数据中的名字相同吗

不可以,因为vue源码中的 initData() 方法会取出 methods 中的方法进行判断,如果有重复的就会报错

30. 如何在子组件访问父组件的实例

  • this.$parent.event

  • $emit触发父组件的事件,父组件监听这个事件

  • 父组件把方法传入子组件,子组件里直接调用这个方法

31. 说一说vue的优缺点

优点: 数据双向绑定、单页面、组件化、容易入门学习

缺点: 不支持ie8以下浏览器、耗内存,因为每个组件都会实例化一个实例,实例上面有很多属性和方法、定义在data里面的对象,实例化时,都会递归的遍历转成响应式数据,然而有的响应式数据我们并不会用到,造成性能上的浪费

32. 导航守卫的勾子函数执行顺序

  • 全局类型的勾子函数

    • router.beforeEach 全局前置守卫

    • router.beforeResolve 全局解析守卫

    • router.afterEach 全局后置守卫 没有next参数

  • 路由勾子函数

    • router.beforeEnter 路由进入之前
  • 组件内的勾子函数

    • beforeRouteEnter 组件进入之前

    • beforeRouteUpdate 组件更新之前

    • beforeRouteLeave 组件离开之前

Snipaste_2021-07-25_09-37-02.png

如果是更新组件执行顺序是:beforeEach > beforeRouteUpdate > beforeResolve > afterEach

33. vue中不需要响应式的数据如何处理?

开发过程中,会遇到一些非响应式的数据,比如某些selected下拉框的选项,如果我们都定义到data里面是会增加性能消耗的

  • 定义在data外面
data(){
  this.options = [{}...]
  return {
  }
}
  • 使用Object.freeze()冻结对象或数组,被冻结的对象属性不能被修改
data(){
  return {
     options: Object.freeze([])
  }
}

34. vue中自定义指令

// 注册一个全局自定义指令 `v-focus` 
Vue.directive('focus', { 
  // 当被绑定的元素插入到 DOM 中时…… 
  inserted: function (el) { 
     // 聚焦元素 
     el.focus() 
   } 
})

自定义指令包含了以下几个钩子函数:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

这些钩子函数接受以下几个参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用
    • (还有几个不常用就不列举额)
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

函数简写,bind和update时候执行:

Vue.directive('color-swatch', function (el, binding) {
    el.style.backgroundColor = binding.value 
})

35. vue中的mixin混入

vue2选项式api,抽离逻辑变得复杂,vue提供了一种混入的方式来提取复用逻辑,并在组件中合并执行

// 定义混入对象mixin
var myMixin = {
  data: function(){
    return {
      name: 'zz'
    }
  }
  created: function () { this.hello() }, 
  methods: { 
      hello: function () { 
        console.log('hello from mixin!')
      } 
  } 
}

// 组件内使用
var Component = Vue.extend(
  { 
    mixins: [myMixin] 
  }
)
  • 合并数据对象时,发生重名冲突以组件数据优先
  • 合并同名钩子函数时,比如created等等,组件和mixin都会执行,并且mixin先执行
  • 合并其他对象选项时,比如methodscomponents 和 directives合并为同一个对象,如果有重名冲突,取值组件的

36. 相同的路由组件如何重新渲染

  • router-view上加一个key

37. vue插件机制

如同element-ui一样,我们在使用的时候通过Vue.use(),Vue也提供了一个方法支持自定义插件,使我们可以添加一些全局的方法或属性、指令、mixin以及 为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) {
    // 逻辑...
  }
}