写点什么

字节前端必会 vue 面试题集锦

作者:bb_xiaxia1998
  • 2023-01-06
    浙江
  • 本文字数:18507 字

    阅读完需:约 61 分钟

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 组件的生命周期。

为什么要用虚拟 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 等。

为什么 Vue 采用异步渲染呢?

Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick


dep.notify() 通知 watcher 进行更新, subs[i].update 依次调用 watcher 的 update queueWatcher 将 watcher 去重放入队列, nextTick( flushSchedulerQueue )在下一 tick 中刷新 watcher 队列(异步)。

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 停止录制


vue 中使用了哪些设计模式

1.工厂模式 - 传入参数即可创建实例


虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode


2.单例模式 - 整个程序有且仅有一个实例


vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉


3.发布-订阅模式 (vue 事件机制)


4.观察者模式 (响应式数据原理)


5.装饰模式: (@装饰器的用法)


6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

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


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

keep-alive 中的生命周期哪些

keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。


如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。


当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。

二、如何解决

解决跨域的方法有很多,下面列举了三种:


  • JSONP

  • CORS

  • Proxy


而在vue项目中,我们主要针对CORSProxy这两种方案进行展开


CORS


CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应


CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源


只要后端实现了 CORS,就实现了跨域


!



koa框架举例


添加中间件,直接设置Access-Control-Allow-Origin响应头


