写点什么

美团前端 vue 面试题(边面边更)

作者:bb_xiaxia1998
  • 2022-10-17
    浙江
  • 本文字数:15510 字

    阅读完需:约 1 分钟

Vue 修饰符有哪些

vue 中修饰符分为以下五种

  • 表单修饰符

  • 事件修饰符

  • 鼠标按键修饰符

  • 键值修饰符

  • v-bind修饰符


1. 表单修饰符


在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model


关于表单的修饰符有如下:


  • lazy


在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步


<input type="text" v-model.lazy="value"><p>{{value}}</p>
复制代码


  • trim


自动过滤用户输入的首空格字符,而中间的空格不会过滤


<input type="text" v-model.trim="value">
复制代码


  • number


自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值


<input v-model.number="age" type="number">
复制代码


2. 事件修饰符


事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符


  • .stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法


<div @click="shout(2)">  <button @click.stop="shout(1)">ok</button></div>//只输出1
复制代码


  • .prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法


<form v-on:submit.prevent="onSubmit"></form>
复制代码


  • .capture 使用事件捕获模式,使事件触发从包含这个元素的顶层开始往下触发


<div @click.capture="shout(1)">    obj1<div @click.capture="shout(2)">    obj2<div @click="shout(3)">    obj3<div @click="shout(4)">    obj4</div></div></div></div>// 输出结构: 1 2 4 3 
复制代码


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


<div v-on:click.self="doThat">...</div>
复制代码


使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击


  • .once 绑定了事件以后只能触发一次,第二次就不会触发


<button @click.once="shout(1)">ok</button>
复制代码


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


在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符


<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --><!-- 而不会等待 `onScroll` 完成  --><!-- 这其中包含 `event.preventDefault()` 的情况 --><div v-on:scroll.passive="onScroll">...</div>
复制代码


  • 不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。

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


  • native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件


<my-component v-on:click.native="doSomething"></my-component>
<!-- 使用.native修饰符来操作普通HTML标签是会令事件失效的 -->
复制代码


3. 鼠标按钮修饰符


鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:


  • .left 左键点击

  • .right 右键点击

  • .middle 中键点击


<button @click.left="shout(1)">ok</button><button @click.right="shout(1)">ok</button><button @click.middle="shout(1)">ok</button>
复制代码


4. 键盘事件的修饰符


键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的,有如下:


keyCode存在很多,但 vue 为我们提供了别名,分为以下两种:


  • 普通键entertabdeletespaceescupdownleftright...)

  • 系统修饰键ctrlaltmetashift...)


<!-- 只有按键为keyCode的时候才触发 --><input type="text" @keyup.keyCode="shout()">
复制代码


还可以通过以下方式自定义一些全局的键盘码别名


Vue.config.keyCodes.f2 = 113
复制代码


5. v-bind 修饰符


v-bind修饰符主要是为属性进行操作,用来分别有如下:


  • async 能对props进行一个双向绑定


//父组件<comp :myMessage.sync="bar"></comp> //子组件this.$emit('update:myMessage',params);
复制代码


以上这种方法相当于以下的简写


//父亲组件<comp :myMessage="bar" @update:myMessage="func"></comp>func(e){ this.bar = e;}
//子组件jsfunc2(){ this.$emit('update:myMessage',params);}
复制代码


使用async需要注意以下两点:


  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致

  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用

  • prop 设置自定义标签属性,避免暴露数据,防止污染 HTML 结构


<input id="uid" title="title1" value="1" :index.prop="index">
复制代码


  • camel 将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox


<svg :viewBox="viewBox"></svg>
复制代码

应用场景

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:


  • .stop:阻止事件冒泡

  • .native:绑定原生事件

  • .once:事件只执行一次

  • .self :将事件绑定在自身身上,相当于阻止事件冒泡

  • .prevent:阻止默认事件

  • .caption:用于事件捕获

  • .once:只触发一次

  • .keyCode:监听特定键盘按下

  • .right:右键

生命周期钩子是如何实现的

Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)


相关代码如下


