前端面试题【背完最低10k】

面试题分五部分详解

前端部分

主要从五个方面来准备,包括基础知识(html + css + javascript)、框架(vue2、vue3)、打包工具(webpack)、协议(http、https、websocket)、其它。

感谢先辈们的链接,我就毫不客气的引用,同时结合自身面试情况。第二弹来啦(~ ̄▽ ̄)~,第三弹正在筹备中…

框架(vue2、vue3)

0.vue 解决了什么问题?( Vue.js介绍 )

1.虚拟 DOM:DOM 操作时非常耗性能,vue不再使用原生的 DOM 操作节点,极大的解放 DOM 操作。但具体操作的还是 DOM
2.视图、数据、结构分离:使数据更改更为简单。不需要逻辑代码的更改,只需要操作数据就能完成相关操作
3.组件化:把一个单页面应用中的各种模块拆分到一个一个单独的组件中,便于开发,以及后期的维护

vue 的核心是 数据驱动 + 组件化

优点:
	1.双向数据绑定
	2.组件化
	3.渐进式
	4.轻量快捷
	5.数据和结构分离

1.vue 的生命周期

vue 的生命周期是指:从创建 vue 对象到销毁 vue 对象的过程。在这一过程中,钩子函数是 vue 框架中内置的一些函数,会随着 vue 的生命周期阶段,自动执行。

钩子函数的作用:特定的时间,执行特定的操作

钩子函数的分类学:四大阶段,八大方法
初始化
	beforeCreate:在实例初始化之后调用。data 和 methods 都还没有初始化完成,通过 this 不能访问
	created:此时 data 和 methods 都已经初始化完成,可以通过 this 去操作,也可以发 ajax 请求
挂载
	beforeMount:模板已经在内存中编译,但还没有挂载到页面上。不能通过 ref 找到对应的标签对象
	mounted:页面已经初始显示,可以通过 ref 找到对应的标签,也可以发 ajax 请求
更新
	beforeUpdate:在数据更新之后,界面更新前调用。只能访问到原有的界面
	updated:在界面更新之后调用。此时可以访问到最新的界面
销毁
	beforeDestory:实例销毁之前调用。但实例仍可以正常工作
	destoryed:实例销毁之后调用。实例已经无法正常工作

1.1.生命周期钩子是如何实现的

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

2.vue 的双向数据绑定的实现原理

采用数据劫持结合发布者 - 订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter、getter,在数据变动时发布信息给订阅者,触发相应监听回调。
vue 的数据双向绑定将 MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者。通过 Observer 来监听自己的 model 的数据变化,通过 Compile 来解析编译模板指令(vue中用来解析 {{}})。最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -->> 视图更新

(vue2)
1.在生命周期的 initState() 中将 data、props、computed、watch、methods 中的数据劫持,通过 observe() 与 Object.defineProperty() 将相关对象转为 Observer 对象
2.在 initRender() 中解析模板,通过 Watcher 对象、Dep 对象与观察者模式将模板中的指令与对象的数据建立依赖关系,使用全局对象 Dep.target 实现依赖收集
3.当数据变化时,setter 被调用,触发 Object.defineProperty() 中的 dep.notify() 遍历该数据依赖列表,执行器 update() 通知 Watcher 进行视图更新

(vue3)
Vue2.x 的双向绑定是基于 JavaScript 的 Object.defineProperty() 的 get、set 属性;而 vue3.x 使用 ES6 的 Proxy 作为观察者机制。
1.Object.defineProperty() 的缺点
	1)ES5 的 Object.defineProperty() 无法监听对象属性的删除和添加
	2)不能监听数组的变化,除了 push/pop/unshift/shift/splice/reverse/sort,其他都不行
	3)Object.defineProperty() 只能遍历对象属性直接修改(需要深拷贝进行修改)

2.Proxy【译为代理,可以拦截属性的一些行为来做一些特殊处理】的优点
	1)直接监听对象而非属性
	2)直接监听数组的变化
	3)拦截的方式有很多种(
    	handler.get
    	handler.set
    	handler.has
    	handler.apply
    	handler.construct
    	handler.deleteProperty
    	handler.defineProperty
	)
	4)Proxy 返回一个新对象,可以操作新对象达到目的

Proxy 的缺点
	Proxy 有兼容性问题。不能用 polyfill 來兼容(polyfill 主要抚平不同浏览器之间对 js 实现的差异)    

2.1.defineProperty 的属性值

configurable:是否可以删除属性和属性描述
enumerable:是否可以出现在对象的枚举属性中
value:初始值
writable:是否能被赋值运算符改变
get:存取描述符之一,给属性提供一个 getter 方法,当访问属性时会被触发
set:存取描述符之一,给属性提供一个 setter 方法,当给属性赋值时会被触发

2.2.vue2.x 与 vue3.x 的不同

1.双向数据绑定原理上:
· vue2 的双向数据绑定是利用了 ES5 的 API Object.definepropert() 对数据进行劫持,并结合发布订阅模式来实现的。vue3 中使用了 ES6 的 API proxy 对数据进行处理
· 相比与 vue2,使用 proxy API 优势有:
    defineProperty 只能监听某个属性,不能对全对象进行监听
    可以省去 for...in 、闭包等内容来提升效率(直接绑定整个对象即可)
    可以监听数组,不用再去单独的对数组做特异性操作,vue3 可以检测到数组内部数据的变化

2.vue3 支持碎片(Fragments):也就是说可以拥有多个根节点
  vue3 新组件:Fragment(片段)、Teleport(瞬移)、Suspense(不确定的)

3.Composition API:
· vue2 使用选项类型 API,vue3 使用合成型 API。
· 选项型 API 在代码里分割了不同的属性:data、computed、methods、watch等等;合成型 API 使用方法来分割不同属性,这样代码会更加简便和整洁

