写点什么

滴滴前端常考 vue 面试题

作者:yyds2026
  • 2023-02-28
    浙江
  • 本文字数:8719 字

    阅读完需:约 29 分钟

Computed 和 Methods 的区别

可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的


不同点:


  • computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;

  • method 调用总会执行该函数。

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

(1)param 方式


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

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

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


1)路由定义


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


2)路由跳转


// 方法1:<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
// 方法2:this.$router.push({name:'users',params:{uname:wade}})
// 方法3:this.$router.push('/user/' + wade)
复制代码


3)参数获取通过 $route.params.userid 获取传递的值


(2)query 方式


  • 配置路由格式:/router,也就是普通配置

  • 传递的方式:对象中使用 query 的 key 作为传递方式

  • 传递后形成的路径:/route?id=123


1)路由定义


//方式1:直接在router-link 标签上以对象的形式<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式<button @click='profileClick'>我的</button>
profileClick(){ this.$router.push({ path: "/profile", query: { name: "kobi", age: "28", height: 198 } });}
复制代码


2)跳转方法


// 方法1:<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:this.$router.push('/user?uname=' + jsmes)
复制代码


3)获取参数


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

Class 与 Style 如何动态绑定

Class 可以通过对象语法和数组语法进行动态绑定


对象语法:


<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: { isActive: true, hasError: false}
复制代码


数组语法:


<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: { activeClass: 'active', errorClass: 'text-danger'}
复制代码


Style 也可以通过对象语法和数组语法进行动态绑定


对象语法:


<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: { activeColor: 'red', fontSize: 30}
复制代码


数组语法:


<div v-bind:style="[styleColor, styleSize]"></div>
data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' }}
复制代码

ref 和 reactive 异同

这是Vue3数据响应式中非常重要的两个概念,跟我们写代码关系也很大


const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
const obj = reactive({ count: 0 })obj.count++
复制代码


  • ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象

  • 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式

  • 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题

  • ref返回的响应式数据在 JS 中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.valueref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref 对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。

  • reactive内部使用Proxy代理传入对象并拦截该对象各种操作,从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式

Vue Ref 的作用

  • 获取dom元素this.$refs.box

  • 获取子组件中的datathis.$refs.box.msg

  • 调用子组件中的方法this.$refs.box.open()

router-link 和 router-view 是如何起作用的

分析


vue-router中两个重要组件router-linkrouter-view,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度


回答范例


  1. vue-router中两个重要组件router-linkrouter-view,分别起到路由导航作用和组件内容渲染作用

  2. 使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。

  3. router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。


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

Vue 中封装的数组方法有哪些,其如何实现页面更新

在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:


// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 需要进行功能拓展的方法const methodsToPatch = [  "push",  "pop",  "shift",  "unshift",  "splice",  "sort",  "reverse"];
/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(method) { // 缓存原生数组方法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组功能 const result = original.apply(this, args); // 响应式处理 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 通知依赖更新 // 返回原生数组方法的执行结果 return result; });});
复制代码


简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。

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

考点: Vue 的变化侦测原理


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


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


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


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

简述 mixin、extends 的覆盖逻辑

(1)mixin 和 extends mixin 和 extends 均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。


  • mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。

  • extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。


(2)mergeOptions 的执行过程


  • 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)

  • 对未合并的选项,进行判断


if (!child._base) {  if (child.extends) {    parent = mergeOptions(parent, child.extends, vm);  }  if (child.mixins) {    for (let i = 0, l = child.mixins.length; i < l; i++) {      parent = mergeOptions(parent, child.mixins[i], vm);    }  }}
复制代码


  • 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。

  • 返回合并结果 options。

MVVM 的优缺点?

优点:


  • 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定不同的"View"上,当 View 变化的时候 Model 不可以不变,当 Model 变化的时候 View 也可以不变。你可以把⼀些视图逻辑放在⼀个 ViewModel⾥⾯,让很多 view 重⽤这段视图逻辑

  • 提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代码

  • ⾃动更新 dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动 dom 中解放


缺点:


  • Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的

  • ⼀个⼤的模块中 model 也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存

  • 对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼。

使用 Object.defineProperty() 来进行数据劫持有什么缺点?

在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。


在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用 Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。

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

对 SSR 的理解

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


SSR 的优势:


  • 更好的 SEO

  • 首屏加载速度更快


SSR 的缺点:


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

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

  • 更多的服务端负载。

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 子组件和父组件执行顺序

加载渲染过程:


  1. 父组件 beforeCreate

  2. 父组件 created

  3. 父组件 beforeMount

  4. 子组件 beforeCreate

  5. 子组件 created

  6. 子组件 beforeMount

  7. 子组件 mounted

  8. 父组件 mounted


更新过程:


  1. 父组件 beforeUpdate

  2. 子组件 beforeUpdate

  3. 子组件 updated

  4. 父组件 updated


销毁过程:


  1. 父组件 beforeDestroy

  2. 子组件 beforeDestroy

  3. 子组件 destroyed

  4. 父组件 destoryed

Vuex 有哪几种属性?

有五种,分别是 State、 Getter、Mutation 、Action、 Module


  • state => 基本数据(数据源存放地)

  • getters => 从基本数据派生出来的数据

  • mutations => 提交更改数据的方法,同步

  • actions => 像一个装饰器,包裹 mutations,使之可以异步。

  • modules => 模块化 Vuex

什么是 mixin ?

  • Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。

  • 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。

  • 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

Vue 模版编译原理

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


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

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

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

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组


push();pop();shift();unshift();splice();sort();reverse();
复制代码


由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。


Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

二、如何解决

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


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


用户头像

yyds2026

关注

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

还未添加个人简介

评论

发布
暂无评论
滴滴前端常考vue面试题_Vue_yyds2026_InfoQ写作社区