写点什么

哪些 vue 面试题是经常会被问到的

作者:bb_xiaxia1998
  • 2022 年 9 月 25 日
    浙江
  • 本文字数:30431 字

    阅读完需:约 100 分钟

Vuex 和单纯的全局对象有什么区别?

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

  • 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。

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面试题详细解答

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.observable 你有了解过吗?说说看

一、Observable 是什么

Observable 翻译过来我们可以理解成可观察的


我们先来看一下其在Vue中的定义


Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象


返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器


Vue.observable({ count : 1})
复制代码


其作用等同于


new vue({ count : 1})
复制代码


Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象


Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

二、使用场景

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择


创建一个js文件


// 引入vueimport Vue from 'vue// 创建state对象,使用observable让state对象可响应export let state = Vue.observable({  name: '张三',  'age': 38})// 创建对应的方法export let mutations = {  changeName(name) {    state.name = name  },  setAge(age) {    state.age = age  }}
复制代码


.vue文件中直接使用即可


<template>  <div>    姓名:{{ name }}    年龄:{{ age }}    <button @click="changeName('李四')">改变姓名</button>    <button @click="setAge(18)">改变年龄</button>  </div></template>import { state, mutations } from '@/storeexport default {  // 在计算属性中拿到值  computed: {    name() {      return state.name    },    age() {      return state.age    }  },  // 调用mutations里面的方法,更新数据  methods: {    changeName: mutations.changeName,    setAge: mutations.setAge  }}
复制代码

三、原理分析

源码位置:src\core\observer\index.js


