写点什么

美团前端经典 vue 面试题总结

作者:yyds2026
  • 2023-03-01
    浙江
  • 本文字数:19698 字

    阅读完需:约 65 分钟

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新


源码相关


get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
复制代码

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

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

  • Vue.delete直接删除了数组 改变了数组的键值。


var a=[1,2,3,4]var b=[1,2,3,4]delete a[0]console.log(a)  //[empty,2,3,4]this.$delete(b,0)console.log(b)  //[2,3,4]
复制代码

params 和 query 的区别

用法:query 要用 path 来引入,params 要用 name 来引入,接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name


url 地址显示:query 更加类似于 ajax 中 get 传参,params 则类似于 post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示


注意:query 刷新不会丢失 query 里面的数据 params 刷新会丢失 params 里面的数据。

构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么

  • vue.jsvue-cli工程的核心,主要特点是 双向数据绑定 和 组件系统。

  • vue-routervue官方推荐使用的路由框架。

  • vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护vue组件间共用的一些 变量 和 方法。

  • axios( 或者 fetchajax ):用于发起 GET 、或 POSThttp请求,基于 Promise 设计。

  • vuex等:一个专为vue设计的移动端 UI 组件库。

  • 创建一个emit.js文件,用于vue事件机制的管理。

  • webpack:模块加载和vue-cli工程打包器。

vue-cli 工程常用的 npm 命令有哪些

  • 下载 node_modules 资源包的命令:


npm install
复制代码


  • 启动 vue-cli 开发环境的 npm 命令:


npm run dev
复制代码


  • vue-cli 生成 生产环境部署资源 的 npm命令:


npm run build
复制代码


  • 用于查看 vue-cli 生产环境部署资源文件大小的 npm命令:


npm run build --report
复制代码


在浏览器上自动弹出一个 展示 vue-cli 工程打包后 app.jsmanifest.jsvendor.js 文件里面所包含代码的页面。可以具此优化 vue-cli 生产环境部署的静态资源,提升 页面 的加载速度

v-once 的使用场景有哪些

分析


v-onceVue中内置指令,很有用的API,在优化方面经常会用到


体验


仅渲染元素和组件一次,并且跳过未来更新


<!-- single element --><span v-once>This will never change: {{msg}}</span><!-- the element have children --><div v-once>  <h1>comment</h1>  <p>{{msg}}</p></div><!-- component --><my-component v-once :comment="msg"></my-component><!-- `v-for` directive --><ul>  <li v-for="i in list" v-once>{{i}}</li></ul>
复制代码


回答范例


  • v-oncevue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新

  • 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段

  • 我们只需要作用的组件或元素上加上v-once即可

  • vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了

  • 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算


原理


下面例子使用了v-once


<script setup>import { ref } from 'vue'const msg = ref('Hello World!')</script><template>  <h1 v-once>{{ msg }}</h1>  <input v-model="msg"></template>
复制代码


我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:


// ...return (_ctx, _cache) => {  return (_openBlock(), _createElementBlock(_Fragment, null, [    // 从缓存获取vnode    _cache[0] || (      _setBlockTracking(-1),      _cache[0] = _createElementVNode("h1", null, [        _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)      ]),      _setBlockTracking(1),      _cache[0]    ),// ...
复制代码


参考 前端进阶面试题详细解答

vue-loader 是什么?它有什么作用?

回答范例


  1. vue-loader是用于处理单文件组件(SFCSingle-File Component)的webpack loader

  2. 因为有了vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为<template><script><style>,代码会异常清晰。结合其他loader我们还可以用Pug编写<template>,用SASS编写<style>,用TS编写<script>。我们的<style>还可以单独作用当前组件

  3. webpack打包时,会以loader的方式调用vue-loader

  4. vue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理。最后将这些单独的块装配成最终的组件模块


原理


vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面


// source.vue被vue-loader处理之后返回的代码// import the <template> blockimport render from 'source.vue?vue&type=template'// import the <script> blockimport script from 'source.vue?vue&type=script'export * from 'source.vue?vue&type=script'// import <style> blocksimport 'source.vue?vue&type=style&index=1'script.render = renderexport default script
复制代码


