前端面试题vue合集
文章目录
- 题1:说说你对渐进式框架的理解
- 题2:vue中v-show和v-if有什么区别
- 题3:vue3.0为什么要用Proxy API替代defineProperty API?
- 题4:SSR是什么?vue中怎么实现?
- (1)什么是 SSR:
- (2)SSR 的优势包括:
- (3)怎么使用 Vue 框架实现 SSR:
- 题5:vue的祖孙组件的通信方案有哪些?
- 题6:如何打破scope对样式隔离的限制?
- 题7:Scoped Styles为什么可以实现样式隔离?
- 题8:vue中怎么实现样式隔离?
- 题9:created和mounted有什么区别?
- 题10:vue是怎么把template模板编译成render函数的?
- 题11:谈谈你对vue中双向绑定的理解?
- 一、什么是双向绑定
- 三、实现双向绑定
- 题12:v-model的原理?
- 题13:说说vuex的原理?
- 题14:vue中给对象添加新属性时,界面不刷新怎么办?
- 一、直接添加属性的问题
- 二、原理分析
- 题15:vue组件间通信方式有哪些?
- 题16:vue3有了解过吗?能说说跟vue2的区别吗?
- 题17:自定义指令是什么?有什么应用场景?
- 题18.说说vue中key的原理?
- 题19:vue常用的修饰符有哪些?分别有什么应用场景?
- 一、修饰符是什么
- 三、应用场景
- 题20:vue的响应式开发比命令式有哪些优势?
- 题21:vue的route和route和route和router有什么区别?
- 题22:vue3.0的Treeshaking特性是什么,并举例进行说明?
- 一、是什么
- 二、如何做
- 三、作用
- 题23:vue和react在技术层面上有哪些区别?
- 题24:单页应用如何提高加载速度?
- 题25:v-if和v-for为何不建议一起用?
- 题26:vue项目如何进行部署的?是否有遇到部署服务器刷新404问题?
- 一、如何部署
- 二、404问题
- 题27:说说vue中的diff算法?
- 一、是什么
- 二、比较方式
- 题28:vue模板是如何编译的?
- 题29:说说vue中CSS scoped的原理?
- 题30:vue3中怎么设置全局变量?
- 题31:刷新浏览器后,vuex的数据是否存在?如何解决?
- 题32:说下Vite的原理?
- 什么是Vite?
- 基本用法
- 实现原理
- 题33:说说vue3中的响应式设计原理?
- 一、Vue 3 响应式使用
- 1. Vue 3 中的使用
- 二、Proxy 和 Reflect
- 题34:vue路由中,history和hash两种模式有什么区别?
- 题35:VNode有哪些属性?
- 题36:vue2.0为什么不能检查数组的变化,该怎么解决?
- 题37:说说vue页面渲染过程?
- 题38:react和vue有什么区别?
- 题39:vue中的computed和watch区别?
- computed 计算属性
- watch 监听属性:
- 题40:computed怎么实现的缓存?
- 题41:vue-loader做了哪些事情?
- 题42:vue中,假设data中有一个数组对象,修改数组元素时,是否会触发视图更新?
- 题43:vuex中的辅助函数怎么使用?
- 如何使用辅助函数
- 题44:Vuex有几种属性,它们存在的意义分别是什么?
- 题45:vuex是什么?
- 题46:谈谈你对Vue中keep-alive的理解?
- 题47:如果使用vue3.0实现一个Model,你会怎么进行设计?
- 三、实现流程
- 题48:什么是虚拟DOM?
- 题49:Vue3.0所采用的Composition Api与Vue2.x使用的Options Api有什么不同?
- 题50:Vue3.0性能提升主要通过哪几方面体现的?
- 题51:Vue3.0的设计目标是什么?做了哪些优化?
- 题52:你是怎么处理vue项目中的错误的?
- 题53:vue项目中如何解决跨域问题?
- 题54:vue怎么实现权限管理?控制到按钮级别的权限怎么做?
- 题55:大型项目中,Vue项目怎么划分结构和划分组件比较合理?
- 题56:vue项目中有封装过axios吗?怎么封装的?
- 一、axios是什么
- 三、如何封装
- 题57:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路
- 二、为什么需要虚拟DOM
- 题58:说说你对keep-alive的理解?
- 三、原理分析
- 四、思考题:缓存后如何获取数据
- 题59:Vue.observable是什么?
- 题60:说说你对slot的理解?slot使用场景有哪些?
- 题61:说说你对vue的mixin的理解,以及有哪些应用场景?
- 题62:Vue中的$nextTick有什么作用?
- 题63:Vue中组件和插件有什么区别?
- 题64:为什么Vue中的data是一个函数而不是一个对象?
- 二、组件data定义函数与对象的区别
- 三、原理分析
- 题65:SPA首页加载速度慢怎么解决?
- 三、解决方案
- 题66:谈谈你对Vue生命周期的理解
- 题67:Vue实例挂载的过程中发生了什么?
- 题68:说说你对vue的理解?
题1:说说你对渐进式框架的理解
渐进式的含义:
没有多做职责之外的事,只做了自己该做的事,没有做不该做的事,仅此而已。
更直白一点就是,用你想用或者能用的功能特性,你不想用的部分功能可以先不用。VUE不强求你一次性接受并使用它的全部功能特性。
比如以下两种场景,Vue 发挥了很大的优点:
场景一:公司刚开始一个项目,技术人员对Vue的掌握也不足够。那么我们就不能使用VUE了么?当然不是,如果你只是使用VUE做些基础操作,如:页面渲染、表单处理提交功能,那还是非常简单的,成熟技术人员上手也就一两天。完全可以用它去代替jquery。并不需要你去引入其他复杂特性功能。
场景二:我们的项目规模逐渐的变大了,我们可能会逐渐用到前端路由
、状态集中管理
、并最终实现一个高度工程化的前端项目
。这些功能特性我们可以逐步引入,当然不用也可以。
Vue 的适用面很广,你可以用它代替老项目中的JQuery。也可以在新项目启动初期,有限的使用VUE的功能特性,从而降低上手的成本。
题2:vue中v-show和v-if有什么区别
v-if 和 v-show 是 Vue.js 中用于条件渲染的指令,它们的作用是根据条件来控制元素的显示和隐藏。
它们之间有一些重要的区别:
- 编译时刻 vs 运行时刻:
v-if 是一个“惰性”指令,在编译时刻,条件为 false,元素根本不会被编译和渲染到 DOM 中。
v-show 是一个“非惰性”指令,在编译时刻,元素总是会被编译和渲染到 DOM 中。但是,根据条件的值,v-show 会通过 CSS控制元素的显示和隐藏,不会从 DOM 中移除元素。
- 显示隐藏方式:
v-if 在条件为 true 时会
渲染元素到 DOM
,而在条件为 false 时会从 DOM
中移除元素。v-if也可以触发组件创建和销毁的生命钩子
。
v-show 在条件为 true 时会通过 CSS 设置元素的 display属性为可见(通常是 display: block),在条件为 false 时设置为隐藏(display: none)。元素始终存在于 DOM
中,只是通过 CSS 控制其显示状态。
- 切换开销:
v-if 在条件切换时,如果条件从 true 切换为 false,会
销毁并重新创建元素
,这涉及到 DOM的删除和重新插入
,可能会有一定的性能开销
。
v-show 在条件切换时,只是简单地通过 CSS控制元素的显示和隐藏,不会销毁和重新创建元素,因此切换的开销较小。
- 初始渲染开销:
v-if 在初始渲染时,如果条件为 false,元素不会被渲染到 DOM 中,因此在初始渲染时可能会有一定的性能优势。
v-show在初始渲染时,元素总是会被渲染到 DOM 中,因此在初始渲染时可能会有一些额外的开销。
综上所述,当需要频繁切换元素的显示状态时,且元素可能处于不同的状态,推荐使用 v-show。而当条件不会频繁改变,且希望在条件为 false 时不渲染元素到 DOM 中,推荐使用 v-if。在实际使用中,根据具体的场景和性能需求来选择合适的指令。
题3:vue3.0为什么要用Proxy API替代defineProperty API?
在 Vue 3.0 中,使用 Proxy API 替代 defineProperty API 是为了改进响应式系统的性能和功能:
主要有以下五个层面:性能提升;更全面的拦截能力;更好的数组变化检测;更易于处理嵌套对象;更好的错误提示。
- 性能提升:Proxy API 比 defineProperty API 在许多情况下具有更好的性能。
defineProperty 使用Object.defineProperty 方法来拦截对象属性的访问和修改,但它需要遍历每个属性进行拦截。
而 Proxy API允许拦截整个对象
,可以更高效地捕获对对象的访问和修改。
- 更全面的拦截能力:
Proxy API 提供了更多的拦截方法,比 defineProperty API更灵活、丰富。它支持拦截目标的各种操作,包括
读取、设置、删除、枚举
等,甚至还可以拦截函数调用
和构造函数实例化
。
- 更好的数组变化检测:
Vue 3.0 使用 Proxy API 改善了
数组的变化检测机制
。
Proxy 可以直接拦截数组的索引访问和修改
,使得对数组的变化更容易被监听到,从而提供了更可靠的响应式行为。
- 更易于处理嵌套对象:
Proxy API 能够
递归地拦截对象的嵌套属性
,而 defineProperty 无法自动递归处理嵌套对象。这使得在 Vue 3.0中处理嵌套对象更加简单和方便。
- 更好的错误提示:
相比于 defineProperty,Proxy API 提供了更好的
错误追踪
和调试信息
。当使用 Proxy API时,如果访问或修改了一个不存在的属性,会直接抛出错误
,从而更容易发现和修复问题。
使用 Proxy API 取代 defineProperty API 是为了提升性能、增强功能,并提供更好的开发体验和错误提示。这些改进使得 Vue 3.0 的响应式系统更加高效、灵活和可靠。
题4:SSR是什么?vue中怎么实现?
(1)什么是 SSR:
SSR(Server-Side Rendering,服务器端渲染)是一种将应用程序的界面在
服务器上进行预先渲染
并以HTML形式发送到客户端
的技术。与传统的客户端渲染(CSR)相比,SSR 在服务器端生成完整的 HTML页面,然后将其发送到浏览器,以提供更好的性能和搜索引擎优化。
在传统的客户端渲染中,浏览器会下载一个包含 JavaScript 代码的文件,并在客户端执行该代码来构建和呈现页面。这意味着页面初始加载时只是一个空壳,页面内容需要在浏览器中通过 JavaScript 进行渲染。
而在 SSR 中,服务器接收到请求后,会根据请求的路由和数据,预先生成完整的 HTML 页面,其中包含了初始状态下的页面内容。服务器将这个完整的 HTML 页面发送给浏览器,浏览器无需再执行额外的 JavaScript,即可直接展示出页面内容。
(2)SSR 的优势包括:
更快的首次渲染、更好的搜索引擎优化(SEO)、更好的用户体验
• 1、更快的首次渲染:
由于服务器在响应请求时已经生成了完整的 HTML 页面,所以用户打开页面时可以立即看到内容,
无需
等待JavaScript 下载和执行
。
• 2、更好的搜索引擎优化(SEO):
搜索引擎爬虫
能够抓取到完整的 HTML 页面,并且页面内容可直接被搜索引擎索引
。
• 3、更好的用户体验:
页面内容在服务器端渲染完成后即可展示,减少了
白屏时间
和加载等待
。
需要注意的是,SSR 可能会增加服务器负载和响应时间,并且涉及到一些复杂性,例如处理路由、状态管理等。因此,在选择是否使用 SSR 时,需要根据项目需求和复杂性来权衡利弊。
(3)怎么使用 Vue 框架实现 SSR:
可以按照以下步骤进行操作:
1. 安装相关依赖:
首先,确保你的项目中已经安装了 Vue 相关的依赖和构建工具,如 Vue、Vue Router、Vue Server Renderer 等。
创建服务器入口文件:
在项目中创建一个服务器入口文件
,通常命名为 server.js
或类似名称。
在该文件中,引入必要的模块,包括 Vue
、Vue Server Renderer
、Express
(或其他后端框架)等。
创建一个 Express 应用实例
,并设置路由处理器来处理不同请求。
2. 编写服务器端渲染逻辑:
在服务器入口文件中,编写服务器端渲染的逻辑。
创建一个 Vue 实例,并配置路由、数据等相关内容。
使用 Vue Server Renderer 的 createRenderer 方法创建一个 renderer 实例。
在路由处理器中调用 renderer 实例的 renderToString
方法来将 Vue 实例渲染为字符串。
3. 处理静态资源:
在服务器端渲染时,需要处理静态资源(如样式表、图片等)的加载和引用。
可以使用 Webpack 进行服务器端渲染的配置
,以处理静态资源的导出和加载。
4. 客户端激活:
在服务器端渲染后,需要在客户端激活 Vue 实例,以便能够响应交互事件和更新页面。
可以通过在 HTML 中插入一个 JavaScript 脚本,并在脚本中用 createApp
方法来创建客户端应用程序实例
。
以上步骤是一个简单的 SSR 实现流程,可以参考 Vue 官方文档中提供的 SSR 指南获取更详细的信息和示例代码。
题5:vue的祖孙组件的通信方案有哪些?
在 Vue 中,祖孙组件之间的通信可以通过以下几种方式来实现:
1、Props / $emit;2、Provide / Inject;3、Event Bus;4、Vuex
1、Props / $emit:
祖组件通过 props 将数据传递给子组件,并且子组件通过
$emit
触发事件将数据传递回祖组件。
这是一种常见的父子组件通信方式,通过属性(props)和自定义事件($emit)进行数据交流。
2、Provide / Inject:
使用 provide 在祖组件中提供数据,然后使用 inject 在孙组件中注入这些数据。
这种方式允许祖组件向下级组件共享数据,无需显式地将数据逐层传递。但要注意潜在的耦合性。
3、Event Bus:
创建一个全局的事件总线(Event Bus),用于在祖孙组件之间发送和接收事件。
通过在事件总线上注册事件监听器和触发器,组件可以相互通信,传递数据和触发特定操作。
4、Vuex:
使用 Vuex 进行状态管理,可以在祖孙组件之间共享和更新数据。
Vuex 是 Vue 的官方状态管理库,提供了集中式存储管理和响应式更新,使得不同组件之间的通信更加简单和可预测。
这些通信方式各有特点,可以根据具体情况选择合适的方式来实现祖孙组件之间的通信。对于简单的父子组件通信,Props / $emit 是常用的方式;而对于更复杂的应用程序状态管理和跨层级通信,使用 Vuex 或 Event Bus 可能更适合。
题6:如何打破scope对样式隔离的限制?
在 Vue 中,作用域样式(Scoped Styles)的目的是将样式限制在单个组件的作用域中
,以确保样式不会被其他组件影响。然而,有时候你可能需要打破作用域限制,让样式能够在组件外部生效。以下是几种打破作用域限制的方式:
1、使用 /deep/ 或 ::v-deep:
在样式中使用 /deep/
或 ::v-deep
(Vue 2.x 中的别名)选择器可以覆盖作用域限制。
这样可以使得样式选择器的范围扩大到所有子组件,甚至是整个应用程序的 DOM 树。
例如,使用 .container /deep/ .child 可以选择 .child 类名的元素,即使 .child 是在另一个组件中定义的。
2、使用全局样式:
如果你希望一些样式在多个组件之间共享,并且不受作用域限制,可以使用全局样式。
在 Vue 单文件组件中,可以在 <style>
标签外部或使用 @import 引入全局样式文件,这样样式将不受作用域限制。
3、使用类名继承:
如果你希望某些样式继承自父组件或特定组件的样式,可以使用类名继承。
在子组件的 <style> 标签
中使用 @extend 来继承父组件或其他组件的样式,这样可以打破作用域限制。
需要注意的是,打破作用域限制可能会导致样式冲突和不可预测的结果。建议尽量遵循作用域限制,仅在必要时才使用上述方法来打破限制。同时,合理地组织组件结构和样式层级,可以更好地管理样式和避免冲突。
题7:Scoped Styles为什么可以实现样式隔离?
在 Vue 中,作用域样式(Scoped Styles)是通过以下原理实现的:
1、唯一选择器;2、编译时转换;3、渲染时应用。
1、唯一选择器:
当 Vue 编译单文件组件时,在样式中使用 scoped 特性或 module 特性时,Vue 会为每个样式选择器生成一个唯一的属性选择器。
这里的唯一选择器是类似于 [data-v-xxxxxxx]
的属性选择器,其中 xxxxxxx 是一个唯一的标识符。
2、编译时转换:
Vue 在编译过程中会解析单文件组件的模板
,并对样式进行处理。
对于具有 scoped
特性的样式,Vue 会将选择器转换为带有唯一属性选择器的形式,例如 .class 会被转换为 .class[data-v-xxxxxxx]
。
对于具有 module 特性的样式,Vue 会为每个选择器生成一个唯一的类名,并将类名与元素关联起来。
3、渲染时应用:
在组件渲染过程中,Vue 会为组件的根元素添加一个属性值为唯一标识符的属性,例如 data-v-xxxxxxx
。
当组件渲染完成后,样式选择器中的唯一属性选择器或唯一类名将与组件根元素的属性匹配,从而实现样式的隔离。
这样,只有具有相同属性值的元素才会应用相应的样式,避免了样式冲突和泄漏。
通过以上原理,Vue 实现了作用域样式的隔离。每个组件的样式都被限制在自己的作用域内,不会影响其他组件或全局样式。这种方式实现了组件级别的样式隔离,使得组件可以更好地封装和重用,同时减少了样式冲突的可能性。
题8:vue中怎么实现样式隔离?
Vue 提供了几种方式来实现样式的隔离:
1.作用域样式(Scoped Styles):
在 Vue 单文件组件中,可以使用 scoped 特性将样式限定于当前组件的作用域。
使用<style scoped>标签
包裹的样式只对当前组件起作用,不会影响其他组件或全局样式。
Vue 实现作用域样式的方式是通过给每个选择器添加一个唯一的属性选择器,以确保样式仅适用于当前组件。
2.CSS Modules:
Vue 支持使用 CSS Modules 来实现样式的模块化和隔离。
在 Vue 单文件组件中,可以借助 module 特性启用 CSS Modules 功能,在样式文件中使用类似 :local(.className) 的语法来定义局部样式。
CSS Modules 会自动生成唯一的类名,并在编译时将类名与元素关联起来,从而实现样式的隔离和局部作用域。
CSS-in-JS 方案:
3.Vue 也可以结合 CSS-in-JS 库
(如 styled-components、emotion 等)来实现样式的隔离。
使用这种方式,可以直接在组件代码中编写样式,并通过 JavaScript 对象或模板字符串的形式动态生成样式。
CSS-in-JS
方案将样式与组件紧密关联,实现了更高程度的样式隔离和可重用性。
这些方法各有特点,可以根据实际需求选择合适的方式来实现样式的隔离。作用域样式和 CSS Modules 是 Vue 官方提供的内置功能,而 CSS-in-JS 则是通过第三方库来实现。根据项目的规模和需求,选择适合的方式可以更好地管理和维护样式。
题9:created和mounted有什么区别?
在Vue中,created和mounted是两个常用的生命周期钩子函数,它们在组件的生命周期中扮演着不同的角色:
created:
created是组件生命周期中的一个钩子函数,在Vue实例被创建后立即调用。
在created钩子函数中,Vue实例已经完成了数据观测(data
observation),但尚未渲染真实DOM。这意味着你可以访问实例中的数据、方法、计算属性等,但不能保证实例已经被插入到DOM中。
created常用于一些初始化操作,例如数据请求、事件监听或其他非DOM相关的任务。因为此时,组件的模板还未被编译成真实DOM。
mounted:
mounted是组件生命周期中的一个钩子函数,在
Vue实例挂载到DOM后调用
。
在mounted钩子函数中,Vue实例已经完成了模板编译,并且已经将生成的虚拟DOM渲染到真实DOM中。
mounted常用于需要对DOM进行操作的任务,例如初始化第三方库、绑定事件监听器、执行动画等。因为此时,组件已经被插入到DOM中,可以安全地访问和操作DOM元素。
区别总结:
created在实例创建后被调用,适合处理数据初始化和非DOM相关的任务。
mounted在实例挂载到DOM后被调用,适合进行DOM操作、初始化第三方库和绑定事件监听。
题10:vue是怎么把template模板编译成render函数的?
1.解析模板;2.优化AST;3.生成渲染函数;4.渲染虚拟DOM;5.生成DOM
Vue的编译过程将模板转换为渲染函数,这是Vue在运行时动态编译和渲染组件的关键步骤。下面是Vue将模板编译成渲染函数的大致过程:
1.解析模板:首先,Vue会解析模板字符串,将其转化为抽象语法树(AST)
。AST是一个表示模板结构和内容的树状数据结构。
2.优化AST:接下来,Vue会对AST进行优化处理,以提升渲染性能。这包括标记静态节点
、静态属性
和静态文本
等。
3.生成渲染函数:利用优化后的AST,Vue会生成渲染函数。渲染函数是一个JavaScript函数,它接收一个上下文对象作为参数,并返回一个虚拟DOM树(VNode)
。
4.渲染虚拟DOM:当执行渲染函数时,它将生成一个新的虚拟DOM树。如果之前已经存在真实的DOM树,Vue将通过比较新旧VNode来计算最小的更新操作并应用在真实DOM上,从而进行局部更新,提高效率。
5.生成DOM:最后,Vue将根据最新的VNode生成真实的DOM元素,并将其插入到页面中,完成渲染。
Vue的编译过程通常在构建时(比如使用Vue CLI)或运行时的初始阶段完成
,以便在实际渲染组件时获得更好的性能。这样一来,渲染函数会被缓存并重复使用,而不需要每次重新编译模板。
Vue还可以使用render函数直接编写组件而不依赖于模板。这种情况下,手动编写的render函数会跳过模板解析和优化的步骤,直接生成渲染函数并进行渲染。这种方式可以在需要更高级别的动态和灵活性时使用。
题11:谈谈你对vue中双向绑定的理解?
一、什么是双向绑定
我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定举个栗子:当用户填写表单时,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定
二、双向绑定的原理是什么
我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成:数据层、视图层、业务逻辑层
;
1.数据层(Model):应用的数据及业务逻辑
2.视图层(View):应用的展示效果,各类UI组件
3.业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理12:03 2024/1/15
理解ViewModel
它的主要职责就是:
数据变化后更新视图
视图变化后更新数据
当然,它还有两个主要部分组成
1.
监听器(Observer)
:对所有数据的属性进行监听
2.解析器(Compiler)
:对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
三、实现双向绑定
我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的
1.new Vue()首先执行初始化,对data执行响应化处理
,这个过程发生Observe中
2.同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图
,这个过程发生在Compile中
3.同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4.由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
5.将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
实现
1.先来一个构造函数:执行初始化,对data执行响应化处理
class Vue { constructor(options) { this.$options = options; this.$data = options.data; // 对data选项做响应式处理 observe(this.$data); // 代理data到vm上 proxy(this); // 执行编译 new Compile(options.el, this); }
}
2.对data选项执行响应化具体操作
function observe(obj) { if (typeof obj !== "object" || obj == null) { return; } new Observer(obj);
} class Observer { constructor(value) { this.value = value; this.walk(value); } walk(obj) { Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); }
}
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile { constructor(el, vm) { this.$vm = vm; this.$el = document.querySelector(el); // 获取dom if (this.$el) { this.compile(this.$el); } } compile(el) { const childNodes = el.childNodes; Array.from(childNodes).forEach((node) => { // 遍历子元素 if (this.isElement(node)) { // 判断是否为节点 console.log("编译元素" + node.nodeName); } else if (this.isInterpolation(node)) { console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}} } if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素 this.compile(node); // 对子元素进行递归遍历 } }); } isElement(node) { return node.nodeType == 1; } isInterpolation(node) { return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent); }
}
依赖收集
视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知
实现思路
1.defineReactive时为每⼀个key创建⼀个Dep实例
2.初始化视图时读取某个key,例如name1,创建⼀个watcher1
3.由于触发name1的getter方法,便将watcher1添加到name1对应的Dep中
4.当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
// 负责更新视图
class Watcher { constructor(vm, key, updater) { this.vm = vm this.key = key this.updaterFn = updater // 创建实例时,把当前实例指定到Dep.target静态属性上 Dep.target = this // 读一下key,触发get vm[key] // 置空 Dep.target = null } // 未来执行dom更新函数,由dep调用的 update() { this.updaterFn.call(this.vm, this.vm[this.key]) }
}
声明Dep:
class Dep { constructor() { this.deps = []; // 依赖管理 } addDep(dep) { this.deps.push(dep); } notify() { this.deps.forEach((dep) => dep.update()); }
}
创建watcher时触发getter:
class Watcher { constructor(vm, key, updateFn) { Dep.target = this; this.vm[this.key]; Dep.target = null; }
}
依赖收集,创建Dep实例:
function defineReactive(obj, key, val) { this.observe(val); const dep = new Dep(); Object.defineProperty(obj, key, { get() { Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例 return val; }, set(newVal) { if (newVal === val) return; dep.notify(); // 通知dep执行更新方法 }, });
}
题12:v-model的原理?
v-model是Vue.js框架中的一个指令,用于在
表单元素
和组件
之间实现双向数据绑定
。它提供了一种简洁的方式来将表单输入的值与Vue实例的属性进行关联。
当使用v-model指令时,Vue会根据表单元素的类型(如input、select、textarea等)自动
为其添加相应的事件监听器
,并在用户输入时更新绑定的数据。
具体地讲,v-model的原理如下:
1.在模板中,我们可以使用v-model指令来绑定一个变量到表单元素(或组件)上,例如:<input v-model="message">
。
2.Vue解析模板时,会将v-model指令转换成合适的属性和事件绑定。对于大多数表单元素,它会将value属性与输入框的当前值进行绑定,并监听input事件来实时更新绑定的数据。
3.当用户在输入框中键入或选择内容时,触发input事件
。Vue会捕获该事件并更新绑定的数据,以及根据数据的变化重新渲染视图
。
4.同样地,如果在表单元素上使用v-model的lazy修饰符,Vue会监听change事件而不是input事件。这样,只有当用户完成输入并触发change事件时,才会更新绑定的数据。
v-model指令实现双向绑定的原理是通过监听表单元素的输入事件
(如input或change),将用户的输入同步到Vue实例中的属性
,并在属性值变化时重新渲染视图。这使得我们可以轻松地将表单数据与Vue实例的状态保持同步,消除了手动监听和更新的冗余代码。
题13:说说vuex的原理?
Vuex是Vue.js应用程序开发的状态管理模式和库。它为Vue应用程序提供了一个集中式的存储机制,用于管理应用程序的所有组件的状态。Vuex的设计受到了Flux和Redux的影响,它通过以下几个核心概念来工作:
1.State(状态):应用程序的数据存储在一个单一的状态树
中,即state。这个状态树是响应式的,当状态发生变化时,相关的组件将自动更新。
2.Getter(获取器):getter允许从state中派生出一些衍生的状态,类似于计算属性。可以使用getter来对state进行处理和计算
,并将其暴露给组件使用。
3.Mutation(突变):mutation是用于修改state的唯一途径
。它定义了一些操作函数,每个函数都有一个特定的名称(称为type),并且可以在这些函数中改变state的值。mutation必须是同步的
,以确保状态变更是可追踪的。
4.Action(动作):action用于处理异步操作和复杂的业务逻辑
。类似于mutation,但action可以包含异步操作,可以在action中触发多个mutation
,也可以在action中调用其他action。
5.Module(模块):为了更好地组织和拆分大型的应用程序,Vuex允许将state、getter、mutation和action划分为模块。每个模块都有自己的state、getter、mutation和action,并且可以被嵌套和组合。
通过以上的核心概念,Vuex提供了一种可预测的状态管理方式,使得多个组件之间共享和同步状态变得更加容易和可控。它简化了应用程序的状态管理,提高了代码的可维护性和复用性。
题14:vue中给对象添加新属性时,界面不刷新怎么办?
若想实现数据与视图同步更新,可采取下面三种解决方案:
1.Vue.set();2.Object.assign();3.$forcecUpdated()
情景:
一、直接添加属性的问题
我们从一个例子开始:
定义一个p标签,通过v-for指令进行遍历
然后给botton标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面也 新增一行
<p v-for="(value,key) in item" :key="key">{{ value }}
</p>
<button @click="addProperty">动态添加新属性</button>
实例化一个vue实例,定义data属性和methods方法
const app = new Vue({el:"#app",data:()=>{item:{oldProperty:"旧属性"}},methods:{addProperty(){this.items.newProperty = "新属性" // 为items添加新属性console.log(this.items) // 输出带有newProperty的items}}
})
点击按钮,发现结果不及预期,数据虽然更新了(console打印出了新属性),但页面并没有更新
二、原理分析
为什么产生上面的情况呢?
下面来分析一下
vue2是用过Object.defineProperty实现数据响应式
const obj = {}
Object.defineProperty(obj, 'foo', {get() {console.log(`get foo:${val}`);return val},set(newVal) {if (newVal !== val) {console.log(`set foo:${newVal}`);val = newVal}}
})
当我们访问foo属性或者设置foo值的时候都能够触发setter与getter
obj.foo
obj.foo = ‘new’
但是我们为obj添加新属性的时候,却无法触发事件属性的拦截
obj.bar = ‘新属性’
原因是一开始obj的foo属性被设成了响应式数据,而bar是后面新增的属性,并没有通过Object.defineProperty设置成响应式数据
三、解决方案
Vue 不允许在已经创建的实例上动态添加新的响应式属性
若想实现数据与视图同步更新,可采取下面三种解决方案:
1.Vue.set();2.Object.assign();3.$forcecUpdated()。
1.Vue.set()
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
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实现属性拦截
大致代码如下:
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}}})
}
2.Object.assign()
直接使用Object.assign()添加到对象的新属性不会触发更新
应创建一个新的对象,合并原对象和混入对象的属性
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
3.$forceUpdate
如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事
$forceUpdate迫使 Vue 实例重新渲染
PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
小结:
如果为对象添加少量的新属性,可以直接采用Vue.set() 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)
PS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式
题15:vue组件间通信方式有哪些?
一、组件间通信的分类
组件间通信的分类可以分成以下
1.父子组件之间的通信
2.兄弟组件之间的通信
3.祖孙与后代组件之间的通信
4.非关系组件间之间的通信
二、组件间通信的方案
vue中8种常规的通信方案
1.通过 props 传递
2.通过 emit触发自定义事件3.使用ref4.EventBus5.emit 触发自定义事件 3.使用 ref 4.EventBus 5.emit触发自定义事件3.使用ref4.EventBus5.parent 或$root
6.attrs 与 listeners
7.Provide 与 Inject
8.Vuex
1.props传递数据
适用场景:父组件传递数据给子组件
子组件设置props属性,定义接收父组件传递过来的参数
父组件在使用子组件标签中通过字面量来传递值
Children.vue:
props:{ // 字符串形式 name:String // 接收的类型参数 // 对象形式 age:{ type:Number, // 接收的类型为数值 defaule:18, // 默认值为18 require:true // age属性必须传递 }
}
Father.vue组件:
<Children name="jack" age=18 />
2.$emit 触发自定义事件
适用场景:子组件传递数据给父组件
子组件通过emit触发自定义事件,emit触发自定义事件,emit触发自定义事件,emit第二个参数为传递的数值
父组件绑定监听器获取到子组件传递过来的参数
Chilfen.vue组件:
this.$emit('add', good)
Father.vue组件:
<Children @add="cartAdd($event)" />
3.ref
父组件在使用子组件的时候设置ref
父组件通过设置子组件ref来获取数据
父组件:
<Children ref="foo" />
this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据
4.EventBus
使用场景:兄弟组件传值
创建一个中央事件总线EventBus
兄弟组件通过emit触发自定义事件,emit触发自定义事件,emit触发自定义事件,emit第二个参数为传递的数值
另一个兄弟组件通过$on监听自定义事件
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的功能
Children1.vue组件:
this.$bus.$emit('foo')
Children2.vue组件:
this.$bus.$on('foo', this.handle)
5.$parent 或 $root
通过共同祖辈parent或者parent或者parent或者root搭建通信侨联
兄弟组件:
this.$parent.$on('add',this.add)
另一个兄弟组件:
this.$parent.$emit('add')
6.attrs与attrs 与attrs与 listeners
适用场景:祖先传递数据给子孙
设置批量向下传属性$attrs和 $listeners
包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。
可以通过 v-bind="$attrs"
传⼊内部组件
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p> // parent
<HelloWorld foo="foo"/> // 给Grandson隔代传值,communication/index.vue
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2> // Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson> // Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">
{{msg}}
</div>
7.provide 与 inject
在祖先组件定义provide属性,返回传递的值
在后代组件通过inject接收组件传递过来的值
祖先组件:
provide(){ return { foo:'foo' }
}
后代组件:
inject:['foo'] // 获取到祖先组件传递过来的值
8.vuex
适用场景: 复杂关系的组件数据传递
Vuex作用相当于一个用来存储共享变量的容器
state用来存放共享变量的地方
getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
mutations用来存放修改state的方法。
actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作
小结
父子关系的组件数据传递选择
props
与$emit
进行传递,也可选择ref
兄弟关系的组件数据传递可选择$bus
,其次可以选择$parent
进行传递 祖先与后代组件数据传递可选择attrs与listeners或者
Provide
与Inject
复杂关系的组件数据传递可以通过vuex存放共享的变量
题16:vue3有了解过吗?能说说跟vue2的区别吗?
以下是一些主要区别的总结:
1.响应式系统(Reactivity System):
Vue 3 引入了 Composition API,这是一种新的响应式系统。Composition API提供了更灵活和强大的组件状态和逻辑管理方式,使代码组织和重用更加方便。
Composition API使用函数而不是对象,可以提高摇树优化(Tree Shaking)
并减小打包体积。
2.更小的包体积:
Vue 3 通过更好的 Tree Shaking 和更高效的运行时代码生成,相较于 Vue 2,打包体积更小。Vue 3
的响应式系统也经过优化,性能更好。
3.性能改进:
Vue 3 采用了更快、更高效的渲染机制,得益于新的编译器。虚拟 DOM 的差异化算法经过优化,减少不必要的更新,提升渲染性能。
4.作用域插槽替代为 <slot>
:
在 Vue 3 中,作用域插槽的概念被更直观、更简化的
<slot>
语法所取代,使得在组件组合中定义和使用插槽更加容易。
5.引入 Teleport 组件:
Vue 3 引入了 Teleport 组件,可以
在 DOM 树中的不同位置渲染内容
,用于创建模态框
、工具提示
和其他覆盖层效果
。
6.片段(Fragments):
Vue 3 引入了一个名为片段(Fragment)的内置组件,允许
将多个元素进行分组
,而无需添加额外的包装元素。
7.更好的 TypeScript 支持:
Vue 3 默认提供了更好的 TypeScript 支持,具有增强的类型推断和与 TypeScript 工具更好的集成。
8.简化的 API:
Vue 3 对许多 API 进行了简化和优化,使得学习和使用框架更加容易。新的 API 提供了更好的一致性,并与 JavaScript
标准更加对齐。
虽然 Vue 3 引入了这些变化,但它保持与 Vue 2 API 的向后兼容性,允许现有的 Vue 2 项目逐步升级。Vue 3 提供了一个迁移构建版本,与大多数 Vue 2 代码兼容,从而使开发者的过渡更加平滑。
总体而言,Vue 3 在性能、包体积和开发者体验方面带来了显著的改进,同时引入了 Composition API作为管理组件状态和逻辑的更强大工具。
题17:自定义指令是什么?有什么应用场景?
在 Vue 中,自定义指令(Custom Directive)是一种
用于扩展 Vue 的模板语法的机制
。通过自定义指令,你可以在 DOM元素上添加自定义行为,并在元素插入、更新和移除时进行相应的操作。
自定义指令由 Vue.directive 函数定义
,它接收两个参数:指令名称
和指令选项对象
。指令选项对象包含一系列钩子函数,用于定义指令的行为。
以下是一些常见的自定义指令应用场景:
1.操作 DOM:
自定义指令可以用于直接操作 DOM 元素,例如修改元素的样式、属性、事件绑定等。你可以通过在指令的钩子函数
中访问和操作 DOM 元素
。
2.表单验证:
你可以创建自定义指令来实现表单验证逻辑。通过自定义指令,你可以监听输入框的值变化,并根据自定义的验证规则进行验证,以便提供实时的反馈。
3.权限控制:
自定义指令可以用于权限控制场景,例如根据用户权限来隐藏或禁用某些元素。你可以在自定义指令中根据用户权限进行条件判断,并修改元素的显示或行为。
4.第三方库集成:
当你需要在 Vue 中使用第三方库或插件时,可以使用自定义指令来进行集成。你可以创建一个自定义指令,在其中初始化和配置第三方库,并在适当的时机调用库的方法。
5.动画和过渡效果:
自定义指令可以与 Vue 的过渡系统一起使用,实现自定义的动画和过渡效果。你可以在自定义指令中监听过渡钩子函数
,并根据需要操作元素的样式或类名来实现过渡效果。
这只是一些常见的应用场景,实际上自定义指令的应用范围非常广泛,可以根据具体需求进行灵活的使用。通过自定义指令,你可以扩展 Vue 的能力,实现更复杂和灵活的交互行为。
题18.说说vue中key的原理?
在 Vue 中
,key 是用于帮助 Vue 识别和跟踪虚拟 DOM 的变化的特殊属性
。当 Vue 更新渲染真实 DOM 时,它使用 key属性来比较新旧节点,并尽可能地复用已存在的真实 DOM 节点,以提高性能。
Vue 在进行虚拟 DOM
的 diff
算法时,会使用 key 来匹配新旧节点,以确定节点的更新、移动或删除。它通过 key 属性来判断两个节点是否代表相同的实体,而不仅仅是根据它们的内容是否相同。这样可以保留节点的状态和避免不必要的 DOM 操作。
key 的工作原理如下:
1.当 Vue 更新渲染真实 DOM 时,它会对
新旧节点进行比较
,找出它们之间的差异
。
2.如果两个节点具有相同的 key 值,则 Vue 认为它们是相同的节点,会尝试复用已存在的真实 DOM 节点。
3.如果节点具有不同的 key 值,Vue 会将其视为不同的节点,并进行适当的更新、移动或删除操作。
使用 key 可以提供更准确的节点识别和跟踪,避免出现一些常见的问题,比如在列表中重新排序时导致的元素闪烁、输入框内容丢失等。
key 必须是唯一且稳定
的,最好使用具有唯一标识的值,例如使用数据的唯一 ID。同时,不推荐使用随机数作为 key,因为在每次更新时都会生成新的 key,导致所有节点都重新渲染,无法复用已有的节点,降低性能。
题19:vue常用的修饰符有哪些?分别有什么应用场景?
一、修饰符是什么
在程序世界里,修饰符是用于
限定类型
以及类型成员的声明
的一种符号
在Vue中,修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理
vue中修饰符分为以下五种:
1.表单修饰符
2.事件修饰符
3.鼠标按键修饰符
4.键值修饰符
5.v-bind修饰符
二、修饰符的作用
1.表单修饰符
在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model
关于表单的修饰符有如下:
1)lazy
2)trim
3)number
1)lazy
在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步
<input type="text" v-model.lazy="value">
<p>{{value}}</p>
2)trim
自动过滤用户输入的首空格字符,而中间的空格不会过滤
<input type="text" v-model.trim="value">
3)number
自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值
<input v-model.number="age" type="number">
2.事件修饰符
事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:
stop prevent self once capture passive native
1)stop
阻止了事件冒泡,相当于调用了event.stopPropagation方法
<div @click="shout(2)"><button @click.stop="shout(1)">ok</button>
</div>
//只输出1
2)prevent
阻止了事件的默认行为,相当于调用了event.preventDefault方法
<form v-on:submit.prevent="onSubmit"></form>
3)self
只当在 event.target 是当前元素自身时触发处理函数
<div v-on:click.self="doThat">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击
4)once
绑定了事件以后只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
5)capture
使事件触发从包含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">obj1
<div @click.capture="shout(2)">obj2
<div @click="shout(3)">obj3
<div @click="shout(4)">obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3
6)passive
在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。
passive 会告诉浏览器你不想阻止事件的默认行为
7)native
让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>
使用.native修饰符来操作普通HTML标签是会令事件失效的
3.鼠标按钮修饰符
鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:
left 左键点击
right 右键点击
middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>
4.键盘修饰符
键盘修饰符是用来修饰键盘事件(onkeyup,onkeydown)的,有如下:
keyCode存在很多,但vue为我们提供了别名,分为以下两种:
普通键(enter、tab、delete、space、esc、up…)
系统修饰键(ctrl、alt、meta、shift…)
// 只有按键为keyCode的时候才触发
<input type="text" @keyup.keyCode="shout()">
还可以通过以下方式自定义一些全局的键盘码别名
Vue.config.keyCodes.f2 = 113
5.v-bind修饰符
v-bind修饰符主要是为属性进行操作,用来分别有如下:
sync
prop
camel
sync
能对props进行一个双向绑定
//父组件
<comp :myMessage.sync="bar"></comp> //子组件
this.$emit('update:myMessage',params);
以上这种方法相当于以下的简写//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){this.bar = e;
}//子组件js
func2(){this.$emit('update:myMessage',params);
}
使用sync需要注意以下两点:
使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的
props
设置自定义标签属性,避免暴露数据,防止污染HTML结构
<input id="uid" title="title1" value="1" :index.prop="index">
camel
将命名变为驼峰命名法,如将 view-Box属性名转换为 viewBox
<svg :viewBox="viewBox"></svg>
三、应用场景
根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:
.stop:阻止事件冒泡
.native:绑定原生事件
.once:事件只执行一次
.self :将事件绑定在自身身上,相当于阻止事件冒泡
.prevent:阻止默认事件
.capture:用于事件捕获
.once:只触发一次
.keyCode:监听特定键盘按下
.right:右键
题20:vue的响应式开发比命令式有哪些优势?
Vue 的响应式开发相较于命令式开发有以下优势:
1.简化代码;
2.提高可维护性;
3.增强用户体验;
4.支持复杂组件设计。
1.简化代码:
在 Vue 中,通过将数据和模板绑定起来实现视图更新的自动化,从而避免了手动操作 DOM 的繁琐和容易出错的操作。因此,可以大幅减少编写样板代码和调试代码所需的时间。
2.提高可维护性:
使用 Vue 的响应式开发可以帮助我们更方便地管理应用程序的状态,并对状态变化进行统一处理。这不仅可以提高代码的可读性和可维护性,还可以更方便地进行单元测试和集成测试。
3.增强用户体验:
通过 Vue 的响应式开发,可以实现局部更新、异步加载等功能,从而提升用户体验。例如,在列表中添加或删除项目时,只需要更新相应的项目,而不是重新渲染整个列表。又比如,在加载大量图片时,可以通过异步加载和懒加载的方式,提高页面加载速度和用户体验。
4.支持复杂组件设计:
Vue 的响应式开发支持组件化设计,它能够轻松地将一个大型应用程序拆分成多个小型、可重用的组件。这些组件可以根据需要进行嵌套和组合,形成更为复杂和丰富的 UI 界面,而且每个组件都具有独立的状态和生命周期。
总之,Vue 的响应式开发可以帮助我们更高效、更方便、更灵活地进行前端开发,从而提供更好的用户体验和更高的代码质量。
题21:vue的route和route和route和router有什么区别?
在 Vue.js 中,$route 和 $router 都是与路由相关的对象,但它们之间有以下区别:
$route:
$route
是一个当前路由信息的对象
,包括当前 URL 路径、查询参数、路径参数等信息。
$route对象是只读的
,不可以直接修改其属性值,而需要通过路由跳转来更新。
$router:
$router
是Vue Router 的实例对象
,包括了许多用于导航控制和路由操作的 API,
例如push、replace、go、forward
等方法。$router 可以用来动态地改变 URL,从而实现页面间的无刷新跳转。
因此,$route
和 $router
在功能上有所不同,$route
主要用于获取当前路由信息,$router
则是用于进行路由操作,例如跳转到指定的路由、前进、后退等。通常来说,$route 和 $router 是紧密关联的,并且常常一起使用。
题22:vue3.0的Treeshaking特性是什么,并举例进行说明?
一、是什么
Tree shaking 是一种通过
清除多余代码方式
来优化项目打包体积
的技术,专业术语叫 Dead code elimination
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去
而treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕
也就是说 ,tree shaking 其实是找出使用的代码
在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到
import Vue from 'vue'
Vue.nextTick(() => {})
而Vue3源码
引入tree shaking特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中
import { nextTick, observable } from 'vue'nextTick(() => {})
二、如何做
Tree shaking是基于
ES6模板语法(import与export)
,主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Tree shaking无非就是做了两件事:
编译阶段利用ES6 Module判断哪些模块已经加载
判断那些模块和变量未被使用或者引用,进而删除对应代码
下面就来举个例子:
通过脚手架vue-cli安装Vue2与Vue3项目
vue create vue-demo
Vue2 项目
组件中使用data属性
<script>export default {data: () => ({count: 1,}),};
</script>
对项目进行打包,体积如下图
为组件设置其他属性(compted、watch)
export default {data: () => ({question:"", count: 1,}),computed: {double: function () {return this.count * 2;},},watch: {question: function (newQuestion, oldQuestion) {this.answer = 'xxxx'}
};
再一次打包,发现打包出来的体积并没有变化
Vue3 项目
组件中简单使用
import { reactive, defineComponent } from "vue";
export default defineComponent({setup() {const state = reactive({count: 1,});return {state,};},
});
将项目进行打包
在组件中引入computed和watch
import { reactive, defineComponent, computed, watch } from "vue";
export default defineComponent({setup() {const state = reactive({count: 1,});const double = computed(() => {return state.count * 2;});watch(() => state.count,(count, preCount) => {console.log(count);console.log(preCount);});return {state,double,};},
});
再次对项目进行打包,可以看到在引入computer和watch之后,项目整体体积变大了
三、作用
通过Tree shaking,Vue3给我们带来的好处是:
减少程序体积(更小)
减少程序执行时间(更快)
便于将来对程序架构进行优化(更友好)
题23:vue和react在技术层面上有哪些区别?
React 和 Vue 是当前比较流行的前端框架,它们在技术层面有以下区别:
1.组件化方式不同;2.数据驱动方式不同;3.模板语法不同;4.生命周期不同;5.状态管理方式不同;6.性能优化方式不同。
1.组件化方式不同:
React 是基于组件实现的,组件包含了状态和行为,所有组件共享一个状态树。
Vue也是基于组件实现的,但是每个组件都有自己的状态,并且可以很容易地将数据和行为绑定在一起。
2.数据驱动方式不同:
React 使用
单向数据流来管理数据
,即从父组件到子组件的传递,所以 React 中组件之间的数据交互相对更加复杂。
Vue 则使用双向数据绑定来管理数据
,使得组件之间的数据交互更加简洁。
3.模板语法不同:
React 使用
JSX 语法
,将 HTML 和 JavaScript 结合在一起,使得编写组件更加直观和灵活。
Vue 则使用模板语法
,并且支持模板内的表达式和指令,使得编写组件具有更高的可读性和可维护性。
4.生命周期不同:
React 组件的生命周期分为三个阶段:初始化、更新和卸载。Vue 组件的生命周期分为八个阶段:创建、挂载、更新、销毁等。
5.状态管理方式不同:
React 使用 Redux 或者 MobX 来管理应用程序的状态。 Vue 则提供了自己的状态管理库
Vuex,可以更方便地管理组件之间的共享状态。
6.性能优化方式不同:
React 使用
虚拟 DOM 技术
来实现高效的渲染性能,可以减少每次渲染时需要操作真实 DOM 的次数。
Vue则使用模板编译
和响应式系统
来实现高效的渲染性能,并且还提供了一些优化技术,例如懒加载和缓存等。
开发人员可以根据项目需求和个人喜好选择合适的框架。
题24:单页应用如何提高加载速度?
1.使用代码分割:
将代码拆分成小块并按需加载(懒加载),以避免不必要的网络请求和减少加载时间。
2.缓存资源:
利用浏览器缓存来存储重复使用的文件,例如 CSS 和 JS 文件、图片等。
3.预加载关键资源:
在首次渲染之前,先提前加载关键资源,例如首页所需的 JS、CSS 或数据,以保证关键内容的快速呈现。
4.使用合适的图片格式:
选择合适的图片格式(例如 JPEG、PNG、WebP 等),并根据需要进行压缩以减少文件大小。对于一些小图标,可以使用 iconfont 等字体文件来代替。
5.启用 Gzip 压缩:
使用服务器端的 Gzip 压缩算法对文件进行压缩,以减少传输时间和带宽消耗。
6.使用 CDN:
使用内容分发网络(CDN)来缓存和传递文件,以提高文件的下载速度和可靠性。
7.优化 API 请求:
尽可能地减少 API 调用的数量,并使用缓存和延迟加载等技术来优化 API 请求的效率。
8.使用服务器端渲染:
使用服务器端渲染(SSR)来生成 HTML,以减少客户端渲染所需的时间和资源。但需要注意,SSR 也可能增加了服务器的负担并使网站更复杂。
题25:v-if和v-for为何不建议一起用?
一、作用
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染
v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名
在 v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化
两者在用法上
<Modal v-if="isShow" /><li v-for="item in items" :key="item.id">{{ item.label }}
</li>
二、优先级
v-if与v-for都是vue模板系统中的指令
在vue模板编译的时候,会将指令系统转化成可执行的render函数
在 Vue2 当中,v-for的优先级更高,
而在 Vue3 当中,则是v-if的优先级更高。
在 Vue3 当中,做了v-if的提升优化,去除了没有必要的计算,但同时也会带来一个无法取到 v-for 当中遍历的item问题,这就需要开发者们采取其他灵活的方式去解决这种问题。
三、注意事项
永远不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
<template v-if="isShow"><p v-for="item in items">
</template>
如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项
computed: {items: function() {return this.list.filter(function (item) {return item.isShow})}
}
题26:vue项目如何进行部署的?是否有遇到部署服务器刷新404问题?
一、如何部署
前后端分离开发模式下,前后端是独立布署的,前端
只需要将最后的构建物
上传至目标服务器的web容器
指定的静态目录下
即可
我们知道vue项目在构建后,是生成一系列的静态文件
常规布署我们只需要将这个目录上传至目标服务器即可
// scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路径
scp dist.zip user@host:/xx/xx/xx
让web容器跑起来,以nginx为例
server {listen 80;server_name www.xxx.com;location / {index /data/dist/index.html;}
}
配置完成记得重启nginx
// 检查配置是否正确
nginx -t // 平滑重启
nginx -s reload
操作完后就可以在浏览器输入域名进行访问了
当然上面只是提到最简单也是最直接的一种布署方式
什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开
二、404问题
这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?
我们先还原一下场景:
vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了404错误
先定位一下,HTTP 404 错误意味着链接指向的资源不存在
问题在于为什么不存在?且为什么只有history模式下会出现这个问题?
为什么history模式下有问题
Vue是属于单页应用(single-page application)
而SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面
,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html
现在,我们回头来看一下我们的nginx配置
server {listen 80;server_name www.xxx.com;location / {index /data/dist/index.html;}
}
可以根据 nginx 配置得出,当我们在地址栏输入 www.xxx.com
时,这时会打开我们 dist 目录下的 index.html 文件
,然后我们在跳转路由进入到 www.xxx.com/login
关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况
为什么hash模式下没有问题
router hash 模式我们都知道是用符号#表示的,如 website.com/#/login, hash 的值为 #/login
它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误
解决方案
看到这里我相信大部分同学都能想到怎么解决问题了,
产生问题的本质是因为我们的路由是通过JS来执行视图切换的,
当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404
所以我们只需要配置将任意页面
都重定向
到 index.html
,把路由交由前端处理
对nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;
server {listen 80;server_name www.xxx.com;location / {index /data/dist/index.html;try_files $uri $uri/ /index.html;}
}
修改完配置文件后记得配置的更新
nginx -s reload
这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件
为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面
const router = new VueRouter({mode: 'history',routes: [{ path: '*', component: NotFoundComponent }]
})
关于后端配置方案还有:Apache、nodejs等,思想是一致的,这里就不展开述说了
题27:说说vue中的diff算法?
一、是什么
diff 算法是一种通过同层
的树节点
进行比较的高效算法
其有两个特点:
比较只会在
同层级进行
, 不会跨层级比较
在diff比较的过程中,循环从两边向中间比较
diff 算法的在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
二、比较方式
diff整体策略为:深度优先,同层比较
比较只会在同层级进行, 不会跨层级比较
比较的过程中,循环从两边向中间收拢
vue通过diff算法更新的例子
题28:vue模板是如何编译的?
new Vue({render: h => h(App)
})
这个大家都熟悉,调用 render 就会得到传入的模板(.vue文件)对应的虚拟 DOM,那么这个 render 是哪来的呢?它是怎么把 .vue 文件转成浏览器可识别的代码的呢?
render 函数是怎么来的有两种方式
第一种就是经过
模板编译生成 render 函数
第二种是我们自己在组件里定义了 render 函数
,这种会跳过模板编译的过程
本文将为大家分别介绍这两种,以及详细的编译过程原理
认识模板编译
我们知道 <template></template>
这个是模板,不是真实的 HTML,浏览器是不认识模板的,所以我们需要把它编译成浏览器认识的原生的 HTML
这一块的主要流程就是
提取
出模板中的原生 HTML 和非原生 HTML,比如绑定的属性、事件、指令等等
经过一些处理生成 render 函数
render
函数再将模板内容生成对应的 vnode
再经过patch 过程( Diff )
得到要渲染到视图中
的 vnode
最后根据vnode创建真实的 DOM 节点
,也就是原生 HTML 插入到视图中,完成渲染
上面的 1、2、3 条就是模板编译的过程了
那它是怎么编译,最终生成 render 函数的呢?
模板编译详解——源码
题29:说说vue中CSS scoped的原理?
现象:
每个组件都会拥有一个[data-v-hash:8]
插入HTML标签,子组件标签上也具体父组件[data-v-hash:8]
;
如果style标签加了scoped属性,里面的选择器都会变成(Attribute Selector) [data-v-hash:8]
;
如果子组件选择器跟父组件选择器完全一样,那么就会出现子组件样式被父组件覆盖,因为子组件会优先于父组件mounted
题30:vue3中怎么设置全局变量?
方法一 config.globalProperties
vue2.x挂载全局是使用 Vue.prototype.$xxxx=xxx
的形式来挂载,然后通过 this.$xxx
来获取挂载到全局的变量或者方法。
这在 Vue 3 中,就等同于 config.globalProperties
。这些 property 将被复制到应用中作为实例化组件的一部分。
// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}
方法二 Provide / Inject
vue3新的 provide/inject 功能可以穿透多层组件,实现数据从父组件传递到子组件。
可以将全局变量放在根组件的 provide 中,这样所有的组件都能使用到这个变量。
如果需要变量是响应式的,就需要在 provide 的时候使用 ref 或者 reactive 包装变量。
题31:刷新浏览器后,vuex的数据是否存在?如何解决?
在vue项目中用vuex来做全局的状态管理, 发现当刷新网页后,保存在vuex实例store里的数据会丢失。
原因:因为 store 里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载vue实例,store里面的数据就会被重新赋值初始化。
我们有两种方法解决该问题:
1.使用 vuex-along
2.使用 localStorage 或者 sessionStroage
1.使用vuex-along:
vuex-along 的实质也是将 vuex 中的数据存放到 localStorage 或者 sessionStroage 中,只不过这个存取过程组件会帮我们完成,我们只需要用vuex的读取数据方式操作就可以了,简单介绍一下 vuex-along 的使用方法。
安装 vuex-along:
npm install vuex-along --save
配置 vuex-along: 在 store/index.js 中最后添加以下代码:
import VueXAlong from 'vuex-along' //导入插件
export default new Vuex.Store({//modules: {//controler //模块化vuex//},plugins: [VueXAlong({name: 'store', //存放在localStroage或者sessionStroage 中的名字local: false, //是否存放在local中 false 不存放 如果存放按照下面session的配置session: { list: [], isFilter: true } //如果值不为false 那么可以传递对象 其中 当isFilter设置为true时, list 数组中的值就会被过滤调,这些值不会存放在seesion或者local中})]
});
2.使用 localStorage 或者 sessionStroage
created() {//在页面加载时读取sessionStorage里的状态信息if (sessionStorage.getItem("store")) {this.$store.replaceState(Object.assign({},this.$store.state,JSON.parse(sessionStorage.getItem("store"))));}//在页面刷新时将vuex里的信息保存到sessionStorage里window.addEventListener("beforeunload", () => {sessionStorage.setItem("store", JSON.stringify(this.$store.state));});
},
题32:说下Vite的原理?
当前工程化痛点:
现在常用的构建工具如Webpack,主要是通过抓取-编译-构建
整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码
。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给dev server(开发服务器)
。
Webpack等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server(开发服务器)性能遇到瓶颈:
缓慢的服务启动: 大型项目中dev server启动时间达到几十秒甚至几分钟。
缓慢的HMR热更新: 即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。
缓慢的开发环境,大大降低了开发者的幸福感,在以上背景下Vite应运而生。
什么是Vite?
基于esbuild与Rollup,依靠浏览器自身ESM编译功能, 实现极致开发体验的新一代构建工具!
概念
先介绍以下文中会经常提到的一些基础概念:
依赖:
指开发不会变动的部分(npm包、UI组件库),esbuild进行预构建。
源码:
浏览器不能直接执行的非js代码(.jsx、.css、.vue等),vite只在浏览器请求相关源码的时候进行转换,以提供ESM源码。
开发环境:
利用浏览器原生的ES Module编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server只提供轻量服务。
浏览器执行ESM的import时,会向dev server发起该模块的ajax请求,服务器对源码做简单处理后返回给浏览器。
Vite中HMR是在原生 ESM 上执行的
。当编辑一个文件时,Vite 只需要精确地使已编辑的模块失活,使得无论应用大小如何,HMR 始终能保持快速更新。
使用esbuild处理项目依赖
,esbuild使用go编写
,比一般node.js编写的编译器快几个数量级。
生产环境:
集成Rollup打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制。
处理流程对比:
Webpack通过先将整个应用打包,再将打包后代码提供给dev server,开发者才能开始开发。
Vite****直接将源码交给浏览器,实现dev server秒开
,浏览器显示页面需要相关模块时,再向dev server发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。
基本用法
创建vite项目
$ npm create vite@latest
选取模板
Vite 内置6种常用模板与对应的TS版本,可满足前端大部分开发场景,可以点击下列表格中模板直接在 StackBlitz 中在线试用,还有其他更多的 社区维护模板可以使用。 |JavaScript | TypeScript | | ----------------------------------- | ----------------------------------------- | | vanilla | vanilla-ts | | vue | vue-ts | | react | react-ts | | preact | preact-ts | | lit | lit-ts | | svelte | svelte-ts|
启动
{"scripts": {"dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`"build": "vite build", // 为生产环境构建产物"preview": "vite preview" // 本地预览生产构建产物}
}
实现原理
ESbuild 编译
esbuild 使用go编写
,cpu密集下更具性能优势,编译速度更快,以下摘自官网的构建速度对比:
浏览器:“开始了吗?”
服务器:“已经结束了。”
开发者:“好快,好喜欢!!”
依赖预构建
模块化兼容: 如开头背景所写,现仍共存多种模块化标准代码,Vite在
预构建阶段
将依赖中各种其他模块化规范(CommonJS、UMD)转换 成ESM
,以提供给浏览器。
性能优化: npm包中大量的ESM代码,大量的import请求,会造成网络拥塞。Vite使用esbuild
,将有大量内部模块的ESM关系转换成单个模块
,以减少 import模块请求次数
。
按需加载 服务器只在接受到import请求的时候,才会编译对应的文件,将ESM源码返回给浏览器,实现真正的按需加载。
缓存
HTTP缓存:
充分利用http缓存做优化,依赖(不会变动的代码)部分用max-age,immutable 强缓存,源码部分用304协商缓存,提升页面打开速度。
文件系统缓存:
Vite在预构建阶段
,将构建后的依赖缓存到node_modules/.vite
,相关配置更改时,或手动控制时才会重新构建,以提升预构建速度。
重写模块路径
浏览器import只能引入相对/绝对路径,而开发代码经常使用npm包名直接引入node_module中的模块,需要做路径转换后交给浏览器。
es-module-lexer 扫描 import 语法
magic-string 重写模块的引入路径
// 开发代码
import { createApp } from ‘vue’
// 转换后
题33:说说vue3中的响应式设计原理?
ue 3 中的响应式原理可谓是非常之重要,通过学习 Vue3 的响应式原理,不仅能让我们学习到 Vue.js 的一些设计模式和思想,还能帮助我们提高项目开发效率和代码调试能力。
一、Vue 3 响应式使用
1. Vue 3 中的使用
当我们在学习 Vue 3 的时候,可以通过一个简单示例,看看什么是 Vue 3 中的响应式:
<!-- HTML 内容 -->
<div id="app"><div>Price: {{price}}</div><div>Total: {{price * quantity}}</div><div>getTotal: {{getTotal}}</div>
</div>const app = Vue.createApp({ // ① 创建 APP 实例data() {return {price: 10,quantity: 2}},computed: {getTotal() {return this.price * this.quantity * 1.1}}
})
app.mount('#app') // ② 挂载 APP 实例
通过创建 APP 实例和挂载 APP 实例即可,这时可以看到页面中分别显示对应数值:(图见小程序)
当我们修改 price 或 quantity 值的时候,页面上引用它们的地方,内容也能正常展示变化后的结果。这时,我们会好奇为何数据发生变化后,相关的数据也会跟着变化,那么我们接着往下看。
2. 实现单个值的响应式
在普通 JS 代码执行中,并不会有响应式变化,比如在控制台执行下面代码:
let price = 10, quantity = 2;
const total = price * quantity;
console.log(`total: ${total}`); // total: 20
price = 20;
console.log(`total: ${total}`); // total: 20
从这可以看出,在修改 price 变量的值后, total 的值并没有发生改变。
那么如何修改上面代码,让 total 能够自动更新呢?我们其实可以将修改 total 值的方法保存起来,等到与 total 值相关的变量(如 price 或 quantity 变量的值)发生变化时,触发该方法,更新 total 即可。我们可以这么实现:
let price = 10, quantity = 2, total = 0;
const dep = new Set(); // ①
const effect = () => { total = price * quantity };
const track = () => { dep.add(effect) }; // ②
const trigger = () => { dep.forEach( effect => effect() )}; // ③track();
console.log(`total: ${total}`); // total: 0
trigger();
console.log(`total: ${total}`); // total: 20
price = 20;
trigger();
console.log(`total: ${total}`); // total: 40
上面代码通过 3 个步骤,实现对 total 数据进行响应式变化:
① 初始化一个 Set 类型的 dep 变量,用来存放需要执行的副作用( effect 函数),这边是修改 total 值的方法;
② 创建 track() 函数,用来将需要执行的副作用保存到 dep 变量中(也称收集副作用);
③ 创建 trigger() 函数,用来执行 dep 变量中的所有副作用;
在每次修改 price 或 quantity 后,调用 trigger() 函数执行所有副作用后, total 值将自动更新为最新值。(图见小程序)
3. 实现单个对象的响应式
通常,我们的对象具有多个属性,并且每个属性都需要自己的 dep。我们如何存储这些?比如:
let product = { price: 10, quantity: 2 };
从前面介绍我们知道,我们将所有副作用保存在一个 Set 集合中,而该集合不会有重复项,这里我们引入一个 Map 类型集合(即 depsMap ),其 key 为对象的属性(如: price 属性), value 为前面保存副作用的 Set 集合(如: dep 对象),大致结构如下图。(图见小程序,这里放不下)
4. 实现多个对象的响应式
如果我们有多个响应式数据,比如同时需要观察对象 a 和对象 b 的数据,那么又要如何跟踪每个响应变化的对象?
这里我们引入一个 WeakMap 类型的对象,将需要观察的对象作为 key ,值为前面用来保存对象属性的 Map 变量。(图和源码解析见小程序,这里放不下)
二、Proxy 和 Reflect
在上一节内容中,介绍了如何在数据发生变化后,自动更新数据,但存在的问题是,每次需要手动通过触发 track() 函数搜集依赖,通过 trigger() 函数执行所有副作用,达到数据更新目的。
这一节将来解决这个问题,实现这两个函数自动调用。
1. 如何实现自动操作
这里我们引入 JS 对象访问器的概念,解决办法如下:
在读取(GET 操作)数据时,自动执行 track() 函数自动收集依赖;
在修改(SET 操作)数据时,自动执行 trigger()函数执行所有副作用;
那么如何拦截 GET 和 SET 操作?接下来看看 Vue2 和 Vue3 是如何实现的:
在 Vue2 中,使用 ES5 的 Object.defineProperty() 函数实现; 在 Vue3 中,使用 ES6 的 Proxy 和 Reflect API 实现;
需要注意的是:Vue3 使用的 Proxy 和 Reflect API 并不支持 IE。
Object.defineProperty() 函数这边就不多做介绍,可以阅读文档,下文将主要介绍 Proxy 和 Reflect API。
2. 如何使用 Reflect
通常我们有三种方法读取一个对象的属性:
使用 . 操作符:leo.name ;
使用 [] : leo['name'] ;
使用 Reflect API: Reflect.get(leo, 'name') 。
这三种方式输出结果相同。
3. 如何使用 Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法如下:
const p = new Proxy(target, handler)
参数如下:
target : 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler : 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
我们通过官方文档,体验一下 Proxy API:
let product = { price: 10, quantity: 2 };
let proxiedProduct = new Proxy(product, {get(target, key){console.log('正在读取的数据:',key);return target[key];}
})
console.log(proxiedProduct.price);
// 正在读取的数据: price
// 10
这样就保证我们每次在读取 proxiedProduct.price 都会执行到其中代理的 get 处理函数。
接下来还有很多源码解析,特别特别重要,一定要去看小程序啊。这里真的放不下,如果你想通过面试,一定要去看。
题34:vue路由中,history和hash两种模式有什么区别?
前端路由有两种模式:hash 模式和 history 模式,接下来分析这两种模式的实现方式和优缺点。
1.hash 模式
hash 模式是一种把前端路由的路径用井号 # 拼接在真实 URL 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 hashchange 事件。
示例:
我们新建一个 hash.html 文件,内容为:
<a href="#/a">A页面</a>
<a href="#/b">B页面</a>
<div id="app"></div>
<script>function render() {app.innerHTML = window.location.hash}window.addEventListener('hashchange', render)render()
</script>
在上面的例子中,我们利用 a 标签设置了两个路由导航,把 app 当做视图渲染容器,当切换路由的时候触发视图容器的更新,这其实就是大多数前端框架哈希路由的实现原理。
总结一下 hash 模式的优缺点:
优点:浏览器兼容性较好,连 IE8 都支持
缺点:路径在井号 # 的后面,比较丑
2.history 模式
history API 是 H5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求。
示例:
我们新建一个 history.html,内容为:
<a href="javascript:toA();">A页面</a>
<a href="javascript:toB();">B页面</a>
<div id="app"></div>
<script>function render() {app.innerHTML = window.location.pathname}function toA() {history.pushState({}, null, '/a')render()}function toB() {history.pushState({}, null, '/b')render()}window.addEventListener('popstate', render)
</script>
history API 提供了丰富的函数供开发者调用,我们不妨把控制台打开,然后输入下面的语句来观察浏览器地址栏的变化:
history.replaceState({}, null, '/b') // 替换路由
history.pushState({}, null, '/a') // 路由压栈
history.back() // 返回
history.forward() // 前进
history.go(-2) // 后退2次
上面的代码监听了 popstate 事件,该事件能监听到:
用户点击浏览器的前进和后退操作
手动调用 history 的 back、forward 和 go 方法
监听不到:
history 的 pushState 和 replaceState方法
这也是为什么上面的 toA 和 toB 函数内部需要手动调用 render 方法的原因。另外,大家可能也注意到 light-server 的命令多了 --historyindex ‘/history.html’ 参数,这是干什么的呢?
浏览器在刷新的时候,会按照路径发送真实的资源请求,如果这个路径是前端通过 history API 设置的 URL,那么在服务端往往不存在这个资源,于是就返回 404 了。上面的参数的意思就是如果后端资源不存在就返回 history.html 的内容。
因此在线上部署基于 history API 的单页面应用的时候,一定要后端配合支持才行,否则会出现大量的 404。以最常用的 Nginx 为例,只需要在配置的 location / 中增加下面一行即可:
try_files $uri /index.html;
总结一下 history 模式的优缺点:
优点:路径比较正规,没有井号 #
缺点:兼容性不如 hash,且需要服务端支持,否则一刷新页面就404了
题35:VNode有哪些属性?
Vue内部定义的Vnode对象包含了以下属性:
__v_isVNode: true,内部属性,有该属性表示为Vnode
__v_skip: true,内部属性,表示跳过响应式转换,reactive转换时会根据此属性进行判断
isCompatRoot?: true,用于是否做了兼容处理的判断
type: VNodeTypes,虚拟节点的类型
props: (VNodeProps & ExtraProps) | null,虚拟节点的props
key: string | number | null,虚拟阶段的key,可用于diff
ref: VNodeNormalizedRef | null,虚拟阶段的引用
scopeId: string | null,仅限于SFC(单文件组件),在设置currentRenderingInstance当前渲染实例时,一期设置
slotScopeIds: string[] | null,仅限于单文件组件,与单文件组件的插槽有关
children: VNodeNormalizedChildren,子节点
component: ComponentInternalInstance | null,组件实例
dirs: DirectiveBinding[] | null,当前Vnode绑定的指令
transition: TransitionHooks | null,TransitionHooks
DOM相关属性
el: HostNode | null,宿主阶段
anchor: HostNode | null // fragment anchor
target: HostElement | null ,teleport target 传送的目标
targetAnchor: HostNode | null // teleport target anchor
staticCount: number,包含的静态节点的数量
suspense 悬挂有关的属性
suspense: SuspenseBoundary | null
ssContent: VNode | null
ssFallback: VNode | null
optimization only 用于优化的属性
shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null
根节点会有的属性
appContext: AppContext | null,实例上下文
可以看到在Vue内部,对于一个Vnode描述对象的属性大概有二十多个。
Vue为了给用于减轻一定的负担,但又不至于太封闭,就创建了渲染h。可以在用户需要的时候,通过h函数创建对应的Vnode即可。
这样就给为一些高阶玩家保留了自由发挥的空间。
题36:vue2.0为什么不能检查数组的变化,该怎么解决?
前言
我们都知道,Vue2.0对于响应式数据的实现有一些不足:
1.无法检测数组/对象的新增
2.无法检测通过索引改变数组的操作。
分析
无法检测数组/对象的新增?
Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的,因为是在构造函数中就已经为所有属性做了这个检测绑定操作。
无法检测通过索引改变数组的操作。即vm.items[indexOfItem] = newValue?
官方文档中对于这两点都是简要的概括为“由于JavaScript的限制”无法实现,而Object.defineProperty是实现检测数据改变的方案,这个限制是指Object.defineProperty
思考
vm.items[indexOfItem] = newValue真的不能被监听么?
Vue对数组的7个变异方法(push、pop、shift、unshift、splice、sort、reverse
)实现了响应式。这里就不做测试了。我们测试一下通过索引改变数组的操作,能不能被监听到。
遍历数组,用Object.defineProperty对每一项进行监测
function defineReactive(data, key, value) {Object.defineProperty(data, key, {enumerable: true,configurable: true,get: function defineGet() {console.log(`get key: ${key} value: ${value}`)return value},set: function defineSet(newVal) {console.log(`set key: ${key} value: ${newVal}`)value = newVal}})
}function observe(data) {Object.keys(data).forEach(function(key) {defineReactive(data, key, data[key])})
}let arr = [1, 2, 3]
observe(arr)
测试说明
通过索引改变arr[1],我们发现触发了set,也就是Object.defineProperty是可以检测到通过索引改变数组的操作的,那Vue2.0为什么没有实现呢?是尤大能力不行?这肯定毋庸置疑。那他为什么不实现呢?(图见小程序)
小结:
是出于对性能原因的考虑,没有去实现它。而不是不能实现。
对于对象而言,每一次的数据变更都会对对象的属性进行一次枚举,一般对象本身的属性数量有限,所以对于遍历枚举等方式产生的性能损耗可以忽略不计,但是对于数组而言呢?数组包含的元素量是可能达到成千上万,假设对于每一次数组元素的更新都触发了枚举/遍历,其带来的性能损耗将与获得的用户体验不成正比,故vue无法检测数组的变动。
不过Vue3.0用proxy代替了defineProperty之后就解决了这个问题。
解决方案
数组
1.this.$set(array, index, data)
//这是个深度的修改,某些情况下可能导致你不希望的结果,因此最好还是慎用
this.dataArr = this.originArr
this.$set(this.dataArr, 0, {data: '修改第一个元素'})
console.log(this.dataArr)
console.log(this.originArr) //同样的 源数组也会被修改 在某些情况下会导致你不希望的结果
2.splice
//因为splice会被监听有响应式,而splice又可以做到增删改。
3.利用临时变量进行中转
let tempArr = [...this.targetArr]
tempArr[0] = {data: 'test'}
this.targetArr = tempArr
对象
1.this.$set(obj, key ,value) - 可实现增、改
2.watch时添加deep:true深度监听,只能监听到属性值的变化,新增、删除属性无法监听
this.$watch('blog', this.getCatalog, {deep: true// immediate: true // 是否第一次触发});
3.watch时直接监听某个key
watch: {'obj.name'(curVal, oldVal) {// TODO}
}
题37:说说vue页面渲染过程?
总结
初始化调用 $mount 挂载组件
。
_render 开始构建 VNode,核心方法为 createElement
,一般会创建普通的 VNode ,遇到组件就创建组件类型的 VNode,否则就是未知标签的 VNode,构建完成传递给 _update。
patch 阶段根据 VNode 创建真实节点树
,核心方法为 createElm
,首先遇到组件类型的 VNode,内部会执行 $mount
,再走一遍相同的流程。普通节点类型则创建一个真实节点,如果它有子节点开始递归调用
createElm,使用 insert 插入子节点
,直到没有子节点就填充内容节点。最后递归完成
后,同样也是使用 insert 将整个节点树插入到页面中,再将旧的根节点移除。
题38:react和vue有什么区别?
前言
React 是由Facebook创建的JavaScript UI框架,React推广了 Virtual DOM( 虚拟 DOM )并创造了 JSX 语法
。JSX 语法的出现允许我们在 javascript 中书写 HTML 代码。
VUE 是由尤雨溪开发的,VUE 使用了模板系统
而不是JSX,因其实模板系统都是用的普通的 HTML,所以对应用的升级更方便、更容易,而不需要整体重构。
VUE 相较于 React 更容易上手,如果是一个有一定开发经验的开发者,甚至都不需要花额外的时间去学习,直接一遍开发一遍查文挡即可。
VUE 与 React 区别
React 的思路是 HTML in JavaScript 也可以说是 All in JavaScript,通过 JavaScript 来生成 HTML,所以设计了 JSX 语法,还有通过 JS 来操作 CSS,社区的styled-component、JSS等。
Vue 是把 HTML,CSS,JavaScript 组合到一起,用各自的处理方式,Vue 有单文件组件,可以把 HTML、CSS、JS 写到一个文件中,HTML 提供了模板引擎来处理。
React 整体是函数式的思想
,在 React 中是单向数据流,推崇结合 immutable 来实现数据不可变。
而 Vue 的思想是响应式的,
也就是基于是数据可变的,通过对每一个属性建立 Watcher 来监听,当属性变化的时候,响应式的更新对应的虚拟 DOM。
如上,所以 React 的性能优化需要手动去做
,而Vue的性能优化是自动的
,但是Vue的响应式机制也有问题,就是当 state 特别多的时候,Watcher 会很多,会导致卡顿。
React 与 VUE 共同点
React 与 Vue 存在很多共同点,例如他们都是 JavaScript 的 UI 框架,专注于创造前端的富应用。不同于早期的 JavaScript 框架“功能齐全”,Reat 与 Vue 只有框架的骨架,其他的功能如路由、状态管理等是框架分离的组件。
优势
React
灵活性和响应性:它提供最大的灵活性和响应能力。 丰富的JavaScript库:来自世界各地的贡献者正在努力添加更多功能。
可扩展性:由于其灵活的结构和可扩展性,React已被证明对大型应用程序更好。
不断发展:React得到了Facebook专业开发人员的支持,他们不断寻找改进方法。
Web或移动平台: React提供React Native平台,可通过相同的React组件模型为iOS和Android开发本机呈现的应用程序。
Vue
易于使用: Vue.js包含基于HTML的标准模板,可以更轻松地使用和修改现有应用程序。
更顺畅的集成:无论是单页应用程序还是复杂的Web界面,Vue.js都可以更平滑地集成更小的部件,而不会对整个系统产生任何影响。
更好的性能,更小的尺寸:它占用更少的空间,并且往往比其他框架提供更好的性能。
精心编写的文档:通过详细的文档提供简单的学习曲线,无需额外的知识; HTML和JavaScript将完成工作。
适应性:整体声音设计和架构使其成为一种流行的JavaScript框架。 它提供无障碍的迁移,简单有效的结构和可重用的模板。
总结
如上所说的 Vue 的响应式机制也有问题,当 state 特别多的时候,Watcher 会很多,会导致卡顿,所以大型应用(状态特别多的)一般用 React,更加可控。
可对于易用性来说,VUE 是更容易上手的,对于项目来说新人更容易接手。
使用 React 的公司:Facebook,Instagram,Netflix,纽约时报,雅虎,WhatsApp,Codecademy,Dropbox,Airbnb,Asana,微软等。
使用 Vue 的公司:Facebook,Netflix,Adobe,Grammarly,Behance,小米,阿里巴巴,Codeship,Gitlab和Laracasts等。
所以,技术没有哪个更好或者是更优秀,只要适合自己的才是最合适的。
题39:vue中的computed和watch区别?
computed 和 watch看似都能实现对数据的监听,但还是有区别。
以下通过一个小栗子来理解一下这两者的区别。
computed 计算属性
计算属性基于 data 中声明过或者父组件传递的 props中的数据通过计算得到的一个新值,这个新值只会根据已知值的变化而变化,简言之:这个属性依赖其他属性,由其他属性计算而来的。
<p>姓名:{{ fullName }}</p>
... ...
data: {firstName: 'David',lastName: 'Beckham'
},
computed: {fullName: function() { //方法的返回值作为属性值return this.firstName + ' ' + this.lastName}
}
在 computed 属性对象中定义计算属性的方法,和取data对象里的数据属性一样以属性访问的形式调用,即在页面中使用 {{ 方法名 }} 来显示计算的结果。
注:计算属性 fullName 不能在 data 中定义,而计算属性值的相关已知值在data中;
如果 fullName 在 data 中定义了会报错如下图:
因为如果 computed 属性值是一个函数,那么默认会走 get 方法,必须要有一个返回值,函数的返回值就是属性的属性值。计算属性定义了 fullName 并返回对应的结果给这个变量,变量不可被重复定义和赋值。
在官方文档中,还强调了 computed 一个重要的特点,就是 computed 带有缓存功能。比如我在页面中多次显示 fullName:
<p>姓名:{{ fullName }}</p>
<p>姓名:{{ fullName }}</p>
<p>姓名:{{ fullName }}</p>
<p>姓名:{{ fullName }}</p>
<p>姓名:{{ fullName }}</p>
... ... computed: {fullName: function () {console.log('computed') // 在控制台只打印了一次return this.firstName + ' ' + this.lastName}
}
我们知道 computed 内定义的 function 只执行一次,仅当初始化显示或者相关的 data、props 等属性数据发生变化的时候调用;
而 computed 属性值默认会缓存计算结果,计算属性是基于它们的响应式依赖进行缓存的;
只有当 computed 属性被使用后,才会执行 computed 的代码,在重复的调用中,只要依赖数据不变,直接取缓存中的计算结果。只有依赖型数据发生改变,computed 才会重新计算。
计算属性的高级:
在computed 中的属性都有一个 get 和一个 set 方法,当数据变化时,调用 set 方法。下面我们通过计算属性的
getter/setter 方法来实现对属性数据的显示和监视,即双向绑定。
computed: {fullName: {get() { //读取当前属性值的回调,根据相关的数据计算并返回当前属性的值return this.firstName + ' ' + this.lastName},set(val) { // 当属性值发生改变时回调,更新相关的属性数据,val就是fullName的最新属性值const names = val ? val.split(' ') : [];this.firstName = names[0]this.lastName = names[1]}}
}
watch 监听属性:
通过
vm 对象
的$watch()
或watch
配置来监听 Vue实例上的属性变化,或某些特定数据的变化,然后执行某些具体的业务逻辑操作。当属性变化时,回调函数自动调用,在函数内部进行计算。其可以监听的数据来源:data,props,computed内的数据。
以上示例通过 watch 来实现:
watch: {// 监听 data 中的 firstName,如果发生了变化,就把变化的值给 data 中的 fullName, val 就是 firstName 的最新值firstName: function(val) { this.fullName = val + ' ' + this.lastName},lastName: function(val) {this.fullName = this.firstName + ' ' + val}
}
// 由上可以看出 watch 要监听两个数据,而且代码是同类型的重复的,所以相比用 computed 更简洁
注: 监听函数有两个参数,第一个参数是最新的值,第二个参数是输入之前的值,顺序一定是新值,旧值,如果只写一个参数,那就是最新属性值。
在使用时选择 watch 还是 computed,还有一个参考点就是官网说的:当需要在数据变化时执行异步或开销较大的操作时,watch方式是最有用的。所以 watch 一定是支持异步的。
上面仅限监听简单数据类型,监听复杂数据类型就需要用到深度监听 deep。
deep:为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。
data: {fullName: {firstName: 'David',lastName: 'Beckham'}
},
watch: {fullName: {handler(newVal, oldVal) {console.log(newVal);console.log(oldVal);},deep: true}
}
打印出来的 newVal 和 oldVal 值是一样的,所以深度监听虽然可以监听到对象的变化,但是无法监听到对象里面哪个具体属性的变化。这是因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。
若果要监听对象的单个属性的变化,有两种方法:
1.直接监听对象的属性:
watch:{fullName.firstName: function(newVal,oldVal){console.log(newVal,oldVal);}
}
2.与 computed 属性配合使用,computed 返回想要监听的属性值,watch 用来监听
computed: {firstNameChange() {return this.fullName.firstName}
},
watch: {firstNameChange() {console.log(this.fullName)}
}
总结:
watch和computed都是以Vue的
依赖追踪机制
为基础的,当某一个依赖型数据(依赖型数据:简单理解即放在 data
等对象下的实例数据)发生变化的时候,所有依赖这个数据的相关数据会自动发生变化,即自动调用相关的函数,来实现数据的变动。
当依赖的值变化时,在watch中,是可以做一些复杂的操作的,而computed中的依赖,仅仅是一个值依赖于另一个值,是值上的依赖。
应用场景:
computed: 用于处理复杂的逻辑运算;一个数据受一个或多个数据影响;用来处理watch和methods无法处理的,或处理起来不方便的情况。例如处理模板中的复杂表达式、购物车里面的商品数量和总金额之间的变化关系等。
watch: 用来处理当一个属性发生变化时,需要执行某些具体的业务逻辑操作,或要在数据变化时执行异步或开销较大的操作;一个数据改变影响多个数据。例如用来监控路由、inpurt入框值的特殊处理等。
区别:
computed
初始化显示
或者相关的 data、props 等属性数据发生变化的时候调用;
计算属性不在 data 中,它是基于data 或 props中的数据通过计算
得到的一个新值,这个新值根据已知值的变化而变化
;
在 computed属性对象中定义计算属性的方法,和取data对象里的数据属性一样,以属性访问的形式调用; 如果 computed 属性值是函数,那么默认会走get 方法,必须要有一个返回值,函数的返回值就是属性的属性值; computed属性值默认会缓存计算结果,在重复的调用中,只要依赖数据不变,直接取缓存中的计算结果,只有依赖型数据发生改变,computed 才会重新计算;在computed中的,属性都有一个 get 和一个 set 方法,当数据变化时,调用 set 方法。
watch
主要用来
监听
某些特定数据的变化
,从而进行某些具体的业务逻辑操作,可以看作是 computed 和 methods 的结合体;
可以监听的数据来源:data,props,computed内的数据
; watch支持异步;
不支持缓存,监听的数据改变,直接会触发相应的操作;
监听函数有两个参数,第一个参数是最新的值,第二个参数是输入之前的值,顺序一定是新值,旧值。
题40:computed怎么实现的缓存?
总结一下
1.初始化data和computed,分别代理其set
以及get
方法, 对data中的所有属性生成唯一的dep实例。
2.对computed中的sum生成唯一watcher,并保存在vm._computedWatchers
中
3.执行render函数时会访问sum属性,从而执行initComputed时定义的getter方法,会将Dep.target指向sum的watcher,并调用该属性具体方法sum。
4.sum方法中访问this.count,即会调用this.count代理的get方法,将this.count的dep加入sum的watcher,同时该dep中的subs添加这个watcher。
5.设置vm.count = 2,调用count代理的set方法触发dep的notify方法,因为是computed属性,只是将watcher中的dirty设置为true。
6.最后一步vm.sum,访问其get方法时,得知sum的watcher.dirty为true,调用其watcher.evaluate()方法获取新的值。
题41:vue-loader做了哪些事情?
vue-loader 工作原理
通过 vue-loader, webpack 可以将 .vue 文件 转化为 浏览器可识别的javascript。
vue-loader 的工作流程, 简单来说,分为以下几个步骤:
1.将一个 .vue 文件 切割成 template、script、styles
三个部分。
2.template 部分 通过 compile
生成 render
、 staticRenderFns
。
3.获取 script 部分 返回的配置项对象 scriptExports
。
4 .styles 部分 ,会通过 css-loader
、vue-style-loader
, 添加到 head
中, 或者通过 css-loader、MiniCssExtractPlugin 提取到一个 公共的css文件 中。
5.使用 vue-loader 提供的 normalizeComponent 方法, 合并 scriptExports、render、staticRenderFns
, 返回 构建vue组件需要的配置项对象 - options, 即 {data, props, methods, render, staticRenderFns…}。
题42:vue中,假设data中有一个数组对象,修改数组元素时,是否会触发视图更新?
不会触发视图更新
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。这里需要注意的问题是浏览器控制台在打印数据对象时 getter/setter 的格式化并不同,所以你可能需要安装 vue-devtools 来获取更加友好的检查接口。 每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
触发视图更新的方法有如下几种:
1.Vue.set
可以设置对象或数组的值,通过key或数组索引,可以触发视图更新
数组修改
Vue.set(array, indexOfItem, newValue)对象修改
Vue.set(obj, keyOfItem, newValue)
2.Vue.delete
删除对象或数组中元素,通过key或数组索引,可以触发视图更新
数组修改
Vue.delete(array, indexOfItem)对象修改
Vue.delete(obj, keyOfItem)
3.数组对象直接修改属性,可以触发视图更新
this.array[0].isShow= true;
this.array.forEach(function(item){item.isShow= true;
});
4.数组赋值为新数组,可以触发视图更新
this.array = this.array.filter(...)
this.array = this.array.concat(...)
5.Vue提供了如下的数组的变异方法,可以触发视图更新
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。
这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
题43:vuex中的辅助函数怎么使用?
在实际开发中,我们经常会用到 vuex 来对数据进行管理,随着数据越来越多,我们逐渐开始使用一些语法糖来帮助我们快速开发。 即 vuex 中的 mapState、mapGetters、mapMutations、mapActions 等辅助函数是我们经常使用到的。
辅助函数
通过辅助函数mapState、mapActions、mapMutations,把vuex.store中的属性映射到vue实例身上,这样在vue实例中就能访问vuex.store中的属性了,对于操作vuex.store就很方便了。
state辅助函数为mapState,actions辅助函数为mapActions,mutations辅助函数为mapMutations。(Vuex实例身上有mapState、mapActions、mapMutations属性,属性值都是函数)
如何使用辅助函数
首先,需要在当前组件中引入Vuex。
然后,通过Vuex来调用辅助函数。
辅助函数如何去映射vuex.store中的属性
1、mapState:把state属性映射到computed身上
computed:{...Vuex.mapState({input:state=>state.inputVal,n:state=>state.n})
}
state:用来存储公共的状态 在state中的数据都是响应式的。
响应式原因:state里面有一个getters、setters方法;data中的数据也是响应式的,因为里面也有getters和setters方法
在computed属性中来接收state中的数据,接收方式有2种(数组和对象,推荐对象).
优点:
1.本身key值是别名,要的是val的值,key的值a 和 val="a"一样就行,随意写。减少state里面长的属性名。
2.可以在函数内部查看state中的数据,数组方式的话,必须按照state中的属性名。
computed:Vuex.mapState({key:state=>state.属性})
如果自身组件也需要使用computed的话,通过解构赋值去解构出来
computed:{...Vuex.mapState({key:state=>state.属性})}
2、mapAcions:把actions里面的方法映射到methods中
methods:{...Vuex.mapActions({add:"handleTodoAdd", //val为actions里面的方法名称change:"handleInput" })}
add、change为action方法别名,直接代用add和change方法就行,不过要记得在actions里面做完数据业务逻辑的操作。
等价于如下的函数调用,
methods: {handleInput(e){ let val = e.target.value;this.$store.dispatch("handleInput",val )},handleAdd(){this.$store.dispatch("handleTodoAdd")}
}
actions里面的函数主要用来处理异步的函数以及一些业务逻辑,每一个函数里面都有一个形参,这个形参是一个对象,里面有一个commit方法,这个方法用来触发mutations里面的方法
3、mapMutations:把mutations里面的方法映射到methods中
只是做简单的数据修改(例如n++),它没有涉及到数据的处理,没有用到业务逻辑或者异步函数,可以直接调用mutations里的方法修改数据。
methods:{...Vuex.mapMutations({handleAdd:"handlMutationseAdd"})}
mutations里面的函数主要用来修改state中的数据。mutations里面的所有方法都会有2个参数,一个是store中的state,另外一个是需要传递的参数。
理解state、actions、mutations,可以参考MVC框架。
state看成一个数据库,只是它是响应式的,刷新页面数据就会改变;
actions看成controller层,做数据的业务逻辑;
mutations看成model层,做数据的增删改查操作。
4、mapGetters:把getters属性映射到computed身上
computed:{...Vuex.mapGetters({NumN:"NumN"})}
getters类似于组件里面computed,同时也监听属性的变化,当state中的属性发生改变的时候就会触发getters里面的方法。getters里面的每一个方法中都会有一个参数 state。
5、modules属性: 模块
把公共的状态按照模块进行划分
每个模块都相当于一个小型的Vuex
每个模块里面都会有state getters actions mutations
切记在导出模块的时候加一个 namespaced:true 主要的作用是将每个模块都有独立命名空间
namespace:true在多人协作开发的时候,可能子模块和主模块中的函数名字会相同,这样在调用函数的时候,相同名字的函数都会被调用,就会发生问题。为了解决这个问题,导出模块的时候要加namespace:true.
那么怎么调用子模块中的函数呢?假如我的子模块名字为todo.js。 函数名字就需要改成todo/函数名字。输出模块后的store实例如下图所示:
可以看到模块化后,store实例的state属性的访问方式也改变了,this.$store.state.todo.inputVal
可以简单总结一下辅助函数通过vuex使用,比喻成映射关系为:
mapState/mapGettes--->computed ;
mapAcions/mapMutations---->methods
命名空间
模块开启命名空间后,享有独自的命名空间。示例代码如下:
export default {namespaced: true,....
}
mapState、mapGetters、mapMutations、mapActions第一个参数是字符串(命名空间名称),第二个参数是数组(不需要重命名)/对象(需要重命名)。
mapXXXs('命名空间名称',['属性名1','属性名2'])mapXXXs('命名空间名称',{'组件中的新名称1':'Vuex中的原名称1','组件中的新名称2':'Vuex中的原名称2',})
题44:Vuex有几种属性,它们存在的意义分别是什么?
有五种,分别是 State、 Getter、Mutation 、Action、 Module。
1.State
Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
State属性是Vuex的单一状态树
2.Getter
有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数
Getter类似于Vue的 computed 对象。是根据业务逻辑来处理State,使得生成业务所需的属性。
3.Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
Mutation是唯一用来更改Vuex中状态的方法。
4.Action
Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
Action是用来解决异步操作而产生的,它提交的是Mutation。
5.Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
Module是将Vuex模块化的对象,目的是更好的维护。
题45:vuex是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
简单点总结,Vuex是一种状态管理模式,存在的目的是共享可复用的组件状态。
题46:谈谈你对Vue中keep-alive的理解?
什么是 keep-alive
在平常开发中,有部分组件没有必要多次初始化,这时,我们需要将组件进行持久化,使组件的状态维持不变,在下一次展示时,也不会进行重新初始化组件。
也就是说,keepalive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染,也就是所谓的组件缓存。
是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 相似, 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
include和exclude指定是否缓存某些组件
include属性
include 包含的意思。值为字符串或正则表达式或数组。只有组件的名称与include的值相同的才会被缓存,即指定哪些被缓存,可以指定多个被缓存。这里以字符串为例,指定多个组件缓存,语法是用逗号隔开。如下:
// 指定home组件和about组件被缓存
<keep-alive include="home,about" ><router-view></router-view>
</keep-alive>
exclude属性
exclude相当于include的反义词,就是除了的意思,指定哪些组件不被缓存,用法和include类似,如下:
// 除了home组件和about组件别的都缓存,本例中就是只缓存detail组件
<keep-alive exclude="home,about" ><router-view></router-view>
</keep-alive>
使用keep-alive的钩子函数执行顺序问题
首先使用了keep-alive的组件以后,组件上就会自动加上了activated钩子和deactivated钩子。
activated 当组件被激活(使用)的时候触发 可以简单理解为进入这个页面的时候触发
deactivated 当组件不被使用(inactive状态)的时候触发 可以简单理解为离开这个页面的时候触发
假设我们只缓存home组件,我们先看一下代码,再在钩子中打印出对应的顺序。就知道钩子执行的顺序了,自己动手印象深刻
<template>
<div><el-checkbox v-model="checked">备选项</el-checkbox>
</div>
</template>
<script>
export default {
name: "home",
data() { return { checked: false } },
created() {console.log("我是created钩子");
},
mounted() {console.log("我是mounted钩子");
},
activated() {console.log("我是activated钩子");
},
deactivated() {console.log("我是deactivated钩子");
},
beforeDestroy() {console.log("我是beforeDestroy钩子");所以我们可以得出结论:
},
};
</script>
进入组件打印结果如下:
我是created钩子
我是mounted钩子
我是activated钩子
离开组件打印结果如下:
我是deactivated钩子
得出结论:
初始进入和离开 created —> mounted —> activated --> deactivated 后续进入和离开activated --> deactivated
keep-alive的应用场景举例
1.查看表格某条数据详情页,返回还是之前的状态,比如还是之前的筛选结果,还是之前的页数等
2.填写的表单的内容路由跳转返回还在,比如input框、下选择拉框、开关切换等用户输入了一大把东西,跳转再回来不能清空啊,不用让用户再写一遍
题47:如果使用vue3.0实现一个Model,你会怎么进行设计?
一、组件设计
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式
现在有一个场景,点击新增与编辑都弹框出来进行填写,功能上大同小异,可能只是标题内容或者是显示的主体内容稍微不同
这时候就没必要写两个组件,只需要根据传入的参数不同,组件显示不同内容即可
这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少的 Bug 和更少的程序体积
二、需求分析
实现一个Modal组件,首先确定需要完成的内容:
·遮罩层
·标题内容
·主体内容
·确定和取消按钮
主体内容需要灵活,所以可以是字符串,也可以是一段 html 代码
特点是它们在当前vue实例之外独立存在,通常挂载于body之上
除了通过引入import的形式,我们还可通过API的形式进行组件的调用
还可以包括配置全局样式、国际化、与typeScript结合
三、实现流程
首先看看大致流程:
1.目录结构
Modal组件相关的目录结构
├── plugins
│ └── modal
│ ├── Content.tsx // 维护 Modal 的内容,用于 h 函数和 jsx 语法
│ ├── Modal.vue // 基础组件
│ ├── config.ts // 全局默认配置
│ ├── index.ts // 入口
│ ├── locale // 国际化相关
│ │ ├── index.ts
│ │ └── lang
│ │ ├── en-US.ts
│ │ ├── zh-CN.ts
│ │ └── zh-TW.ts
│ └── modal.type.ts // ts类型声明相关
因为 Modal 会被 app.use(Modal) 调用作为一个插件,所以都放在plugins目录下
2.组件内容
首先实现modal.vue的主体显示内容大致如下
<Teleport to="body" :disabled="!isTeleport"><div v-if="modelValue" class="modal"><divclass="mask":style="style"@click="maskClose && !loading && handleCancel()"></div><div class="modal__main"><div class="modal__title line line--b"><span>{{ title || t("r.title") }}</span><spanv-if="close":title="t('r.close')"class="close"@click="!loading && handleCancel()">✕</span></div><div class="modal__content"><Content v-if="typeof content === 'function'" :render="content" /><slot v-else>{{ content }}</slot></div><div class="modal__btns line line--t"><button :disabled="loading" @click="handleConfirm"><span class="loading" v-if="loading"> ❍ </span>{{ t("r.confirm") }}</button><button @click="!loading && handleCancel()">{{ t("r.cancel") }}</button></div></div></div>
</Teleport>
最外层上通过Vue3 Teleport 内置组件
进行包裹,其相当于传送门
,将里面的内容传送至body之上
并且从DOM结构上来看,把modal该有的内容(遮罩层、标题、内容、底部按钮)都实现了
关于主体内容
<div class="modal__content"><Content v-if="typeof content==='function'":render="content" /><slot v-else>{{content}}</slot>
</div>
可以看到根据传入content的类型不同,对应显示不同得到内容
最常见的则是通过调用字符串和默认插槽的形式
// 默认插槽
<Modal v-model="show"title="演示 slot"><div>hello world~</div>
</Modal>// 字符串
<Modal v-model="show"title="演示 content"content="hello world~" />
通过 API 形式调用Modal组件的时候,content可以使用下面两种
h 函数
$modal.show({title: '演示 h 函数',content(h) {return h('div',{style: 'color:red;',onClick: ($event: Event) => console.log('clicked', $event.target)},'hello world ~');}
});
JSX
$modal.show({title: '演示 jsx 语法',content() {return (<divonClick={($event: Event) => console.log('clicked', $event.target)}>hello world ~</div>);}
});
3.实现 API 形式
那么组件如何实现API形式调用Modal组件呢?
在Vue2中,我们可以借助Vue实例
以及Vue.extend
的方式获得组件实例,然后挂载到body上
import Modal from './Modal.vue';
const ComponentClass = Vue.extend(Modal);
const instance = new ComponentClass({ el: document.createElement("div") });
document.body.appendChild(instance.$el);
虽然Vue3移除了Vue.extend方法,但可以通过createVNode实现import Modal from './Modal.vue';
const container = document.createElement('div');
const vnode = createVNode(Modal);
render(vnode, container);
const instance = vnode.component;
document.body.appendChild(container);
在Vue2中,可以通过this的形式调用全局 API
export default {install(vue) {vue.prototype.$create = create}
}
而在 Vue3 的 setup 中已经没有 this 概念了,需要调用app.config.globalProperties
挂载到全局
export default {install(app) {app.config.globalProperties.$create = create}
}
4.事件处理
下面再看看看Modal组件内部是如何处理「确定」「取消」事件的,既然是Vue3,当然采用Compositon API 形式
// Modal.vue
setup(props, ctx) {let instance = getCurrentInstance(); // 获得当前组件实例onBeforeMount(() => {instance._hub = {'on-cancel': () => {},'on-confirm': () => {}};});const handleConfirm = () => {ctx.emit('on-confirm');instance._hub['on-confirm']();};const handleCancel = () => {ctx.emit('on-cancel');ctx.emit('update:modelValue', false);instance._hub['on-cancel']();};return {handleConfirm,handleCancel};
}
在上面代码中,可以看得到除了使用传统emit的形式使父组件监听,还可通过_hub属性中添加 on-cancel,on-confirm方法实现在API中进行监听
app.config.globalProperties.$modal = {show({}) {/* 监听 确定、取消 事件 */}
}
下面再来目睹下_hub
是如何实现
// index.ts
app.config.globalProperties.$modal = {show({/* 其他选项 */onConfirm,onCancel}) {/* ... */const { props, _hub } = instance;const _closeModal = () => {props.modelValue = false;container.parentNode!.removeChild(container);};// 往 _hub 新增事件的具体实现Object.assign(_hub, {async 'on-confirm'() {if (onConfirm) {const fn = onConfirm();// 当方法返回为 Promiseif (fn && fn.then) {try {props.loading = true;await fn;props.loading = false;_closeModal();} catch (err) {// 发生错误时,不关闭弹框console.error(err);props.loading = false;}} else {_closeModal();}} else {_closeModal();}},'on-cancel'() {onCancel && onCancel();_closeModal();}});
}
};
5.其他完善
关于组件实现国际化、与typsScript结合,大家可以根据自身情况在此基础上进行更改
题48:什么是虚拟DOM?
虚拟DOM(VDOM)它是
真实DOM的内存表示
,一种编程概念
,一种模式。它会和真实的DOM同步,比如通过ReactDOM这种库,这个同步的过程叫做调和(reconcilation)。
虚拟DOM更多是一种模式,不是一种特定的技术。
题49:Vue3.0所采用的Composition Api与Vue2.x使用的Options Api有什么不同?
开始之前
Composition API 可以说是Vue3的最大特点,那么为什么要推出Composition Api,解决了什么问题?
通常使用Vue2开发的项目,普遍会存在以下问题:
代码的可读性随着组件变大而变差
每一种代码复用的方式,都存在缺点 TypeScript支持有限
以上通过使用Composition Api都能迎刃而解
正文
一、Options Api
Options
API,即大家常说的选项API,即以vue为后缀的文件,通过定义methods,computed,watch,data等属性与方法,共同处理页面逻辑
可以看到Options代码编写方式,如果是组件状态,则写在data属性上,如果是方法,则写在methods属性上…
用组件的选项 (data、computed、methods、watch) 组织逻辑在大多数情况下都有效
然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解
二、Composition Api
在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的
高内聚,低耦合
)即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API
三、对比
下面对Composition Api 与Options Api进行两大方面的比较
逻辑组织
逻辑复用
逻辑组织
Options API
假设一个组件是一个大型组件,其内部有很多处理逻辑关注点
可以看到,这种碎片化使得理解和维护复杂组件变得困难
选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块
Compostion API
而Compositon API正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去
小结
1.在逻辑组织和逻辑复用方面,Composition API是优于Options API
2.因为Composition API几乎是函数,会有更好的类型推断
。
3.Composition API 对 tree-shaking 友好,代码也更容易压缩
4.Composition API中见不到this的使用,减少了this指向不明的情况
5.如果是小型组件,可以继续使用Options API,也是十分友好的
题50:Vue3.0性能提升主要通过哪几方面体现的?
一、编译阶段
回顾Vue2,我们知道每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染
试想一下,一个组件结构如下图
<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在编译阶段,做了进一步优化。主要有如下:
1.diff算法优化
vue3在diff算法中相比vue2增加了静态标记
关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记
,下次发生变化的时候直接找该地方进行比较
下图这里,已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高
关于静态类型枚举如下
export const enum PatchFlags {TEXT = 1,// 动态的文本节点CLASS = 1 << 1, // 2 动态的 classSTYLE = 1 << 2, // 4 动态的 stylePROPS = 1 << 3, // 8 动态属性,不包括类名和样式FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 FragmentKEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 FragmentUNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 FragmentNEED_PATCH = 1 << 9, // 512DYNAMIC_SLOTS = 1 << 10, // 动态 soltHOISTED = -1, // 特殊标志是负整数表示永远不会用作 diffBAIL = -2 // 一个特殊的标志,指代差异算法
}
2.静态提升
Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用
这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
<span>你好</span><div>{{ message }}</div>
没有做静态提升之前
export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock(_Fragment, null, [_createVNode("span", null, "你好"),_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)], 64 /* STABLE_FRAGMENT */))
}
做了静态提升之后
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock(_Fragment, null, [_hoisted_1,_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)], 64 /* STABLE_FRAGMENT */))
}// Check the console for the AST
静态内容_hoisted_1被放置在render 函数外,每次渲染的时候只要取 _hoisted_1 即可
同时 _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff
3.事件监听缓存
默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化
<div><button @click = 'onClick'>点我</button>
</div>
没开启事件监听器缓存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 //动态属性,但不包含类名和样式]))
})
开启事件侦听器缓存后
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,就不需要创建对象,然后根据对象渲染
div><div><span>你好</span></div>... // 很多个静态属性<div><span>{{ message }}</span></div>
</div>
编译后
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>`)
}
二、源码体积
相比Vue2,Vue3整体体积变小了,除了移出一些不常用的API
,再重要的是Tree shanking
任何一个函数,如ref、reavtived、computed等
,仅仅在用到的时候才打包
,没用到的模块都被摇掉
,打包的整体体积变小
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++) // 19return {age,state,readOnlyAge}}
});
三、响应式系统
vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式
vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历
可以监听动态属性的添加
可以监听到数组的索引和数组length属性
可以监听删除属性
关于这两个 API 具体的不同,我们下篇文章会进行一个更加详细的介绍
题51:Vue3.0的设计目标是什么?做了哪些优化?
一、设计目标
不以解决实际业务痛点的更新都是耍流氓,下面我们来列举一下Vue3之前我们或许会面临的问题
1.随着功能的增长,复杂组件的代码变得越来越难以维护
2.缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制
3.类型推断不够友好
4.bundle的时间太久了
而 Vue3 经过长达两三年时间的筹备,做了哪些事情?
我们从结果反推
1.更小
2.更快
3.TypeScript支持
4.API设计一致性
5.提高自身可维护性
6.开放更多底层功能
一句话概述,就是更小更快更友好了
1.更小
Vue3移除一些不常用的 API
引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了
2.更快
主要体现在编译方面:
1)diff算法优化
2)静态提升
3)事件监听缓存
4)SSR优化
下篇文章我们会进一步介绍
3.更友好
vue3在兼顾vue2的options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力
这里代码简单演示下:
存在一个获取鼠标位置的函数
import { toRefs, reactive } from 'vue';
function useMouse(){const state = reactive({x:0,y:0});const update = e=>{state.x = e.pageX;state.y = e.pageY;}onMounted(()=>{window.addEventListener('mousemove',update);})onUnmounted(()=>{window.removeEventListener('mousemove',update);})return toRefs(state);
}
我们只需要调用这个函数,即可获取x、y的坐标,完全不用关注实现过程
试想一下,如果很多类似的第三方库,我们只需要调用即可,不必关注实现过程,开发效率大大提高
同时,VUE3是基于typescipt编写的,可以享受到自动的类型定义提示
三、优化方案
vue3从很多层面都做了优化,可以分成三个方面:
1.源码
2.性能
3.语法 API
1.源码
源码可以从两个层面展开:
源码管理
TypeScript
源码管理
vue3整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到packages 目录下面不同的子目录中
这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性
另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue 使用的,这样用户如果只想使用 Vue3 的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue
TypeScript
Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导
性能
vue3是从什么哪些方面对性能进行进一步优化呢?
体积优化
编译优化
数据劫持优化
这里讲述数据劫持:
在vue2中,数据劫持是通过Object.defineProperty ,这个 API 有一些缺陷,并不能检测对象属性的添加和删除
Object.defineProperty(data, 'a',{get(){// track},set(){// trigger}
})
尽管 Vue为了解决这个问题提供了 set 和delete 实例方法,但是对于用户来说,还是增加了一定的心智负担
同时在面对嵌套层级比较深的情况下,就存在性能问题
default {data: {a: {b: {c: {d: 1}}}}
}
相比之下,vue3是通过proxy监听整个对象,那么对于删除还是监听当然也能监听到
同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归
语法 API
这里当然说的就是composition API,其两大显著的优化:
优化逻辑组织
优化逻辑复用
逻辑组织
一张图,我们可以很直观地感受到 Composition API 在逻辑组织方面的优势
相同功能的代码编写在一块,而不像options API那样,各个功能的代码混成一块
逻辑复用
在vue2中,我们是通过mixin实现功能混合,如果多个mixin混合,会存在两个非常明显的问题:命名冲突和数据来源不清晰
而通过composition这种形式,可以将一些复用的代码抽离出来作为一个函数,只要的使用的地方直接进行调用即可
同样是上文的获取鼠标位置的例子
import { toRefs, reactive, onUnmounted, onMounted } from 'vue';
function useMouse(){const state = reactive({x:0,y:0});const update = e=>{state.x = e.pageX;state.y = e.pageY;}onMounted(()=>{window.addEventListener('mousemove',update);})onUnmounted(()=>{window.removeEventListener('mousemove',update);})return toRefs(state);
}
组件使用
import useMousePosition from './mouse'
export default {setup() {const { x, y } = useMousePosition()return { x, y }}
}
可以看到,整个数据来源清晰了,即使去编写更多的hook函数,也不会出现命名冲突的问题
题52:你是怎么处理vue项目中的错误的?
一、错误类型
任何一个框架,对于错误的处理都是一种必备的能力
在Vue 中,则是定义了一套对应的错误处理规则给到使用者,且在源代码级别,对部分必要的过程做了一定的错误处理。
主要的错误来源包括:
1.后端接口错误
2.代码中本身逻辑错误
二、如何处理
1.后端接口错误
通过axios的interceptor实现网络请求的response先进行一层拦截
apiClient.interceptors.response.use(response => {return response;},error => {if (error.response.status == 401) {router.push({ name: "Login" });} else {message.error("出错了");return Promise.reject(error);}}
);
代码逻辑问题
全局设置错误处理
设置全局错误处理函数
三、源码分析
异常处理源码
小结
1.handleError在需要捕获异常的地方调用,首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用errorCaptured 方法,在遍历调用完所有 errorCaptured 方法或 errorCaptured 方法有报错时,调用
globalHandleError 方法
2.globalHandleError 调用全局的 errorHandler 方法,再通过logError判断环境输出错误信息
3.invokeWithErrorHandling更好的处理异步错误信息
4.logError判断环境,选择不同的抛错方式。非生产环境下,调用warn方法处理错误
题53:vue项目中如何解决跨域问题?
解决跨域的方法有很多,下面列举了三种:
1.JSONP
2.CORS
3.Proxy
而在vue项目中,我们主要针对CORS或Proxy这两种方案进行展开
1.CORS
CORS (Cross-Origin Resource Sharing,
跨域资源共享
)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript代码获取跨域请求的响应
CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源
只要后端实现了 CORS,就实现了跨域
以 koa框架举例
添加中间件,直接设置Access-Control-Allow-Origin请求头
app.use(async (ctx, next)=> {ctx.set('Access-Control-Allow-Origin', '*');ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');if (ctx.method == 'OPTIONS') {ctx.body = 200; } else {await next();}
})
ps: Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host
2.Proxy
代理(Proxy)也称
网络代理
,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击
方案一:
如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象
通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域
在vue.config.js文件,新增以下代码
amodule.exports = {devServer: {host: '127.0.0.1',port: 8084,open: true,// vue项目启动时自动打开浏览器proxy: {'/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址changeOrigin: true, //是否跨域pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替'^/api': "" }}}}
}
通过axios发送请求中,配置请求的根路径
axios.defaults.baseURL = '/api'
方案二:
此外,还可通过服务端实现代理请求转发
以express框架为例
var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false}));
module.exports = app
方案三:
通过配置nginx实现代理
server {listen 80;# server_name www.josephxia.com;location / {root /var/www/html;index index.html index.htm;try_files $uri $uri/ /index.html;}location /api {proxy_pass http://127.0.0.1:3000;proxy_redirect off;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}
}
题54:vue怎么实现权限管理?控制到按钮级别的权限怎么做?
一、是什么
权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源
而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发
页面加载触发
页面上的按钮点击触发
总的来说,所有的请求发起都触发自前端路由或视图
所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:
路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页
视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截
二、如何做
前端权限控制可以分为四个方面:
1.接口权限
2.路由权限
3.菜单权限
4.按钮权限
1.接口权限
接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录
登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token
axios.interceptors.request.use(config => {config.headers['token'] = cookie.get('token')return config
})
axios.interceptors.response.use(res=>{},{response}=>{if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误router.push('/login')}
})
2.路由权限控制
方案一
初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验
const routerMap = [{path: '/permission',component: Layout,redirect: '/permission/index',alwaysShow: true, // will always show the root menumeta: {title: 'permission',icon: 'lock',roles: ['admin', 'editor'] // you can set roles in root nav},children: [{path: 'page',component: () => import('@/views/permission/page'),name: 'pagePermission',meta: {title: 'pagePermission',roles: ['admin'] // or you can only set roles in sub nav}}, {path: 'directive',component: () => import('@/views/permission/directive'),name: 'directivePermission',meta: {title: 'directivePermission'// if do not set roles, means: this page does not require permission}}]}]
这种方式存在以下四种缺点:
1)加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。
2)全局路由守卫里,每次路由跳转都要做权限判断。
3)菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
4)菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
方案二
初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制
登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookieNProgress.configure({ showSpinner: false })// NProgress Configuration// permission judge function
function hasPermission(roles, permissionRoles) {if (roles.indexOf('admin') >= 0) return true // admin permission passed directlyif (!permissionRoles) return truereturn roles.some(role => permissionRoles.indexOf(role) >= 0)
}const whiteList = ['/login', '/authredirect']// no redirect whitelistrouter.beforeEach((to, from, next) => {NProgress.start() // start progress barif (getToken()) { // determine if there has token/* has token*/if (to.path === '/login') {next({ path: '/' })NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it} else {if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息store.dispatch('GetUserInfo').then(res => { // 拉取user_infoconst roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record})}).catch((err) => {store.dispatch('FedLogOut').then(() => {Message.error(err || 'Verification failed, please login again')next({ path: '/' })})})} else {// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓if (hasPermission(store.getters.roles, to.meta.roles)) {next()//} else {next({ path: '/401', replace: true, query: { noGoBack: true }})}// 可删 ↑}}} else {/* has no token*/if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入next()} else {next('/login') // 否则全部重定向到登录页NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it}}
})router.afterEach(() => {NProgress.done() // finish progress bar
})
按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限
这种方式也存在了以下的缺点:
1)全局路由守卫里,每次路由跳转都要做判断 2)菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
3)菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
3.菜单权限
菜单权限可以理解成将页面与理由进行解耦
方案一
菜单与路由分离,菜单由后端返回
前端定义路由信息
{name: "login",path: "/login",component: () => import("@/pages/Login.vue")
}
name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验
全局路由守卫里做判断
function hasPermission(router, accessMenu) {if (whiteList.indexOf(router.path) !== -1) {return true;}let menu = Util.getMenuByName(router.name, accessMenu);if (menu.name) {return true;}return false;}Router.beforeEach(async (to, from, next) => {if (getToken()) {let userInfo = store.state.user.userInfo;if (!userInfo.name) {try {await store.dispatch("GetUserInfo")await store.dispatch('updateAccessMenu')if (to.path === '/login') {next({ name: 'home_index' })} else {//Util.toDefaultPage([...routers], to.name, router, next);next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由}} catch (e) {if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入next()} else {next('/login')}}} else {if (to.path === '/login') {next({ name: 'home_index' })} else {if (hasPermission(to, store.getters.accessMenu)) {Util.toDefaultPage(store.getters.accessMenu,to, routes, next);} else {next({ path: '/403',replace:true })}}}} else {if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入next()} else {next('/login')}}let menu = Util.getMenuByName(to.name, store.getters.accessMenu);Util.title(menu.title);
});Router.afterEach((to) => {window.scrollTo(0, 0);
});
每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的
如果根据路由name找不到对应的菜单,就表示用户有没权限访问
如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载
这种方式的缺点:
菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
全局路由守卫里,每次路由跳转都要做判断
方案二
菜单和路由都由后端返回
前端统一定义路由组件
const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {home: Home,userInfo: UserInfo
};
后端路由组件返回以下格式
[{name: "home",path: "/",component: "home"},{name: "home",path: "/userinfo",component: "userInfo"}
]
在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件
如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理
这种方法也会存在缺点:
全局路由守卫里,每次路由跳转都要做判断
前后端的配合要求更高
4.按钮权限
方案一
按钮权限也可以用v-if判断
但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断
这种方式就不展开举例了
方案二
通过自定义指令进行按钮权限的判断
首先配置路由
{path: '/permission',component: Layout,name: '权限测试',meta: {btnPermissions: ['admin', 'supper', 'normal']},//页面需要的权限children: [{path: 'supper',component: _import('system/supper'),name: '权限测试页',meta: {btnPermissions: ['admin', 'supper']} //页面需要的权限},{path: 'normal',component: _import('system/normal'),name: '权限测试页',meta: {btnPermissions: ['admin']} //页面需要的权限}]
}
自定义权限鉴定指令
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {bind: function (el, binding, vnode) {// 获取页面按钮权限let btnPermissionsArr = [];if(binding.value){// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。btnPermissionsArr = Array.of(binding.value);}else{// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。btnPermissionsArr = vnode.context.$route.meta.btnPermissions;}if (!Vue.prototype.$_has(btnPermissionsArr)) {el.parentNode.removeChild(el);}}
});
// 权限检查方法
Vue.prototype.$_has = function (value) {let isExist = false;// 获取用户按钮权限let btnPermissionsStr = sessionStorage.getItem("btnPermissions");if (btnPermissionsStr == undefined || btnPermissionsStr == null) {return false;}if (value.indexOf(btnPermissionsStr) > -1) {isExist = true;}return isExist;
};
export {has}
在使用的按钮中只需要引用v-has指令
<el-button @click='editClick' type="primary" v-has>编辑</el-button>
小结
关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离
权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断
题55:大型项目中,Vue项目怎么划分结构和划分组件比较合理?
一、为什么要划分
使用vue构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更高
在划分项目结构的时候,需要遵循一些基本的原则:
1.文件夹和文件夹内部文件的语义一致性
2.单一入口/出口
3.就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
4.公共的文件应该以绝对路径的方式从根目录引用
5./src 外的文件不应该被引入
6.文件夹和文件夹内部文件的语义一致性
我们的目录结构都会有一个文件夹是按照路由模块来划分的,如pages文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件夹
这样做的好处在于一眼就从 pages文件夹看出这个项目的路由有哪些
1.单一入口/出口
举个例子,在pages文件夹里面存在一个seller文件夹,这时候seller 文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js 应该作为外部引入 seller 模块的唯一入口
// 错误用法
import sellerReducer from 'src/pages/seller/reducer'// 正确用法
import { reducer as sellerReducer } from 'src/pages/seller'
这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点
2.就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
使用相对路径可以保证模块内部的独立性
// 正确用法
import styles from './index.module.scss'
// 错误用法
import styles from 'src/pages/seller/index.module.scss'
举个例子
假设我们现在的 seller 目录是在 src/pages/seller,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller。
如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller 文件夹内部不需要做任何变更。
但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import 的路径做修改
3.公共的文件应该以绝对路径的方式从根目录引用
公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components下
在使用到的页面中,采用绝对路径的形式引用
// 错误用法
import Input from '../../components/input'
// 正确用法
import Input from 'src/components/input'
同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换
再加上绝对路径有全局的语义,相对路径有独立模块的语义
4./src 外的文件不应该被引入
vue-cli脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src文件夹,里面放着所有的项目需要的资源,js, css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件
这样的好处是方便划分项目代码文件和配置文件
二、目录结构
单页面目录结构
project
│ .browserslistrc
│ .env.production
│ .eslintrc.js
│ .gitignore
│ babel.config.js
│ package-lock.json
│ package.json
│ README.md
│ vue.config.js
│ yarn-error.log
│ yarn.lock
│
├─public
│ favicon.ico
│ index.html
│
|-- src|-- components|-- input|-- index.js|-- index.module.scss|-- pages|-- seller|-- components|-- input|-- index.js|-- index.module.scss|-- reducer.js|-- saga.js|-- index.js|-- index.module.scss|-- buyer|-- index.js|-- index.js
多页面目录结构
my-vue-test:.
│ .browserslistrc
│ .env.production
│ .eslintrc.js
│ .gitignore
│ babel.config.js
│ package-lock.json
│ package.json
│ README.md
│ vue.config.js
│ yarn-error.log
│ yarn.lock
│
├─public
│ favicon.ico
│ index.html
│
└─src├─apis //接口文件根据页面或实例模块化│ index.js│ login.js│├─components //全局公共组件│ └─header│ index.less│ index.vue│├─config //配置(环境变量配置不同passid等)│ env.js│ index.js│├─contant //常量│ index.js│├─images //图片│ logo.png│├─pages //多页面vue项目,不同的实例│ ├─index //主实例│ │ │ index.js│ │ │ index.vue│ │ │ main.js│ │ │ router.js│ │ │ store.js│ │ ││ │ ├─components //业务组件│ │ └─pages //此实例中的各个路由│ │ ├─amenu│ │ │ index.vue│ │ ││ │ └─bmenu│ │ index.vue│ ││ └─login //另一个实例│ index.js│ index.vue│ main.js│├─scripts //包含各种常用配置,工具函数│ │ map.js│ ││ └─utils│ helper.js│├─store //vuex仓库│ │ index.js│ ││ ├─index│ │ actions.js│ │ getters.js│ │ index.js│ │ mutation-types.js│ │ mutations.js│ │ state.js│ ││ └─user│ actions.js│ getters.js│ index.js│ mutation-types.js│ mutations.js│ state.js│└─styles //样式统一配置│ components.less│├─animation│ index.less│ slide.less│├─base│ index.less│ style.less│ var.less│ widget.less│└─commonindex.lessreset.lessstyle.lesstransition.less
小结
项目的目录结构很重要,因为目录结构能体现很多东西,怎么规划目录结构可能每个人有自己的理解,但是按照一定的规范去进行目录的设计,能让项目整个架构看起来更为简洁,更加易用
题56:vue项目中有封装过axios吗?怎么封装的?
一、axios是什么
axios 是一个轻量的 HTTP客户端
基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选
特性
1.从浏览器中创建 XMLHttpRequests
2.从 node.js 创建 http请求
3.支持 Promise API
4.拦截请求和响应
5.转换请求数据和响应数据
6.取消请求
7.自动转换 JSON 数据
8.客户端支持防御XSRF
基本使用
安装
// 项目中安装
npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
导入
import axios from 'axios'
发送请求
axios({ url:'xxx', // 设置请求的地址method:"GET", // 设置请求方法params:{ // get请求使用params进行参数凭借,如果是post请求用datatype: '',page: 1}
}).then(res => { // res为后端返回的数据console.log(res);
})
并发请求axios.all([])
function getUserAccount() {return axios.get('/user/12345');
}function getUserPermissions() {return axios.get('/user/12345/permissions');
}axios.all([getUserAccount(), getUserPermissions()]).then(axios.spread(function (res1, res2) { // res1第一个请求的返回的内容,res2第二个请求返回的内容// 两个请求都执行完成才会执行
}));
二、为什么要封装
axios 的 API 很友好,你完全可以很轻松地在项目中直接使用。
不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍
这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用
举个例子:
axios('http://localhost:3000/data', {// 配置代码method: 'GET',timeout: 1000,withCredentials: true,headers: {'Content-Type': 'application/json',Authorization: 'xxx',},transformRequest: [function (data, headers) {return data;}],// 其他请求配置...
})
.then((data) => {// todo: 真正业务逻辑代码console.log(data);
}, (err) => {// 错误处理代码 if (err.response.status === 401) {// handle authorization error}if (err.response.status === 403) {// handle server forbidden error}// 其他错误处理.....console.log(err);
});
如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了
这时候我们就需要对axios进行二次封装,让使用更为便利
三、如何封装
封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间…
1.设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分2.请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)3.状态码: 根据接口返回的不同status , 来执行不同的业务,这块需要和后端约定好4.请求方法:根据get、post等方法进行一个再次封装,使用起来更为方便5.请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问6.响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务
1.设置接口请求前缀
利用node环境变量来作判断,用来区分开发、测试、生产环境
if (process.env.NODE_ENV === 'development') {axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {axios.defaults.baseURL = 'http://prod.xxx.com'
}
在本地调试的时候,还需要在vue.config.js文件中配置devServer实现代理转发,从而实现跨域devServer: {proxy: {'/proxyApi': {target: 'http://dev.xxx.com',changeOrigin: true,pathRewrite: {'/proxyApi': ''}}}}
2.设置请求头与超时时间
大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置
const service = axios.create({...timeout: 30000, // 请求 30s 超时headers: {get: {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来},post: {'Content-Type': 'application/json;charset=utf-8'// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来}},
})
3.封装请求方法
先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去
// get 请求
export function httpGet({url,params = {}
}) {return new Promise((resolve, reject) => {axios.get(url, {params}).then((res) => {resolve(res.data)}).catch(err => {reject(err)})})
}// post
// post请求
export function httpPost({url,data = {},params = {}
}) {return new Promise((resolve, reject) => {axios({url,method: 'post',transformRequest: [function (data) {let ret = ''for (let it in data) {ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'}return ret}],// 发送的数据data,// url参数params}).then(res => {resolve(res.data)})})
}
把封装的方法放在一个api.js文件中
import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })
页面中就能直接调用// .vue
import { getorglist } from '@/assets/js/api'getorglist({ id: 200 }).then(res => {console.log(res)
})
这样可以把api统一管理起来,以后维护修改只需要在api.js文件操作即可
4.请求拦截器
请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便
// 请求拦截器
axios.interceptors.request.use(config => {// 每次发送请求之前判断是否存在token// 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的hl-&& (config.headers.Authorization = token)return config},error => {return Promise.error(error)})
5.响应拦截器
响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权
// 响应拦截器
axios.interceptors.response.use(response => {// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据// 否则的话抛出错误if (response.status === 200) {if (response.data.code === 511) {// 未授权调取授权接口} else if (response.data.code === 510) {// 未登录跳转登录页} else {return Promise.resolve(response)}} else {return Promise.reject(response)}
}, error => {// 我们可以在这里对异常状态作统一处理if (error.response.status) {// 处理请求失败的情况// 对不同返回码对相应处理return Promise.reject(error.response)}
})
小结
封装是编程中很有意义的手段,简单的axios封装,就可以让我们可以领略到它的魅力
封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案
题57:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路
一、什么是虚拟DOM
虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,从 React 到 Vue ,虚拟 DOM 为这两个框架都带来了跨平台的能力(React-Native 和 Weex)
实际上它只是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
在Javascript对象中,虚拟DOM 表现为一个 Object 对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别
创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应
在vue中同样使用到了虚拟DOM技术
定义真实DOM
<div id="app"><p class="p">节点内容</p><h3>{{ foo }}</h3>
</div>
实例化vue
const app = new Vue({el:"#app",data:{foo:"foo"}
})
观察render的render,我们能得到虚拟DOM
(function anonymous(
) {with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"},[_v("节点内容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})
通过VNode,vue可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff算法得出一些需要修改的最小单位,再更新视图,减少了dom操作,提高了性能
二、为什么需要虚拟DOM
DOM是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起的
真实的DOM节点,哪怕一个最简单的div也包含着很多属性,可以打印出来直观感受一下:
由此可见,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验
举个例子:
你用传统的原生api或jQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程
当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程
而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓计算
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI
虚拟DOM的源码解析和实现原理实在太多了,自己去看小程序。
题58:说说你对keep-alive的理解?
一、Keep-alive 是什么
keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive可以设置以下props属性:
include - 字符串或正则表达式。只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
max - 数字。最多可以缓存多少组件实例
关于keep-alive的基本用法:
<keep-alive><component :is="view"></component>
</keep-alive>
使用includes和exclude:<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 缓存的组件,会多出两个生命周期钩子(activated与deactivated):
首次进入组件时: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: '列表页'}
}
使用
<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>
三、原理分析
keep-alive是vue中内置的一个组件
源码位置:src/core/components/keep-alive.js
export default {name: 'keep-alive',abstract: true,props: {include: [String, RegExp, Array],exclude: [String, RegExp, Array],max: [String, Number]},created () {this.cache = Object.create(null)this.keys = []},destroyed () {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys)}},mounted () {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.defaultconst vnode = getFirstComponentChild(slot)/* 获取该组件节点的componentOptions */const componentOptions = vnode && vnode.componentOptionsif (componentOptions) {/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */const name = getComponentName(componentOptions)const { include, exclude } = this/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */if ((include && (!name || !matches(include, name))) ||// excluded(exclude && name && matches(exclude, name))) {return vnode}const { cache, keys } = this/* 获取组件的key值 */const key = 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]) {vnode.componentInstance = cache[key].componentInstance// make current key freshestremove(keys, key)keys.push(key)}/* 如果没有命中缓存,则将其设置进缓存 */else {cache[key] = vnodekeys.push(key)// prune oldest entry/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */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] = nullremove(keys, key)
}
在mounted钩子函数中观测 include 和 exclude 的变化,如下:mounted () {this.$watch('include', val => {pruneCache(this, name => matches(val, name))})this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))})
}
如果include 或exclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:
function pruneCache (keepAliveInstance, filter) {const { cache, keys, _vnode } = keepAliveInstancefor (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] = vnodekeys.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,如果超过了,则把第一个缓存组件删掉
四、思考题:缓存后如何获取数据
解决方案可以有以下两种:
1.beforeRouteEnter
2.actived
1.beforeRouteEnter
每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){next(vm=>{console.log(vm)// 每次进入路由执行vm.getData() // 获取数据})
},
2.actived
在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){this.getData() // 获取数据
},
注意:服务器端渲染期间avtived不被调用
题59:Vue.observable是什么?
一、Observable 是什么
Observable 翻译过来我们可以理解成可观察的
我们先来看一下其在Vue中的定义
Vue.observable,让一个对象变成响应式数据
。Vue 内部会用它来处理 data 函数返回的对象
返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器
Vue.observable({ count : 1})
其作用等同于
new vue({ count : 1})
在 Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象
在 Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的
二、使用场景
在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择
创建一个js文件
// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响应
export let state = Vue.observable({name: '张三','age': 38
})
// 创建对应的方法
export let mutations = {changeName(name) {state.name = name},setAge(age) {state.age = age}
}
在.vue文件中直接使用即可
<template><div>姓名:{{ name }}年龄:{{ age }}<button @click="changeName('李四')">改变姓名</button><button @click="setAge(18)">改变年龄</button></div>
</template>
import { state, mutations } from '@/store
export default {// 在计算属性中拿到值computed: {name() {return state.name},age() {return state.age}},// 调用mutations里面的方法,更新数据methods: {changeName: mutations.changeName,setAge: mutations.setAge}
}
三、原理分析
源码位置:src\core\observer\index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {if (!isObject(value) || value instanceof VNode) {return}let ob: Observer | void// 判断是否存在__ob__响应式属性if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__} else if (shouldObserve &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {// 实例化Observer响应式对象ob = new Observer(value)}if (asRootData && ob) {ob.vmCount++}return ob
}
Observer类
export class Observer {value: any;dep: Dep;vmCount: number; // number of vms that have this object as root $dataconstructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)if (Array.isArray(value)) {if (hasProto) {protoAugment(value, arrayMethods)} else {copyAugment(value, arrayMethods, arrayKeys)}this.observeArray(value)} else {// 实例化对象是一个对象,进入walk方法this.walk(value)}
}
walk函数
walk (obj: Object) {const keys = Object.keys(obj)// 遍历key,通过defineReactive创建响应式对象for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])}
}
defineReactive方法
export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean
) {const dep = new Dep()const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {return}// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.setif ((!getter || setter) && arguments.length === 2) {val = obj[key]}let childOb = !shallow && observe(val)// 接下来调用Object.defineProperty()给对象定义响应式属性Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : valif (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()if (Array.isArray(value)) {dependArray(value)}}}return value},set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val/* eslint-disable no-self-compare */if (newVal === value || (newVal !== newVal && value !== value)) {return}/* eslint-enable no-self-compare */if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()}// #7981: for accessor properties without setterif (getter && !setter) returnif (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = !shallow && observe(newVal)// 对观察者watchers进行通知,state就成了全局响应式对象dep.notify()}})
}
题60:说说你对slot的理解?slot使用场景有哪些?
一、slot是什么
在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个
占位符
该占位符可以在后期使用自己的标记语言填充
举个栗子
<template id="element-details-template"><slot name="element-name">Slot template</slot>
</template>
<element-details><span slot="element-name">1</span>
</element-details>
<element-details><span slot="element-name">2</span>
</element-details>
template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,
customElements.define('element-details',class extends HTMLElement {constructor() {super();const template = document.getElementById('element-details-template').content;const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(template.cloneNode(true));}
})
在Vue中的概念也是如此
Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口
可以将其类比为插卡式的FC游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容)
二、使用场景
通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情
通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用
比如布局组件、表格列、下拉选、弹框显示内容等
三、分类
slot可以分来以下三种:
1.默认插槽
子组件用<slot>标签
来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面
父组件在使用的时候,直接在子组件的标签内写入内容即可
子组件Child.vue
<template><slot><p>插槽后备的内容</p></slot>
</template>
父组件
<Child><div>默认插槽</div>
</Child>
2.具名插槽
子组件用name属性来表示插槽的名字,不传为默认插槽
父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值
子组件Child.vue
<template><slot>插槽后备的内容</slot><slot name="content">插槽后备的内容</slot>
</template>
父组件
<child><template v-slot:default>具名插槽</template><!-- 具名插槽⽤插槽名做参数 --><template v-slot:content>内容...</template>
</child>
3.作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上
父组件中在使用时通过v-slot:(简写:#)
获取子组件的信息,在内容中使用
子组件Child.vue
<template> <slot name="footer" testProps="子组件的值"><h3>没传footer插槽</h3></slot>
</template>
父组件
<child> <!-- 把v-slot的值指定为作⽤域上下⽂对象 --><template v-slot:default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template><template #default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template>
</child>
小结:
v-slot属性只能在<template>
上使用,但在只有默认插槽时可以在组件标签上使用
默认插槽名为default,可以省略default直接写v-slot
缩写为#时不能不写参数,写成#default
可以通过解构获取v-slot={user},还可以重命名v-slot=“{user: newName}“和定义默认值v-slot=”{user = ‘默认值’}”
四、原理分析
slot本质上是返回VNode的函数,一般情况下,Vue中的组件要渲染到页面上需要经过template -> render function -> VNode -> DOM
过程,这里看看slot如何实现:
编写一个buttonCounter组件,使用匿名插槽
Vue.component('button-counter', {template: '<div> <slot>我是默认内容</slot></div>'
})
使用该组件
new Vue({el: '#app',template: '<button-counter><span>我是slot传入内容</span></button-counter>',components:{buttonCounter}
})
获取buttonCounter组件渲染函数(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})
_v表示穿件普通文本节点,_t表示渲染插槽的函数
渲染插槽函数renderSlot(做了简化)
function renderSlot (name,fallback,props,bindObject
) {// 得到渲染插槽内容的函数 var scopedSlotFn = this.$scopedSlots[name];var nodes;// 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回// 否则使用默认值nodes = scopedSlotFn(props) || fallback;return nodes;
}
name属性表示定义插槽的名字,默认值为default,fallback表示子组件中的slot节点的默认值
关于this.$scopredSlots是什么,我们可以先看看vm.slot
function initRender (vm) {...vm.$slots = resolveSlots(options._renderChildren, renderContext);...
}
resolveSlots函数会对children节点做归类和过滤处理,返回slots
function resolveSlots (children,context) {if (!children || !children.length) {return {}}var slots = {};for (var i = 0, l = children.length; i < l; i++) {var child = children[i];var data = child.data;// remove slot attribute if the node is resolved as a Vue slot nodeif (data && data.attrs && data.attrs.slot) {delete data.attrs.slot;}// named slots should only be respected if the vnode was rendered in the// same context.if ((child.context === context || child.fnContext === context) &&data && data.slot != null) {// 如果slot存在(slot="header") 则拿对应的值作为keyvar name = data.slot;var slot = (slots[name] || (slots[name] = []));// 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面if (child.tag === 'template') {slot.push.apply(slot, child.children || []);} else {slot.push(child);}} else {// 如果没有就默认是default(slots.default || (slots.default = [])).push(child);}}// ignore slots that contains only whitespacefor (var name$1 in slots) {if (slots[name$1].every(isWhitespace)) {delete slots[name$1];}}return slots
}
_render渲染函数通过normalizeScopedSlots得到vm.$scopedSlots
vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots
);
作用域插槽中父组件能够得到子组件的值是因为在renderSlot的时候执行会传入props,也就是上述_t第三个参数,父组件则能够得到子组件传递过来的值
题61:说说你对vue的mixin的理解,以及有哪些应用场景?
一、mixin是什么
Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类
Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂
Vue中的mixin
先来看一下官方定义
mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods 、created、computed等等
我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来
在Vue中我们可以局部混入跟全局混入
1.局部混入
定义一个mixin对象,有组件options的data、methods属性
var myMixin = {created: function () {this.hello()},methods: {hello: function () {console.log('hello from mixin!')}}
}
组件通过mixins属性调用mixin对象
Vue.component('componentA',{mixins: [myMixin]
})
该组件在使用的时候,混合了mixin里面的方法,在自动执行create生命钩子,执行hello方法
2.全局混入
通过Vue.mixin()进行全局的混入
Vue.mixin({created: function () {console.log("全局混入")}
})
使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)
PS:全局混入常用于插件的编写
注意事项:
当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项
但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子
二、使用场景
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立
这时,可以通过Vue的mixin功能将相同或者相似的代码提出来
举个例子
定义一个modal弹窗组件,内部通过isShowing来控制显示
const Modal = {template: '#modal',data() {return {isShowing: false}},methods: {toggleShow() {this.isShowing = !this.isShowing;}}
}
定义一个tooltip提示框,内部通过isShowing来控制显示
const Tooltip = {template: '#tooltip',data() {return {isShowing: false}},methods: {toggleShow() {this.isShowing = !this.isShowing;}}
}
通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin就派上用场了
首先抽出共同代码,编写一个mixin
const toggle = {data() {return {isShowing: false}},methods: {toggleShow() {this.isShowing = !this.isShowing;}}
}
两个组件在使用上,只需要引入mixin
const Modal = {template: '#modal',mixins: [toggle]
};const Tooltip = {template: '#tooltip',mixins: [toggle]
}
通过上面小小的例子,让我们知道了Mixin对于封装一些可复用的功能如此有趣、方便、实用
三、源码分析
首先从Vue.mixin入手
源码位置:/src/core/global-api/mixin.js
export function initMixin (Vue: GlobalAPI) {Vue.mixin = function (mixin: Object) {this.options = mergeOptions(this.options, mixin)return this}
}
主要是调用merOptions方法
源码位置:/src/core/util/options.js
export function mergeOptions (parent: Object,child: Object,vm?: Component
): Object {if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并for (let i = 0, l = child.mixins.length; i < l; i++) {parent = mergeOptions(parent, child.mixins[i], vm)}
}const options = {} let keyfor (key in parent) {mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并}for (key in child) {if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了mergeField(key) // 处理child中的key 也就parent中没有处理过的key}}function mergeField (key) {const strat = strats[key] || defaultStratoptions[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并}return options
}
从上面的源码,我们得到以下几点:
优先递归处理 mixins
先遍历合并parent 中的key,调用mergeField方法进行合并,然后保存在变量options
再遍历 child,合并补上 parent 中没有的key,调用mergeField方法进行合并,保存在变量options
通过 mergeField 函数进行了合并
下面是关于Vue的几种类型的合并策略
替换型
合并型
队列型
叠加型
替换型
替换型合并有props、methods、inject、computed
strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal: ?Object,childVal: ?Object,vm?: Component,key: string
): ?Object {if (!parentVal) return childVal // 如果parentVal没有值,直接返回childValconst ret = Object.create(null) // 创建一个第三方对象 retextend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中return ret
}
strats.provide = mergeDataOrFn
同名的props、methods、inject、computed会被后来者代替
合并型
和并型合并有:data
strats.data = function(parentVal, childVal, vm) { return mergeDataOrFn(parentVal, childVal, vm)
};function mergeDataOrFn(parentVal, childVal, vm) { return function mergedInstanceDataFn() { var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象var parentData = parentVal.call(vm, vm) if (childData) { return mergeData(childData, parentData) // 将2个对象进行合并 } else { return parentData // 如果没有childData 直接返回parentData}}
}function mergeData(to, from) { if (!from) return to var key, toVal, fromVal; var keys = Object.keys(from); for (var i = 0; i < keys.length; i++) {key = keys[i];toVal = to[key];fromVal = from[key]; // 如果不存在这个属性,就重新设置if (!to.hasOwnProperty(key)) {set(to, key, fromVal);} // 存在相同属性,合并对象else if (typeof toVal =="object" && typeof fromVal =="object") {mergeData(toVal, fromVal);}} return to
}
mergeData函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:
当目标 data 对象不包含当前属性时,调用 set 方法进行合并(set方法其实就是一些合并重新赋值的方法)
当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性
队列性
队列性合并有:全部生命周期和watch
function mergeHook (parentVal: ?Array<Function>,childVal: ?Function | ?Array<Function>
): ?Array<Function> {return childVal? parentVal? parentVal.concat(childVal): Array.isArray(childVal)? childVal: [childVal]: parentVal
}LIFECYCLE_HOOKS.forEach(hook => {strats[hook] = mergeHook
})// watch
strats.watch = function (parentVal,childVal,vm,key
) {// work around Firefox's Object.prototype.watch...if (parentVal === nativeWatch) { parentVal = undefined; }if (childVal === nativeWatch) { childVal = undefined; }/* istanbul ignore if */if (!childVal) { return Object.create(parentVal || null) }{assertObjectType(key, childVal, vm);}if (!parentVal) { return childVal }var ret = {};extend(ret, parentVal);for (var key$1 in childVal) {var parent = ret[key$1];var child = childVal[key$1];if (parent && !Array.isArray(parent)) {parent = [parent];}ret[key$1] = parent? parent.concat(child): Array.isArray(child) ? child : [child];}return ret
};
生命周期钩子和watch被合并为一个数组,然后正序遍历一次执行
叠加型
叠加型合并有:component、directives、filters
strats.components=
strats.directives=strats.filters = function mergeAssets(parentVal, childVal, vm, key
) { var res = Object.create(parentVal || null); if (childVal) { for (var key in childVal) {res[key] = childVal[key];} } return res
}
叠加型主要是通过原型链进行层层的叠加
小结:
1.替换型策略有props、methods、inject、computed,就是将新的同名参数替代旧的参数
2.合并型策略是data, 通过set方法进行合并和重新赋值
3.队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
4.叠加型有component、directives、filters,通过原型链进行层层的叠加
题62:Vue中的$nextTick有什么作用?
一、NextTick是什么
官方对其的定义
在下次 DOM 更新循环结束之后
执行延迟回调
。在修改数据之后立即使用这个方法,获取更新后的 DOM
什么意思呢?
我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列
,视图需要等队列中所有数据变化完成之后,再统一进行更新
举例一下
Html结构
<div id="app"> {{ message }} </div>
构建一个vue实例
const vm = new Vue({el: '#app',data: {message: '原始值'}
})
修改message
this.message = ‘修改后的值1’
this.message = ‘修改后的值2’
this.message = ‘修改后的值3’
这时候想获取页面最新的DOM节点,却发现获取到的是旧值
console.log(vm.$el.textContent) // 原始值
这是因为message数据在发现变化的时候,vue并不会立刻去更新Dom,而是将修改数据的操作放在了一个异步操作队列中
如果我们一直修改相同数据,异步操作队列还会进行去重
等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM的更新
为什么要有nexttick
举个例子
{{num}}
for(let i=0; i<100000; i++){num = i
}
如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略
二、使用场景
如果想要在修改数据后立刻得到更新后的DOM结构
,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM结构)
第二个参数为:执行函数上下文
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {// DOM 更新了console.log(vm.$el.textContent) // 修改后的值
})
组件内使用 vm.$nextTick()
实例方法只需要通过this.$nextTick()
,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {console.log(this.$el.textContent) // => '修改后的值'
})
$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'
三、实现原理
源码位置:/src/core/util/next-tick.js
callbacks也就是异步操作队列
callbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一次
export function nextTick(cb?: Function, ctx?: Object) {let _resolve;// cb 回调函数会经统一处理压入 callbacks 数组callbacks.push(() => {if (cb) {// 给 cb 回调函数执行加上了 try-catch 错误处理try {cb.call(ctx);} catch (e) {handleError(e, ctx, 'nextTick');}} else if (_resolve) {_resolve(ctx);}});// 执行异步延迟函数 timerFuncif (!pending) {pending = true;timerFunc();}// 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用if (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve;});}
}
timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:
Promise.then、MutationObserver、setImmediate、setTimeout
通过上面任意一种方法,进行降级操作
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {//判断1:是否原生支持Promiseconst p = Promise.resolve()timerFunc = () => {p.then(flushCallbacks)if (isIOS) setTimeout(noop)}isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {//判断2:是否原生支持MutationObserverlet counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {//判断3:是否原生支持setImmediatetimerFunc = () => {setImmediate(flushCallbacks)}
} else {//判断4:上面都不行,直接用setTimeouttimerFunc = () => {setTimeout(flushCallbacks, 0)}
}
无论是微任务还是宏任务,都会放到flushCallbacks使用
这里将callbacks里面的函数复制一份,同时callbacks置空
依次执行callbacks里面的函数
function flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}
}
小结:
把回调函数放入callbacks等待执行
将执行函数放到微任务或者宏任务中
事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
题63:Vue中组件和插件有什么区别?
一、组件是什么
回顾以前对组件的定义:
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件
组件的优势
1.降低整个系统的耦合度
,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
2.调试方便
,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
3.提高可维护性
,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
二、插件是什么
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
1.添加全局方法或者属性。如: vue-custom-element
2.添加全局资源:指令/过滤器/过渡等。如 vue-touch
3.通过全局混入来添加一些组件选项。如 vue-router
4.添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
5.一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
三、两者的区别
两者的区别主要表现在以下几个方面:
1.编写形式
2.注册形式
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. 添加全局方法或 propertyVue.myGlobalMethod = function () {// 逻辑...}// 2. 添加全局资源Vue.directive('my-directive', {bind (el, binding, vnode, oldVnode) {// 逻辑...}...})// 3. 注入组件选项Vue.mixin({created: function () {// 逻辑...}...})// 4. 添加实例方法Vue.prototype.$myMethod = function (methodOptions) {// 逻辑...}
}
2.注册形式
组件注册
vue组件注册主要分为全局注册
与局部注册
全局注册通过Vue.component
方法,第一个参数为组件的名称,第二个参数为传入的配置项
Vue.component('my-component-name', { /* ... */ })
局部注册只需在用到的地方通过components属性注册一个组件const component1 = {...} // 定义一个组件export default {components:{component1 // 局部注册}
}
插件注册
插件的注册通过Vue.use()
的方式进行注册(安装)
,第一个参数为插件的名字,第二个参数是可选择的配置项
Vue.use(插件名字,{ /* ... */} )
注意的是:
注册插件的时候,需要在调用 new Vue() 启动应用之前完成
Vue.use会自动阻止多次注册相同插件,只会注册一次
3.使用场景
具体的其实在插件是什么章节已经表述了,这里在总结一下
组件 (Component) 是用来构成你的
App 的业务模块
,它的目标是 App.vue插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身
简单来说,插件就是指对Vue的功能的增强或补充
题64:为什么Vue中的data是一个函数而不是一个对象?
面试官:为什么data属性是一个函数而不是一个对象?
一、实例和组件定义data的区别
vue实例的时候定义data属性既可以是一个对象,也可以是一个函数
const app = new Vue({el:"#app",// 对象格式data:{foo:"foo"},// 函数格式data(){return {foo:"foo"}}
})
组件中定义data属性,只能是一个函数
如果为组件data直接定义为一个对象
Vue.component('component1',{template:`<div>组件</div>`,data:{foo:"foo"}
})
则会得到警告信息
警告说明:返回的data应该是一个函数在每一个组件实例中
二、组件data定义函数与对象的区别
上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?
在我们定义好一个组件的时候,vue最终都会通过Vue.extend()
构成组件实例
这里我们模仿组件构造函数,定义data属性,采用对象的形式
function Component(){}
Component.prototype.data = {count : 0
}
创建两个组件实例
const componentA = new Component()
const componentB = new Component()
修改componentA组件data属性的值,componentB中的值也发生了改变
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 1
产生这样的原因这是两者共用了
同一个内存地址
,componentA修改的内容,同样对componentB产生了影响
如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)
function Component(){this.data = this.data()
}
Component.prototype.data = function (){return {count : 0}
}
修改componentA组件data属性的值,componentB中的值不受影响
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 0
vue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染
三、原理分析
首先可以看看vue初始化data的代码,data的定义可以是函数也可以是对象
源码位置:/vue-dev/src/core/instance/state.js
function initData (vm: Component) {let data = vm.$options.datadata = vm._data = typeof data === 'function'? getData(data, vm): data || {}...
}
data既能是object也能是function,那为什么还会出现上文警告呢?
别急,继续看下文
组件在创建的时候,会进行选项的合并
源码位置:/vue-dev/src/core/util/options.js
自定义组件会进入mergeOptions进行选项合并
Vue.prototype._init = function (options?: Object) {...// merge optionsif (options && options._isComponent) {// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm, options)} else {vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}...}
定义data会进行数据校验
源码位置:/vue-dev/src/core/instance/init.js
这时候vm实例为undefined,进入if判断,若data类型不是function,则出现警告提示
strats.data = function (parentVal: any,childVal: any,vm?: Component
): ?Function {if (!vm) {if (childVal && typeof childVal !== "function") {process.env.NODE_ENV !== "production" &&warn('The "data" option should be a function ' +"that returns a per-instance value in component " +"definitions.",vm);return parentVal;}return mergeDataOrFn(parentVal, childVal);}return mergeDataOrFn(parentVal, childVal, vm);
};
四、结论
1.
根实例对象data可以是对象也可以是函数
(根实例是单例),不会产生数据污染情况
2.组件实例对象data必须为函数
,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
题65:SPA首页加载速度慢怎么解决?
一、什么是首屏加载
首屏时间(First Contentful Paint),指的是浏览器从响应用户
输入网址地址
,到首屏内容渲染完成
的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中最重要的环节
关于计算首屏时间
利用performance.timing
提供的数据:
通过DOMContentLoad
或者performance
来计算出首屏时间
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{name: "first-contentful-paint",entryType: "paint",startTime: 507.80000002123415,duration: 0,
};
二、加载慢的原因
在页面渲染的过程,导致加载速度慢的因素可能如下:
1.网络延时问题
2.资源文件体积是否过大
3.资源是否重复发送请求去加载了
4.加载脚本的时候,渲染内容堵塞了
三、解决方案
常见的几种SPA首屏优化方式:
1.减小入口文件积
2.静态资源本地缓存
3.UI框架按需加载
4.图片资源的压缩
5.组件重复打包
6.开启GZip压缩
7.使用SSR
1.减小入口文件体积
常用的手段是路由懒加载
,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
在vue-router配置路由的时候,采用动态加载路由的形式
routes:[ path: 'Blogs',name: 'ShowBlogs',component: () => import('./components/ShowBlogs.vue')
]
以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
2.静态资源本地缓存
后端返回资源问题:
1)采用HTTP缓存
,设置Cache-Control
,Last-Modified
,Etag
等响应头
2)采用Service Worker离线缓存
前端合理利用localStorage
3.UI框架按需加载
在日常使用UI框架,例如element-UI、或者antd,我们经常性直接饮用整个UI库
import ElementUI from 'element-ui'
Vue.use(ElementUI)
但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)
4.组件重复打包
假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载
解决方案:在webpack的config文件中,修改CommonsChunkPlugin的配置
minChunks: 3
minChunks为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
5.图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上
,用以减轻http请求压力。
6.开启GZip压缩
拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin
cnmp i compression-webpack-plugin -D
在vue.congig.js中引入并修改webpack配置
const CompressionPlugin = require('compression-webpack-plugin')configureWebpack: (config) => {if (process.env.NODE_ENV === 'production') {// 为生产环境修改配置...config.mode = 'production'return {plugins: [new CompressionPlugin({test: /\.js$|\.html$|\.css/, //匹配文件名threshold: 10240, //对超过10k的数据进行压缩deleteOriginalAssets: false //是否删除原文件})]}}
在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip,就发送给它gzip
格式的文件 我的服务器是用express框架搭建的 只要安装一下compression就能使用
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用
6.使用SSR
SSR(Server side ),也就是服务端渲染
,组件或页面通过服务器生成html字符串,再发送到浏览器
从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染
小结:
减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化
和 页面渲染优化
大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化
题66:谈谈你对Vue生命周期的理解
一、生命周期是什么
生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程在Vue中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程我们可以把组件比喻成工厂里面的一条流水线,每个工人(生命周期)站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作PS:在Vue生命周期钩子会自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())
二、生命周期有哪些
Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期
生命周期 描述
beforeCreate 组件实例被创建之初
created 组件实例已经完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上去之后
beforeUpdate 组件数据发生变化,更新之前
updated 数据数据更新之后
beforeDestroy 组件实例销毁之前
destroyed 组件实例销毁之后
activated keep-alive 缓存的组件激活时
deactivated keep-alive 缓存的组件停用时调用
errorCaptured 捕获一个来自子孙组件的错误时被调用
三、生命周期整体流程
Vue生命周期流程图
具体分析
beforeCreate -> created
初始化vue实例,进行数据观测
created
完成数据观测,属性与方法的运算,watch、event事件回调的配置
可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computed和watch完成数据计算
此时vm.$el 并没有被创建
created -> beforeMount
判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译
优先级:render > template > outerHTML
vm.el获取到的是挂载DOM的
beforeMount
在此阶段可获取到vm.el
此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
beforeMount -> mounted
此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM
mounted
vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM
beforeUpdate
更新的数据必须是被渲染在模板上的(el、template、render之一)
此时view层还未更新
若在beforeUpdate中再次修改数据,不会再次触发更新方法
updated
完成view层的更新
若在updated中再次修改数据,会再次触发更新方法(beforeUpdate、updated)
beforeDestroy
实例被销毁前调用,此时实例属性与方法仍可访问
destroyed
完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
并不能清除DOM,仅仅销毁实例
使用场景分析
生命周期 描述
1.beforeCreate 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
2.created 组件初始化完毕,各种数据可以使用,常用于异步数据获取
3.beforeMount 未执行渲染、更新,dom未创建
4.mounted 初始化结束,dom已创建,可用于获取访问数据和dom元素
5.beforeUpdate 更新前,可用于获取更新前各种状态
6.updated 更新后,所有状态已是最新
7.beforeDestroy 销毁前,可用于一些定时器或订阅的取消
8.destroyed 组件已销毁,作用同上
四、题外话:数据请求在created和mouted的区别
created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成mounted是在页面dom节点渲染完毕之后就立刻执行的触发时机上created是比mounted要更早的两者相同点:都能拿到实例对象的属性和方法讨论这个问题本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况建议:放在create生命周期当中
题67:Vue实例挂载的过程中发生了什么?
一和二主要是源码分析,详细去查看小程序。
三、结论
new Vue的时候调用会调用_init方法
定义 $set、 get、get 、get、delete、$watch 等方法
定义 on、on、on、off、emit、emit、emit、off 等事件
定义 _update、forceUpdate、forceUpdate、forceUpdate、destroy生命周期
调用$mount进行页面的挂载
挂载的时候主要是通过mountComponent方法
定义updateComponent更新函数
执行render生成虚拟DOM
_update将虚拟DOM生成真实DOM结构,并且渲染到页面中
题68:说说你对vue的理解?
Vue.js(/vjuː/,或简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。
Vue 是一套用于构建用户界面的渐进式MVVM框架。那怎么理解渐进式呢?渐进式含义:强制主张最少。
Vue.js包含了声明式渲染、组件化系统、客户端路由、大规模状态管理、构建工具、数据持久化、跨平台支持等,但在实际开发中,并没有强制要求开发者之后某一特定功能,而是根据需求逐渐扩展。
Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。
Vue.js的核心库只关心视图渲染,且由于渐进式的特性,Vue.js便于与第三方库或既有项目整合。Vue.js 实现了一套声明式渲染引擎,并在runtime或者预编译时将声明式的模板编译成渲染函数,挂载在观察者 Watcher 中,在渲染函数中(touch),响应式系统使用响应式数据的getter方法对观察者进行依赖收集(Collect as Dependency),使用响应式数据的setter方法通知(notify)所有观察者进行更新,此时观察者 Watcher 会触发组件的渲染函数(Trigger re-render),组件执行的 render 函数,生成一个新的 Virtual DOM Tree,此时 Vue 会对新老 Virtual DOM Tree 进行 Diff,查找出需要操作的真实 DOM 并对其进行更新。