4.建立数据 data
vue2 是把数据放入data中,vue3 就需要使用一个新的 setup() 方法,此方法在组件初始化构造得时候触发。使用以下三个步骤来建立响应式数据:
    · 从 vue 引入 reactive
    · 使用 reactive() 方法来声明数据为响应式数据
    · 使用 setup()方 法来返回我们的响应式数据,从而 template 可以获取这些响应式数据

5.生命周期
	vue2                        vue3
	beforeCreate                setup()
	created                     setup()
	beforeMount                 onBeforeMount
	mounted                     onMounted
	beforeUpdate                onBeforeUpdate
	updated                     onUpdated
	beforeDestory               onBeforeUnmount
	destoryed                   onUnmounted
	activated                   onActivated
	deactivated                 onDeactivated

6.父子传参不同
	vue2 中:props/emit 传参
	vue3 中:父组件向子组件传值用的是 props,子组件向父组件传值用的是 setup(props, {attrs, slots, emit}) 中 emit

7.setup() 函数特性
	· setup(props, context)    setup(props, {attrs, slots, emit})
    · setup 函数是处于生命周期 beforeCreate 和 created 俩个钩子函数之前
    · 执行 setup 时,组件实例尚未被创建(在setup()内部,this 不会是该活跃实例得引用,即不指向 vue 实例。vue 为了避免我们错误的使用,直接将 setup 函数中得 this 修改成了 undefined)
	· 与模板一起使用时,需要返回一个对象
	· 因为 setup() 中,props 是响应式的,当传入新的 prop 时,它将会被更新。如需解构 props,可以通过使用 setup() 中得 toRefs 来完成此操作
	· setup() 只能是同步的不能是异步的

2.3.数据劫持、脏数据检测、响应式原理

数据劫持:当我们访问或设置对象的属性的时候,都会触发相对应的函数,然后在这个函数里返回或设置属性的值。我们可以在触发函数的时候做一些想要的做事情,这就是 "劫持" 操作

脏数据检测:当触发了指定事件后会进入脏数据检测,这时会调用 $digest 循环遍历所有的数据观察者,判断当前值是否和先前的值有区别。如果检测到变化,会调用 $watch 函数,然后再次调用 $digest 循环直到发现没有变化。循环至少为 2 次,至多为 10 次。
        脏数据检测虽然存在低效的问题,但是不关心数据是通过什么方式改变的,都可以完成任务,但是这在 vue 中的双向数据绑定是存在问题的。并且脏数据检测可以实现批量检测出更新的之,再去统一更新 UI,大大减少了操作 DOM 的次数

响应式:数据改变,对应的视图也会改变

vue2.x 响应式原理:采用数据劫持结合发布-订阅者模式的方式,通过 Object.defineProperty() 来劫持 data 里面各个属性的 setter 和 getter。在数据变动的时候,触发 set 方法,检测到数据发生变化,会发布消息给订阅者,触发相应的监听回调,生成新的虚拟 DOM 树,视图更新;

vue3.x 响应式原理:通过 ES6 的代理对象 Proxy 进行响应式,代理 data 对象里所有的属性及数组。访问属性时触发get(),改变属性值时触发set(),然后发布消息给订阅者,重新渲染页面;

2.4.vue 的 template 的编译过程

1.vue template 模板编译的过程经过 parse() 生成 AST(抽象语法树),optimize 对静态节点优化,generate() 生成 render 字符串
2.之后调用 new Watcher() 函数,用来监听数据的变化,render 函数就是数据监听回调所调用的,其结果便是重新生成 vnode
3.当这个 render 函数字符串在第一次 mount、或者绑定的数据更新的时候,都会被调用,生成 Vnode
4.如果是数据的更新,那么 Vnode 会与数据改变之前的 Vnode 做 diff,对内容做改动之后,就会更新到我们真正的 DOM

或者

1.首先,会解析模板。生成抽象语法树(一种用 JavaScript 对象的形式来描述整个模板),使用大量的正则表达式对模板进行解析,遇到标签、文本的时候会调用对应的钩子函数进行相关处理
2.Vue 的数据是响应式。但其实模板中并不是所有数据都是响应式的,一些数据首次渲染后就不会在发生变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历抽象树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)可以跳过对比,这对运行时的模板起到很大的优化作用
3.编译的最后一步就是将优化后的抽象语法树转换为可执行的代码

2.5.Vue 中如何进行依赖收集

原因:目的在于我们观察数据的属性值发生变化时,可以通知哪些视图层使用了该数据

依赖收集的核心思想:是 "事件发布订阅模式"。所谓的依赖,其实就是 Watcher。至于如何收集依赖,就是在 getter 中收集依赖,在 setter 中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了

3.区别单向数据流与双向数据绑定
在这里插入图片描述

state:驱动应用的数据源
view:以声明方式将 state 映射到视图
actions:响应在 view 上的用户输入导致的状态变化
用户在 view 上操作,通过 actions 改变 state 里面的值,同时 state 驱动 view 层显示

单向数据流表示数据单一方向传输。那么对于 Vue 来说,组件之间的数据传递具有单向数据流这样的特性

eg:对于父子组件来说,父组件总是通过 Props 向子组件传递数据。所有的 prop 都使得其父子 prop 之间形成一个单向下行绑定:父级 prop 的更新会向下流动到子组件中。
反过来则不行,这样避免子组件直接修改父组件的状态而出现应用数据流混乱的状态。
而且,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。

双向数据绑定:原理其实就是 MVVM

4.什么是mvvm mvc是什么区别 原理
在这里插入图片描述
在这里插入图片描述

MVVM是 Model-View-ViewModel 的简写。即模型-视图-视图模型。
	【模型】指的是后端传递的数据。
	【视图】指的是所看到的页面。
	【视图模型】指的是 Vue 实例化对象。mvvm模式的核心,它是连接 view 和 model 的桥梁。
	
	它有两个方向:一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
             	 二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
	这两个方向都实现的,我们称之为数据的双向绑定