我们想要script块中的内容被作为js处理(当然如果是<script lang="ts">被作为ts理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC


import script from 'source.vue?vue&type=script
复制代码


将被展开为:


import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
复制代码


类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,对下面的代码


<style scoped lang="scss">
复制代码


vue-loader将会返回给我们下面结果:


import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
复制代码


然后webpack会展开如下:


import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
复制代码


  • 当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader

  • 对于<script>块,处理到这就可以了,但是<template><style>还有一些额外任务要做,比如

  • 需要用 Vue 模板编译器编译template,从而得到render函数

  • 需要对 <style scoped>中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前


实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:


// <template lang="pug">import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'// <style scoped lang="scss">import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
复制代码

Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期

  • 考点: Vue的变化侦测原理

  • 前置知识: 依赖收集、虚拟DOM、响应式系统


根本原因是VueReact的变化侦测方式有所不同


  • 当 React 知道发生变化后,会使用Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要 shouldComponentUpdate 进行手动操作来减少diff,从而提高程序整体的性能

  • Vue在一开始就知道那个组件发生了变化,不需要手动控制diff,而组件内部采用的diff方式实际上是可以引入类似于shouldComponentUpdate相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前Vue并没有考虑引入shouldComponentUpdate这种手动优化的生命周期

Vue 为什么需要虚拟 DOM?优缺点有哪些

由于在浏览器中操作 DOM是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象


优点:


  • 保证性能下限 : 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;

  • 无需手动操作 DOM : 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

  • 跨平台 : 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。


缺点:


  • 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

  • 首次渲染大量DOM时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。


虚拟 DOM 实现原理?


虚拟 DOM 的实现原理主要包括以下 3 部分:


  • JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;

  • diff 算法 — 比较两棵虚拟 DOM 树的差异;

  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。


说说你对虚拟 DOM 的理解?回答范例


思路


  • vdom是什么

  • 引入vdom的好处

  • vdom如何生成,又如何成为dom

  • 在后续的diff中的作用


回答范例


  1. 虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构

  2. 通过引入vdom我们可以获得如下好处:


  • 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能

  • 直接操作 dom 是有限制的,比如:diffclone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了

  • 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流

  • 方便实现跨平台

  • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android)变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等

  • Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染


  1. vdom如何生成?在 vue 中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom



  1. 挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图


为什么要用 vdom?案例解析


现在有一个场景,实现以下需求:


[      { name: "张三", age: "20", address: "北京"},      { name: "李四", age: "21", address: "武汉"},      { name: "王五", age: "22", address: "杭州"},]
复制代码


将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用 jQuery 实现如下:


<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>Document</title></head>
<body> <div id="container"></div> <button id="btn-change">改变</button>
<script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script> <script> const data = [{ name: "张三", age: "20", address: "北京" }, { name: "李四", age: "21", address: "武汉" }, { name: "王五", age: "22", address: "杭州" }, ]; //渲染函数 function render(data) { const $container = $('#container'); $container.html(''); const $table = $('<table>'); // 重绘一次 $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>')); data.forEach(item => { //每次进入都重绘 $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`)) }) $container.append($table); }
$('#btn-change').click(function () { data[1].age = 30; data[2].address = '深圳'; render(data); }); </script></body></html>
复制代码


  • 这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,table标签都得重新创建,也就是说table下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM重绘相当消耗浏览器性能。

  • 因此我们采用 JS 对象模拟的方法,将DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。

  • 当然有人说虚拟 DOM 并不比真实的DOM快,其实也是有道理的。当上述table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在jsdiff算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。


如下DOM结构:


<ul id="list">    <li class="item">Item1</li>    <li class="item">Item2</li></ul>
复制代码


映射成虚拟DOM就是这样:


{  tag: "ul",  attrs: {    id: "list"  },  children: [    {      tag: "li",      attrs: { className: "item" },      children: ["Item1"]    }, {      tag: "li",      attrs: { className: "item" },      children: ["Item2"]    }  ]} 
复制代码


使用 snabbdom 实现 vdom


这是一个简易的实现vdom功能的库,相比vuereact,对于vdom这块更加简易,适合我们学习vdomvdom里面有两个核心的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的功能在于做虚拟dom的比对和将vdom挂载到真实DOM


简单介绍一下这两个函数的用法:


h('标签名', {属性}, [子元素])h('标签名', {属性}, [文本])patch(container, vnode) // container为容器DOM元素patch(vnode, newVnode)
复制代码


现在我们就来用snabbdom重写一下刚才的例子:


