Vue项目性能优化-详细
Vue
框架通过数据双向绑定和虚拟DOM
技术,帮我们处理了前端开发中最脏最累的DOM
操作部分, 我们不再需要去考虑如何操作DOM
以及如何最高效地操作DOM
;但Vue
项目中仍然存在项目首屏优化、Webpack
编译配置优化等问题,所以我们仍然需要去关注Vue
项目性能方面的优化,使项目具有更高效的性能、更好的用户体验
代码层面的优化
1. v-if 和 v-show 区分使用场景
v-if
是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块v-show
就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS
display
的none/block
属性进行切换。- 所以,
v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show
则适用于需要非常频繁切换条件的场景
2. computed 和 watch 区分使用场景
computed
: 是计算属性,依赖其它属性值,并且computed
的值有缓存,只有它依赖的属性值发生改变,下一次获取computed
的值时才会重新计算 computed 的值;watch
: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用
computed
,因为可以利用computed
的缓存特性,避免每次获取值时,都要重新计算; - 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用
watch
,使用watch
选项允许我们执行异步操作 ( 访问一个API
),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的
3. v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
v-for
遍历必须为item
添加key
- 在列表数据进行遍历渲染时,需要为每一项
item
设置唯一key
值,方便Vue.js
内部机制精准找到该条列表数据。当state
更新时,新的状态值和旧的状态值对比,较快地定位到diff
- 在列表数据进行遍历渲染时,需要为每一项
v-for
遍历避免同时使用v-if
vue2.x
中v-for
比v-if
优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成computed
属性
推荐:
代码语言:javascript复制<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
不推荐:
代码语言:html复制<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
</li>
</ul>
4. 长列表性能优化
代码语言:javascript复制
Vue
会通过Object.defineProperty
对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止Vue
劫持我们的数据呢?可以通过Object.freeze
方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
5. 事件的销毁
Vue
组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js
内使用 addEventListener
等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
created() {
addEventListener('click', this.click, false)
},
beforeDestroy() {
removeEventListener('click', this.click, false)
}
6. 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue
的 vue-lazyload
插件
npm install vue-lazyload --save-dev
在入口文件 man.js
中引入并使用
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 或者添加自定义选项
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})
在 vue
文件中将 img
标签的 src
属性直接改为 v-lazy
,从而将图片显示方式更改为懒加载显示
<img v-lazy="/static/img/1.png">
以上为 vue-lazyload
插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址(opens new window)
7. 路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak
打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来
路由懒加载:
代码语言:javascript复制const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
8. 第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component
,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui
组件库为例
npm install babel-plugin-component -D
将 .babelrc
修改为:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
在 main.js
中引入部分组件:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)
9. 优化无限列表性能
如果你的应用存在非常长或者无限滚动的列表,那么需要采用虚拟列表
的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom
节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window)来优化这种无限列表的场景的
10. 服务端渲染 SSR or 预渲染
服务端渲染是指 Vue
在客户端将标签渲染成的整个 html
片段的工作在服务端完成,服务端形成的 html
片段直接返回给客户端这个过程就叫做服务端渲染。
- 如果你的项目的
SEO
和首屏渲染
是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和SEO
- 如果你的
Vue
项目只需改善少数营销页面(例如/
,/about
,/contact
等)的SEO
,那么你可能需要预渲染,在构建时简单地生成针对特定路由的静态HTML
文件。 优点是设置预渲染更简单 ,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin (opens new window) 就可以轻松地添加预渲染
Webpack 层面的优化
1. Webpack 对图片进行压缩
对小于 limit
的图片转化为 base64
格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader
来压缩图片
npm install image-webpack-loader --save-dev
代码语言:javascript复制{
test: /.(png|jpe?g|gif|svg)(?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
2. 减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码
代码语言:javascript复制class HelloWebpack extends Component{...}
这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
代码语言:javascript复制babel-runtime/helpers/createClass // 用于实现 class 语法
babel-runtime/helpers/inherits // 用于实现 extends 语法
在默认情况下, Babel
会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass')
的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime
插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小
npm install babel-plugin-transform-runtime --save-dev
修改 .babelrc
配置文件为:
"plugins": [
"transform-runtime"
]
3. 提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
- 相同的资源被重复加载,浪费用户的流量和服务器的成本。
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack
内置了专门用于提取多个Chunk
中的公共部分的插件 CommonsChunkPlugin
,我们在项目中 CommonsChunkPlugin
的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module, count) {
return (
module.resource &&
/.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
);
}
}),
// 抽取出代码模块的映射关系
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
4. 模板预编译
- 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
- 预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
- 如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数
5. 提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存
6. 优化 SourceMap
我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap
出现了,它就是为了解决不好调式代码问题的
SourceMap
的可选值如下(号越多,代表速度越快,
-
号越多,代表速度越慢,o
代表中等速度)
- 开发环境推荐:
cheap-module-eval-source-map
- 生产环境推荐:
cheap-module-source-map
原因如下:
cheap
: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加cheap
的基本类型来忽略打包前后的列信息;module
:不管是开发环境还是正式环境,我们都希望能定位到bug
的源代码具体的位置,比如说某个Vue
文件报错了,我们希望能定位到具体的Vue
文件,因此我们也需要module
配置;soure-map
:source-map
会为每一个打包后的模块生成独立的soucemap
文件 ,因此我们需要增加source-map
属性;eval-source-map
:eval
打包代码的速度非常快,因为它不生成map
文件,但是可以对eval
组合使用eval-source-map
使用会将map
文件以DataURL
的形式存在打包后的js
文件中。在正式环境中不要使用eval-source-map
, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。
7. 构建结果输出分析
Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
执行 $ npm run build --report
后生成分析报告如下
基础的 Web 技术优化
1. 开启 gzip 压缩
gzip
是GNUzip
的缩写,最早用于UNIX
系统的文件压缩。HTTP
协议上的gzip
编码是一种用来改进web
应用程序性能的技术,web
服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip
压缩效率非常高,通常可以达到70%
的压缩率,也就是说,如果你的网页有30K
,压缩之后就变成了9K
左右
以下我们以服务端使用我们熟悉的 express
为例,开启 gzip
非常简单,相关步骤如下:
npm install compression --save
代码语言:javascript复制var compression = require('compression');
var app = express();
app.use(compression())
重启服务,观察网络面板里面的 response header
,如果看到如下红圈里的字段则表明 gzip
开启成功
Nginx开启gzip压缩
代码语言:shell复制#是否启动gzip压缩,on代表启动,off代表开启
gzip on;
#需要压缩的常见静态资源
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
#由于nginx的压缩发生在浏览器端而微软的ie6很坑爹,会导致压缩后图片看不见所以该选
项是禁止ie6发生压缩
gzip_disable "MSIE [1-6].";
#如果文件大于1k就启动压缩
gzip_min_length 1k;
#以16k为单位,按照原始数据的大小以4倍的方式申请内存空间,一般此项不要修改
gzip_buffers 4 16k;
#压缩的等级,数字选择范围是1-9,数字越小压缩的速度越快,消耗cpu就越大
gzip_comp_level 2;
要想配置生效,记得重启nginx
服务
nginx -t
nginx -s reload
2. 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)
3. CDN 的使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率
4. 使用 Chrome Performance 查找性能瓶颈
Chrome
的 Performance
面板可以录制一段时间内的 js
执行细节及时间。使用 Chrome
开发者工具分析页面性能的步骤如下。
- 打开
Chrome
开发者工具,切换到Performance
面板 - 点击
Record
开始录制 - 刷新页面或展开某个节点
- 点击
Stop
停止录制
Vue3有了解过吗?能说说跟vue2的区别吗?
1. 哪些变化
从上图中,我们可以概览Vue3
的新特性,如下:
- 速度更快
- 体积减少
- 更易维护
- 更接近原生
- 更易使用
1.1 速度更快
vue3
相比vue2
- 重写了虚拟
Dom
实现 - 编译模板的优化
- 更高效的组件初始化
undate
性能提高1.3~2倍SSR
速度提高了2~3倍
1.2 体积更小
通过webpack
的tree-shaking
功能,可以将无用模块“剪辑”,仅打包需要的
能够tree-shaking
,有两大好处:
- 对开发人员,能够对
vue
实现更多其他的功能,而不必担忧整体体积过大 - 对使用者,打包出来的包体积变小了
vue
可以开发出更多其他的功能,而不必担忧vue
打包出来的整体体积过多
1.3 更易维护
compositon Api
- 可与现有的
Options API
一起使用 - 灵活的逻辑组合与复用
Vue3
模块可以和其他框架搭配使用
更好的Typescript支持
VUE3
是基于typescipt
编写的,可以享受到自动的类型定义提示
1.4 编译器重写
1.5 更接近原生
可以自定义渲染 API
1.6 更易使用
响应式 Api
暴露出来
轻松识别组件重新渲染原因
2. Vue3新增特性
Vue 3 中需要关注的一些新功能包括:
framents
Teleport
composition Api
createRenderer
2.1 framents
在 Vue3.x
中,组件现在支持有多个根节点
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
2.2 Teleport
Teleport
是一种能够将我们的模板移动到 DOM
中 Vue app
之外的其他位置的技术,就有点像哆啦A梦的“任意门”
在vue2
中,像 modals
,toast
等这样的元素,如果我们嵌套在 Vue
的某个组件内部,那么处理嵌套组件的定位、z-index
和样式就会变得很困难
通过Teleport
,我们可以在组件的逻辑位置写模板代码,然后在 Vue
应用范围之外渲染它
<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>
2.3 createRenderer
通过createRenderer
,我们能够构建自定义渲染器,我们能够将 vue
的开发模型扩展到其他平台
我们可以将其生成在canvas
画布上
关于createRenderer
,我们了解下基本使用,就不展开讲述了
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement,
// ...
})
export { render, createApp }
export * from '@vue/runtime-core'
2.4 composition Api
composition Api,也就是组合式api
,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理
关于compositon api
的使用,这里以下图展开
简单使用:
代码语言:javascript复制export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value
}
onMounted(() => console.log('component mounted!'))
return {
count,
double,
increment
}
}
}
3. 非兼容变更
3.1 Global API
- 全局
Vue API
已更改为使用应用程序实例 - 全局和内部
API
已经被重构为可tree-shakable
3.2 模板指令
- 组件上
v-model
用法已更改 <template v-for>
和 非v-for
节点上key
用法已更改- 在同一元素上使用的
v-if
和v-for
优先级已更改 v-bind="object"
现在排序敏感v-for
中的ref
不再注册ref
数组
3.3 组件
- 只能使用普通函数创建功能组件
functional
属性在单文件组件(SFC)
- 异步组件现在需要
defineAsyncComponent
方法来创建
3.4 渲染函数
- 渲染函数
API
改变 $scopedSlots
property 已删除,所有插槽都通过$slots
作为函数暴露- 自定义指令 API 已更改为与组件生命周期一致
- 一些转换
class
被重命名了:v-enter
->v-enter-from
v-leave
->v-leave-from
- 组件
watch
选项和实例方法$watch
不再支持点分隔字符串路径,请改用计算函数作为参数 - 在
Vue 2.x
中,应用根容器的outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x
现在使用应用程序容器的innerHTML
。
3.5 其他小改变
destroyed
生命周期选项被重命名为unmounted
beforeDestroy
生命周期选项被重命名为beforeUnmount
[prop default
工厂函数不再有权访问this
是上下文- 自定义指令 API 已更改为与组件生命周期一致
data
应始终声明为函数- 来自
mixin
的data
选项现在可简单地合并 attribute
强制策略已更改- 一些过渡
class
被重命名 - 组建 watch 选项和实例方法
$watch
不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。 <template>
没有特殊指令的标记 (v-if/else-if/else
、v-for
或v-slot
) 现在被视为普通元素,并将生成原生的<template>
元素,而不是渲染其内部内容。- 在
Vue 2.x
中,应用根容器的outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x
现在使用应用容器的innerHTML
,这意味着容器本身不再被视为模板的一部分。
3.6 移除 API
keyCode
支持作为v-on
的修饰符$on
,$off
和$once
实例方法- 过滤
filter
- 内联模板
attribute
$destroy
实例方法。用户不应再手动管理单个Vue
组件的生命周期。
Vue路由hash模式和history模式
1. hash
模式
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash
的值就是 URL
中 #
后面的内容。比如下面这个网站,它的 location.hash
的值为 '#search'
https://interview2.poetries.top#search
hash 路由模式的实现主要是基于下面几个特性
URL
中hash
值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash
部分不会被发送;hash
值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash
的切换;- 可以通过
a
标签,并设置href
属性,当用户点击这个标签后,URL
的hash
值会发生改变;或者使用JavaScript
来对loaction.hash
进行赋值,改变URL
的hash
值; - 我们可以使用
hashchange
事件来监听hash
值的变化,从而对页面进行跳转(渲染)
window.addEventListener("hashchange", funcRef, false);
每一次改变 hash
(window.location.hash
),都会在浏览器的访问历史中增加一个记录利用 hash
的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点 :兼容性好但是不美观
2. history
模式
history
采用HTML5
的新特性;且提供了两个新方法: pushState()
, replaceState()
可以对浏览器历史记录栈进行修改,以及popState
事件的监听到状态变更
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL
改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 路由模式的实现主要基于存在下面几个特性:
pushState
和repalceState
两个API
来操作实现URL
的变化 ;- 我们可以使用
popstate
事件来监听url
的变化,从而对页面进行跳转(渲染); history.pushState()
或history.replaceState()
不会触发popstate
事件,这时我们需要手动触发页面跳转(渲染)。
特点 :虽然美观,但是刷新会出现 404
需要后端进行配置
Vue3.2 setup 语法糖汇总
提示:vue3.2
版本开始才能使用语法糖!
在 Vue3.0
中变量必须 return
出来, template
中才能使用;而在 Vue3.2
中只需要在 script
标签上加上 setup
属性,无需 return
, template
便可直接使用,非常的香啊!
1. 如何使用setup语法糖
只需在 script
标签上写上 setup
<template>
</template>
<script setup>
</script>
<style scoped lang="less">
</style>
2. data数据的使用
由于 setup
不需写 return
,所以直接声明数据即可
<script setup>
import {
ref,
reactive,
toRefs,
} from 'vue'
const data = reactive({
patternVisible: false,
debugVisible: false,
aboutExeVisible: false,
})
const content = ref('content')
//使用toRefs解构
const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)
</script>
3. method方法的使用
代码语言:html复制<template >
<button @click="onClickHelp">帮助</button>
</template>
<script setup>
import {reactive} from 'vue'
const data = reactive({
aboutExeVisible: false,
})
// 点击帮助
const onClickHelp = () => {
console.log(`帮助`)
data.aboutExeVisible = true
}
</script>
4. watchEffect的使用
代码语言:html复制<script setup>
import {
ref,
watchEffect,
} from 'vue'
let sum = ref(0)
watchEffect(()=>{
const x1 = sum.value
console.log('watchEffect所指定的回调执行了')
})
</script>
5. watch的使用
代码语言:html复制<script setup>
import {
reactive,
watch,
} from 'vue'
//数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
// 两种监听格式
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变了',newValue,oldValue)
},
{immediate:true}
)
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:true})
</script>
6. computed计算属性的使用
computed
计算属性有两种写法(简写和考虑读写的完整写法)
<script setup>
import {
reactive,
computed,
} from 'vue'
// 数据
let person = reactive({
firstName:'poetry',
lastName:'x'
})
// 计算属性简写
person.fullName = computed(()=>{
return person.firstName '-' person.lastName
})
// 完整写法
person.fullName = computed({
get(){
return person.firstName '-' person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
</script>
7. props父子传值的使用
父组件代码如下(示例):
代码语言:html复制<template>
<child :name='name'/>
</template>
<script setup>
import {ref} from 'vue'
// 引入子组件
import child from './child.vue'
let name= ref('poetry')
</script>
子组件代码如下(示例):
代码语言:html复制<template>
<span>{{props.name}}</span>
</template>
<script setup>
import { defineProps } from 'vue'
// 声明props
const props = defineProps({
name: {
type: String,
default: 'poetries'
}
})
// 或者
//const props = defineProps(['name'])
</script>
8. emit子父传值的使用
父组件代码如下(示例):
代码语言:html复制<template>
<AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
import { reactive } from 'vue'
// 导入子组件
import AdoutExe from '../components/AdoutExeCom'
const data = reactive({
aboutExeVisible: false,
})
// content组件ref
// 关于系统隐藏
const aboutExeHandleCancel = () => {
data.aboutExeVisible = false
}
</script>
子组件代码如下(示例):
代码语言:html复制<template>
<a-button @click="isOk">
确定
</a-button>
</template>
<script setup>
import { defineEmits } from 'vue';
// emit
const emit = defineEmits(['aboutExeVisible'])
/**
* 方法
*/
// 点击确定按钮
const isOk = () => {
emit('aboutExeVisible');
}
</script>
9. 获取子组件ref变量和defineExpose暴露
即vue2
中的获取子组件的ref
,直接在父组件中控制子组件方法和变量的方法
父组件代码如下(示例):
代码语言:html复制<template>
<button @click="onClickSetUp">点击</button>
<Content ref="content" />
</template>
<script setup>
import {ref} from 'vue'
// content组件ref
const content = ref('content')
// 点击设置
const onClickSetUp = ({ key }) => {
content.value.modelVisible = true
}
</script>
<style scoped lang="less">
</style>
子组件代码如下(示例):
代码语言:html复制<template>
<p>{{data }}</p>
</template>
<script setup>
import {
reactive,
toRefs
} from 'vue'
/**
* 数据部分
* */
const data = reactive({
modelVisible: false,
historyVisible: false,
reportVisible: false,
})
defineExpose({
...toRefs(data),
})
</script>
10. 路由useRoute和useRouter的使用
代码语言:html复制<script setup>
import { useRoute, useRouter } from 'vue-router'
// 声明
const route = useRoute()
const router = useRouter()
// 获取query
console.log(route.query)
// 获取params
console.log(route.params)
// 路由跳转
router.push({
path: `/index`
})
</script>
11. store仓库的使用
代码语言:html复制<script setup>
import { useStore } from 'vuex'
import { num } from '../store/index'
const store = useStore(num)
// 获取Vuex的state
console.log(store.state.number)
// 获取Vuex的getters
console.log(store.state.getNumber)
// 提交mutations
store.commit('fnName')
// 分发actions的方法
store.dispatch('fnName')
</script>
12. await的支持
setup
语法糖中可直接使用await
,不需要写async
,setup
会自动变成async setup
<script setup>
import api from '../api/Api'
const data = await Api.getData()
console.log(data)
</script>
13. provide 和 inject 祖孙传值
父组件代码如下(示例):
代码语言:html复制<template>
<AdoutExe />
</template>
<script setup>
import { ref,provide } from 'vue'
import AdoutExe from '@/components/AdoutExeCom'
let name = ref('py')
// 使用provide
provide('provideState', {
name,
changeName: () => {
name.value = 'poetries'
}
})
</script>
子组件代码如下(示例):
代码语言:html复制<script setup>
import { inject } from 'vue'
const provideState = inject('provideState')
provideState.changeName()
</script>
参考:前端vue面试题详细解答
Vuex 页面刷新数据丢失怎么解决
体验
可以从localStorage
中获取作为状态初始值:
const store = createStore({
state () {
return {
count: localStorage.getItem('count')
}
}
})
业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage
不太优雅
store.commit('increment')
localStorage.setItem('count', store.state.count)
回答范例
vuex
只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来localStorage
就很合适,提交mutation
的时候同时存入localStorage
,store
中把值取出作为state
的初始值即可。- 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用
vuex
提供的subscribe
方法做一个统一的处理。甚至可以封装一个vuex
插件以便复用。 - 类似的插件有
vuex-persist
、vuex-persistedstate
,内部的实现就是通过订阅mutation
变化做统一处理,通过插件的选项控制哪些需要持久化
原理
可以看一下vuex-persist (opens new window)内部确实是利用subscribe
实现的
vue-router 路由钩子函数是什么 执行顺序是什么
路由钩子的执行流程, 钩子函数种类有:
全局守卫
、路由守卫
、组件守卫
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2
)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5
)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发
DOM
更新。 - 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入
对Vue SSR的理解
Vue.js
是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出Vue
组件,进行生成DOM
和操作DOM
。然而,也可以将同一个组件渲染为服务端的HTML
字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
SSR
也就是服务端渲染,也就是将Vue
在客户端把标签渲染成HTML
的工作放在服务端完成,然后再把html
直接返回给客户端
- 优点 :
SSR
有着更好的SEO
、并且首屏加载速度更快- 因为
SPA
页面的内容是通过Ajax
获取,而搜索引擎爬取工具并不会等待Ajax
异步完成后再抓取页面内容,所以在SPA
中是抓取不到页面通过Ajax
获取到的内容;而SSR
是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面 - 更快的内容到达时间(首屏加载更快):
SPA
会等待所有Vue
编译后的js
文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR
直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间
- 因为
- 缺点 : 开发条件会受到限制,服务器端渲染只支持
beforeCreate
和created
两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js
的运行环境。服务器会有更大的负载需求- 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的
server
更加大量占用CPU
资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略
- 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的
其基本实现原理
app.js
作为客户端与服务端的公用入口,导出Vue
根实例,供客户端entry
与服务端entry
使用。客户端entry
主要作用挂载到DOM
上,服务端entry
除了创建和返回实例,还进行路由匹配与数据预获取。webpack
为客服端打包一个Client Bundle
,为服务端打包一个Server Bundle
。- 服务器接收请求时,会根据
url
,加载相应组件,获取和解析异步数据,创建一个读取Server Bundle
的BundleRenderer
,然后生成html
发送给客户端。 - 客户端混合,客户端收到从服务端传来的
DOM
与自己的生成的 DOM 进行对比,把不相同的DOM
激活,使其可以能够响应后续变化,这个过程称为客户端激活 。为确保混合成功,客户端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到stroe
里,这样,在客户端挂载到DOM
之前,可以直接从store
里取数据。首屏的动态数据通过window.__INITIAL_STATE__
发送到客户端
Vue SSR
的实现,主要就是把Vue
的组件输出成一个完整HTML
,vue-server-renderer
就是干这事的
Vue SSR
需要做的事多点(输出完整 HTML),除了complier -> vnode
,还需如数据获取填充至 HTML
、客户端混合(hydration
)、缓存等等。相比于其他模板引擎(ejs
, jade
等),最终要实现的目的是一样的,性能上可能要差点
实际工作中,你总结的vue最佳实践有哪些
从编码风格、性能、安全等方面说几条:
编码风格方面:
- 命名组件时使用“多词”风格避免和
HTML
元素冲突 - 使用“细节化”方式定义属性而不是只有一个属性名
- 属性名声明时使用“驼峰命名”,模板或
jsx
中使用“肉串命名” - 使用
v-for
时务必加上key
,且不要跟v-if
写在一起
性能方面:
- 路由懒加载减少应用尺寸
- 利用
SSR
减少首屏加载时间 - 利用
v-once
渲染那些不需要更新的内容 - 一些长列表可以利用虚拟滚动技术避免内存过度占用
- 对于深层嵌套对象的大数组可以使用
shallowRef
或shallowReactive
降低开销 - 避免不必要的组件抽象
安全:
- 不使用不可信模板,例如使用用户输入拼接模板:
template: <div> userProvidedString </div>
- 避免使用
v-html
,:url
,:style
等,避免html
、url
、样式等注入
为什么要使用异步组件
- 节省打包出的结果,异步组件分开打包,采用
jsonp
的方式进行加载,有效解决文件过大的问题。 - 核心就是包组件定义变成一个函数,依赖
import()
语法,可以实现文件的分割加载。
components:{
AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([])
}
原理
代码语言:javascript复制export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend
// 第二次渲染时Ctor不为undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染占位符 空虚拟节点
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {
if (isDef(factory.resolved)) {
// 3.在次渲染时可以拿到获取的最新组件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true) //2. 强制更新视图重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true forceRender(true)
}
})
const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后
sync = false
return factory.resolved
}
Vue 组件通讯有哪几种方式
- props 和$emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
- $parent,$children 获取当前组件的父组件和当前组件的子组件
- $attrs 和$listeners A->B->C。Vue 2.4 开始提供了$attrs 和$listeners 来解决这个问题
- 父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)
- $refs 获取组件实例
- envetBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式
- vuex 状态管理
Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
了解nextTick吗?
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
如何定义动态路由?如何获取传过来的动态参数?
(1)param方式
- 配置路由格式:
/router/:id
- 传递的方式:在path后面跟上对应的值
- 传递后形成的路径:
/router/123
1)路由定义
代码语言:javascript复制//在APP.vue中
<router-link :to="'/user/' userId" replace>用户</router-link>
//在index.js
{
path: '/user/:userid',
component: User,
},
2)路由跳转
代码语言:javascript复制// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:wade}})
// 方法3:
this.$router.push('/user/' wade)
3)参数获取
通过 $route.params.userid
获取传递的值
(2)query方式
- 配置路由格式:
/router
,也就是普通配置 - 传递的方式:对象中使用query的key作为传递方式
- 传递后形成的路径:
/route?id=123
1)路由定义
代码语言:javascript复制//方式1:直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式
<button @click='profileClick'>我的</button>
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobi",
age: "28",
height: 198
}
});
}
2)跳转方法
代码语言:javascript复制// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' jsmes)
3)获取参数
代码语言:javascript复制通过$route.query 获取传递的值
watch 原理
watch
本质上是为每个监听属性 setter
创建了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创建一个watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象setter
,因此引入deep
能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。immediate
:在初始化时直接调用回调函数,可以通过在created
阶段手动调用回调函数实现相同的效果
computed和watch有什么区别?
computed:
computed
是计算属性,也就是计算值,它更多用于计算值的场景computed
具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算computed
适用于计算比较消耗性能的计算场景
watch:
- 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察
props
$emit
或者本组件的值,当数据变化时来执行回调进行后续操作 - 无缓存性,页面重新渲染时值不变化也会执行
小结:
- 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
- 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化
为什么要用虚拟DOM
(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能 看一下页面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler 下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶
- 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
- 虚拟DOM∶ 生成vNode DOMDiff+必要的dom更新
Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。 (2)跨平台 Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
$nextTick 是什么?
Vue 实现响应式并不是在数据发生后立即更新 DOM,使用 vm.$nextTick
是在下次 DOM 更新循环结束之后立即执行延迟回调。在修改数据之后使用,则可以在回调中获取更新后的 DOM。
v-if 和 v-show 的区别
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
Vue中v-html会导致哪些问题
- 可能会导致
xss
攻击 v-html
会替换掉标签内部的子元素
let template = require('vue-template-compiler');
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`)
// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})}
console.log(r.render);
// _c 定义在core/instance/render.js
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {
if (vnode.children) vnode.children.length = 0
if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property
if (elm.childNodes.length === 1) {
elm.removeChild(elm.childNodes[0])
}
}
写过自定义指令吗 原理是什么
指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind
代码语言:txt复制1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
5. unbind:只调用一次,指令与元素解绑时调用。