写点什么

一份 vue 面试考点清单

作者:bb_xiaxia1998
  • 2022-11-14
    浙江
  • 本文字数:18396 字

    阅读完需:约 60 分钟

Vue 项目性能优化-详细

Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验

代码层面的优化

1. v-if 和 v-show 区分使用场景


  • v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块

  • v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS displaynone/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.xv-forv-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性


推荐:


<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    })  }}
复制代码


不推荐:


<ul>  <li    v-for="user in users"    v-if="user.isActive"    :key="user.id">    {{ user.name }}  </li></ul>
复制代码


4. 长列表性能优化


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. 图片资源懒加载


对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vuevue-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 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来


路由懒加载:


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
复制代码


{  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 代码


class HelloWebpack extends Component{...}
复制代码


这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:


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-mapsource-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;

  • eval-source-mapeval 打包代码的速度非常快,因为它不生成 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 压缩


gzipGNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE 等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右


以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:


npm install compression --save
复制代码


var compression = require('compression');var app = express();app.use(compression())
复制代码


重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功



Nginx 开启 gzip 压缩


#是否启动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 -tnginx -s reload
复制代码


2. 浏览器缓存


为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)


3. CDN 的使用


浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且 CDN 具有更好的可用性,更低的网络延迟和丢包率


4. 使用 Chrome Performance 查找性能瓶颈


ChromePerformance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。


  • 打开 Chrome 开发者工具,切换到 Performance 面板

  • 点击 Record 开始录制

  • 刷新页面或展开某个节点

  • 点击 Stop 停止录制


Vue3 有了解过吗?能说说跟 vue2 的区别吗?

1. 哪些变化



从上图中,我们可以概览Vue3的新特性,如下:


  • 速度更快

  • 体积减少

  • 更易维护

  • 更接近原生

  • 更易使用


1.1 速度更快


vue3相比vue2


  • 重写了虚拟Dom实现

  • 编译模板的优化

  • 更高效的组件初始化

  • undate性能提高 1.3~2 倍

  • SSR速度提高了 2~3 倍



1.2 体积更小


通过webpacktree-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 是一种能够将我们的模板移动到 DOMVue 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的使用,这里以下图展开



简单使用:


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-ifv-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 应始终声明为函数

  • 来自 mixindata 选项现在可简单地合并

  • attribute 强制策略已更改

  • 一些过渡 class 被重命名

  • 组建 watch 选项和实例方法 $watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。

  • <template> 没有特殊指令的标记 (v-if/else-if/elsev-forv-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 路由模式的实现主要是基于下面几个特性


  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;

  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;

  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URLhash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URLhash 值;

  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)


window.addEventListener("hashchange", funcRef, false);
复制代码


每一次改变 hashwindow.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了


特点 :兼容性好但是不美观


2. history模式


history采用HTML5的新特性;且提供了两个新方法: pushState()replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更


window.history.pushState(null, null, path);window.history.replaceState(null, null, path);
复制代码


这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。


history 路由模式的实现主要基于存在下面几个特性:


  • pushStaterepalceState 两个 API 来操作实现 URL 的变化 ;

  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);

  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。


特点 :虽然美观,但是刷新会出现 404 需要后端进行配置

Vue3.2 setup 语法糖汇总

提示:vue3.2 版本开始才能使用语法糖!


Vue3.0 中变量必须 return 出来, template 中才能使用;而在 Vue3.2 中只需要在 script 标签上加上 setup 属性,无需 returntemplate 便可直接使用,非常的香啊!


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 方法的使用


<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 的使用


<script setup>import {  ref,  watchEffect,} from 'vue'
let sum = ref(0)
watchEffect(()=>{ const x1 = sum.value console.log('watchEffect所指定的回调执行了')})</script>
复制代码


5. watch 的使用


<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 父子传值的使用


父组件代码如下(示例):


<template>  <child :name='name'/>  </template>
<script setup> import {ref} from 'vue' // 引入子组件 import child from './child.vue' let name= ref('poetry')</script>
复制代码


子组件代码如下(示例):


<template>  <span>{{props.name}}</span></template>
<script setup>import { defineProps } from 'vue'// 声明propsconst props = defineProps({ name: { type: String, default: 'poetries' }}) // 或者//const props = defineProps(['name'])</script>
复制代码


8. emit 子父传值的使用


父组件代码如下(示例):


<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>
复制代码


子组件代码如下(示例):


<template>  <a-button @click="isOk">    确定  </a-button></template><script setup>import { defineEmits } from 'vue';
// emitconst emit = defineEmits(['aboutExeVisible'])/** * 方法 */// 点击确定按钮const isOk = () => { emit('aboutExeVisible');}</script>
复制代码


9. 获取子组件 ref 变量和 defineExpose 暴露


vue2中的获取子组件的ref,直接在父组件中控制子组件方法和变量的方法


父组件代码如下(示例):