<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>Document</title></head><body>  <div id="container"></div>  <button id="btn-change">改变</button>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>  <script>    let snabbdom = window.snabbdom;
// 定义patch let patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]);
//定义h let h = snabbdom.h;
const data = [{ name: "张三", age: "20", address: "北京" }, { name: "李四", age: "21", address: "武汉" }, { name: "王五", age: "22", address: "杭州" }, ]; data.unshift({name: "姓名", age: "年龄", address: "地址"});
let container = document.getElementById('container'); let vnode; const render = (data) => { let newVnode = h('table', {}, data.map(item => { let tds = []; for(let i in item) { if(item.hasOwnProperty(i)) { tds.push(h('td', {}, item[i] + '')); } } return h('tr', {}, tds); }));
if(vnode) { patch(vnode, newVnode); } else { patch(container, newVnode); } vnode = newVnode; }
render(data);
let btnChnage = document.getElementById('btn-change'); btnChnage.addEventListener('click', function() { data[1].age = 30; data[2].address = "深圳"; //re-render render(data); }) </script></body></html>
复制代码



你会发现, 只有改变的栏目才闪烁,也就是进行重绘 ,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销


vue 中使用h函数生成虚拟DOM返回


const vm = new Vue({  el: '#app',  data: {    user: {name:'poetry'}  },  render(h){    // h()    // h(App)    // h('div',[])    let vnode = h('div',{},'hello world');    return vnode  }});
复制代码

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

组件中写 name 属性的好处

可以标识组件的具体名称方便调试和查找对应属性


// 源码位置 src/core/global-api/extend.js
// enable recursive self-lookupif (name) { Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx}
复制代码

你有对 Vue 项目进行哪些优化?

(1)代码层面的优化


  • v-if 和 v-show 区分使用场景

  • computed 和 watch 区分使用场景

  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  • 长列表性能优化

  • 事件的销毁

  • 图片资源懒加载

  • 路由懒加载

  • 第三方插件的按需引入

  • 优化无限列表性能

  • 服务端渲染 SSR or 预渲染


(2)Webpack 层面的优化


  • Webpack 对图片进行压缩

  • 减少 ES6 转为 ES5 的冗余代码

  • 提取公共代码

  • 模板预编译

  • 提取组件的 CSS

  • 优化 SourceMap

  • 构建结果输出分析

  • Vue 项目的编译优化


(3)基础的 Web 技术的优化


  • 开启 gzip 压缩

  • 浏览器缓存

  • CDN 的使用

  • 使用 Chrome Performance 查找性能瓶颈

Vue 模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步


第一步是将 模板字符串 转换成 element ASTs(解析器)第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
复制代码


相关代码如下


export function compileToFunctions(template) {  // 我们需要把html字符串变成render函数  // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法  // 很多库都运用到了ast 比如 webpack babel eslint等等  let ast = parse(template);  // 2.优化静态节点  // 这个有兴趣的可以去看源码  不影响核心功能就不实现了  //   if (options.optimize !== false) {  //     optimize(ast, options);  //   }
// 3.通过ast 重新生成代码 // 我们最后生成的代码需要和render函数一样 // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))) // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本 let code = generate(ast); // 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值 let renderFn = new Function(`with(this){return ${code}}`); return renderFn;}
复制代码

Vue 模版编译原理

vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。


  • 解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。

  • 优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。

  • 生成阶段:将最终的 AST 转化为 render 函数字符串。

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。


其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并


  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。

  • Vue.component你可以创建 ,也可以取组件。


相关代码如下


export default function initExtend(Vue) {  let cid = 0; //组件的唯一标识  // 创建子类继承Vue父类 便于属性扩展  Vue.extend = function (extendOptions) {    // 创建子类的构造函数 并且调用初始化方法    const Sub = function VueComponent(options) {      this._init(options); //调用Vue初始化方法    };    Sub.cid = cid++;    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类    Sub.prototype.constructor = Sub; //constructor指向自己    Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options    return Sub;  };}
复制代码

谈谈 Vue 和 React 组件化的思想

  • 1.我们在各个页面开发的时候,会产生很多重复的功能,比如 element 中的 xxxx。像这种纯粹非页面的 UI,便成为我们常用的 UI 组件,最初的前端组件也就仅仅指的是 UI 组件

  • 2.随着业务逻辑变得越来多是,我们就想要我们的组件可以处理很多事,这就是我们常说的组件化,这个组件就不是 UI 组件了,而是包具体业务的业务组件

  • 3.这种开发思想就是分而治之。最大程度的降低开发难度和维护成本的效果。并且可以多人协作,每个人写不同的组件,最后像撘积木一样的把它构成一个页面

怎么实现路由懒加载呢

这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效


// 将// import UserDetails from './views/UserDetails'// 替换为const UserDetails = () => import('./views/UserDetails')const router = createRouter({  // ...  routes: [{ path: '/users/:id', component: UserDetails }],})
复制代码


回答范例


  1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段

  2. 一般来说,对所有的路由都使用动态导入是个好主意

  3. component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }

  4. 结合注释 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 可以做webpack代码分块

Vue 的基本原理

当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0 使用 proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

diff 算法

答案


时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue 进行优化转化成 O(n)


理解:


  • 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点

  • 扩展 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加 key 只会移动减少操作 DOM。

  • 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。

  • 只进行同层比较,不会进行跨层比较。


diff 算法的优化策略:四种命中查找,四个指针


  1. 旧前与新前(先比开头,后插入和删除节点的这种情况)

  2. 旧后与新后(比结尾,前插入或删除的情况)

  3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)

  4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)


--- 问完上面这些如果都能很清楚的话,基本 O 了 ---


以下的这些简单的概念,你肯定也是没有问题的啦😉


用户头像

yyds2026

关注

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

还未添加个人简介

评论

发布
暂无评论
美团前端经典vue面试题总结_Vue_yyds2026_InfoQ写作社区