export function callHook(vm, hook) {  // 依次执行生命周期对应的方法  const handlers = vm.$options[hook];  if (handlers) {    for (let i = 0; i < handlers.length; i++) {      handlers[i].call(vm); //生命周期里面的this指向当前实例    }  }}
// 调用的时候Vue.prototype._init = function (options) { const vm = this; vm.$options = mergeOptions(vm.constructor.options, options); callHook(vm, "beforeCreate"); //初始化数据之前 // 初始化状态 initState(vm); callHook(vm, "created"); //初始化数据之后 if (vm.$options.el) { vm.$mount(vm.$options.el); }};
复制代码

说一下 Vue 的生命周期

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


  1. beforeCreate(创建前):数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到 data、computed、watch、methods 上的方法和数据。

  2. created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。

  3. beforeMount(挂载前):在挂载开始之前被调用,相关的 render 函数首次被调用。实例已完成以下的配置:编译模板,把 data 里面的数据和模板生成 html。此时还没有挂载 html 到页面上。

  4. mounted(挂载后):在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的 html 内容替换 el 属性指向的 DOM 对象。完成模板中的 html 渲染到 html 页面中。此过程中进行 ajax 交互。

  5. beforeUpdate(更新前):响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。

  6. updated(更新后) :在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

  7. beforeDestroy(销毁前):实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。

  8. destroyed(销毁后):实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。


另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。

Vue 组件通讯有哪几种方式

  1. props 和emit 触发事件来做到的

  2. children 获取当前组件的父组件和当前组件的子组件

  3. listeners A->B->C。Vue 2.4 开始提供了listeners 来解决这个问题

  4. 父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)

  5. $refs 获取组件实例

  6. envetBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式

  7. vuex 状态管理

什么是 mixin ?

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

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

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

了解 nextTick 吗?

异步方法,异步渲染最后一步,与 JS 事件循环联系紧密。主要使用了宏任务微任务(setTimeoutpromise那些),定义了一个异步方法,多次调用nextTick会将方法存入队列,通过异步方法清空当前队列。

Vue 生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法

  • 内部会对钩子函数进行处理,将钩子函数维护成数组的形式


Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)


<script>    // Vue.options 中会存放所有全局属性
// 会用自身的 + Vue.options 中的属性进行合并 // Vue.mixin({ // beforeCreate() { // console.log('before 0') // }, // }) debugger; const vm = new Vue({ el: '#app', beforeCreate: [ function() { console.log('before 1') }, function() { console.log('before 2') } ] }); console.log(vm);</script>
复制代码


相关代码如下


