乐趣区

关于vue.js:字节前端必会vue面试题集锦

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 -t
nginx -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> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
​
script.render = render
export 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.vue
mounted() {this.$emit("mounted");
}

以上须要手动通过 $emit 触发父组件的事件,更简略的形式能够在父组件援用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {console.log('父组件监听到 mounted 钩子函数 ...');
},

//  Child.vue
mounted(){console.log('子组件触发 mounted 钩子函数 ...');
},    

// 以上输入程序为:// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     

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

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

思路剖析

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

  • vuex需要剖析
  • 如何实现这些需要

答复范例

  1. 官网说 vuex 是一个状态管理模式和库,并确保这些状态以可预期的形式变更。可见要实现一个vuex
  2. 要实现一个 Store 存储全局状态
  3. 要提供批改状态所需 API:commit(type, payload), dispatch(type, payload)
  4. 实现 Store 时,能够定义 Store 类,构造函数接管选项 options,设置属性state 对外裸露状态,提供 commitdispatch批改属性 state。这里须要设置state 为响应式对象,同时将 Store 定义为一个 Vue 插件
  5. 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.$store
Vue.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 的回调函数,创立好的组件实例会作为回调函数的参数传入。
退出移动版