总结:在MVVM的框架下视图和模型是不能直接通信的,它们通过 ViewModel 来通信。
  	  ViewModel 通常要实现一个 observer 观察者。当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新;而当用户操作视图,ViewModel 也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。并且 MVVM 中的 View 和 ViewModel 可以互相通信。


MVC是Model-View- Controller的简写。即模型-视图-控制器
	view 传达指令到 Controller,
	Controller 完成业务逻辑,要求 Model 改变状态,
	Model 将新的数据发送到 View,用户得到反馈

区别:
	1.MVVM 各部分的通信是双向的,MVC 各部分通信是单向的
	2.MVVM 是真正将页面与数据逻辑分离放到 JS 里去实现,MVC 里面未分离
	3.在 MVC 里,View 是可以直接访问 Model 的,所以 View 里会包含 Model 信息以及一些业务逻辑。这就会导致要更改 View 比较困难,而且那些业务逻辑无法重用;
	  在 MVVM 中,把数据绑定工作放到一个 JS 里去实现,而这个 JS 文件的主要功能是完成数据的绑定。而且,MVVM的一个重要特性就是双向绑定,这就使得开发人员更方便去同时维护页面上都依赖于某个字段的 N 个区域,不需要手动更新它们。

5.组件之间的通信方式
在这里插入图片描述

1.使用 props 和 emit 进行父子组件的通信
2.使用 event-bus 事件总线,进行任意组件的通信
3.使用 Vuex 共享数据,进行任意组件的通信
	Vuex核心:
    	1)State:保存所有组件的共享状态
    	2)Getters:类似状态值的计算属性
    	3)Mutations:修改 State 中状态值的唯一方法,里面包含状态变化监听器和记录器
    	4)Actions:用于异步处理 State 中状态值,异步函数结束后调用 Mutations
    	5)Modules:当一个 State 对象比较庞大时,可以将 State 分割成多个 Modules 模块
	eg:通过 Vuex 保存用户登录返回的信息,实现各组件共享用户信息

4.使用 $attrs / $listeners,实现父组件传递数据给子/孙组件
5.使用 provide / inject:当父组件数据更新后,后代组件的数据不会动态更新【不推荐】
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
provide 和 inject 实现模式也是发布者订阅者的父组件通过配置 provide 公开各类信息,子孙组件通过 inject 订阅自己想要的信息
provide 可以是一个对象或返回对象的函数
inject 可以是一个字符串、数组或对象
注: 
    1.provide/inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中
    2.若想实现双向数据绑定,父组件多提供一个修改父组件内变量的方法,后代组件使用该方法并传递相应参数,父组件变量修改后,后代组件相应的地方会响应式的进行修改:https://blog.csdn.net/Dark_programmer/article/details/121247714

5.1.vuex 原理

1.vuex 可以理解为一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里的 data
2.state 里存放的数据是响应式的,vue 组件从 store 读取数据。若 store 中的数据发生改变,依赖这项数据的组件也会发生更新
3.vuex 通过 mapState 把全局的 state 的 getters 映射到当前组件的 computed 计算属性

5.1.1.vue 单页面应用中刷新页面,vuex 数据丢失的原因及解决方法

原因:
	1.Vuex 数据保存在运行内存中,vue 实例初始化的时候为其分配内存
	2.当页面刷新的时候,重新初始化 Vuex 实例。为 Vuex 分配内存,从而导致之前保存的数据丢失

解决方法:
	F1:Vuex 的数据都是每次组件加载时候动态请求获取数据保存
	F2:将 Vuex 中的数据每次同步更新保存到 sessionStorage 中
	F3:在页面刷新之前获取 Vuex 的数据,将数据保存到 sessionStorage 中,页面加载后从 sessionStorage 中获取

5.2. ref 介绍

ref 被用来给元素或子组件注册引用信息,引用信息将会注册在父组件的 $refs 对象上。
如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向改子组件实例

this.$refs是一个对象,持有当前组件中注册过 ref 特性的所有 DOM 元素和子组件实例。注:$refs 只有在组件渲染完成后才填充,在初始渲染的时候不能访问它们,并且它是非响应式,因此不能用它在模板中做数据绑定

通俗的讲,ref 特性就是为元素或子组件赋予一个 ID 引用,通过 this.$refs.refName 来访问元素或子组件的实例

5.3.封装组件的过程

首先,组件可以提升整个项目的开发效率,能够把页面抽象成多个相对立的模块,解决了我们传统项目开发中效率低、难维护、复用性差等问题
过程:使用 Vue.extend() 创建一个组件,然后使用 Vue.component() 注册组件

5.4. 对 vue 组件的理解

1.组件是独立和可复用的代码组织单元。组件系统是 Vue 核心特性之一,开发者可以使用小型、独立和通常可复用的组件构建大型应用
2.组件化开发能大幅提高应用开发效率、测试性、复用性等
3.组件使用按分类有:页面组件、业务组件、通用组件
4.Vue 的组件是基于配置的。通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,它们是基于 VueComponent,扩展于 Vue
5.Vue 中常见组件化技术有:属性 props、自定义事件、插槽等等。它们主要用于组件通信、扩展等等
6.合理的划分组件,有助于提升应用性能
7.组件应该是高内聚、低耦合的
8.遵循单向数据流的原则

5.5.父组件与子组件生命周期钩子的执行顺序

1.加载渲染过程
父beforeCreate  ->  父created  ->  父beforeMount  ->  子beforeCreate  ->  子created  ->  子beforeMount  ->  子mounted  ->  父mounted

2.父组件更新过程(父组件影响子组件的情况)
父beforeUpdate  ->  子beforeUpdate  ->  子updated  ->  父updated