export function callHook(vm, hook) {  // 依次执行生命周期对应的方法  const handlers = vm.$options[hook];  if (handlers) {    for (let i = 0; i < handlers.length; i++) {      handlers[i].call(vm); //生命周期里面的this指向当前实例    }  }}
// 调用的时候Vue.prototype._init = function (options) { const vm = this; vm.$options = mergeOptions(vm.constructor.options, options); callHook(vm, "beforeCreate"); //初始化数据之前 // 初始化状态 initState(vm); callHook(vm, "created"); //初始化数据之后 if (vm.$options.el) { vm.$mount(vm.$options.el); }};
// 销毁实例实现Vue.prototype.$destory = function() { // 触发钩子 callHook(vm, 'beforeDestory') // 自身及子节点 remove() // 删除依赖 watcher.teardown() // 删除监听 vm.$off() // 触发钩子 callHook(vm, 'destoryed')}
复制代码


原理流程图


v-for 为什么要加 key

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


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


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

Vue.js 的 template 编译

简而言之,就是先转化成 AST 树,再得到的 render 函数返回 VNode(Vue 的虚拟 DOM 节点),详细步骤如下:


首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。


然后,AST 会经过 generate(将 AST 语法树转化成 render funtion 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有(标签名、子节点、文本等等)


参考:前端vue面试题详细解答

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

写过自定义指令吗?原理是什么

回答范例


  1. Vue有一组默认指令,比如v-modelv-for,同时Vue也允许用户注册自定义指令来扩展 Vue 能力

  2. 自定义指令主要完成一些可复用低层级DOM操作

  3. 使用自定义指令分为定义、注册和使用三步:


  • 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在mounted 和updated时执行

  • 注册自定义指令类似组件,可以使用app.directive()全局注册,使用{directives:{xxx}}局部注册

  • 使用时在注册名称前加上v-即可,比如v-focus


  1. 我在项目中常用到一些自定义指令,例如:


  • 复制粘贴 v-copy

  • 长按 v-longpress

  • 防抖 v-debounce

  • 图片懒加载 v-lazy

  • 按钮权限 v-premission

  • 页面水印 v-waterMarker

  • 拖拽指令 v-draggable


  1. vue3中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在v3.2之后,可以在setup中以一个小写v开头方便的定义自定义指令,更简单了

基本使用

当 Vue 中的核心内置指令不能够满足我们的需求时,我们可以定制自定义的指令用来满足开发的需求


我们看到的v-开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令


// 指令使用的几种方式://会实例化一个指令,但这个指令没有参数 `v-xxx`
// -- 将值传到指令中`v-xxx="value"`
// -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"``v-xxx="'string'"`
// -- 传参数(`arg`),如`v-bind:class="className"``v-xxx:arg="value"`
// -- 使用修饰符(`modifier`)`v-xxx:arg.modifier="value"`
复制代码


注册一个自定义指令有全局注册与局部注册


// 全局注册注册主要是用过Vue.directive方法进行注册// Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函数// 注册一个全局自定义指令 `v-focus`Vue.directive('focus', {  // 当被绑定的元素插入到 DOM 中时……  inserted: function (el) {    // 聚焦元素    el.focus()  // 页面加载完成之后自动让输入框获取到焦点的小功能  }})
// 局部注册通过在组件options选项中设置directive属性directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能 } }}
// 然后你可以在模板中任何元素上使用新的 v-focus property,如下:
<input v-focus />
复制代码


钩子函数


  1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。

  4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。

  5. unbind:只调用一次,指令与元素解绑时调用。


所有的钩子函数的参数都有以下:


  • el:指令所绑定的元素,可以用来直接操作 DOM

  • binding:一个对象,包含以下 property

  • name:指令名,不包括 v- 前缀。

  • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2

  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。

  • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"

  • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"

  • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

  • vnodeVue 编译生成的虚拟节点

  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用


除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行


<div v-demo="{ color: 'white', text: 'hello!' }"></div><script>    Vue.directive('demo', function (el, binding) {    console.log(binding.value.color) // "white"    console.log(binding.value.text)  // "hello!"    })</script>
复制代码


应用场景


使用自定义组件组件可以满足我们日常一些场景,这里给出几个自定义组件的案例:


  1. 防抖


// 1.设置v-throttle自定义指令Vue.directive('throttle', {  bind: (el, binding) => {    let throttleTime = binding.value; // 防抖时间    if (!throttleTime) { // 用户若不设置防抖时间,则默认2s      throttleTime = 2000;    }    let cbFun;    el.addEventListener('click', event => {      if (!cbFun) { // 第一次执行        cbFun = setTimeout(() => {          cbFun = null;        }, throttleTime);      } else {        event && event.stopImmediatePropagation();      }    }, true);  },});// 2.为button标签设置v-throttle自定义指令<button @click="sayHello" v-throttle>提交</button>
复制代码


  1. 图片懒加载


设置一个v-lazy自定义组件完成图片懒加载


const LazyLoad = {    // install方法    install(Vue,options){       // 代替图片的loading图        let defaultSrc = options.default;        Vue.directive('lazy',{            bind(el,binding){                LazyLoad.init(el,binding.value,defaultSrc);            },            inserted(el){                // 兼容处理                if('InterpObserver' in window){                    LazyLoad.observe(el);                }else{                    LazyLoad.listenerScroll(el);                }
}, }) }, // 初始化 init(el,val,def){ // src 储存真实src el.setAttribute('src',val); // 设置src为loading图 el.setAttribute('src',def); }, // 利用InterpObserver监听el observe(el){ let io = new InterpObserver(entries => { let realSrc = el.dataset.src; if(entries[0].isIntersecting){ if(realSrc){ el.src = realSrc; el.removeAttribute('src'); } } }); io.observe(el); }, // 监听scroll事件 listenerScroll(el){ let handler = LazyLoad.throttle(LazyLoad.load,300); LazyLoad.load(el); window.addEventListener('scroll',() => { handler(el); }); }, // 加载真实图片 load(el){ let windowHeight = document.documentElement.clientHeight let elTop = el.getBoundingClientRect().top; let elBtm = el.getBoundingClientRect().bottom; let realSrc = el.dataset.src; if(elTop - windowHeight<0&&elBtm > 0){ if(realSrc){ el.src = realSrc; el.removeAttribute('src'); } } }, // 节流 throttle(fn,delay){ let timer; let prevTime; return function(...args){ let currTime = Date.now(); let context = this; if(!prevTime) prevTime = currTime; clearTimeout(timer);
if(currTime - prevTime > delay){ prevTime = currTime; fn.apply(context,args); clearTimeout(timer); return; }
timer = setTimeout(function(){ prevTime = Date.now(); timer = null; fn.apply(context,args); },delay); } }
}export default LazyLoad;
复制代码


  1. 一键 Copy 的功能