<template>  <button @click="onClickSetUp">点击</button>  <Content ref="content" /></template>
<script setup>import {ref} from 'vue'
// content组件refconst content = ref('content')// 点击设置const onClickSetUp = ({ key }) => { content.value.modelVisible = true}</script><style scoped lang="less"></style>
复制代码


子组件代码如下(示例):


<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 的使用


<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 仓库的使用


<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,不需要写asyncsetup会自动变成async setup


<script setup>  import api from '../api/Api'  const data = await Api.getData()  console.log(data)</script>
复制代码


13. provide 和 inject 祖孙传值


父组件代码如下(示例):


<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>
复制代码


子组件代码如下(示例):


<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)
复制代码


回答范例


  1. vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来

  2. localStorage就很合适,提交mutation的时候同时存入localStoragestore中把值取出作为state的初始值即可。

  3. 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。

  4. 类似的插件有vuex-persistvuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化


原理


可以看一下vuex-persist (opens new window)内部确实是利用subscribe实现的

vue-router 路由钩子函数是什么 执行顺序是什么

路由钩子的执行流程, 钩子函数种类有:全局守卫路由守卫组件守卫


  1. 导航被触发。

  2. 在失活的组件里调用 beforeRouteLeave 守卫。

  3. 调用全局的 beforeEach 守卫。

  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。

  5. 在路由配置里调用 beforeEnter

  6. 解析异步路由组件。

  7. 在被激活的组件里调用 beforeRouteEnter

  8. 调用全局的 beforeResolve 守卫 (2.5+)。

  9. 导航被确认。

  10. 调用全局的 afterEach 钩子。

  11. 触发 DOM 更新。

  12. 调用 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 有更快的内容到达时间

  • 缺点 : 开发条件会受到限制,服务器端渲染只支持 beforeCreatecreated 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。服务器会有更大的负载需求

  • 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略


其基本实现原理


  • app.js 作为客户端与服务端的公用入口,导出 Vue 根实例,供客户端 entry 与服务端 entry 使用。客户端 entry 主要作用挂载到 DOM 上,服务端 entry 除了创建和返回实例,还进行路由匹配与数据预获取。

  • webpack 为客服端打包一个 Client Bundle ,为服务端打包一个 Server Bundle

  • 服务器接收请求时,会根据 url,加载相应组件,获取和解析异步数据,创建一个读取 Server BundleBundleRenderer,然后生成 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渲染那些不需要更新的内容

  • 一些长列表可以利用虚拟滚动技术避免内存过度占用

  • 对于深层嵌套对象的大数组可以使用shallowRefshallowReactive降低开销

  • 避免不必要的组件抽象


安全:


  • 不使用不可信模板,例如使用用户输入拼接模板:template: <div> + userProvidedString + </div>

  • 避免使用v-html:url:style等,避免htmlurl、样式等注入

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。

  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。


components:{   AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }
复制代码


原理


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 组件通讯有哪几种方式

  1. props 和emit 触发事件来做到的

  2. children 获取当前组件的父组件和当前组件的子组件

  3. listeners A->B->C。Vue 2.4 开始提供了listeners 来解决这个问题

  4. 父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)

  5. $refs 获取组件实例

  6. envetBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式

  7. vuex 状态管理

Vue 模版编译原理知道吗,能简单说一下吗?

简单说,Vue 的编译过程就是将template转化为render函数的过程。会经历以下阶段:


  • 生成 AST 树

  • 优化

  • codegen


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


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


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

了解 nextTick 吗?

异步方法,异步渲染最后一步,与 JS 事件循环联系紧密。主要使用了宏任务微任务(setTimeoutpromise那些),定义了一个异步方法,多次调用nextTick会将方法存入队列,通过异步方法清空当前队列。

如何定义动态路由?如何获取传过来的动态参数?

(1)param 方式


  • 配置路由格式:/router/:id

  • 传递的方式:在 path 后面跟上对应的值

  • 传递后形成的路径:/router/123


1)路由定义


//在APP.vue中<router-link :to="'/user/'+userId" replace>用户</router-link>    
//在index.js{ path: '/user/:userid', component: User,},
复制代码


2)路由跳转


// 方法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)路由定义


//方式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)跳转方法


// 方法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)获取参数


通过$route.query 获取传递的值
复制代码

watch 原理

watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下


  • deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。

  • immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果

computed 和 watch 有什么区别?

computed:


  1. computed是计算属性,也就是计算值,它更多用于计算值的场景

  2. computed具有缓存性,computed 的值在 getter 执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取 computed 的值时才会重新调用对应的 getter 来计算

  3. computed适用于计算比较消耗性能的计算场景


watch:


  1. 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作

  2. 无缓存性,页面重新渲染时值不变化也会执行


小结:


  1. 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为 computed

  2. 如果你需要在某个数据变化时做一些事情,使用 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,jsif (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


1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
5. unbind:只调用一次,指令与元素解绑时调用。
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
一份vue面试考点清单_Vue_bb_xiaxia1998_InfoQ写作社区