3.父组件更新过程(父组件不影响子组件的情况)
父beforeUpdate  ->  父updated

4.销毁过程
父beforeDestroy  ->  子beforeDestroy  ->  子destroyed  ->  父destroyed

5.6.组件的渲染流程、更新流程

模板编译生成一个 render 函数,执行 render 生成一个 Vnode 节点,会触发 data 中的 getter,然后收集依赖,把这个 data 的变量变成一个响应式数据观察起来。一旦修改这个变量则会触发 setter 通知这个 Watcher,再重新触发 render 函数,循环往复

渲染流程
1.解析模板为 render 函数
    把 Vue 语法编译成 JS 语法,通过执行 Vue-template-compiler 的 compiler 函数,得到 render
2.触发响应式,监听 data 属性的 getter、setter
    响应式的关键是 Object.defineProperty(),将模板初次渲染使用到的变量绑定到 Object.defineProperty() 中,首次渲染会触发 getter
3.执行 render 函数,生成 vnode、patch(ele, vnode)

更新流程:执行 render 函数,生成虚拟 DOM 树,监听数据变动。一旦数据发生变动,触发 setter 函数,通知 Watcher,重新执行 render 函数,循环往复
1.修改 data,触发 setter(此前在 getter 中已被监听)
2.重新执行 render 函数,生成 newVnode
3.执行 patch(vnode, newVnode)

(渲染和更新流程)
在这里插入图片描述

5.7.Vue 的函数式组件

函数式组件:组件没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法时,可以将组件标记为 functional,这意味着它无状态(没有响应式数据),也没有实例(没有 this 上下文)。

组件需要的一切都是通过 context 参数传递,它是一个包含如下字段的对象:
	props:提供所有 prop 对象
	children:VNode 子节点的数组
	slots:一个函数,返回了包含所有插槽的对象
	scopedSlots:一个暴露传入的作用域插槽对象。也以函数形式暴露普通插槽
	data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
	parent:对父组件的引用
	listeners:一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名
injections:如果使用了 inject 选项,则该对象包含了应当被注入的 property

使用函数式组件的原因:【速度】
	1.因为函数式组件没有状态,所以它们不需要像 Vue 的响应式系统一样需要经过额外的初始化
	2.函数式组件会对相应的变化做出响应式改变,比如转入新的 props。但是在组件本身中,他无法知道数据何时发生更改,因为它不维护自己的状态

使用场景:
	1.一个简单的展示组件,也就是所谓的 dumb 组件。例如, buttons, pills, tags, cards,甚至整个页面都是静态文本。比如 About 页面
	2."高阶组件" -- 用于接收一个组件作为参数,返回一个被包装过的组件
	3.v-for 循环中的每项通常都是很好的候选项

5.7.1.vue 的高阶组件

高阶组件:就是一个函数,且该函数接收一个组件作为参数,并返回一个新的组件。高阶组件其实就是装饰者模式的应用:在不改变对象自身的前提下,在程序运行期间,动态的给对象添加一些额外的属性或行为

应用场景:
1.el-table 表格删除数据后,el-pagination 自动向前翻页:https://blog.csdn.net/qq_20667737/article/details/123137094
2.权限控制、日志记录、数据校验、异常处理、统计上报等等

5.8.vue 组件之全局注册和局部注册

全局注册:当 vue 创建的时候,不管这个组件是否使用,都会被加载,这样的方式会比较占内存

局部注册:当我们使用到某个组件,该组件才会被创建,如果不使用该组件,那么该组件不会被创建

5.9.组件中写 name 选项有什么作用

1.项目使用 keep-alive 时,可搭配组件的 name 进行缓存过滤
2.DOM 做递归组件时需要调用自身的 name
3.vue-devtools 调试工具里显示的组件名称是由 vue 中组件的 name 决定的

6.vue 中是否可以在子组件修改 props 数据

可以修改,会有警告

6.1. vue 子组件修改 props 数据 会不会影响父组件的状态值

如果传递的基本数据类型,不会影响
如果传递的引用数据类型,会影响

6.2.插槽的理解

插槽用于决定所携带的内容,插入到子组件指定的某个位置
但内容必须在父组件中子组件标签内定义,在子组件中使用 <slot></slot> 标签接收
slot 是组件内部的占位符

7.Vue 组件 data 为什么必须是一个函数,而 Vue 的根实例则没有此限制?【**】

在创建或注册模板的时候传入一个 data 属性作为用来绑定的数据,且 data 必须是一个函数。

因为每一个 vue 组件都是一个 vue 实例,通过 new Vue() 实例化,引用同一个对象。如果 data 是一个对象的话,那么一旦修改其中一个组件的数据,其他组件相同数据就会被改变。而 data 是函数的话,每个 vue 组件的 data 都因为函数有了自己的作用域,互不干扰

简单点:
	data 是一个函数时,每个组件实例都有自己的作用域。每个实例相互独立,不会相互影响。Object 是引用数据类型,如果不用 function 返回,每个组件的 data 都是内存的同一个地址,一个数据改变了,其他的也改变

7.1. vue 根实例 data 为 JSON 缘由

根实例不会出现被复用的情况

7.2. vue 重置 data

使用 Object.assign()。vm.$data 可以获取当前状态下的 data;vm.$options.data 可以获取组件初始化状态下的 data

Object.assign(this.$data, this.$options.data())

8.watch 和 computed 的区别

1.功能上:
	computed 是计算属性
	watch 是监听一个值的变化,然后执行对应的回调
2.是否调用缓存:
	computed 中的函数所依赖的属性没有发生变化,那么调用当前函数的时候,会从缓存中读取
	watch 在每次监听的值发生变化的时候都会执行回调

3.是否调用 return
	computed 中的函数必须要用 return 返回
	watch 中的函数不是必须要用 return