export function observe (value: any, asRootData: ?boolean): Observer | void {  if (!isObject(value) || value instanceof VNode) {    return  }  let ob: Observer | void  // 判断是否存在__ob__响应式属性  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {    ob = value.__ob__  } else if (    shouldObserve &&    !isServerRendering() &&    (Array.isArray(value) || isPlainObject(value)) &&    Object.isExtensible(value) &&    !value._isVue  ) {    // 实例化Observer响应式对象    ob = new Observer(value)  }  if (asRootData && ob) {    ob.vmCount++  }  return ob}
复制代码


Observer


export class Observer {    value: any;    dep: Dep;    vmCount: number; // number of vms that have this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { // 实例化对象是一个对象,进入walk方法 this.walk(value) }}
复制代码


walk函数


walk (obj: Object) {    const keys = Object.keys(obj)    // 遍历key,通过defineReactive创建响应式对象    for (let i = 0; i < keys.length; i++) {        defineReactive(obj, keys[i])    }}
复制代码


defineReactive方法


export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: ?Function,  shallow?: boolean) {  const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return }
// cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] }
let childOb = !shallow && observe(val) // 接下来调用Object.defineProperty()给对象定义响应式属性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // 对观察者watchers进行通知,state就成了全局响应式对象 dep.notify() } })}
复制代码

Vue-Router 的懒加载如何实现

非懒加载:


import List from '@/components/list.vue'const router = new VueRouter({  routes: [    { path: '/list', component: List }  ]})
复制代码


(1)方案一(常用):使用箭头函数+import 动态加载


const List = () => import('@/components/list.vue')const router = new VueRouter({  routes: [    { path: '/list', component: List }  ]})
复制代码


(2)方案二:使用箭头函数+require 动态加载


const router = new Router({  routes: [   {     path: '/list',     component: resolve => require(['@/components/list'], resolve)   }  ]})
复制代码


(3)方案三:使用 webpack 的 require.ensure 技术,也可以实现按需加载。 这种情况下,多个路由指定相同的 chunkName,会合并打包成一个 js 文件。


// r就是resolveconst List = r => require.ensure([], () => r(require('@/components/list')), 'list');// 路由也是正常的写法  这种是官方推荐的写的 按模块划分懒加载 const router = new Router({  routes: [  {    path: '/list',    component: List,    name: 'list'  } ]}))
复制代码

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

Vue 中的过滤器了解吗?过滤器的应用场景有哪些?

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数


Vue 允许你自定义过滤器,可被用于一些常见的文本格式化


ps: Vue3中已废弃filter

如何用

vue 中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:


<!-- 在双花括号中 -->{ message | capitalize }
<!-- 在 `v-bind` 中 --><div v-bind:id="rawId | formatId"></div>
复制代码

定义 filter

在组件的选项中定义本地的过滤器


filters: {  capitalize: function (value) {    if (!value) return ''    value = value.toString()    return value.charAt(0).toUpperCase() + value.slice(1)  }}
复制代码


定义全局过滤器:


Vue.filter('capitalize', function (value) {  if (!value) return ''  value = value.toString()  return value.charAt(0).toUpperCase() + value.slice(1)})
new Vue({ // ...})
复制代码


注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器


过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数


过滤器可以串联:


{ message | filterA | filterB }
复制代码


在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。


过滤器是 JavaScript函数,因此可以接收参数:


{{ message | filterA('arg1', arg2) }}
复制代码


这里,filterA 被定义为接收三个参数的过滤器函数。


其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数


举个例子:


<div id="app">  <p>{{ msg | msgFormat('疯狂','--')}}</p></div>
<script> // 定义一个 Vue 全局的过滤器,名字叫做 msgFormat Vue.filter('msgFormat', function(msg, arg, arg2) { // 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则 return msg.replace(/单纯/g, arg+arg2) })</script>
复制代码


小结:


  • 部过滤器优先于全局过滤器被调用

  • 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右

应用场景

平时开发中,需要用到过滤器的地方有很多,比如单位转换数字打点文本格式化时间格式化之类的等


比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器


Vue.filter('toThousandFilter', function (value) {  if (!value) return ''  value = value.toString()  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')})
复制代码

原理分析

使用过滤器


{{ message | capitalize }}
复制代码


在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters,我们放到最后讲


_s(_f('filterFormat')(message))
复制代码


首先分析一下_f


_f 函数全名是:resolveFilter,这个函数的作用是从this.$options.filters中找出注册的过滤器并返回


// 变为this.$options.filters['filterFormat'](message) // message为参数
复制代码


关于resolveFilter


import { indentity,resolveAsset } from 'core/util/index' 
export function resolveFilter(id){ return resolveAsset(this.$options,'filters',id,true) || identity}
复制代码


内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;


resolveAsset的代码如下:


export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西  if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回      return   }  const assets = options[type]  // 将我们注册的所有过滤器保存在变量中  // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配  if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器  // 没有找到,代码继续执行  const camelizedId  = camelize(id) // 万一你是驼峰的呢  if(hasOwn(assets,camelizedId)) return assets[camelizedId]  // 没找到,继续执行  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢  if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]  // 如果还是没找到,则检查原型链(即访问属性)  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]  // 如果依然没找到,则在非生产环境的控制台打印警告  if(process.env.NODE_ENV !== 'production' && warnMissing && !result){    warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)  }  // 无论是否找到,都返回查找结果  return result}
复制代码


下面再来分析一下_s


_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的 text 属性中,渲染到视图中


function toString(value){  return value == null  ? ''  : typeof value === 'object'    ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距    : String(value)}
复制代码


最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式


function parseFilters (filter) {    let filters = filter.split('|')    let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组    let i    if (filters) {        for(i = 0;i < filters.length;i++){            experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数        }    }    return expression}// warpFilter函数实现function warpFilter(exp,filter){    // 首先判断过滤器是否有其他参数    const i = filter.indexof('(')    if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接        return `_f("${filter}")(${exp})`    }else{        const name = filter.slice(0,i) // 过滤器名称        const args = filter.slice(i+1) // 参数,但还多了 ‘)’        return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'    }}
复制代码


小结:


  • 在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)

  • 编译后通过调用resolveFilter函数找到对应过滤器并返回结果

  • 执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnodetext属性中,渲染到视图

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;

  • 属性改变后,通知计算属性重新计算


实现时,主要如下


  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter

  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。

  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。

  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

在 Vue 中使用插件的步骤

  • 采用ES6import ... from ...语法或CommonJSrequire()方法引入插件

  • 使用全局方法Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })

SPA 首屏加载速度慢的怎么解决

一、什么是首屏加载

首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容


首屏加载可以说是用户体验中最重要的环节


关于计算首屏时间


利用performance.timing提供的数据:



通过DOMContentLoad或者performance来计算出首屏时间


// 方案一:document.addEventListener('DOMContentLoaded', (event) => {    console.log('first contentful painting');});// 方案二:performance.getEntriesByName("first-contentful-paint")[0].startTime
// performance.getEntriesByName("first-contentful-paint")[0]// 会返回一个 PerformancePaintTiming的实例,结构如下:{ name: "first-contentful-paint", entryType: "paint", startTime: 507.80000002123415, duration: 0,};
复制代码

二、加载慢的原因

在页面渲染的过程,导致加载速度慢的因素可能如下:


  • 网络延时问题

  • 资源文件体积是否过大

  • 资源是否重复发送请求去加载了

  • 加载脚本的时候,渲染内容堵塞了

三、解决方案

常见的几种 SPA 首屏优化方式


  • 减小入口文件积

  • 静态资源本地缓存

  • UI 框架按需加载

  • 图片资源的压缩

  • 组件重复打包

  • 开启 GZip 压缩

  • 使用 SSR


1. 减小入口文件体积


常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加



vue-router配置路由的时候,采用动态加载路由的形式


routes:[     path: 'Blogs',    name: 'ShowBlogs',    component: () => import('./components/ShowBlogs.vue')]
复制代码


以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件


2. 静态资源本地缓存


后端返回资源问题:


  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头

  • 采用Service Worker离线缓存


前端合理利用localStorage


3. UI 框架按需加载


在日常使用UI框架,例如element-UI、或者antd,我们经常性直接引用整个UI


import ElementUI from 'element-ui'Vue.use(ElementUI)
复制代码


但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用


import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';Vue.use(Button)Vue.use(Input)Vue.use(Pagination)
复制代码


4. 组件重复打包


假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载


解决方案:在webpackconfig文件中,修改CommonsChunkPlugin的配置


minChunks: 3
复制代码


minChunks为 3 表示会把使用 3 次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件


5. 图片资源的压缩


图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素


对于所有的图片资源,我们可以进行适当的压缩


对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。


6. 开启 GZip 压缩


拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin


cnmp i compression-webpack-plugin -D
复制代码


vue.congig.js中引入并修改webpack配置


const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => { if (process.env.NODE_ENV === 'production') { // 为生产环境修改配置... config.mode = 'production' return { plugins: [new CompressionPlugin({ test: /\.js$|\.html$|\.css/, //匹配文件名 threshold: 10240, //对超过10k的数据进行压缩 deleteOriginalAssets: false //是否删除原文件 })] } }
复制代码


在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip,就发送给它gzip格式的文件 我的服务器是用express框架搭建的 只要安装一下compression就能使用


const compression = require('compression')app.use(compression())  // 在其他中间件使用之前调用
复制代码


7. 使用 SSR


SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成 html 字符串,再发送到浏览器


从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染

四、小结

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化页面渲染优化


下图是更为全面的首屏优化的方案



大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化

v-model 实现原理

我们在 vue 项目中主要使用 v-model 指令在表单 inputtextareaselect 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖(可以看成是value + input方法的语法糖),v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:


  • texttextarea 元素使用 value 属性和 input 事件

  • checkboxradio 使用 checked 属性和 change 事件

  • select 字段将 value 作为 prop 并将 change 作为事件


所以我们可以 v-model 进行如下改写:


<input v-model="sth" /><!-- 等同于 --><input :value="sth" @input="sth = $event.target.value" />
复制代码


当在input元素中使用v-model实现双数据绑定,其实就是在输入的时候触发元素的input事件,通过这个语法糖,实现了数据的双向绑定


  • 这个语法糖必须是固定的,也就是说属性必须为value,方法名必须为:input

  • 知道了v-model的原理,我们可以在自定义组件上实现v-model


//Parent<template>  {{num}}  <Child v-model="num"></template>export default {  data(){    return {      num: 0    }  }}
//Child<template> <div @click="add">Add</div></template>export default { props: ['value'], // 属性必须为value methods:{ add(){ // 方法名为input this.$emit('input', this.value + 1) } }}
复制代码


原理


会将组件的 v-model 默认转化成value+input


const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); 
// 观察输出的渲染函数:// with(this) { // return _c('el-checkbox', { // model: { // value: (check), // callback: function ($$v) { check = $$v }, // expression: "check" // } // }) // }
复制代码


// 源码位置 core/vdom/create-component.js line:155
function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing) } } else { on[event] = callback } }
复制代码