app.use(async (ctx, next)=> {  ctx.set('Access-Control-Allow-Origin', '*');  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');  if (ctx.method == 'OPTIONS') {    ctx.body = 200;   } else {    await next();  }})
复制代码


ps: Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host


Proxy


代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击


方案一


如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象


通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域


vue.config.js文件,新增以下代码


amodule.exports = {    devServer: {        host: '127.0.0.1',        port: 8084,        open: true,// vue项目启动时自动打开浏览器        proxy: {            '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的                target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址                changeOrigin: true, //是否跨域                pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替                    '^/api': ""                 }            }        }    }}
复制代码


通过axios发送请求中,配置请求的根路径


axios.defaults.baseURL = '/api'
复制代码


方案二


此外,还可通过服务端实现代理请求转发


express框架为例


var express = require('express');const proxy = require('http-proxy-middleware')const app = express()app.use(express.static(__dirname + '/'))app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false                      }));module.exports = app
复制代码


方案三


通过配置nginx实现代理


server {    listen    80;       location / {        root  /var/www/html;        index  index.html index.htm;        try_files $uri $uri/ /index.html;    }    location /api {        proxy_pass  http://127.0.0.1:3000;        proxy_redirect   off;        proxy_set_header  Host       $host;        proxy_set_header  X-Real-IP     $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;    }}
复制代码

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

v-for 为什么要加 key

如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速


更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。


更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

Vue 组件间通信有哪几种方式?

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。


(1)props / $emit 适用 父子组件通信 这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。


(2)ref 与 $parent / $children 适用 父子组件通信


  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问父 / 子实例


(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。


(4)$attrs/$listeners 适用于 隔代组件通信


  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on事件监听器。它可以通过 v-on="$listeners" 传入内部组件


(5)provide / inject 适用于 隔代组件通信 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。 (6)Vuex 适用于 父子、隔代、兄弟组件通信 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。


  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

请说出 vue cli 项目中 src 目录每个文件夹和文件的用法

  • assets文件夹是放静态资源;

  • components是放组件;

  • router是定义路由相关的配置;

  • view视图;

  • app.vue是一个应用主组件;

  • main.js是入口文件

谈谈对 keep-alive 的了解

keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性 include/exclude ,2个生命周期 activated deactivated

父组件可以监听到子组件的生命周期吗

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:


// Parent.vue<Child @mounted="doSomething"/>
// Child.vuemounted() {this.$emit("mounted");}
复制代码


以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:


//  Parent.vue<Child @hook:mounted="doSomething" ></Child>
doSomething() { console.log('父组件监听到 mounted 钩子函数 ...');},
// Child.vuemounted(){ console.log('子组件触发 mounted 钩子函数 ...');},
// 以上输出顺序为:// 子组件触发 mounted 钩子函数 ...// 父组件监听到 mounted 钩子函数 ...
复制代码


当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:createdupdated 等都可以监听

如果让你从零开始写一个 vuex,说说你的思路

思路分析


这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态 API。


  • vuex需求分析

  • 如何实现这些需求


回答范例


  1. 官方说vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex


  • 要实现一个Store存储全局状态

  • 要提供修改状态所需 API:commit(type, payload), dispatch(type, payload)


  1. 实现Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commitdispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件

  2. commit(type, payload)方法中可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果


实践


Store的实现:


class Store {    constructor(options) {        this.state = reactive(options.state)        this.options = options    }    commit(type, payload) {        this.options.mutations[type].call(this, this.state, payload)    }}
复制代码


vuex 简易版


/** * 1 实现插件,挂载$store * 2 实现store */
let Vue;
class Store { constructor(options) { // state响应式处理 // 外部访问: this.$store.state.*** // 第一种写法 // this.state = new Vue({ // data: options.state // })
// 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更 this._vm = new Vue({ data: { $$state: options.state } })
this._mutations = options.mutations this._actions = options.actions this.getters = {} options.getters && this.handleGetters(options.getters)
this.commit = this.commit.bind(this) this.dispatch = this.dispatch.bind(this) }
get state () { return this._vm._data.$$state }
set state (val) { return new Error('Please use replaceState to reset state') }
handleGetters (getters) { Object.keys(getters).map(key => { Object.defineProperty(this.getters, key, { get: () => getters[key](this.state) }) }) }
commit (type, payload) { let entry = this._mutations[type] if (!entry) { return new Error(`${type} is not defined`) }
entry(this.state, payload) }
dispatch (type, payload) { let entry = this._actions[type] if (!entry) { return new Error(`${type} is not defined`) }
entry(this, payload) }}
const install = (_Vue) => { Vue = _Vue
Vue.mixin({ beforeCreate () { if (this.$options.store) { Vue.prototype.$store = this.$options.store } }, })}

export default { Store, install }
复制代码


验证方式


import Vue from 'vue'import Vuex from './vuex'// this.$storeVue.use(Vuex)
export default new Vuex.Store({ state: { counter: 0 }, mutations: { // state从哪里来的 add (state) { state.counter++ } }, getters: { doubleCounter (state) { return state.counter * 2 } }, actions: { add ({ commit }) { setTimeout(() => { commit('add') }, 1000) } }, modules: { }})
复制代码

如何保存页面的当前的状态

既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:


  • 前组件会被卸载

  • 前组件不会被卸载


那么可以按照这两种情况分别得到以下方法:


组件会被卸载:


(1)将状态存储在 LocalStorage / SessionStorage


只需要在组件即将被销毁的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。


比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。


优点:


  • 兼容性好,不需要额外库或工具。

  • 简单快捷,基本可以满足大部分需求。


缺点:


  • 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷贝的缺点)

  • 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象


(2)路由传值


通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。


在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。


优点:


  • 简单快捷,不会污染 LocalStorage / SessionStorage。

  • 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)


缺点:


  • 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。


组件不会被卸载:


(1)单页面渲染


要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。


优点:


  • 代码量少

  • 不需要考虑状态传递过程中的错误


缺点:


  • 增加 A 组件维护成本

  • 需要传入额外的 prop 到 B 组件

  • 无法利用路由定位页面


除此之外,在 Vue 中,还可以是用 keep-alive 来缓存页面,当组件在 keep-alive 内被切换时组件的 activated、deactivated 这两个生命周期钩子函数会被执行被包裹在 keep-alive 中的组件的状态将会被保留:


<keep-alive>    <router-view v-if="$route.meta.keepAlive"></router-view></kepp-alive>
复制代码


router.js


{  path: '/',  name: 'xxx',  component: ()=>import('../src/views/xxx.vue'),  meta:{    keepAlive: true // 需要被缓存  }},
复制代码

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 中是如何检测数组变化的呢?

数组就是使用 object.defineProperty 重新定义数组的每一项,那能引起数组变化的方法我们都是知道的, pop push shift unshift splice sort reverse 这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。


  1. 是用来函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新。

  2. 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)


vue3:改用 proxy ,可直接监听对象数组的变化。

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

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


  • 生成 AST 树

  • 优化

  • codegen


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


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


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

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 的回调函数,创建好的组件实例会作为回调函数的参数传入。


用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
字节前端必会vue面试题集锦_Vue_bb_xiaxia1998_InfoQ写作社区