4.computed 默认第一次加载的时候就开始监听
	watch 默认第一次加载不做监听。如果需要第一次加载做监听,需要添加 immediate 属性,设置为 true (immediate: true)

5.使用场景:
	computed:当一个属性受多个属性影响的时候,使用computed
	watch:当一条数据影响多条数据的时候,使用 watch

9.v-for 中为什么要用 key(Vue中 key 的作用和工作原理吗?说说你对它的理解)

作用:高效的更新虚拟 DOM
工作原理:vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个 patch 过程更加高效。减少 DOM 操作量,提高性能

原因:
	1.vue 中列表循环需加 :key="唯一标识" 唯一标识尽量是 item 里面 id 等,因为 vue 组件高度复用,增加 key 可以标识组件的唯一性;为了更好地区别各个组件,key 的作用主要是为了高效的更新虚拟 DOM。

	2.key 主要用来做 dom diff 算法用的。diff 算法是同级比较,比较当前标签上的 key 还有它当前的标签名;如果 key 和标签名都一样时,只是做了一个移动的操作,不会重新创建元素和删除元素。

	3.没有 key 的时候,默认使用的是 "就地复用" 策略。如果数据项的顺序被改变,Vue 不是移动 Dom 元素来匹配数据项的改变,而是简单复用原来位置的每个元素。如果删除第一个元素,在进行比较时发现标签一样值不一样时,就会复用之前的位置,将新值直接放到该位置,以此类推,最后多出一个就会把最后一个删除掉。

	4.尽量不要使用索引值 index 作 key 值,一定要用唯一标识的值,如id等。因为若用数组索引 index 为 key,当向数组中指定位置插入一个新元素后,因为这时候会重新更新 index 索引,对应着后面的虚拟 DOM 的 key 值全部更新了,这个时候还是会做不必要的更新,就像没有加 key 一样;因此 index 虽然能够解决 key 不冲突的问题,但是并不能解决复用的情况。如果是静态数据,用索引号 index 做 key 值是没有问题的。

	5.标签名一样,key一样,这时候就会就地复用;如果标签名不一样,key一样不会复用。

10.vue 的指令

v-bind:给元素绑定属性
v-on:给元素绑定事件

v-html:给元素绑定数据,且该指令可以解析 HTML 标签
v-text:给元素绑定数据,不解析标签
v-model:数据双向绑定

v-for:遍历数组

v-show:条件渲染,讲不符合条件的数据隐藏(display: none)
v-if:条件渲染,动态在 DOM 内添加或删除 DOM 元素
v-else:条件渲染,必须与 v-if 成对使用
v-else-if:判断多层条件,必须与 v-if 成对使用

v-once:只渲染元素或组件一次
v-cloak:解决插值闪烁问题
v-pre:跳过这个元素以及子元素的编译过程,一次加快整个项目的编译速度


v-if 与 v-show 的区别:
 v-if:在编译的过程中,会被转化为三元表达式,条件不满足时不渲染此节点
 v-show:会被编译成指令。条件不满足时,控制样式将对应节点隐藏(display: none)

10.1.v-model 原理

v-model 是 vue 双向绑定的指令,能将页面上控件输入的值同步更新到相关绑定的data属性,也会在更新 data 绑定属性时候,更新页面上输入控件的值

	1.v-model 在模板编译的时候转换代码
	2.v-model 本质是 :value 和 v-on
	3.v-model 和 :bind 同时使用,前者优先级更高
	4.v-model 因为语法糖的原因,还可以用与父子通信

11.v-if 和 v-for 哪个优先级更高?如果两个同时出现,应该怎么优化得到更好的性能?

1.显然 v-for 优先于 v-if 被解析

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

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

4.如果循环数组中有需要判断的条件,可以先使用 computed 对数组进行过滤,然后渲染,避免 v-for 和 v-if 同时工作,浪费资源

同时出现的弊端:由于 v-for 的优先级比 v-if 高,所以导致每循环一次就会去 v-if 一次,而 v-if 是通过创建和销毁 DOM 元素来控制元素的显示与隐藏,所以就会不停的去创建和销毁元素,造成页面卡顿,性能下降

12.nextTick 的理解

this.$nextTick() 主要是用在随数据改变而改变的 DOM 应用场景中。vue 中数据和 DOM 渲染由于是异步的,所以要让 DOM 结构随数据改变这样的操作都应该放在 this.$nextTick() 的回调函数中。

简单点来说,this.$nextTick() 就是起到了一个等待数据的作用,将一些回调延迟,等到 DOM 更新之后再开始执行。

nextTick 中的回调是在下一次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后,立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

13.vue-router

单页面路由跳转的方式
	1.hash(哈希默认)模式:https://blog.csdn.net/yzhlove_cs/article/details/108687222
   	 1)location.hash 的值实际就是 URL 中 # 后面的东西。特点在于:hash 虽然出现在 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
    	2)可以为 hash 的改变添加监听事件。每一次改变 hash(window.location.hash)都会在浏览器的访问历史中增加一个记录,利用 hash 的特点,可以实现前端路由 "更新视图但不从新请求页面" 的功能
        window.addEventListener('haschange', funcRef, false)
    实现原理:哈希路由的根本原理涉及到了 BOM 中的 location 对象,其中对象中的 location.hash 储存的是路由的地址,可以赋值改变其 URL 的地址。而这会触发 hashchange 事件,然后通过 window.addEventListener 监听 hash 值去匹配对应的路由,从而渲染页面的组件。

	2.history(mode: history)模式
    	依赖 HTML5 History API 和服务器配置,查看 HTML5 History 模式
    	通过 pushState 和 replaceState 切换 url,实现路由切换。需要后端配合。
        	pushState(state, title, url):用于在浏览历史中添加历史记录,但是并不触发跳转
            	state:一个与指定网址相关的状态对象。popstate 事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填 null
            	title:新页面的标题。但是所有浏览器目前都忽略这个值,因此这里可以填 null
            	url:新的网址。必须与当前页面处在同一个域,浏览器的地址栏将显示这个网址
        	replaceState(statem title, url):参数与 pushState 方法一样。区别在于此方法修改历史中当前记录,同样不跳转
        	popstate:每当同一个文档的浏览历史(即 history 对象)出现变化时,就会触发 popstate 事件
            	注:1)仅仅调用 pushState 或 replaceState,不会触发该事件,只有用户点击浏览器倒退/前进按钮,或者使用 JavaScript 调用 back、forward、go 方法才会触发
                    	window.history.back();       // 后退
                    	window.history.forward();    // 前进
                    	window.history.go(-3);       // 后退三个页面
                	2)只针对同一个文档,如果浏览历史的切换导致加载不同的文档,该事件也不会触发

	3.abstract 模式(严格模式):支持 JavaScript 运行环境,如 Node.js 服务器