原生的 v-model,会根据标签的不同生成不同的事件和属性


const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
// with(this) { // return _c('input', { // directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], // domProps: { "value": (value) },// on: {"input": function ($event) { // if ($event.target.composing) return;// value = $event.target.value// }// }// })// }
复制代码


编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js


if (el.component) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false } else if (tag === 'select') {     genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') {     genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') {     genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') {     genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false }
复制代码


运行时:会对元素处理一些关于输入法的问题 platforms/web/runtime/directives/model.js


inserted (el, binding, vnode, oldVnode) {     if (vnode.tag === 'select') { // #6903     if (oldVnode.elm && !oldVnode.elm._vOptions) {         mergeVNodeHook(vnode, 'postpatch', () => {             directive.componentUpdated(el, binding, vnode)         })     } else {         setSelected(el, binding, vnode.context)     }    el._vOptions = [].map.call(el.options, getValue)     } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {         el._vModifiers = binding.modifiers         if (!binding.modifiers.lazy) {             el.addEventListener('compositionstart', onCompositionStart)             el.addEventListener('compositionend', onCompositionEnd)             // Safari < 10.2 & UIWebView doesn't fire compositionend when             // switching focus before confirming composition choice             // this also fixes the issue where some browsers e.g. iOS Chrome            // fires "change" instead of "input" on autocomplete.             el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */             if (isIE9) {                 el.vmodel = true             }        }    }}
复制代码

怎么缓存当前的组件?缓存后怎么更新

缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3keep-alive有比较大的更新,能说的点比较多


思路


  • 缓存用keep-alive,它的作用与用法

  • 使用细节,例如缓存指定/排除、结合routertransition

  • 组件缓存后更新可以利用activated或者beforeRouteEnter

  • 原理阐述


回答范例


  1. 开发中缓存组件使用keep-alive组件,keep-alivevue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM


<keep-alive>  <component :is="view"></component></keep-alive>
复制代码


  1. 结合属性includeexclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive


<router-view v-slot="{ Component }">  <keep-alive>    <component :is="Component"></component>  </keep-alive></router-view>
复制代码


  1. 缓存后如果要获取数据,解决方案可以有以下两种


  • beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter


beforeRouteEnter(to, from, next){  next(vm=>{    console.log(vm)    // 每次进入路由执行    vm.getData()  // 获取数据  })},
复制代码


  • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子


activated(){    this.getData() // 获取数据},
复制代码


  1. keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于componentis属性是个响应式数据,因此只要它变化,keep-aliverender函数就会重新执行

Vue 组件渲染和更新过程

渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用$mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是diff算法


二、如何解决

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


  • 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 组件化的理解

  • 组件化开发能大幅提高开发效率、测试性、复用性等

  • 常用的组件化技术:属性、自定义事件、插槽

  • 降低更新频率,只重新渲染变化的组件

  • 组件的特点:高内聚、低耦合、单向数据流

Vue 组件为什么只能有一个根元素

vue3中没有问题


Vue.createApp({  components: {    comp: {      template: `        <div>root1</div>        <div>root2</div>      `    }  }}).mount('#app')
复制代码


  1. vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。

  2. 之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom

  3. vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新

Vue 的事件绑定原理

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上使用原生事件,需要加.native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。


$on$emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器


EventEmitter(发布订阅模式--简单版)


// 手写发布订阅模式 EventEmitterclass EventEmitter {  constructor() {    this.events = {};  }  // 实现订阅  on(type, callBack) {    if (!this.events) this.events = Object.create(null);
if (!this.events[type]) { this.events[type] = [callBack]; } else { this.events[type].push(callBack); } } // 删除订阅 off(type, callBack) { if (!this.events[type]) return; this.events[type] = this.events[type].filter(item => { return item !== callBack; }); } // 只执行一次订阅事件 once(type, callBack) { function fn() { callBack(); this.off(type, fn); } this.on(type, fn); } // 触发事件 emit(type, ...rest) { this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest)); }}

// 使用如下const event = new EventEmitter();
const handle = (...rest) => { console.log(rest);};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => { console.log(123456);});event.emit("dbClick");event.emit("dbClick");
复制代码


源码分析



  1. 原生 dom 的绑定


  • Vue 在创建真是 dom 时会调用 createElm ,默认会调用 invokeCreateHooks

  • 会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners 方法,内部会传入 add 方法


function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {     if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {         return     }    const on = vnode.data.on || {}     const oldOn = oldVnode.data.on || {}     target = vnode.elm normalizeEvents(on)     updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)     target = undefined }function add ( name: string, handler: Function, capture: boolean, passive: boolean ) {    target.addEventListener( // 给当前的dom添加事件         name,         handler,         supportsPassive ? { capture, passive } : capture     ) }
复制代码


vue 中绑定事件是直接绑定给真实 dom 元素的


  1. 组件中绑定事件


export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) {    target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)    target = undefined }function add (event, fn) {     target.$on(event, fn) }
复制代码


组件绑定事件是通过 vue 中自定义的 $on 方法来实现的

为什么 Vue 采用异步渲染

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



源码相关


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


update () { /* istanbul ignore else */     if (this.lazy) {         this.dirty = true     }     else if (this.sync) {         this.run()     }     else {         queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新     }}
export function queueWatcher (watcher: Watcher) { const id = watcher.id // 会对相同的watcher进行过滤 if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新 } } }
复制代码

watch 原理

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


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

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

说说你对 slot 的理解?slot 使用场景有哪些

一、slot 是什么

在 HTML 中 slot 元素 ,作为 Web Components 技术套件的一部分,是 Web 组件内的一个占位符


该占位符可以在后期使用自己的标记语言填充


举个栗子


<template id="element-details-template">  <slot name="element-name">Slot template</slot></template><element-details>  <span slot="element-name">1</span></element-details><element-details>  <span slot="element-name">2</span></element-details>
复制代码


template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,


customElements.define('element-details',  class extends HTMLElement {    constructor() {      super();      const template = document        .getElementById('element-details-template')        .content;      const shadowRoot = this.attachShadow({mode: 'open'})        .appendChild(template.cloneNode(true));  }})
复制代码


Vue中的概念也是如此


Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口

二、使用场景

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理


如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情


通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用


比如布局组件、表格列、下拉选、弹框显示内容等


SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端


SSR 的优势:


  • 更好的 SEO

  • 首屏加载速度更快


SSR 的缺点:


  • 开发条件会受到限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子;

  • 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境;

  • 更多的服务端负载。

action 与 mutation 的区别

  • mutation 是同步更新, $watch 严格模式下会报错

  • action 是异步操作,可以获取数据后调用 mutation 提交最终数据


前端vue面试题详细解答

slot 是什么?有什么作用?原理是什么?

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。


  • 默认插槽:又名匿名查抄,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。

  • 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。

  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。


实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

Vue 的生命周期方法有哪些

  1. Vue 实例有一个完整的生命周期,也就是从开始创建初始化数据编译模版挂载Dom -> 渲染更新 -> 渲染卸载等一系列过程,我们称这是Vue的生命周期

  2. Vue生命周期总共分为 8 个阶段创建前/后载入前/后更新前/后销毁前/后


beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activated deactivated



其他几个生命周期



  1. 要掌握每个生命周期内部可以做什么事


  • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务

  • created 组件初始化完毕,可以访问各种数据,获取接口数据等

  • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上

  • mounted 实例已经挂载完成,可以进行一些DOM操作

  • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。

  • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。

  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件

  • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消

  • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器



<div id="app">{{name}}</div><script>    const vm = new Vue({        data(){            return {name:'poetries'}        },        el: '#app',        beforeCreate(){            // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。            console.log('beforeCreate');        },        created(){            // 属性和方法的运算, watch/event 事件回调。这里没有$el            console.log('created')        },        beforeMount(){            // 相关的 render 函数首次被调用。            console.log('beforeMount')        },        mounted(){            // 被新创建的 vm.$el 替换            console.log('mounted')        },        beforeUpdate(){            //  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。            console.log('beforeUpdate')        },        updated(){            //  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。            console.log('updated')        },        beforeDestroy(){            // 实例销毁之前调用 实例仍然完全可用            console.log('beforeDestroy')        },        destroyed(){             // 所有东西都会解绑定,所有的事件监听器会被移除            console.log('destroyed')        }    });    setTimeout(() => {        vm.name = 'poetry';        setTimeout(() => {            vm.$destroy()          }, 1000);    }, 1000);</script>
复制代码


  1. 组合式 API 生命周期钩子


你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。


下表包含如何在 setup() 内部调用生命周期钩子:



因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写


export default {  setup() {    // mounted    onMounted(() => {      console.log('Component is mounted!')    })  }}
复制代码


setupcreated谁先执行?


  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好

  • setup:在beforeCreatecreated之间执行

  • created:组件被创建出来,组件的methodsdata已经初始化好了


由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined


import { ref } from "vue"export default {  // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去  setup(){    let count = ref(1)    function myFn(){      count.value +=1    }    return {count,myFn}  },
}
复制代码


  1. 其他问题


  • 什么是 vue 生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载 Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

  • vue 生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个 Vue 实例的过程时更容易形成好的逻辑。

  • vue 生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。

  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted

  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created

  • DOM 渲染在哪个周期中就已经完成?mounted中,

  • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted


  mounted: function () {      this.$nextTick(function () {          // Code that will run only after the          // entire view has been rendered      })    }
复制代码




### Vue 单页应用与多页应用的区别
**概念:**
- SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。- MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。


### Vue的性能优化有哪些
**(1)编码阶段**
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher- v-if和v-for不能连用- 如果需要使用v-for给每项元素绑定事件时使用事件代理- SPA 页面采用keep-alive缓存组件- 在更多的情况下,使用v-if替代v-show- key保证唯一- 使用路由懒加载、异步组件- 防抖、节流- 第三方模块按需导入- 长列表滚动到可视区域动态加载- 图片懒加载
**(2)SEO优化**
- 预渲染- 服务端渲染SSR
**(3)打包优化**
- 压缩代码- Tree Shaking/Scope Hoisting- 使用cdn加载第三方模块- 多线程打包happypack- splitChunks抽离公共文件- sourceMap优化
**(4)用户体验**
- 骨架屏- PWA- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。


## 如何理解Vue中模板编译原理
> `Vue` 的编译过程就是将 `template` 转化为 `render` 函数的过程
* **解析生成AST树** 将`template`模板转化成`AST`语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理* **标记优化** 对静态语法做静态标记 `markup`(静态节点如`div`下有`p`标签内容不会变化) `diff`来做优化 静态节点跳过`diff`操作 * `Vue`的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的`DOM`也不会变化。那么优化过程就是深度遍历`AST`树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用 * 等待后续节点更新,如果是静态的,不会在比较`children`了* **代码生成** 编译的最后一步是将优化后的`AST`树转换为可执行的代码
回答范例
**思路**
* 引入`vue`编译器概念* 说明编译器的必要性* 阐述编译器工作流程
**回答范例**
1. `Vue`中有个独特的编译器模块,称为`compiler`,它的主要作用是将用户编写的`template`编译为`js`中可执行的`render`函数。2. 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用`HTML`来编写视图,直观且高效。手写`render`函数不仅效率底下,而且失去了编译期的优化能力。3. 在`Vue`中编译器会先对`template`进行解析,这一步称为`parse`,结束之后会得到一个`JS`对象,我们称为 **抽象语法树AST** ,然后是对`AST`进行深加工的转换过程,这一步成为`transform`,最后将前面得到的`AST`生成为`JS`代码,也就是`render`函数
**可能的追问**
1. `Vue`中编译器何时执行?
![](https://s.poetries.work/uploads/2022/08/d1162df23e6b6fa4.png)
> 在 `new Vue()`之后。 `Vue` 会调用 `_init` 函数进行初始化,也就是这里的 i`nit` 过程,它会初始化生命周期、事件、 `props`、 `methods`、 `data`、 `computed` 与 `watch`等。其中最重要的是通过 `Object.defineProperty` 设置 `setter` 与 `getter` 函数,用来实现「响应式」以及「依赖收集」
* 初始化之后调用 `$mount` 会挂载组件,如果是运行时编译,即不存在 `render function` 但是存在 `template` 的情况,需要进行「编译」步骤
* `compile`编译可以分成 `parse`、`optimize` 与 `generate` 三个阶段,最终需要得到 `render function`2. `React`有没有编译器?
`react` 使用`babel`将`JSX`语法解析```html<div id="app"></div><script> let vm = new Vue({ el: '#app', template: `<div> // <span>hello world</span> 是静态节点 <span>hello world</span> // <p>{{name}}</p> 是动态节点 <p>{{name}}</p> </div>`, data() { return { name: 'test' } } });</script>
复制代码


源码分析


export function compileToFunctions(template) {  // 我们需要把html字符串变成render函数  // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法  // 很多库都运用到了ast 比如 webpack babel eslint等等  let ast = parse(template);  // 2.优化静态节点:对ast树进行标记,标记静态节点    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 实例挂载的过程中发生了什么

简单

TIP


分析


挂载过程完成了最重要的两件事:


  • 初始化

  • 建立更新机制


把这两件事说清楚即可!


回答范例


  1. 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化建立更新机制

  2. 初始化会创建组件实例、初始化组件状态,创建各种响应式数据

  3. 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数


来看一下源码,在src/core/instance/index.js


function Vue (options) {  if (process.env.NODE_ENV !== 'production' &&    !(this instanceof Vue)  ) {    warn('Vue is a constructor and should be called with the `new` keyword')  }  this._init(options)}
复制代码


可以看到 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法, 该方法在 src/core/instance/init.js 中定义


Vue.prototype._init = function (options?: Object) {  const vm: Component = this  // a uid  vm._uid = uid++
let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) }
// a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
/* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) }
if (vm.$options.el) { vm.$mount(vm.$options.el) }}
复制代码


Vue 初始化主要就干了几件事情,合并配置初始化生命周期初始化事件中心初始化渲染初始化 datapropscomputedwatcher

Vue 中如何检测数组变化

前言


Vue 不能检测到以下数组的变动:


  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

  • 当你修改数组的长度时,例如:vm.items.length = newLength


Vue 提供了以下操作方法


// Vue.setVue.set(vm.items, indexOfItem, newValue)// vm.$set,Vue.set的一个别名vm.$set(vm.items, indexOfItem, newValue)// Array.prototype.splicevm.items.splice(indexOfItem, 1, newValue)
复制代码


分析


数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)


所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新


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

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


原理


Vuedata 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api 时,可以通知依赖更新,如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。



手写简版分析


let oldArray = Object.create(Array.prototype);['shift', 'unshift', 'push', 'pop', 'reverse','sort'].forEach(method => {    oldArray[method] = function() { // 这里可以触发页面更新逻辑        console.log('method', method)        Array.prototype[method].call(this,...arguments);    }});let arr = [1,2,3];arr.__proto__ = oldArray;arr.unshift(4);
复制代码


源码分析


// 拿到数组原型拷贝一份const arrayProto = Array.prototype // 然后将arrayMethods继承自数组原型// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
methodsToPatch.forEach(function (method) { // 重写原型方法 const original = arrayProto[method] // 调用原数组的方法
def(arrayMethods, method, function mutator (...args) { // 这里保留原型方法的执行结果 const result = original.apply(this, args) // 这句话是关键 // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例 const ob = this.__ob__
// 这里的标志就是代表数组有新增操作 let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测 if (inserted) ob.observeArray(inserted)
ob.dep.notify() // 当调用数组方法后,手动通知视图更新
return result }) })
this.observeArray(value) // 进行深度监控
复制代码


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

双向数据绑定的原理

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:


  1. 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化

  2. compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

  3. Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个 update()方法 ③待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。

  4. MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

Vue 修饰符有哪些

事件修饰符


  • .stop 阻止事件继续传播

  • .prevent 阻止标签默认行为

  • .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理

  • .self 只当在 event.target 是当前元素自身时触发处理函数

  • .once 事件将只会触发一次

  • .passive 告诉浏览器你不想阻止事件的默认行为


v-model 的修饰符


  • .lazy 通过这个修饰符,转变为在 change 事件再同步

  • .number 自动将用户的输入值转化为数值类型

  • .trim 自动过滤用户输入的首尾空格


键盘事件的修饰符


  • .enter

  • .tab

  • .delete (捕获“删除”和“退格”键)

  • .esc

  • .space

  • .up

  • .down

  • .left

  • .right


系统修饰键


  • .ctrl

  • .alt

  • .shift

  • .meta


鼠标按钮修饰符


  • .left

  • .right

  • .middle

Vue 的基本原理

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

Vue complier 实现

  • 模板解析这种事,本质是将数据转化为一段 html ,最开始出现在后端,经过各种处理吐给前端。随着各种 mv* 的兴起,模板解析交由前端处理。

  • 总的来说,Vue complier 是将 template 转化成一个 render 字符串。


可以简单理解成以下步骤:


  • parse 过程,将 template 利用正则转化成AST 抽象语法树。

  • optimize 过程,标记静态节点,后 diff 过程跳过静态节点,提升性能。

  • generate 过程,生成 render 字符串

vue 初始化页面闪动问题

使用 vue 开发时,在 vue 初始化之前,由于 div 是不归 vue 管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。


首先:在 css 里加上以下代码:


[v-cloak] {    display: none;}
复制代码


如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"

对 React 和 Vue 的理解,它们的异同

相似之处:


  • 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;

  • 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;

  • 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能;

  • 都有 props 的概念,允许组件间的数据传递;

  • 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。


不同之处 :


1)数据流


Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流


2)虚拟 DOM


Vue2.x 开始引入"Virtual DOM",消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。


  • Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。

  • 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。


3)组件化


React 与 Vue 最大的不同是模板的编写。


  • Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。

  • React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。


具体来讲:React 中 render 函数是支持闭包特性的,所以 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 4)监听数据变化的实现原理不同


  • Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能

  • React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。


5)高阶组件


react 可以通过高阶组件(HOC)来扩展,而 Vue 需要通过 mixins 来扩展。


高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不能采用 HOC 来实现。


6)构建工具


两者都有自己的构建工具:


  • React ==> Create React APP

  • Vue ==> vue-cli


7)跨平台


  • React ==> React Native

  • Vue ==> Weex

能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?

(1)hash 模式的实现原理


早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':


https://www.word.com#search
复制代码


hash 路由模式的实现主要是基于下面几个特性:


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


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


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

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


(2)history 模式的实现原理


HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:


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


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


  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;

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

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

template 和 jsx 的有什么分别?

对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用vue-loader编译.vue 文件,内部依赖的vue-template-compiler模块,在 webpack 构建过程中,将 template 预编译成 render 函数。与 react 类似,在添加了 jsx 的语法糖解析器babel-plugin-transform-vue-jsx之后,就可以直接手写 render 函数。


所以,template 和 jsx 的都是 render 的一种表现形式,不同的是:JSX 相对于 template 而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。

nextTick 使用场景和原理

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法


相关代码如下


let callbacks = [];let pending = false;function flushCallbacks() {  pending = false; //把标志还原为false  // 依次执行回调  for (let i = 0; i < callbacks.length; i++) {    callbacks[i]();  }}let timerFunc; //定义异步方法  采用优雅降级if (typeof Promise !== "undefined") {  // 如果支持promise  const p = Promise.resolve();  timerFunc = () => {    p.then(flushCallbacks);  };} else if (typeof MutationObserver !== "undefined") {  // MutationObserver 主要是监听dom变化 也是一个异步方法  let counter = 1;  const observer = new MutationObserver(flushCallbacks);  const textNode = document.createTextNode(String(counter));  observer.observe(textNode, {    characterData: true,  });  timerFunc = () => {    counter = (counter + 1) % 2;    textNode.data = String(counter);  };} else if (typeof setImmediate !== "undefined") {  // 如果前面都不支持 判断setImmediate  timerFunc = () => {    setImmediate(flushCallbacks);  };} else {  // 最后降级采用setTimeout  timerFunc = () => {    setTimeout(flushCallbacks, 0);  };}
export function nextTick(cb) { // 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组 callbacks.push(cb); if (!pending) { // 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false pending = true; timerFunc(); }}
复制代码

你有对 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 中使用了哪些设计模式

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


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


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


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


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


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


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


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

用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
哪些vue面试题是经常会被问到的_Vue_bb_xiaxia1998_InfoQ写作社区