前端八股之Vue
有使用过vue吗?说说你对vue的理解
对比维度 | 详情 |
---|---|
Web 发展历程 | 石器时代:静态网页,无数据库,后 CGI 技术实现网页与数据库交互,如 1998 年的 Google 文明时代:2005 年左右 ASP、JSP 出现,取代 CGI 增强交互安全性,但 JSP 不灵活,同年 Ajax 兴起 工业革命时代:移动设备普及,Jquery、SPA 雏形及相关前端框架出现,SPA 面临 SEO 等问题 百花齐放时代:多种技术涌现 |
Vue 是什么 | 开源 JavaScript 框架,用于创建用户界面和单页应用,关注 MVC 视图层,2014 年 2 月发布,作者尤雨溪曾参与 AngularJS 工作 |
Vue 核心特性 | 数据驱动(MVVM):Model 处理业务逻辑与服务器交互;View 展示数据为 UI;ViewModel 连接 Model 和 View 组件化:将逻辑抽象为组件,.vue 文件可视作组件,具有降低耦合度、方便调试和维护等优势 指令系统:v - 前缀特殊属性,表达式值改变响应式影响 DOM,常用指令有 v - if、v - for、v - bind、v - on、v - model |
Vue 与传统开发区别 | 以注册账号为例,Jquery 操作 DOM 实现交互,Vue 通过双向绑定操作数据控制 DOM 节点属性,界面变动由数据自动绑定实现 |
Vue 和 React 对比 | 相同点:组件化思想、支持服务器端渲染、有 Virtual DOM、数据驱动视图、有 native 方案、有构建工具 区别: 数据流向:React 单向,Vue 双向 数据变化原理:React 用不可变数据,Vue 用可变数据 组件化通信:React 用回调函数,Vue 中子组件向父组件传消息可用事件和回调函数 diff 算法:React 用 diff 队列得 patch 树批量更新 DOM,Vue 用双向指针边对比边更新 DOM |
你对SPA单页面的理解,它的优缺点分别是什么?如何实现SPA应用呢
一、SPA 是什么
SPA (single-page application)即单页应用,是一种网络应用程序或网站模型。通过动态重写当前页面与用户交互,避免页面切换打断体验。所有必要代码(HTML、JavaScript 和 CSS)通过单个页面加载检索,或按需动态装载资源添加到页面,页面不会重新加载或转移控制到其他页面。常见的 JS 框架如 react、vue、angular、ember 都属 SPA。
二、SPA 和 MPA 的区别
多页应用MPA(MultiPage-page application),翻译过来就是多页应用在MPA
中,每个页面都是一个主页面,都是独立的当我们在访问另一个页面的时候,都需要重新加载html
、css
、js
文件,公共文件则根据需求按需加载如下图
对比项目 | 单页面应用(SPA) | 多页面应用(MPA) |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url 模式 | 哈希模式 | 历史模式 |
SEO 搜索引擎优化 | 难实现,可使用 SSR 方式改善 | 容易实现 |
数据传递 | 容易 | 通过 url、cookie、localStorage 等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
三、SPA 的优缺点
- 优点
- 具有桌面应用即时性、网站可移植性和可访问性。
- 用户体验好且快,内容改变无需重新加载整个页面。
- 良好的前后端分离,分工更明确。
- 缺点
- 不利于搜索引擎抓取。
- 首次渲染速度相对较慢。
四、实现 SPA
- 原理
- 监听地址栏中 hash 变化驱动界面变化。
- 用 pushState 记录浏览器历史,驱动界面变化。
- 实现方式
- hash 模式:核心是监听 url 中的 hash 进行路由跳转。
// 定义 Router
class Router { constructor () { this.routes = {}; // 存放路由path及callback this.currentUrl = ''; // 监听路由change调用相对应的路由回调 window.addEventListener('load', this.refresh.bind(this), false); window.addEventListener('hashchange', this.refresh.bind(this), false); } route(path, callback){ this.routes[path] = callback; } push(path) { this.routes[path] && this.routes[path]() } refresh() { this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl] && this.routes[this.currentUrl](); }
} // 使用 router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'))
miniRouter.route('/page2', () => console.log('page2')) miniRouter.push('/') // page1
miniRouter.push('/page2') // page2
- history 模式:核心借用 HTML5 history api,该 api 提供丰富 router 相关属性。
// 定义 Router
class Router { constructor () { this.routes = {}; this.listerPopState(); } init(path) { history.replaceState({path: path}, null, path); this.routes[path] && this.routes[path](); } route(path, callback){ this.routes[path] = callback; } push(path) { history.pushState({path: path}, null, path); this.routes[path] && this.routes[path](); } listerPopState () { window.addEventListener('popstate' , (e) => { const path = e.state && e.state.path; this.routes[path] && this.routes[path](); }); }
} // 使用 Router
window.miniRouter = new Router();
miniRouter.route('/', () => console.log('page1'));
miniRouter.route('/page2', () => console.log('page2')); // 跳转
miniRouter.push('/page2'); // page2
五、给 SPA 做 SEO 的方式(基于 Vue)
- SSR 服务端渲染:将组件或页面通过服务器生成 html,再返回给浏览器,如 nuxt.js。
- 静态化
- 通过程序将动态页面抓取并保存为静态页面,实际存在于服务器硬盘。
- 通过 WEB 服务器的 URL Rewrite 方式,按规则将外部 URL 请求转化为内部文件地址,把外部请求的静态地址转化为实际动态页面地址,静态页面实际不存在。
- 使用 Phantomjs 针对爬虫处理:通过 Nginx 配置,判断访问来源是否为爬虫,若是则搜索引擎的爬虫请求转发到一个 node server,再通过 PhantomJS 解析完整的 HTML,返回给爬虫。
v-show和v-if有什么区别?使用场景分别是什么?
对比项目 | v - show | v - if |
---|---|---|
共同点 | - 作用效果:都能控制元素在页面是否显示(不含 v - else) - 用法:相似,如 <Model v - show="isShow" /> 与<Model v - if="isShow" /> - 显示隐藏表现:表达式为 true 时占据页面位置,为false 时不占据页面位置 | |
控制手段 | 通过添加css -- display:none 隐藏元素,DOM 元素保留 | 根据条件添加或删除整个 DOM 元素 |
编译过程 | 仅基于 CSS 切换,无局部编译 / 卸载过程 | 切换时有局部编译 / 卸载过程,会销毁和重建内部事件监听及子组件 |
编译条件 | 无论初始条件如何,元素总会先渲染,通过 CSS 控制显示或隐藏 | 条件为假时不操作,条件为真才渲染 |
对组件生命周期影响 | 由false 变为true 时,不触发组件生命周期 | 由false 变为true 时,触发beforeCreate 、create 、beforeMount 、mounted 钩子;由true 变为false 时,触发beforeDestory 、destoryed 方法 |
性能消耗 | 初始渲染消耗高 | 切换消耗高 |
原理 | - 无论初始条件,元素先渲染 - 有 transition 执行transition ,无则直接设置display 属性 | - 依据表达式值决定是否生成 DOM 节点 - 处理 else 、else - if 等复杂条件 |
使用场景 | 适合频繁切换显示隐藏状态的场景,如频繁展开收起的下拉菜单 | 适用于运行时条件很少改变的场景,如仅管理员可见的高级设置按钮 |
通常情况下,回流的代价比重绘更高,因为回流不仅需要重新绘制元素,还需要重新计算布局。而 display: none
这种操作既改变了布局(触发回流),又改变了外观(触发重绘)
Vue实例挂载的过程
1. new Vue () 做了什么
- 调用构造函数:执行
Vue
构造函数,接收用户传递的配置项options
(如data
、methods
等)。若未使用new
关键字调用,在非生产环境下会抛出警告。构造函数内部调用_init
方法进行初始化。 - 查找
_init
方法来源:_init
方法在initMixin(Vue)
中定义在Vue
原型上。
2. _init
方法执行流程
- 初始化准备工作:
- 为实例分配唯一标识
_uid
。 - 标记
_isVue
为true
,表明是 Vue 实例。 - 合并选项:判断是否为组件初始化,若为组件,执行
initInternalComponent
优化内部组件实例化;否则,通过mergeOptions
合并 Vue 属性。 - 在非生产环境下初始化 proxy 拦截器。
- 暴露
_self
指向自身。
- 为实例分配唯一标识
- 初始化各部分功能:
- 初始化生命周期标志位:执行
initLifecycle(vm)
。 - 初始化组件事件侦听:执行
initEvents(vm)
。 - 初始化渲染方法:执行
initRender(vm)
。 - 调用
beforeCreate
钩子:此时数据初始化未完成,无法访问data
、props
等属性。 - 初始化依赖注入内容:执行
initInjections(vm)
,在初始化data
、props
之前。 - 初始化
props
、data
、methods
、watch
、computed
:执行initState(vm)
,其内部执行顺序为props
、methods
、data
等。data
可定义为函数或对象形式(组件必须为函数形式),在initData(vm)
中,将data
挂载到实例vm
上并进行响应式监听。 - 初始化提供内容:执行
initProvide(vm)
,在初始化data
、props
之后。 - 调用
created
钩子:此时数据已初始化完成,可访问data
、props
,但尚未完成 DOM 挂载,无法访问 DOM 元素。
- 初始化生命周期标志位:执行
- 挂载元素:若配置项中有
el
,调用vm.$mount(vm.$options.el)
进行挂载。
3. $mount
方法执行流程
- 解析模板为
render
函数:- 检查是否直接挂载到
body
或html
上,若如此,在非生产环境下抛出警告并返回。 - 若没有
options.render
,尝试从options.template
或通过el
获取模板内容。 - 对获取到的模板内容,通过
compileToFunctions
方法将其解析为render
函数和staticRenderFns
,解析步骤大致为:将 HTML 文档片段解析成 AST 描述符,再将 AST 描述符解析成字符串,最后生成render
函数。生成的render
函数挂载到options.render
上。
- 检查是否直接挂载到
- 调用
mountComponent
渲染组件:- 检查是否有
render
函数,若没有则抛出警告。 - 调用
beforeMount
钩子。 - 定义
updateComponent
函数,该函数在 Vue 初始化时执行render
生成虚拟 DOM(vnode
),并执行_update
方法将虚拟 DOM 转换为真实 DOM 并更新到页面。在非生产环境且开启性能标记时,updateComponent
函数会添加性能测量代码。 - 创建一个
Watcher
实例监听当前组件状态,当数据变化时,触发beforeUpdate
钩子,并调用updateComponent
更新组件。 - 手动挂载的实例,标记
_isMounted
为true
并调用mounted
钩子。
- 检查是否有
4. render
和 _update
方法
_render
方法:该方法定义在Vue.prototype
上,从vm.$options
中获取render
函数,调用render
函数并传入vm.$createElement
创建虚拟 DOM(vnode
)。若渲染过程出错,会尝试调用renderError
处理错误。最后对生成的vnode
进行一些处理,如确保其为单个节点,并设置其父节点。_update
方法:该方法定义在Vue.prototype
上,接收新的虚拟 DOMvnode
。若为首次渲染(无前一个虚拟 DOMprevVnode
),调用vm.__patch__(vm.$el, vnode, hydrating, false)
执行具体的挂载逻辑;否则,调用vm.__patch__(prevVnode, vnode)
进行更新。__patch__
方法会将虚拟 DOM 转换为真实 DOM,并更新到页面中。
5. 总结
new Vue()
时调用_init
方法,完成各种初始化工作,包括合并选项、初始化生命周期、事件、渲染、状态等。- 调用
$mount
进行挂载,挂载过程主要通过mountComponent
方法,定义更新函数updateComponent
,执行render
生成虚拟 DOM,再通过_update
将虚拟 DOM 转换为真实 DOM 并渲染到页面。 - 同时,在挂载过程中会触发
beforeCreate
、created
、beforeMount
、mounted
等生命周期钩子函数,以及在数据变化时触发beforeUpdate
钩子函数。
1. 开始创建 Vue 实例
当你写下 new Vue()
的时候,就好像在跟 Vue 说 “我要创建一个应用啦” 。Vue 首先会检查你是不是用了 new
关键字来调用它。这就好比你要进一个特定的房间,得用正确的开门方式(用 new
关键字),不然在不是正式发布的环境下(非生产环境),它就会提醒你 “哎呀,你得用正确方式开门呀”(抛出警告) 。
如果开门方式正确,它就会去做初始化的事情,也就是调用 _init
方法 。
2. 初始化准备工作
- 发 “身份证”:Vue 会给这个实例发一个独一无二的编号,就像每个人都有身份证号一样,这个编号叫
_uid
。 - 做标记:给这个实例做个标记,标记它是一个正儿八经的 Vue 实例,这个标记就是
_isVue
,把它设成true
。 - 合并 “包裹” 内容:它要看看你给它的配置项(就像一个包裹里的各种东西,有
data
、methods
这些) 。如果是组件的初始化,它会用一种优化的方式来处理;如果不是,就把这些配置项合并起来 。 - 特殊环境处理(非生产环境):要是你是在测试等非正式发布的环境下,它还会做一些额外的设置,比如初始化 proxy 拦截器 。
- “自我” 暴露:让这个实例能找到自己,就像给自己取个小名方便称呼,把
_self
指向它自己 。
3. 初始化各个功能部分
- 给生命加标记:给这个实例的生命周期做一些标记,就像给一个旅程的不同阶段做记号一样,这是通过
initLifecycle
来做的 。 - 设置 “传话筒”:设置怎么去监听和触发事件,就像给房间里安装传话筒,让不同部分能互相 “说话”,这个工作是
initEvents
来做的 。 - 准备 “画画” 工具:为后面把数据画到页面上做准备,也就是初始化渲染方法,由
initRender
来完成 。 - 出发前的提醒:在这个时候,会调用
beforeCreate
钩子函数,就像出发前的提醒,但这个时候数据还没准备好,像data
、props
这些东西你还不能用 。 - 注入 “宝贝”:把一些依赖的东西先准备好,就像出门前把重要的宝贝先装进行李箱,这是
initInjections
在做的事,而且是在初始化data
、props
之前做 。 - 整理 “行李”(初始化数据等):开始整理最重要的行李啦,也就是初始化
props
、data
、methods
、watch
、computed
这些 。这里面的顺序是先处理props
,再处理methods
,然后是data
。data
可以是函数或者对象的形式(但如果是组件,就只能是函数形式哦) ,在处理data
的时候,会把data
里的数据挂载到实例上,还会让这些数据能响应变化 。 - 分享 “宝贝”:把一些要提供出去的东西准备好,就像你到了目的地,把自己的宝贝分享给别人,这是
initProvide
在做的,而且是在初始化data
、props
之后 。 - 创建完成的欢呼:调用
created
钩子函数,这个时候数据已经准备好了,你可以用data
、props
啦,但这个时候页面还没把东西显示出来,因为还没完成 DOM 挂载 。
4. 挂载阶段(如果配置了挂载元素)
如果在最开始给 Vue 的配置项里有指定的挂载元素(vm.$options.el
有东西 ),那就开始挂载啦,也就是调用 vm.$mount(vm.$options.el)
。
5. 解析模板变成 “画画指南”(render
函数)
- 检查 “房子” 位置:看看你要挂载的地方是不是
body
或者html
,要是这两个地方,在不是正式发布的环境下,它会跟你说 “别挂在这两个地方呀”(抛出警告) 。 - 找 “画画指南”:如果没有现成的
render
函数(就像没有画画指南 ),它就会从options.template
或者通过el
去找模板内容 。 - 制作 “画画指南”:找到模板内容后,它会用
compileToFunctions
这个工具,把模板变成render
函数和staticRenderFns
。这个过程就像是把一堆建筑材料(模板内容 )变成详细的建筑图纸(render
函数 ),步骤大概是先把模板解析成一种描述结构(AST 描述符 ),再把这个描述结构变成字符串,最后生成render
函数 。生成的render
函数就会挂载到options.render
上 。
6. 真正的渲染组件
- 检查 “画画指南”:看看有没有
render
函数,如果没有,就会说 “哎呀,没有画画指南可不行呀”(抛出警告 )。 - 开始前的准备:调用
beforeMount
钩子函数,就像画画前再检查一遍工具 。 - 定义 “更新画画” 函数:定义一个叫
updateComponent
的函数,这个函数会去执行render
函数,生成虚拟 DOM(就像是在脑海里先画好一个草图 ),然后再执行_update
函数,把这个草图变成真正画在纸上的画(把虚拟 DOM 变成真实 DOM 并更新到页面 ) 。 - 安排 “小助手” 监听:创建一个
Watcher
实例,就像安排一个小助手,专门盯着组件的状态。要是数据变了,小助手就会触发beforeUpdate
钩子函数,然后调用updateComponent
来更新组件 。 - 完成后的庆祝(如果是手动挂载):如果是手动挂载的实例,就标记一下说 “我已经挂载好啦”(
_isMounted
设为true
),然后调用mounted
钩子函数,就像庆祝终于画完画啦 。
7. 生成虚拟 DOM(_render
方法)
在执行 render
生成虚拟 DOM 的时候,_render
方法会从实例的配置项里找到 render
函数,然后调用这个 render
函数,并且给它一些画画的工具(vm.$createElement
),让它去创建虚拟 DOM 。要是创建过程中出问题了,就会想办法处理错误,最后再对这个虚拟 DOM 做一些调整,给它安排个 “家长”(设置父节点 ) 。
8. 把虚拟 DOM 变成真实 DOM 并更新到页面(_update
方法)
_update
方法会拿到新的虚拟 DOM 。如果是第一次渲染(以前没有虚拟 DOM ),它就会用一种方式(vm.__patch__(vm.$el, vnode, hydrating, false)
)把虚拟 DOM 挂载到页面上;如果不是第一次,就用另一种方式(vm.__patch__(prevVnode, vnode)
)来更新页面上的 DOM 。
请描述下你对vue生命周期的理解?在created和mounted这两个生命周期中请求数据有什么区别呢?
一、Vue 生命周期通俗理解
想象你要制作一个手工艺品。Vue 的生命周期就像是制作这个手工艺品的一系列流程。从你决定要制作(创建实例)开始,到准备材料(初始化数据),然后开始动手制作(编译模板、挂载 DOM),在制作过程中可能会对某些地方进行修改调整(更新),最后完成制作或者决定放弃这个手工艺品(销毁) 。
在 Vue 里,组件实例从诞生到消失的整个过程就是生命周期。每个阶段都有对应的 “钩子函数”,就像是在制作手工艺品流程中的一个个检查点,到了这个点,就可以做一些特定的事情。而且这些钩子函数里的 this
会自动指向组件实例本身,所以在里面可以很方便地访问组件的数据和方法。但不能用箭头函数来定义钩子函数,因为箭头函数没有自己的 this
,会导致访问不到组件实例相关的东西。
二、Vue 生命周期的各个阶段
- 创建前后
- beforeCreate:这时候组件实例刚刚开始创建,就像你刚有了制作手工艺品的想法,还没开始准备材料呢。在这个阶段,组件的很多东西都还没准备好,比如数据还没初始化,你不能访问
data
里的内容,也不能调用组件里定义的方法。这个阶段一般在开发插件的时候,可能会用来做一些初始化的设置。 - created:组件实例已经基本创建好了,就像材料都准备齐了。这时候数据观测已经完成,也就是 Vue 已经知道哪些数据是需要关注变化的了,属性和方法也都准备好了,
watch
和事件回调也配置好了。你可以调用methods
里的方法,也能访问和修改data
里的数据。不过,这个时候页面上的 DOM 节点还没有创建出来哦。这个阶段很适合去获取异步数据,比如从服务器请求一些数据来填充组件展示的内容。
- beforeCreate:这时候组件实例刚刚开始创建,就像你刚有了制作手工艺品的想法,还没开始准备材料呢。在这个阶段,组件的很多东西都还没准备好,比如数据还没初始化,你不能访问
- 载入前后
- beforeMount:在把组件真正放到页面上(挂载)之前的阶段。就像你要把做好的手工艺品摆到展示台上,还没放上去呢。这个时候可以获取到
vm.el
(组件对应的 DOM 元素相关),虽然 DOM 已经初始化了,但还没有真正挂载到页面指定的位置上。 - mounted:组件成功挂载到页面实例上了,就像手工艺品已经摆到展示台上展示了。此时页面的 DOM 已经创建并渲染好了,你可以通过
vm.$el
访问到真正在页面上的 DOM 元素。可以在这里做一些依赖于 DOM 存在才能做的操作,比如获取 DOM 元素的尺寸等。
- beforeMount:在把组件真正放到页面上(挂载)之前的阶段。就像你要把做好的手工艺品摆到展示台上,还没放上去呢。这个时候可以获取到
- 更新前后
- beforeUpdate:当组件里的数据发生变化,要更新页面之前会触发这个钩子。就像你发现手工艺品有些地方可以改进,在动手改之前的那个时刻。不过要注意,只有被渲染在模板上的数据变化了才会触发这个钩子。这个时候页面还没有更新,而且如果在这个钩子函数里再次修改数据,不会再次触发更新流程。
- updated:数据更新完成,页面也更新好了。就像你把手工艺品改好了。但要小心,如果在这个钩子函数里又修改了数据,会再次触发更新流程,又会调用
beforeUpdate
和updated
。
- 销毁前后
- beforeDestroy:在组件实例要被销毁之前调用,就像你要把展示的手工艺品收起来之前。这个时候组件的属性和方法还是可以访问的,你可以在这里做一些清理工作,比如取消定时器或者一些订阅。
- destroyed:组件实例已经完全销毁了,就像手工艺品已经被彻底收起来不存在了。Vue 会解除它和其他实例的连接,解绑所有的指令和事件监听器。不过要注意,它并不会把 DOM 从页面上清除掉,只是组件相关的实例被销毁了。
- 特殊场景
- activated:当使用
keep-alive
组件缓存了某个组件,然后这个被缓存的组件再次被激活显示的时候,就会触发这个钩子。 - deactivated:同样是在
keep-alive
组件缓存的情况下,当被缓存的组件不再显示(停用时)会触发这个钩子。 - errorCaptured:当捕获到来自子孙组件的错误时会被调用,就像家长发现孩子(子孙组件)出问题了。
- activated:当使用
三、在 created 和 mounted 中请求数据的区别
- 触发时机
- created:组件实例一创建完成就会立刻调用,这个时候页面的 DOM 节点还没有生成呢。它就像你刚把材料准备好,还没开始真正动手制作手工艺品,更没把它摆到展示台上。
- mounted:是在页面的 DOM 节点都已经渲染完毕之后才执行的,就像手工艺品已经做好并且摆到展示台上了。所以
created
的触发时机比mounted
要早。
- 可能产生的页面效果
- 在
mounted
里发起请求数据,如果请求时间比较长,而此时页面 DOM 结构已经生成了,就有可能出现页面闪动的情况。比如页面一开始没有数据展示,是空白的或者有默认内容,等数据请求回来后,页面内容突然改变,就会让用户感觉到页面闪了一下。 - 而在
created
里请求数据,因为页面 DOM 还没生成,要是能在页面加载完成前就把数据请求回来并处理好,就可以避免这种页面闪动的问题。所以一般建议如果是对页面内容的改动相关的数据请求,放在created
生命周期里会比较好。
- 在
- 相同点:在
created
和mounted
这两个阶段,都可以访问到组件实例的属性和方法。因为这两个阶段组件实例都已经创建好了,只是 DOM 的状态不一样。
对比维度 | created | mounted |
---|---|---|
触发时机 | 组件实例创建完成时立刻调用,此时页面 DOM 节点尚未生成 | 页面 DOM 节点渲染完毕后执行,DOM 已创建并挂载 |
数据请求对页面的影响 | 若在页面加载前完成请求,可避免页面闪动问题 | 请求时间长时,可能导致页面闪动(因 DOM 结构已生成,数据返回后页面内容改变) |
可进行的操作 | 可调用methods 中的方法,访问和修改data 数据,触发响应式渲染 DOM,可通过computed 和watch 完成数据计算;适合发起异步数据请求,填充组件展示内容 | 可获取访问数据和 DOM 元素,能进行依赖于 DOM 存在才能做的操作,如获取 DOM 元素尺寸等 |
钩子函数作用 | 组件初始化完毕,各种数据可使用,常用于异步数据获取 | 初始化结束,可用于在 DOM 创建并渲染好后执行一些操 |
v-if和v-for的优先级是什么?
对比项 | 详情 |
---|---|
作用 | v-if:条件性渲染内容,表达式为 true 时渲染 v-for:基于数组渲染列表,需用 item in items 语法,建议设唯一key 值优化 diff 算法 |
优先级(Vue2) | v-for 优先级高于 v-if。即同一元素同时使用时,先执行 v-for 循环,再依据 v-if 条件判断是否渲染元素 |
优先级(Vue3) | v-if 优先级高于 v-for。即同一元素同时使用时,先进行 v-if 条件判断,再执行 v-for 循环 |
同时使用的问题 | 无论 Vue2 还是 Vue3,都不建议在同一元素同时使用。在 Vue2 中同时使用会先循环再条件判断,造成性能浪费;在 Vue3 中同时使用可能使指令优先级不清晰,导致代码难理解维护,还可能因 v-if 条件无法访问 v-for 作用域变量别名引发意外行为(会报错) |
优化建议 | - 外层嵌套 template:在外层嵌套<template> 标签,在这一层进行 v-if 判断,内部进行 v-for 循环- 计算属性过滤:通过计算属性提前过滤不需要显示的项,如 computed: { visibleItems() { return this.items.filter(item => item.isShow); } } |
Vue2 中 v - for
优先级高于 v - if
- 示例分析:当在同一元素上同时使用
v - if
和v - for
时,如<div id="app"><p v - if="isShow" v - for="item in items">{{ item.title }}</p></div>
,生成的render
函数中,_l
(列表渲染函数)内部先进行循环,再依据isShow
条件判断是否渲染<p>
标签。这表明在 Vue2 的模板编译过程中,会先处理v - for
指令,再处理v - if
指令 。 - 源码分析:在
\vue - dev\src\compiler\codegen\index.js
的genElement
函数中,判断顺序是v - for
比v - if
先进行判断,即先处理v - for
相关逻辑,再处理v - if
相关逻辑,进一步证明了v - for
优先级高于v - if
。
Vue3 中 v - if
优先级高于 v - for
- 示例分析:同样在同一元素上同时使用
v - if
和v - for
,在 Vue3 中生成的渲染逻辑与 Vue2 不同。Vue3 会先判断v - if
的条件,再进行v - for
的循环。例如有代码<div><p v - if="condition" v - for="item in list">{{ item }}</p></div>
,Vue3 会先检查condition
是否为真,只有为真时才会对list
进行循环渲染。如果condition
一开始就为假,那么v - for
不会执行循环,从而避免了不必要的循环操作。 - 设计意图:Vue3 这样改变优先级的设计,主要是为了性能优化和逻辑的合理性。在 Vue2 中同一元素上同时使用
v - for
和v - if
时,每次渲染都要先循环再判断,即使条件为假,循环也会执行,造成性能浪费。而 Vue3 先判断条件,只有条件满足才进行循环,减少了不必要的计算,提升了性能 。
综上所述,在使用 v - if
和 v - for
时,需要根据 Vue 的版本注意它们优先级的差异,合理编写代码以避免性能问题和不符合预期的渲染结果。
SPA首屏加载速度慢的怎么解决?
资源加载优化
- 减小入口文件体积:
- 路由懒加载:这是常用手段。在 Vue - Router 配置路由时,采用动态加载路由的形式,如
routes: [ { path: 'Blogs', name: 'ShowBlogs', component: () => import('./components/ShowBlogs.vue') } ]
。这样不同路由对应的组件会被分割成不同代码块,只有在路由被请求时才单独打包加载,减小了入口文件大小,加快加载速度。 - 代码分割:借助 Webpack 等打包工具,将代码按功能或模块进行分割,避免将所有代码都打包进一个大文件。比如把公共代码、第三方库等分离出来单独打包,使首屏加载时只需下载必要的代码。
- 路由懒加载:这是常用手段。在 Vue - Router 配置路由时,采用动态加载路由的形式,如
- 静态资源本地缓存:
- HTTP 缓存:在后端设置
Cache - Control
(如设置缓存策略为max - age=31536000
表示缓存有效期为一年 )、Last - Modified
(标记资源最后修改时间 )、Etag
(资源的唯一标识 )等响应头,让浏览器根据规则判断是否使用缓存资源,减少重复请求。 - Service Worker 离线缓存:利用 Service Worker 在浏览器端缓存静态资源。它可以拦截网络请求,优先从缓存中读取资源,在网络不佳或离线时也能快速展示页面。比如可以使用 Workbox 等工具简化 Service Worker 的配置和管理。
- 前端合理利用 localStorage:将一些不常变化的静态数据(如配置信息、用户信息等)存储在
localStorage
中,下次页面加载时直接读取,减少后端请求。但要注意控制存储数据量,避免过度占用空间。
- HTTP 缓存:在后端设置
- UI 框架按需加载:在使用 UI 框架(如 Element - UI、Antd 等)时,避免直接引入整个 UI 库,而是按需引用实际用到的组件。例如,从
element - ui
按需引入组件:import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element - ui'; Vue.use(Button); Vue.use(Input); Vue.use(Pagination);
,减少不必要的代码引入,降低文件体积。 - 图片资源的压缩:
- 压缩图片:使用工具(如 TinyPNG、ImageOptim 等)对图片进行无损或有损压缩,在不影响图片质量的前提下减小文件大小。
- 使用在线字体图标或雪碧图:对于页面上的图标,使用在线字体图标(如 Iconfont),或者将众多小图标合并成雪碧图。这样可以减少 HTTP 请求数量,提升加载速度。
- 组件重复打包:在 Webpack 的配置文件中,通过调整
CommonsChunkPlugin
(在 Webpack4 及以上版本中可使用optimization.splitChunks
替代 )的配置,例如设置minChunks: 3
,表示将被使用 3 次及以上的包抽离出来,放进公共依赖文件,避免重复加载相同组件,减少整体打包体积。 - 开启 GZip 压缩:
- 前端配置:安装
compression - webpack - plugin
,在vue.config.js
(以 Vue 项目为例)中引入并修改 Webpack 配置,对超过一定大小(如设置threshold: 10240
,即超过 10KB )的文件进行压缩。示例配置如下:
- 前端配置:安装
const CompressionPlugin = require('compression - webpack - plugin');
module.exports = {configureWebpack: (config) => {if (process.env.NODE_ENV === 'production') {config.mode = 'production';return {plugins: [new CompressionPlugin({test: /\.js$|\.html$|\.css/, // 匹配文件名threshold: 10240, // 对超过10k的数据进行压缩deleteOriginalAssets: false // 是否删除原文件})]};}}
};
- 服务器配置:如果服务器使用 Express 框架,安装
compression
中间件,然后在其他中间件使用之前调用app.use(compression())
。这样当发送请求的浏览器支持 GZip 时,就会发送 GZip 格式的文件,减小传输文件大小,加快传输速度。
页面渲染优化
- 使用 SSR(Server - Side Rendering,服务端渲染):
- 原理:组件或页面通过服务器生成 HTML 字符串,再发送到浏览器。相比客户端渲染(CSR),SSR 能让用户更快看到页面内容,因为不需要等待浏览器下载 JavaScript 文件并执行渲染逻辑。
- 工具:对于 Vue 应用,建议使用 Nuxt.js 实现服务端渲染。Nuxt.js 封装了很多 SSR 相关的复杂逻辑,如路由处理、数据预取等,降低了开发成本。但 SSR 也会带来一些额外的开发及维护成本,比如需要关注服务端开发及运维,处理潜在的内存泄露、变量污染等隔离问题,以及 SSR 失败时回退到 CSR 的容灾方案等。
- 优化渲染逻辑:
- 减少重绘和回流:避免频繁修改 DOM 样式和结构。例如,不要在循环中多次修改元素的样式,而是一次性修改 class 来改变样式;在操作 DOM 前,先使用
display: none
隐藏元素,操作完成后再显示,减少回流对性能的影响。 - 虚拟 DOM 优化:合理使用 Vue 等框架提供的特性,如利用
key
属性帮助 Diff 算法更高效地更新 DOM。确保key
值唯一且稳定,避免不必要的 DOM 重新创建和销毁。
- 减少重绘和回流:避免频繁修改 DOM 样式和结构。例如,不要在循环中多次修改元素的样式,而是一次性修改 class 来改变样式;在操作 DOM 前,先使用
- 预渲染:使用工具(如 prerender - spa - plugin)对页面进行预渲染,在构建阶段生成静态 HTML 文件。当用户首次访问时,直接展示预渲染的 HTML 内容,然后再加载 JavaScript 进行交互,加快首屏展示速度。
网络及服务器优化
- 优化网络请求:
- 合并请求:将多个小的 HTTP 请求合并为一个,减少请求开销。例如,将多个 CSS 文件或 JavaScript 文件合并成一个文件加载(但要注意控制文件大小,避免加载时间过长 )。
- 优化请求顺序:优先加载关键资源,如首屏展示所需的 CSS 和 JavaScript 文件,确保页面能尽快渲染出内容。可以通过设置资源的
async
(异步加载,不阻塞页面渲染 )、defer
(延迟到 HTML 解析完成后加载 )属性,或者使用 HTTP/2 协议(支持多路复用,可并行传输多个资源,提高传输效率 )来优化请求顺序。
- 提升服务器性能:
- 选择优质服务器:选择性能好、带宽充足的服务器,确保服务器能够快速响应客户端请求。
- 服务器端代码优化:优化服务器端的业务逻辑代码,提高数据处理和响应速度。例如,对数据库查询进行优化,合理使用缓存(如 Redis 缓存数据 ),减少数据库压力,加快数据返回速度。
一、什么是首屏加载
首屏时间(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,
};
#二、加载慢的原因
在页面渲染的过程,导致加载速度慢的因素可能如下:
- 网络延时问题
- 资源文件体积是否过大
- 资源是否重复发送请求去加载了
- 加载脚本的时候,渲染内容堵塞了
优化方向 | 具体方法 | 说明 |
---|---|---|
减小入口文件体积 | 路由懒加载 | 将路由组件分割,按需加载,缩小入口文件 |
静态资源本地缓存 | HTTP 缓存 Service Worker 离线缓存 前端利用 localStorage | 设置相关响应头缓存资源 拦截请求,优先用缓存资源 存储不常变数据,减少后端请求 |
UI 框架按需加载 | 只引入实际使用组件 | 避免引入整个 UI 库,减少代码体积 |
图片资源压缩 | 图片压缩工具 使用字体图标或雪碧图 | 减小图片文件大小 减少 HTTP 请求数量 |
解决组件重复打包 | 调整 Webpack 配置 | 抽离多次使用的包,避免重复加载 |
开启 GZip 压缩 | 前端配置 compression - webpack - plugin 服务器配置 compression 中间件 | 前后端配合,压缩传输文件,加快速度 |
页面渲染优化 | 使用 SSR(如 Nuxt.js) 优化渲染逻辑 预渲染 | 服务器生成 HTML,加快首屏展示 减少重绘回流,合理用虚拟 DOM 构建时生成静态 HTML,加快首次访问 |
网络及服务器优化 | 优化网络请求 提升服务器性能 | 合并、优化请求顺序 选优质服务器,优化服务器端代码 |
为什么data属性是一个函数而不是一个对象?
- Vue 实例与组件定义 data 的差异
- Vue 实例:在定义 Vue 实例时,
data
属性既可以是对象,也可以是函数。例如:
- Vue 实例:在定义 Vue 实例时,
const app = new Vue({el: "#app",// 对象格式data: {foo: "foo"},// 函数格式data() {return {foo: "foo"}}
})
- 组件:在组件中定义
data
属性时,只能是函数。若直接定义为对象,如:
Vue.component('component1', {template: `<div>组件</div>`,data: {foo: "foo"}
})
会收到警告,提示返回的data
应该是一个函数,用于每个组件实例。
2. 组件 data 定义为函数与对象的区别
- 对象形式的问题:当以对象形式(vue实例)定义组件的
data
时,多个组件实例会共用同一个data
对象。例如:
function Component() { }
Component.prototype.data = {count: 0
};
const componentA = new Component();
const componentB = new Component();
console.log(componentB.data.count); // 0
componentA.data.count = 1;
console.log(componentB.data.count); // 1
这是因为它们共用了相同的内存地址,导致componentA
修改数据影响到componentB
。
- 函数形式的优势:以函数形式定义
data
,每个实例都会得到一个新的data
对象。例如:
function Component() {this.data = this.data();
}
Component.prototype.data = function () {return {count: 0};
};
const componentA = new Component();
const componentB = new Component();
console.log(componentB.data.count); // 0
componentA.data.count = 1;
console.log(componentB.data.count); // 0
在 Vue 中,组件可能有多个实例,使用函数返回全新data
对象,可避免实例间数据污染。
3. 原理分析
initData
对 data 的处理:在 Vue 源码/vue - dev/src/core/instance/state.js
中的initData
函数里,data
既可以是对象也可以是函数。
function initData (vm: Component) {let data = vm.$options.data;data = vm._data = typeof data === 'function'? getData(data, vm): data || {};//...
}
- 选项合并与数据校验:
组件创建时会进行选项合并,在/vue - dev/src/core/util/options.js
中,自定义组件会进入mergeOptions
。在/vue - dev/src/core/instance/init.js
中对data
进行校验,当vm
实例为undefined
时,如果data
不是函数类型,在非生产环境下会发出警告。
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);
};
4.总结
- 根实例:根实例对象的
data
可以是对象也可以是函数,因为根实例是单例,不存在多个实例共用data
导致数据污染的问题。 - 组件实例:组件实例对象的
data
必须为函数。这样在initData
时,每个组件实例将函数作为工厂函数,都会返回全新的data
对象,从而防止多个组件实例之间共用一个data
而产生数据污染。
vue3有了解过吗?能说说跟vue2的区别吗?
一、Vue3介绍
关于vue3
的重构背景,尤大是这样说的:
「Vue 新版本的理念成型于 2018 年末,当时 Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久,但在这段时期,前端世界已经今昔非比了
在我们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题」
简要就是:
- 利用新的语言特性(es6)
- 解决架构问题
哪些变化
从上图中,我们可以概览Vue3
的新特性,如下:
- 速度更快
- 体积减少
- 更易维护
- 更接近原生
- 更易使用
一、性能与体积
- 速度更快
- 虚拟 DOM 重写:Vue 3 对虚拟 DOM 进行了重写,优化了其实现方式,使得虚拟 DOM 的比对和更新更加高效,从而提升整体渲染性能。
- 编译模板优化:通过对编译模板过程的改进,生成更高效的渲染代码,减少不必要的计算。
- 组件初始化优化:更高效的组件初始化过程,减少初始化时间。这些改进使得 Vue 3 的 update 性能提高 1.3 - 2 倍,SSR(服务器端渲染)速度提高 2 - 3 倍。
- 体积更小:借助 webpack 的 tree - shaking 功能,Vue 3 能够将未使用的模块去除,仅打包实际需要的部分。这对开发者而言,可以在不担忧整体体积大幅增加的情况下,为 Vue 添加更多功能;对于使用者,最终打包出来的文件体积变小,加载速度更快。
二、维护与开发体验
- 更易维护
- Composition API:
- 与 Options API 兼容:可与现有的 Options API 一起使用,开发者可以根据实际情况灵活选择使用方式。
- 逻辑组合复用:方便将相关逻辑进行组合与复用,例如将数据逻辑、生命周期钩子逻辑等按照功能进行集中管理,提高代码的可维护性和复用性。不同功能的逻辑可以独立开发和复用,使得代码结构更清晰。
- 框架搭配灵活:Vue 3 模块可以和其他框架搭配使用,拓展了 Vue 的应用场景。
- 更好的 TypeScript 支持:Vue 3 基于 TypeScript 编写,开发者能享受到自动的类型定义提示。这在开发大型项目时,有助于提前发现类型错误,提高代码的稳定性和可维护性,减少潜在的运行时错误。
- 编译器重写:重写编译器进一步优化了代码生成和编译过程,使得 Vue 的编译效率更高,生成的代码质量更好,从底层提升了框架的性能和可维护性。
- Composition API:
- 更接近原生:Vue 3 可以自定义渲染 API,开发者能够将 Vue 的开发模型扩展到其他平台,如将其渲染到 canvas 画布上,为 Vue 的应用拓展了更多可能性,使其能够更好地与原生平台特性相结合。
- 更易使用
- 响应式 Api 暴露:响应式 API 更加直观地暴露出来,开发者可以更方便地使用响应式系统,对数据的响应式处理更加灵活和直接。
- 渲染原因识别:轻松识别组件重新渲染原因,方便开发者调试和优化代码,快速定位性能问题。
三、新增特性
特性名称 | 描述 | 示例 |
---|---|---|
Fragments | 组件支持多个根节点 | <!-- Layout.vue --> <template> <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer> </template> |
Teleport 任意门) | 一种能够将我们的模板移动到 | <button @click="showToast" class="btn">打开 toast</button> <!-- to 属性就是目标位置 --> <teleport to="#teleport-target"> <div v-if="visible" class="toast-wrap"> <div class="toast-msg">我是一个 Toast 文案</div> </div> </teleport> |
createRenderer | 构建自定义渲染器,拓展 Vue 到其他平台 | import { createRenderer } from '@vue/runtime-core' const { render, createApp } = createRenderer({ export { render, createApp } export * from '@vue/runtime-core' |
Composition API | 组合式 | ![]() |
四、 非兼容变更
Global API
对比项 | Vue 2 | Vue 3 |
---|---|---|
全局 API 使用方式 | 旧的全局 API 使用方式 | 使用应用程序实例, 全局和内部 |
模板指令
对比项 | Vue 2 | Vue 3 |
---|---|---|
v - model 用法 | 旧的组件 v - model 用法 | 组件上 v - model 用法更改 |
key 用法 | <template v - for> 和非 v - for 节点上 key 旧用法 | <template v - for> 和非 v - for 节点上 key 用法更改 |
v - if 和 v - for 优先级 | v - for 优先级高于 v - if | v - if 优先级高于 v - for |
v - bind="object" | 排序不敏感 | 排序敏感 |
v - for 中的 ref | 注册 ref 数组 | 不再注册 ref 数组 |
组件
对比项 | Vue 2 | Vue 3 |
---|---|---|
功能组件创建 | 多种方式创建功能组件 | 只能使用普通函数创建功能组件,functional 属性在 SFC 中有不同用法 |
异步组件创建 | 旧的异步组件创建方式 | 需使用 defineAsyncComponent 方法创建异步组件 |
渲染函数
对比项 | Vue 2 | Vue 3 |
---|---|---|
API | 旧的渲染函数 API | 渲染函数 API 改变 |
插槽访问 | 通过 $scopedSlots 访问特定插槽 | 删除,所有插槽通过slots 作为函数暴露 |
自定义指令 API | 旧的自定义指令 API | 自定义指令 API 更改,与组件生命周期一致 |
一些转换 | v - enter、v - leave 等 | v - enter -> v - enter - from v - leave -> v - leave - from |
watch 用法 | 支持点分隔字符串路径监听 | 不再支持点分隔字符串路径,改用计算函数作为参数 |
其他小改变
对比项 | Vue 2 | Vue 3 |
---|---|---|
生命周期选项 | destroyed、beforeDestroy | unmounted(原 destroyed) beforeUnmount(原 beforeDestroy) |
data 声明 | 可声明为对象或函数(组件中推荐函数) | 应始终声明为函数 |
mixin 的 data 选项合并 | 旧的合并方式 | 简单合并 |
attribute 强制策略 | 旧策略 | 策略更改 |
template 渲染 | 无特殊指令时渲染内部内容 | 无特殊指令时视为普通元素,生成原生<template> 元素 |
根容器渲染 | 应用根容器 outerHTML 替换为根组件模板 | 应用容器 innerHTML 用于渲染,容器本身不再视为模板一部分 |
五、 移除 API
移除的 API | 描述 |
---|---|
keyCode 支持 | 不再支持将 keyCode 作为 v - on 的修饰符 |
,off,$once 实例方法 | 移除这三个用于事件处理的实例方法 |
过滤 filter | 移除过滤器功能 |
内联模板 attribute | 不再支持内联模板 attribute |
$destroy 实例方法 | 用户不应再手动管理单个 |
Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?
对比维度 | Options API | Composition API |
---|---|---|
定义方式 | 在.vue 文件中,通过定义data 、computed 、methods 、watch 等属性与方法来组织页面逻辑 | 基于逻辑功能组织组件,将一个功能涉及的所有 API 放在一起,以函数形式进行封装和复用 |
逻辑组织 |
|
|
逻辑复用 |
|
|
类型推断 | 对 TypeScript 支持有限,随着组件复杂度增加,类型声明和推断变得困难 | 由于多以函数形式存在,有更好的类型推断,在使用 TypeScript 时更友好 |
Tree - shaking 友好度 | 不太友好,整个组件的选项内容都会被打包,即使部分逻辑未使用 | 对 Tree - shaking 友好,可按需引入所需功能函数,未使用的函数不会被打包,利于代码压缩 |
this 使用 | 频繁使用this 来访问组件实例的属性和方法,容易出现this 指向不明问题,尤其在箭头函数或复杂作用域嵌套中 | 几乎不见this 的使用,避免了this 指向带来的潜在问题,代码逻辑更清晰 |
适用场景 | 小型组件中使用简单直观,代码结构清晰,开发成本低 | 适用于大型复杂组件,能有效组织复杂逻辑,提高代码复用性和可维护性 |
Vue3.0的设计目标是什么?做了哪些优化
设计目标
- 解决实际业务痛点
- 复杂组件维护困难:随着功能增加,Vue2 中复杂组件代码维护难度增大,Vue3 旨在改善这一状况。
- 逻辑提取与复用机制缺失:缺少简洁有效的在多个组件间提取和复用逻辑的机制,Vue3 期望提供更好的解决方案。
- 类型推断不友好:Vue2 对类型推断支持不足,Vue3 致力于改善以满足现代前端开发需求。
- bundle 时间过长:优化打包过程,缩短 bundle 时间,提升开发效率。
- 具体目标
- 更小:精简体积,移除不常用 API,利用 tree - shaking 技术,仅打包实际需要的模块,减小整体体积。
- 更快:着重在编译层面进行优化,如优化 diff 算法、实现静态提升、缓存事件监听以及优化 SSR 等,提升性能。
- 更友好:兼顾 Vue2 的 Options API,推出 Composition API,增强代码的逻辑组织和复用能力;基于 TypeScript 编写,提供自动类型定义提示。
Vue3.0 的优化方案
- 源码层面
- 源码管理:采用 monorepo 方式维护,将不同功能模块拆分到
packages
目录下的不同子目录。优点是模块拆分细化,职责明确,依赖关系清晰,提高代码可维护性,部分模块(如reactivity
响应式库)可独立于 Vue 使用。 - TypeScript:基于 TypeScript 编写,提供更好的类型检查,支持复杂类型推导,增强代码的健壮性和可维护性。
- 源码管理:采用 monorepo 方式维护,将不同功能模块拆分到
- 性能层面
- 体积优化:移除不常用 API 并结合 tree - shaking,减小打包体积。
- 编译优化:包括 diff 算法优化(添加静态标记提升对比效率)、静态提升(不参与更新的元素只创建一次并复用)、事件监听缓存(缓存事件处理函数减少重复操作)、SSR 优化(静态内容量大时优化生成静态节点方式)。
- 数据劫持优化:Vue2 使用
Object.defineProperty
存在缺陷,如无法检测对象属性添加和删除,嵌套层级深时性能问题突出。Vue3 采用Proxy
监听整个对象,能检测属性的添加、删除,在getter
中递归实现响应式,仅对真正访问到的内部对象进行响应式处理,提升性能并减轻用户心智负担。
- 语法 API 层面
- 优化逻辑组织:Composition API 使相同功能代码集中编写,相较于 Options API,逻辑结构更清晰,便于理解和维护。
- 优化逻辑复用:Vue2 通过 mixin 实现功能混合存在命名冲突和数据来源不清晰问题。Vue3 的 Composition API 可将复用代码抽离为函数,使用时直接调用,数据来源清晰,有效避免命名冲突。
Vue3.0性能提升主要是通过哪几方面体现的?
优化维度 | 具体优化点 | 优化详情 | 性能提升表现 |
---|---|---|---|
编译阶段 | diff 算法优化 |
已经标记静态节点的 | 减少不必要比较操作,提高 diff 效率 |
静态提升 | 不参与更新的元素只创建一次,在渲染时复用,且标记为不参与 Diff | 避免重复创建节点,优化运行时内存占用,大型应用受益明显 | |
事件监听缓存 | 默认事件绑定视为动态,开启缓存后事件处理函数缓存,diff 时直接使用 | 减少事件处理的重复操作 | |
SSR 优化 | 静态内容量大时,用 createStaticVNode 在客户端生成静态 node,直接 innerHTML 插入 | 减少对象创建和渲染开销,加快 SSR 页面生成和传输速度 | |
源码体积 | Tree - shaking | 未使用模块不打包,仅打包实际用到的模块 | 打包体积变小,加载速度加快,提升用户体验 |
响应式系统 | 实现方式改变 | Vue 2 用 Object.defineProperty 需深度遍历,Vue 3 用 Proxy 直接监听整个对象
|
|
优势体现 | 更全面及时监听数据变化,优化响应式机制 |
Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?
Object.defineProperty 实现响应式原理
- 基本定义与用途:
Object.defineProperty()
是 JavaScript 中用于在对象上定义新属性,或者修改现有属性的方法,调用后会返回该对象。它对于实现数据的响应式非常关键。 get
和set
方法:
get
方法:它是属性的访问器函数。当代码尝试访问对象被Object.defineProperty
定义的属性时,get
函数就会被调用。它不接收显式传入的参数,但内部的this
会指向访问该属性时所在的对象(不过由于继承关系,this
不一定是定义该属性的对象)。get
函数的返回值就是该属性被访问时返回的值。例如在以下代码中:function defineReactive(obj, key, val) {Object.defineProperty(obj, key, {get() {console.log(`get ${key}:${val}`);return val},//...}) }
当访问
obj
的key
属性时,get
函数执行,打印日志并返回val
。
set
方法:是属性的设置器函数。当属性值被修改时,set
函数会被调用,它接收一个参数,即被赋予的新值,同时内部的this
指向赋值时的对象。在示例代码中:function defineReactive(obj, key, val) {Object.defineProperty(obj, key, {//...set(newVal) {if (newVal!== val) {val = newValupdate()}}}) }
当
obj
的key
属性值被改变时,set
函数执行,先判断新值与旧值是否不同,若不同则更新val
并调用update
函数,这里的update
函数可用于触发视图更新等操作,从而实现数据的响应式。
- 对象多属性与嵌套对象处理:
- 多属性遍历:当对象存在多个属性时,需要遍历对象的所有属性,并对每个属性都使用
Object.defineProperty
来定义,以实现所有属性的响应式。如:
- 多属性遍历:当对象存在多个属性时,需要遍历对象的所有属性,并对每个属性都使用
function observe(obj) {if (typeof obj!== 'object' || obj == null) {return}Object.keys(obj).forEach(key => {defineReactive(obj, key, obj[key])})
}
这段代码通过 Object.keys
获取对象的所有键,然后对每个键值对调用 defineReactive
函数。
- 嵌套对象递归处理:对于嵌套对象,不仅要对最外层对象的属性进行响应式定义,还要递归处理内部嵌套的对象。在
defineReactive
函数中,会对传入的val
进行判断,如果val
是对象,则再次调用observe
函数,对其进行递归处理,以确保嵌套对象的属性也能实现响应式。
function defineReactive(obj, key, val) {observe(val)Object.defineProperty(obj, key, {//...})
}
- 存在的问题:
- 属性添加与删除检测问题:
Object.defineProperty
无法检测对象属性的添加和删除操作。例如:
- 属性添加与删除检测问题:
const obj = {foo: "foo",bar: "bar"
}
observe(obj)
delete obj.foo // 无法被劫持
obj.jar = 'xxx' // 无法被劫持
即使对 obj
进行了响应式处理,删除 foo
属性或添加 jar
属性时,无法触发相应的更新操作。
- 数组监听问题:直接使用
Object.defineProperty
监听数组时,数组的大部分 API 方法(如push
、pop
、shift
、unshift
等)无法被监听到。如:
const arrData = [1, 2, 3, 4, 5];
arrData.forEach((val, index) => {defineProperty(arrData, index, val)
})
arrData.push() // 无法被劫持
arrData.pop() // 无法被劫持
arrData[0] = 99 // 可以劫持,因为直接修改索引值触发了 `set`
虽然可以通过遍历数组的每个元素,使用 Object.defineProperty
对每个元素进行定义来劫持单个元素的变化,但数组的方法调用不会触发更新。
- 性能问题:对于深层嵌套对象,需要进行深层的递归监听,随着嵌套深度的增加,性能开销会急剧增大,因为每次都要递归遍历对象的所有属性来设置响应式。
Proxy 实现响应式原理
- 整体监听优势:
Proxy
可以直接对整个对象进行监听,而不是像Object.defineProperty
那样逐个属性处理。它创建一个代理对象,该代理对象对目标对象的所有操作进行拦截。例如:
function reactive(obj) {if (typeof obj!== 'object' && obj!= null) {return obj}const observed = new Proxy(obj, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver)return res},set(target, key, value, receiver) {const res = Reflect.set(target, key, value, receiver)return res},deleteProperty(target, key) {const res = Reflect.deleteProperty(target, key)return res}})return observed
}
这里通过 Proxy
创建了一个对 obj
的代理对象 observed
,对 obj
的 get
、set
和 deleteProperty
操作都进行了拦截,并使用 Reflect
对象来执行实际的操作。
- 简单数据操作劫持:对于简单数据操作,
Proxy
能够很好地劫持各种操作。如:
const state = reactive({foo: 'foo'
})
// 1.获取
state.foo // 可以劫持获取操作
// 2.设置已存在属性
state.foo = 'fooooooo' // 可以劫持设置操作
// 3.设置不存在属性
state.dong = 'dong' // 可以劫持添加属性操作
// 4.删除属性
delete state.dong // 可以劫持删除属性操作
- 嵌套对象处理:最初,
Proxy
对嵌套对象内部属性的设置无法直接劫持,如:
const state = reactive({bar: { a: 1 }
})
state.bar.a = 10 // 无法直接劫持
为解决这个问题,需要在 get
方法中对返回值进行判断,如果是对象则再次调用 reactive
进行代理,即:
function reactive(obj) {if (typeof obj!== 'object' && obj!= null) {return obj}const observed = new Proxy(obj, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver)return isObject(res)? reactive(res) : res},})return observed
}
这样就可以对嵌套对象内部属性的变化进行劫持。
- 数组监听:
Proxy
可以直接监听数组的变化,包括push
、shift
、splice
等操作。例如:
const obj = [1, 2, 3]
const proxyObj = reactive(obj)
obj.push(4) // 可以被劫持
- 丰富的拦截方法:
Proxy
拥有多达 13 种拦截方法,比如apply
、ownKeys
、deleteProperty
、has
等。这些丰富的拦截方法使开发者能够更全面地控制对象的各种操作,而Object.defineProperty
仅通过get
和set
来控制属性访问和赋值。
总结两者差异
- 劫持方式:
Object.defineProperty
需要遍历对象的每个属性来进行劫持,对于嵌套对象还需深层递归,操作较为繁琐。而Proxy
直接对整个对象进行劫持,并返回一个新的代理对象,通过操作代理对象实现响应式,更为直接和高效。 - 功能完整性:
Object.defineProperty
存在检测不到对象属性添加和删除、数组 API 方法监听困难等问题,导致在 Vue2 中需要额外实现set
、delete
API 以及重写数组方法来弥补这些缺陷。而Proxy
能够直接监听对象属性的添加、删除以及数组的各种操作,功能更加完整。 - 兼容性:
Object.defineProperty
能支持到 IE9,而Proxy
不兼容 IE,并且没有 polyfill。尽管Proxy
存在兼容性问题,但由于其在实现响应式方面的显著优势,在 Vue3.0 中被用于替代Object.defineProperty
来实现响应式系统。
说说Vue 3.0中Treeshaking特性?举例说明一下?
对比项 | 详情 |
---|---|
特性定义 | Tree - shaking 是一种通过清除多余代码优化项目打包体积的技术,即 Dead code elimination,在保持代码运行结果不变的前提下,去除无用代码 |
Vue 2 与 Vue 3 对比 | Vue 2 的 Vue 3 引入 |
实现原理 | 基于 ES6 模块语法(import 与exports ),利用 ES6 模块静态编译思想,编译阶段确定模块依赖关系及输入输出变量,判断哪些模块已加载以及哪些模块和变量未被使用或引用,删除对应代码 |
示例 - Vue 2 项目 | 简单使用data 属性:<script> export default { data: () => ({ count: 1, }), }; </script> ,打包记录体积增加 computed 和watch 属性:export default { data: () => ({ question:"", count: 1, }), computed: { double: function () { return this.count * 2; }, }, watch: { question: function (newQuestion, oldQuestion) { this.answer = 'xxxx' } }; ,再次打包体积无变化,表明未使用代码仍被打包 |
示例 - Vue 3 项目 | 简单使用reactive :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, }; } }); ,再次打包体积变大,体现 Tree - shaking 特性,按需打包 |
特性作用 | 减少程序体积(更小), 减少程序执行时间(更快), 便于将来对程序架构进行优化(更友好) |