缺点:
	1.abstract 模式在不支持浏览器的 API 环境使用
	2.hash 模式兼容性好,但是不美观,不利于 SEO(搜索引擎优化)
	3.history 美观,historyAPI + popState,但是刷新会出现 404

注:vue-router 的实现原理(核心):更新视图但不重新请求页面

vue-router 登录权限的判断:在全局钩子函数中进行的。在 router.js 文件中的定义路由里,将需要登录权限的页面加上 meta 属性,值是对象的形式,在该对象中自定义一个属性,属性值就是一个 Boolean 值,然后在 main.js 文件的全局钩子函数中进行判断。需要跳转的页面的自定义属性值为 true。

13.1.vue-router 的导航钩子

1.全局钩子
	前置守卫:router.beforeEach
	后置守卫:router.afterEach            无法调用任何切换函数
	全局解析守卫:router.beforeResolve    是在所有组件内守卫和异步路由组件被解析之后(导航被确认之前)调用

2.路由独享钩子:单个路由独享的导航钩子,是在路由配置上直接定义的
beforeEnter(路由独享守卫)
    eg:
        const router = new VueRouter({
            routes: [{
                path: '/demoPage',
                component: () => import("@/components/demoPage.vue"),
                beforeEnter: (to, from, next) => {
                    /*
                        to        router即将进入的路由
                        from      当前导航即将离开的路由
                        next()    进行管道中的一个钩子。如果执行完,那导航的状态就是 confirmed(确认的);否则为 false,终止导航
                    */
                    console.log('do something')
                    next()
                }
            }]
        })

3.组件内导航钩子:以下三种导航钩子是在路由组件内部直接定义的
	beforeRouteEnter    渲染该组件的对应路由被 confirm 之前。且这个时候组件实例还没有被创建,不能获取到组件实例 this
	beforeRouteUpdate   当前路由已经改变,但是已然渲染该组件时。可以获取到实例 this
	beforeRouteLeave    导航离开该组件的对应路由时。可以获取到实例 this


完整的导航解析流程
	1.导航被激活
	2.在失活的组件里调用离开守卫
	3.调用全局的 beforeEach 守卫
	4.在重用的组件里调用 beforeRouterUpdate 守卫
	5.在路由配置里调用 beforeEnter
	6.解析异步路由组件
	7.在被激活的组件里调用 beforeRouterEnter
	8.调用全局的 beforeResolve 守卫
	9.导航被确认
	10.调用全局的 afterEach 钩子
	11.触发 DOM 更新
	12.用创建好的实例调用 beforeRouterEnter 守卫中传给 next 的回调函数

13.2.路由懒加载

懒加载:本质是延迟加载或按需加载,即在需要的时候的时候进行加载。首页不用设置懒加载,一个页面加载过后再次访问不会重复加载

原因:
	1.当进行打包构建应用时,打包后的代码逻辑实现包可能会非常大。当用户要去使用的时候,那么就会把所有的资源都请求下来才可以
	2.当我们把不同的路由对应的组件分别打包,在路由被访问时再进行加载,就会更加高效

路由懒加载所做的事情:将路由对应的组件加载成一个个对应的 js 包,在路由被访问时才将对应的组件加载

vue 异步组件:
	语法:component: resolve => (require(['需要加载的路由地址']), resolve)
	ES import 常用:
    	用户访问组件时,该箭头函数被执行
    	webpack:import 动态导入语法能将该文件单独打包
    	语法:const xxx = () => import('需要加载的路由地址')

懒加载原理:
	原本的 Vue 模块是全部导入在一起的打包文件,运行后用户查看相关模块显示的内容时,会将整个打包的文件引入而后在其中查找对应的模块,然后才将其呈现给用户。这样会使得在打包文件中查找对应模块时,在浏览器中可能会出现短暂的空白页,从而降低用户体验。
	而路由懒加载是将各个模块分开打包,在用户查看下相关模块内容时,就直接引入相关模块的打包文件,然后进行显示,从而有效的解决了浏览器可能出现短暂时间空白页的情况

14.vue 的性能优化

1.对象层次不能过深,否则性能就会差
2.不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
冻结后的对象有以下特性:
    无法被修改、无法添加新属性、无法删除已有属性、无法修改该对象已有属性的可枚举性、可配置性、可写性
    冻结一个对象后该对象的原型也无法修改
3.v-if 和 v-show 区分使用场景
4.computed 和 watch 区分使用场景
5.v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
6.大数据列表与表格性能优化 -- 虚拟列表/虚拟表格
7.防止内部泄露,组件销毁后把全局变量和事件销毁
8.图片懒加载、路由懒加载
9.第三方插件的按需引入
10.运用防抖、节流

11.服务器端渲染 SSR 或 预渲染
12.适当采用 keep-alive 缓存组件

14.1.SSR 理解