import { Message } from 'ant-design-vue';
const vCopy = { // /* bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置 el: 作用的 dom 对象 value: 传给指令的值,也就是我们要 copy 的值 */ bind(el, { value }) { el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到 el.handler = () => { if (!el.$value) { // 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意 Message.warning('无复制内容'); return; } // 动态创建 textarea 标签 const textarea = document.createElement('textarea'); // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 textarea.readOnly = 'readonly'; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; // 将要 copy 的值赋给 textarea 标签的 value 属性 textarea.value = el.$value; // 将 textarea 插入到 body 中 document.body.appendChild(textarea); // 选中值并复制 textarea.select(); // textarea.setSelectionRange(0, textarea.value.length); const result = document.execCommand('Copy'); if (result) { Message.success('复制成功'); } document.body.removeChild(textarea); }; // 绑定点击事件,就是所谓的一键 copy 啦 el.addEventListener('click', el.handler); }, // 当传进来的值更新的时候触发 componentUpdated(el, { value }) { el.$value = value; }, // 指令与元素解绑的时候,移除事件绑定 unbind(el) { el.removeEventListener('click', el.handler); },};
export default vCopy;
复制代码


  1. 拖拽


<div ref="a" id="bg" v-drag></div>
directives: { drag: { bind() {}, inserted(el) { el.onmousedown = (e) => { let x = e.clientX - el.offsetLeft; let y = e.clientY - el.offsetTop; document.onmousemove = (e) => { let xx = e.clientX - x + "px"; let yy = e.clientY - y + "px"; el.style.left = xx; el.style.top = yy; }; el.onmouseup = (e) => { document.onmousemove = null; }; }; }, }, }
复制代码

原理

  • 指令本质上是装饰器,是 vueHTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。

  • 自定义指令有五个生命周期(也叫钩子函数),分别是 bindinsertedupdatecomponentUpdatedunbind


原理


  1. 在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性

  2. 通过 genDirectives 生成指令代码

  3. patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子

  4. 当执行指令对应钩子函数时,调用对应指令定义的方法

Vue-router 除了 router-link 怎么实现跳转

声明式导航


<router-link to="/about">Go to About</router-link>
复制代码


编程式导航


// literal string pathrouter.push('/users/1')// object with pathrouter.push({ path: '/users/1' })// named route with params to let the router build the urlrouter.push({ name: 'user', params: { username: 'test' } })
复制代码


回答范例


  • vue-router导航有两种方式:声明式导航和编程方式导航

  • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定pathnameparams等信息

  • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个 a 标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航

  • 实际上内部两者调用的导航函数是一样的

请说明 Vue 中 key 的作用和原理,谈谈你对它的理解


  • key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速

  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异

  • 尽量不要采用索引作为key

  • 如果不加key,那么vue会选择复用节点(Vue 的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug

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

  • 更快速key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n)Map的时间复杂度仅仅为O(1),比遍历方式更快。


源码如下:


function createKeyToOldIdx (children, beginIdx, endIdx) {  let i, key  const map = {}  for (i = beginIdx; i <= endIdx; ++i) {    key = children[i].key    if (isDef(key)) map[key] = i  }  return map}
复制代码


回答范例


分析


这是一道特别常见的问题,主要考查大家对虚拟DOMpatch细节的掌握程度,能够反映面试者理解层次


思路分析:


  • 给出结论,key的作用是用于优化patch性能

  • key的必要性

  • 实际使用方式

  • 总结:可从源码层面描述一下vue如何判断两个节点是否相同


回答范例:


  1. key的作用主要是为了更高效的更新虚拟DOM

  2. vuepatch过程中 判断两个节点是否是相同节点是key是一个必要条件 ,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能

  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bugvue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果

  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key标签类型(如div)等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的


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



diff 程可以概括为:oldChnewCh各有两个头尾的变量 StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是旧尾新头旧头新尾


相关代码如下


// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用function isSameVnode(oldVnode, newVnode) {  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;}
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置function makeIndexByKey(children) { let map = {}; children.forEach((item, index) => { map[item.key] = index; }); return map;}// 生成的映射表let map = makeIndexByKey(oldCh);
复制代码

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 中如何进行依赖收集?

  • 每个属性都有自己的dep属性,存放他所依赖的watcher,当属性变化之后会通知自己对应的watcher去更新

  • 默认会在初始化时调用render函数,此时会触发属性依赖收集 dep.depend

  • 当属性发生修改时会触发watcher更新dep.notify()



依赖收集简版


let obj = { name: 'poetry', age: 20 };
class Dep { constructor() { this.subs = [] // subs [watcher] } depend() { this.subs.push(Dep.target) } notify() { this.subs.forEach(watcher => watcher.update()) }}Dep.target = null;observer(obj); // 响应式属性劫持
// 依赖收集 所有属性都会增加一个dep属性,// 当渲染的时候取值了 ,这个dep属性 就会将渲染的watcher收集起来// 数据更新 会让watcher重新执行
// 观察者模式
// 渲染组件时 会创建watcherclass Watcher { constructor(render) { this.get(); } get() { Dep.target = this; render(); // 执行render Dep.target = null; } update() { this.get(); }}const render = () => { console.log(obj.name); // obj.name => get方法}
// 组件是watcher、计算属性是watchernew Watcher(render);
function observer(value) { // proxy reflect if (typeof value === 'object' && typeof value !== null) for (let key in value) { defineReactive(value, key, value[key]); }}function defineReactive(obj, key, value) { // 创建一个dep let dep = new Dep();
// 递归观察子属性 observer(value);
Object.defineProperty(obj, key, { get() { // 收集对应的key 在哪个方法(组件)中被使用 if (Dep.target) { // watcher dep.depend(); // 这里会建立 dep 和watcher的关系 } return value; }, set(newValue) { if (newValue !== value) { observer(newValue); value = newValue; // 让key对应的方法(组件重新渲染)重新执行 dep.notify() } } })}
// 模拟数据获取,触发getterobj.name = 'poetries'
// 一个属性一个dep,一个属性可以对应多个watcher(一个属性可以在任何组件中使用、在多个组件中使用)// 一个dep 对应多个watcher // 一个watcher 对应多个dep (一个视图对应多个属性)// dep 和 watcher是多对多的关系
复制代码

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

思路分析:


首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。


  • 借助hash或者 history api实现url跳转页面不刷新

  • 同时监听hashchange事件或者popstate事件处理跳转

  • 根据hash值或者state值从routes表中匹配对应component并渲染


回答范例:


一个SPA应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:


  1. 首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事


  • 保存用户传入的配置项

  • 监听hash或者popstate事件

  • 回调里根据path匹配对应路由


  1. router定义成一个Vue插件,即实现install方法,内部做两件事


  • 实现两个全局组件:router-linkrouter-view,分别实现页面跳转和内容显示

  • 定义两个全局变量:$route$router,组件内可以访问当前路由和路由器实例

Vue 中修饰符.sync 与 v-model 的区别

sync的作用


  • .sync修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model来说,sync修饰符就简单很多了

  • 一个组件上可以有多个.sync修饰符


<!-- 正常父传子 --><Son :a="num" :b="num2" />
<!-- 加上sync之后的父传子 --><Son :a.sync="num" :b.sync="num2" />
<!-- 它等价于 --><Son :a="num" :b="num2" @update:a="val=>num=val" @update:b="val=>num2=val" />
<!-- 相当于多了一个事件监听,事件名是update:a, --><!-- 回调函数中,会把接收到的值赋值给属性绑定的数据项中。 -->
复制代码



v-model的工作原理


<com1 v-model="num"></com1><!-- 等价于 --><com1 :value="num" @input="(val)=>num=val"></com1>
复制代码


  • 相同点

  • 都是语法糖,都可以实现父子组件中的数据的双向通信

  • 区别点

  • 格式不同:v-model="num", :num.sync="num"

  • v-model: @input + value

  • :num.sync: @update:num

  • v-model只能用一次;.sync可以有多个

vue 初始化页面闪动问题

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


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


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


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

vue 如何监听对象或者数组某个属性的变化

当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为 Object.defineProperty()限制,监听不到变化。


解决方式:


  • this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么 value)


this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
复制代码


  • 调用以下几个数组的方法


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


vue 源码里缓存了 array 的原型链,然后重写了这几个方法,触发这几个方法的时候会 observer 数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用 splice 方法会比较好自定义,因为 splice 可以在数组的任何位置进行删除/添加操作


vm.$set 的实现原理是:


  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

Vuex 为什么要分模块并且加命名空间

  • 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块

  • 命名空间 :默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名


用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
美团前端vue面试题(边面边更)_Vue_bb_xiaxia1998_InfoQ写作社区