SSR(服务端渲染):将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 HTML 直接返回给客户端
优点:
	1.有着更好的 SEO(搜索引擎优化)
	2.首屏加载速度更快
缺点:
	1.开发条件受到限制。服务端渲染支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时,需要特殊处理
	2.服务端渲染应用程序也需要处于 nodejs 的运行环境,服务器会有更大的负载需求

14.2. keep-alive 使用场景和原理

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存。当组件切换时,不会对当前组件进行卸载
	1.常用的三个属性 include、exclude、max,允许组件有条件的进行缓存。
    	include 表示缓存组件激活时调用,
    	exclude 表示不缓存。缓存组件失活时调用
    	max 表示最大缓存组件的个数
	2.两个生命周期 actived/deactived,用来得知当前组件是否处于活跃状态
	3.keep-alive 中运用了 LRU(最近最少使用)算法,选择最近最久未使用的组件予以淘汰

14.3.keep-alive 的作用

组件缓存。在组件切换过程中,将状态保留在内存中,防止重复渲染 DOM,减少加载时间及性能消耗,提高用户体验性

15.虚拟 DOM 原理、Vue 为什么要用虚拟 DOM

原理:虚拟 DOM,其实就是用对象的方式取代真实的 DOM 操作,把真实的 DOM 操作放在内存当中,在内存中的对象里做模拟操作

 当页面打开时,浏览器会解析 HTML 元素,构建一棵 DOM 树,将状态全部保存起来,在内存当中模拟我们真实的 DOM 操作。操作完后,又会生成一棵 DOM 树,两棵 DOM 树进行比较,根据 diff 算法比较两棵 DOM 树不同的地方,只渲染一次不同的地方

原因:
	1.虚拟 DOM 就是用 JS 对象来描述真实 DOM,是对真实 DOM 的抽象
	2.由于直接操作 DOM 性能低,但是 JS 层的操作效率高,可以将 DOM 操作转化为对象操作,最终通过 diff 算法对比差异进行更新 DOM
	3.虚拟 DOM 不依赖真实平台环境,可以实现跨平台

15.1.为什么虚拟 dom 会提高性能

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能

具体实现步骤如下:
	1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中。
	2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。
	3. 把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了

15.2.diff 算法

整体策略:深度优先,同层比较(两个节点对比的时候会优先判断是否拥有子节点,做一些操作)

1. diff 算法是虚拟 DOM 技术的必然产物:通过新旧虚拟 DOM 作对比(即 diff),将变化的地方更新在真实 DOM 上。另外,也需要 diff 高效的执行对比过程,从而降低时间复杂度O(n)。【只要用到虚拟 DOM,那就一定会用到 diff 算法】
2.vue 2.x 中为了降低 Watcher 粒度,每个组件只有一个 Watcher 与之对应,只有引入 diff 才能精确找到发生变化的地方
3.vue 中 diff 执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果 oldVnode 和新的渲染结果 newVnode,此过程称为 patch
4.diff 过程整体遵循 深度优先、同层比较 的策略;
  两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是算法的重点。
  首先假设首尾结点可能相同做 4 次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下节点;借助 key 通常可以非常精确找到相同节点,因此整个 patch 过程非常高效

15.3.为什么要使用 differ 检测

Vue 初始化的时候就会对 data 的数据进行依赖收集,因此 Vue 能实时知道哪里发生变化
一般绑定的细粒度过高,会生成大量的 Watcher 实例,则会造成过大的内存和依赖追踪的开销,而细粒度过低无法检测到变化
所以,Vue 采用的是中等细粒度的方案,只针对组件级别的进行响应式监听,这样就可以知道哪个组件发生了变化,再对组件进行 diff 算法找到具体变化的位置

16.$set

1.this.$set 实现是什么功能,为什么要用它?
当你发现你给对象加一个属性,在控制台能打印出来,但是却没有更新到视图上时,也许这个时候就需要用到 this.$set() 这个方法了。
官方解释:响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于响应式对象上添加新属性,因为 vue 无法探测普通的新增属性。

2.怎么用它?
调用方法: this.$set(target, key, value)
	target: 要更改的数据源(可以是对象或者数组)
	key: 要更改的具体数据(下标)
	value: 重新赋的值

17.Vue 修饰符

1.事件修饰符
.stop        阻止事件继续传播
.once        事件将只会触发一次
.prevent     阻止标签默认行为
.passive     告诉浏览器你不想阻止事件的默认行为
.self        只当在 event.target 是当前元素自身时触发处理函数
.capture     使用事件捕获模式。即:元素自身触发的事件先在此处处理,然后才交由内部元素进行处理【给元素添加一个监听器,当元素发生冒泡时,先触发带有该修饰符的元素。若有多个该修饰符,则由外而内触发(就是谁有该事件修饰符,就先触发谁)】
    <div id="obj1" v-on:click.capture="doc">
        obj1
        <div id="obj2" v-on:click.capture="doc">
            obj2
            <div id="obj3" v-on:click="doc">
                obj3
                <div id="obj4" v-on:click="doc">
                    obj4
                </div>
            </div>
        </div>
    </div>
    <!--
        点击obj4的时候,弹出的顺序为:obj1、obj2、obj4、obj3
        由于1,2有修饰符,故而先触发事件,然后就是4本身触发,最后冒泡事件
    -->
注:使用修饰符时,顺序很重要。相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击

2.v-model 的修饰符
.lazy        默认情况下,v-model 同步输入框的值和数据。可以通过这个修饰符,转变为在 change 事件再同步
    <input v-model.lazy="msg">
.number      自动将用户输入值转化为数值类型
    <input v-model.number="msg">
.trim        自动过滤用户输入的首尾空格
    <input v-model.trim="msg">

3.键盘事件的修饰符

4.element-ui 的修饰符
对于 elementUI 的input,我们需要在后面加上.native,因为 elementUI 对 input 进行了封装,原生的事件不起作用
    <el-input v-model="form.name" placeholder="昵称" @keyup.enter.native="submit"></el-input>

18.vue 中使用了那些设计模式

1.工厂模式            传入参数即可创建实例
eg:虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和 组件 Vnode
2.单例模式            整个程序有且仅有一个实例
Vuex 和 Vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式        vue事件机制
4.观察者模式           响应式数据原理
5.装饰模式             @ 装饰器的用法
6.策略模式            对象有个行为,但是在不同场景中,该行为有不同的实现方案。eg:选项的合并策略

19.scoped 原理及穿透方法

当一个 style 标签拥有 scoped 属性的时候,它的 CSS 样式只能用于当前的 Vue 组件,可以使组件的样式不相互污染。如果一个项目的所有 style 标签都加上 scoped 属性,相当于样式的模块化。

原理:
	Vue 中的 scoped 属性的效果主要是通过 PostCss 实现的。
	PostCss 给一个组件中的所有 DOM 添加了一个独一无二的动态属性,给 CSS 选择器额外添加一个对应的属性选择器,来选择组件中的 DOM。
	那么,这种做法使得样式只作用于含有该属性的 DOM 元素 / 组件

穿透方法:
	1.stylus 的样式穿透使用 >>>
	2.sass 和 less 的样式穿透使用 /deep/

20.delete 和 Vue.delete 删除数组的区别

delete 只是被删除的元素变成了 empty/undefined,其他元素的键值不变

Vue.delete 直接删除数组中被选中的元素,改变了数组的键值

let list = [1, 2, 3, 4]
delete list[1]  // [1, empty, 3, 4]
this.$delete(list, 1)  // [1, 3, 4]

21.为什么说 VUE 是一个渐进式的 javascript 框架, 渐进式是什么意思?

1.如果你已经有一个现成的服务端应用,你可以将 vue 作为该应用的一部分嵌入其中,带来更加丰富的交互体验;
2.如果你希望将更多业务逻辑放到前端来实现,那么 VUE 的核心库及其生态系统也可以满足你的各式需求(core + vuex + vue-route)。和其它前端框架一样,VUE 允许你将一个网页分割成可复用的组件,每个组件都包含属于自己的HTML、CSS、JAVASCRIPT 以用来渲染网页中相应的地方。
3.如果我们构建一个大型的应用,在这一点上,我们可能需要将东西分割成为各自的组件和文件,vue有一个命令行工具,使快速初始化一个真实的工程变得非常简单(vue init webpack my-project)。我们可以使用VUE的单文件组件,它包含了各自的 HTML、JAVASCRIPT 以及带作用域的 CSS 或 SCSS。

以上这三个例子,是一步步递进的,也就是说对VUE的使用可大可小,它都会有相应的方式来整合到你的项目中。所以说它是一个渐进式的框架。

22.过滤器(filter)【vue3.x 中已废弃】

过滤器是输送介质管道上不可缺少的一种装置,其实就是把一些不必要的东西过滤掉。过滤器实质上不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理

使用方式:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript表达式的尾部,由 "管道" 符号指示:
{{ message | capitalize }}
<div v-bind:id="rowId | formatId"></div>

注:
    1.局部过滤器优先于全局过滤器被调用
    2.一个表达式可以使用多个过滤器。过滤器之间需要用管道符 "|" 隔开,执行顺序从左往右

使用场景:如:单位转换、数字打点、文本格式化、时间格式化等等

23.vue 如何优化首页的加载速度?vue首页白屏是什么,如何解决?

首页白屏的原因:单页面应用的 html 是靠 js 生成,因为首屏需要加载很大的 js 文件(如:app.js、vendor.js),所以当网速差的时候会产生一定程度的白屏

解决方法:
	1.优化 webpack 减少模块打包体积,code-split 按需加载
	2.服务端渲染(SSR),在服务端事先拼接好首页所需的 html
	3.首页加 loading 或骨架屏(仅仅是优化体验)

24.vue 渲染大量数据时应该怎么优化

1.添加加载动画,优化用户体验
2.利用服务器渲染 SSR,在服务端渲染组件
3.避免浏览器处理大量的 DOM,比如懒加载、异步渲染组件、使用分页
4.对于固定的非响应式数据,使用 Object.freeze 冻结

25.登录拦截(vue + axios + http 拦截)

许多页面在访问之前是需要登录验证的。如果用户没有登录,则需要用户跳转到登录页面进行登录。在 vue 项目中:
方法一,通过 router 实现登录拦截:
    1.requireAuth 属性作用是表明该路由是否需要登录验证,在进行全局拦截时,通过该属性进行判断,该属性包含在 meta 属性中
    2.router.beforeEach:beforeEach 是 router 的钩子函数,该函数在进入每个网页之前调用,该函数接收三个参数:
        from:即将离开的路由
        to:即将要跳转的路由
        next:跳转方法。在 beforeEach 函数中作为结束语句调用,以实现页面跳转
    next(false):中断当前的导航。如果浏览器的 url 改变了(可能是手动或浏览器按钮后退),那么 url 地址会重置到 from 路由对应的地址
    next('/') 或者 next({path: '/'}):跳转到一个不同的地址。当前导航被中断,然后进行一个新的导航
    要确保 next 方法被调用,否则钩子就不会被 resolved
    登陆完成后,在登陆中改变 vuex 的状态
方法二,通过使用 axios 拦截器:如果要统一处理所有的 http 请求和响应,就需要使用 axios 的拦截器。通过配置 http response inteceptor,当后端接口返回错误信息,让用户重新登陆
方法三,http 拦截:这里引入的 element-ui 框架,结合 element 中 loading 和 message 组件来处理的。可以单独建立一个 http 的 js 文件处理 axios,再到 main.js 中引入
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值