前端一面常见vue面试题汇总
Posted bbxiaxia1998
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端一面常见vue面试题汇总相关的知识,希望对你有一定的参考价值。
说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势
Object.defineProperty()
的问题主要有三个:
- 不能监听数组的变化 :无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
- 必须遍历对象的每个属性 :只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。
Proxy
可以劫持整个对象,并返回一个新的对象 - 必须深层遍历嵌套的对象
Proxy的优势如下:
- 针对对象: 针对整个对象,而不是对象的某个属性 ,所以也就不需要对
keys
进行遍历 - 支持数组:
Proxy
不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的 Proxy
的第二个参数可以有13
种拦截方:不限于apply
、ownKeys
、deleteProperty
、has
等等是Object.defineProperty
不具备的Proxy
返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty
只能遍历对象属性直接修改Proxy
作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
proxy详细使用点击查看(opens new window)
Object.defineProperty的优势如下:
兼容性好,支持
IE9
,而Proxy
的存在浏览器兼容性问题,而且无法用polyfill
磨平
defineProperty的属性值有哪些
Object.defineProperty(obj, prop, descriptor)
// obj 要定义属性的对象
// prop 要定义或修改的属性的名称
// descriptor 要定义或修改的属性描述符
Object.defineProperty(obj,"name",
value:"poetry", // 初始值
writable:true, // 该属性是否可写入
enumerable:true, // 该属性是否可被遍历得到(for...in, Object.keys等)
configurable:true, // 定该属性是否可被删除,且除writable外的其他描述符是否可被修改
get: function() ,
set: function(newVal)
)
相关代码如下
import mutableHandlers from "./baseHandlers"; // 代理相关逻辑
import isObject from "./util"; // 工具方法
export function reactive(target)
// 根据不同参数创建不同响应式对象
return createReactiveObject(target, mutableHandlers);
function createReactiveObject(target, baseHandler)
if (!isObject(target))
return target;
const observed = new Proxy(target, baseHandler);
return observed;
const get = createGetter();
const set = createSetter();
function createGetter()
return function get(target, key, receiver)
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res))
// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
return res;
;
function createSetter()
return function set(target, key, value, receiver)
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey)
console.log("属性新增", key, value);
else if (hasChanged(value, oldValue))
console.log("属性值被修改", key, value);
return result;
;
export const mutableHandlers =
get, // 当获取属性时调用此方法
set, // 当修改属性时调用此方法
;
Proxy
只会代理对象的第一层,那么Vue3
又是怎样处理这个问题的呢?
判断当前
Reflect.get的
返回值是否为Object
,如果是则再通过reactive
方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断
key
是否为当前被代理对象target
自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
组件通信
组件通信的方式如下:
(1) props / $emit
父组件通过props
向子组件传递数据,子组件通过$emit
和父组件通信
1. 父组件向子组件传值
props
只能是父组件向子组件进行传值,props
使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。props
可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。props
属性名规则:若在props
中使用驼峰形式,模板中需要使用短横线的形式
// 父组件
<template>
<div id="father">
<son :msg="msgData" :fn="myFunction"></son>
</div>
</template>
<script>
import son from "./son.vue";
export default
name: father,
data()
msgData: "父组件数据";
,
methods:
myFunction()
console.log("vue");
,
,
components: son ,
;
</script>
// 子组件
<template>
<div id="son">
<p> msg </p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default name: "son", props: ["msg", "fn"] ;
</script>
2. 子组件向父组件传值
$emit
绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on
监听并接收参数。
// 父组件
<template>
<div class="section">
<com-article
:articles="articleList"
@onEmitIndex="onEmitIndex"
></com-article>
<p> currentIndex </p>
</div>
</template>
<script>
import comArticle from "./test/article.vue";
export default
name: "comArticle",
components: comArticle ,
data()
return currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] ;
,
methods:
onEmitIndex(idx)
this.currentIndex = idx;
,
,
;
</script>
//子组件
<template>
<div>
<div
v-for="(item, index) in articles"
:key="index"
@click="emitIndex(index)"
>
item
</div>
</div>
</template>
<script>
export default
props: ["articles"],
methods:
emitIndex(index)
this.$emit("onEmitIndex", index); // 触发父组件的方法,并传递参数index
,
,
;
</script>
(2)eventBus事件总线($emit / $on
)
eventBus
事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下: (1)创建事件中心管理组件之间的通信
// event-bus.js
import Vue from \'vue\'
export const EventBus = new Vue()
(2)发送事件 假设有两个兄弟组件firstCom
和secondCom
:
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default components: firstCom, secondCom ;
</script>
在firstCom
组件中发送事件:
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import EventBus from "./event-bus.js"; // 引入事件中心
export default
data()
return num: 0 ;
,
methods:
add()
EventBus.$emit("addition", num: this.num++ );
,
,
;
</script>
(3)接收事件 在secondCom
组件中发送事件:
<template>
<div>求和: count </div>
</template>
<script>
import EventBus from "./event-bus.js";
export default
data()
return count: 0 ;
,
mounted()
EventBus.$on("addition", (param) =>
this.count = this.count + param.num;
);
,
;
</script>
在上述代码中,这就相当于将num
值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
(3)依赖注入(provide / inject)
这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide / inject
是Vue提供的两个钩子,和data
、methods
是同级的。并且provide
的书写形式和data
一样。
provide
钩子用来发送数据或方法inject
钩子用来接收数据或方法
在父组件中:
provide()
return
num: this.num
;
在子组件中:
inject: [\'num\']
还可以这样写,这样写就可以访问父组件中的所有属性:
provide()
return
app: this
;
data()
return
num: 1
;
inject: [\'app\']
console.log(this.app.num)
注意: 依赖注入所提供的属性是非响应式的。
(3)ref / $refs
这种方式也是实现父子组件之间的通信。
ref
: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
export default
data ()
return
name: \'JavaScript\'
,
methods:
sayHello ()
console.log(\'hello\')
在父组件中:
<template>
<child ref="child"></component-a>
</template>
<script>
import child from "./child.vue";
export default
components: child ,
mounted()
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
,
;
</script>
(4)$parent / $children
- 使用
$parent
可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法) - 使用
$children
可以让组件访问子组件的实例,但是,$children
并不能保证顺序,并且访问的数据也不是响应式的。
在子组件中:
<template>
<div>
<span> message </span>
<p>获取父组件的值为: parentVal </p>
</div>
</template>
<script>
export default
data()
return message: "Vue" ;
,
computed:
parentVal()
return this.$parent.msg;
,
,
;
</script>
在父组件中:
// 父组件中
<template>
<div class="hello_world">
<div> msg </div>
<child></child>
<button @click="change">点击改变子组件值</button>
</div>
</template>
<script>
import child from "./child.vue";
export default
components: child ,
data()
return msg: "Welcome" ;
,
methods:
change()
// 获取到子组件
this.$children[0].message = "JavaScript";
,
,
;
</script>
在上面的代码中,子组件获取到了父组件的parentVal
值,父组件改变了子组件中message
的值。 需要注意:
- 通过
$parent
访问到的是上一级父组件的实例,可以使用$root
来访问根组件的实例 - 在组件中使用
$children
拿到的是所有的子组件的实例,它是一个数组,并且是无序的 - 在根组件
#app
上拿$parent
得到的是new Vue()
的实例,在这实例上再拿$parent
得到的是undefined
,而在最底层的子组件拿$children
是个空数组 $children
的值是数组,而$parent
是个对象
(5)$attrs / $listeners
考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/$emit
来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue引入了$attrs / $listeners
,实现组件之间的跨代通信。
先来看一下inheritAttrs
,它的默认值true,继承所有的父组件属性除props
之外的所有属性;inheritAttrs:false
只继承class属性 。
$attrs
:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上$listeners
:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
A组件(APP.vue
):
<template>
<div id="app">
//此处监听了两个事件,可以在B组件或者C组件中直接触发
<child1
:p-child1="child1"
:p-child2="child2"
@test1="onTest1"
@test2="onTest2"
></child1>
</div>
</template>
<script>
import Child1 from "./Child1.vue";
export default
components: Child1 ,
methods:
onTest1()
console.log("test1 running");
,
onTest2()
console.log("test2 running");
,
,
;
</script>
B组件(Child1.vue
):
<template>
<div class="child-1">
<p>props: pChild1 </p>
<p>$attrs: $attrs </p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from "./Child2.vue";
export default
props: ["pChild1"],
components: Child2 ,
inheritAttrs: false,
mounted()
this.$emit("test1"); // 触发APP.vue中的test1方法
,
;
</script>
C 组件 (Child2.vue
):
<template>
<div class="child-2">
<p>props: pChild2 </p>
<p>$attrs: $attrs </p>
</div>
</template>
<script>
export default
props: ["pChild2"],
inheritAttrs: false,
mounted()
this.$emit("test2"); // 触发APP.vue中的test2方法
,
;
</script>
在上述代码中:
- C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了
$listeners
属性 - 在B组件中通过v-bind 绑定
$attrs
属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)
(6)总结
(1)父子组件间通信
- 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
- 通过 ref 属性给子组件设置一个名字。父组件通过
$refs
组件名来获得子组件,子组件通过$parent
获得父组件,这样也可以实现通信。 - 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。
(2)兄弟组件间通信
- 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
- 通过
$parent/$refs
来获取到兄弟组件,也可以进行通信。
(3)任意组件之间
- 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
mixin 和 mixins 区别
mixin
用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
Vue.mixin(
beforeCreate()
// ...逻辑 // 这种方式会影响到每个组件的 beforeCreate 钩子函数
,
);
虽然文档不建议在应用中直接使用 mixin
,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax
或者一些工具函数等等。
mixins
应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins
混入代码,比如上拉下拉加载数据这种逻辑等等。
另外需要注意的是 mixins
混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。
Vue 的父子组件生命周期钩子函数执行顺序
- 渲染顺序 :先父后子,完成顺序:先子后父
- 更新顺序 :父更新导致子更新,子更新完成后父
- 销毁顺序 :先父后子,完成顺序:先子后父
加载渲染过程
父 beforeCreate
->父 created
->父 beforeMount
->子 beforeCreate
->子 created
->子 beforeMount
->子 mounted
->父 mounted
。子组件先挂载,然后到父组件
子组件更新过程
父 beforeUpdate
->子 beforeUpdate
->子 updated
->父 updated
父组件更新过程
父 beforeUpdate
->父 updated
销毁过程
父 beforeDestroy
->子 beforeDestroy
->子 destroyed
->父 destroyed
之所以会这样是因为
Vue
创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted
钩子到队列,等到patch
结束再执行它们,可见子组件的mounted
钩子是先进入到队列中的,因此等到patch
结束执行这些钩子时也先执行。
function patch (oldVnode, vnode, hydrating, removeOnly)
if (isUndef(vnode))
if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return
let isInitialPatch = false
const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组 // somthing ...
createElm(
vnode,
insertedVnodeQueue, oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)// somthing...
// 最终会依次调用收集的insert hook
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index )
// createChildren 会递归创建儿子组件
createChildren(vnode, children, insertedVnodeQueue) // something...
// 将组件的vnode插入到数组中
function invokeCreateHooks (vnode, insertedVnodeQueue)
for (let i = 0; i < cbs.create.length; ++i)
cbs.create[i](emptyNode, vnode)
i = vnode.data.hook // Reuse variable
if (isDef(i))
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
// insert方法中会依次调用mounted方法
insert (vnode: MountedComponentVNode)
const context, componentInstance = vnode
if (!componentInstance._isMounted)
componentInstance._isMounted = true
callHook(componentInstance, \'mounted\')
function invokeInsertHook (vnode, queue, initial)
// delay insert hooks for component root nodes, invoke them after the // element is really inserted
if (isTrue(initial) && isDef(vnode.parent))
vnode.parent.data.pendingInsert = queue
else
for (let i = 0; i < queue.length; ++i)
queue[i].data.hook.insert(queue[i]); // 调用insert方法
Vue.prototype.$destroy = function ()
callHook(vm, \'beforeDestroy\')
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null) // 先销毁儿子
// fire destroyed hook
callHook(vm, \'destroyed\')
在Vue中使用插件的步骤
- 采用
ES6
的import ... from ...
语法或CommonJS
的require()
方法引入插件 - 使用全局方法
Vue.use( plugin )
使用插件,可以传入一个选项对象Vue.use(MyPlugin, someOption: true )
Vue中diff算法原理
DOM
操作是非常昂贵的,因此我们需要尽量地减少DOM
操作。这就需要找出本次DOM
必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用diff算法
vue
的diff
算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)
的方式进行比较。
简单来说,Diff算法有以下过程
- 同级比较,再比较子节点(根据
key
和tag
标签名判断) - 先判断一方有子节点和一方没有子节点的情况(如果新的
children
没有子节点,将旧的子节点移除) - 比较都有子节点的情况(核心
diff
) - 递归比较子节点
- 正常
Diff
两个树的时间复杂度是O(n^3)
,但实际情况下我们很少会进行跨层级的移动DOM
,所以Vue
将Diff
进行了优化,从O(n^3) -> O(n)
,只有当新旧children
都为多个子节点时才需要用核心的Diff
算法进行同层级比较。 Vue2
的核心Diff
算法采用了双端比较
的算法,同时从新旧children
的两端开始进行比较,借助key
值找到可复用的节点,再进行相关操作。相比React
的Diff
算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅- 在创建
VNode
时就确定其类型,以及在mount/patch
的过程中采用位运算来判断一个VNode
的类型,在这个基础之上再配合核心的Diff
算法,使得性能上较Vue2.x
有了提升
vue3中采用最长递增子序列来实现
diff
优化
回答范例
思路
diff
算法是干什么的- 它的必要性
- 它何时执行
- 具体执行方式
- 拔高:说一下
vue3
中的优化
回答范例
Vue
中的diff
算法称为patching
算法,它由Snabbdo
m修改而来,虚拟DOM
要想转化为真实DOM
就需要通过patch
方法转换- 最初
Vue1.x
视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM
和patching
算法支持,但是这样粒度过细导致Vue1.x
无法承载较大应用;Vue 2.x
中为了降低Watcher
粒度,每个组件只有一个Watcher
与之对应,此时就需要引入patching
算法才能精确找到发生变化的地方并高效更新 vue
中diff
执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render
函数获得最新的虚拟DOM
,然后执行patc
h函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM
操作patch
过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3
的patch
为例
- 首先判断两个节点是否为相同同类节点,不同则删除重新创建
- 如果双方都是文本则更新文本内容
- 如果双方都是元素节点则递归更新子元素,同时更新元素属性
- 更新子节点时又分了几种情况
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则直接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
vue3
中引入的更新策略:静态节点标记等
vdom中diff算法的简易实现
以下代码只是帮助大家理解diff
算法的原理和流程
- 将
vdom
转化为真实dom
:
const createElement = (vnode) =>
let tag = vnode.tag;
let attrs = vnode.attrs || ;
let children = vnode.children || [];
if(!tag)
return null;
//创建元素
let elem = document.createElement(tag);
//属性
let attrName;
for (attrName in attrs)
if(attrs.hasOwnProperty(attrName))
elem.setAttribute(attrName, attrs[attrName]);
//子元素
children.forEach(childVnode =>
//给elem添加子元素
elem.appendChild(createElement(childVnode));
)
//返回真实的dom元素
return elem;
- 用简易
diff
算法做更新操作
function updateChildren(vnode, newVnode)
let children = vnode.children || [];
let newChildren = newVnode.children || [];
children.forEach((childVnode, index) =>
let newChildVNode = newChildren[index];
if(childVnode.tag === newChildVNode.tag)
//深层次对比, 递归过程
updateChildren(childVnode, newChildVNode);
else
//替换
replaceNode(childVnode, newChildVNode);
)
参考 前端进阶面试题详细解答
v-model实现原理
我们在
vue
项目中主要使用v-model
指令在表单input
、textarea
、select
等元素上创建双向数据绑定,我们知道v-model
本质上不过是语法糖(可以看成是value + input
方法的语法糖),v-model
在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text
和textarea
元素使用value
属性和input
事件checkbox
和radio
使用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
请说明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
回答范例
分析
这是一道特别常见的问题,主要考查大家对虚拟DOM
和patch
细节的掌握程度,能够反映面试者理解层次
思路分析:
- 给出结论,
key
的作用是用于优化patch
性能 key
的必要性- 实际使用方式
- 总结:可从源码层面描述一下
vue
如何判断两个节点是否相同
回答范例:
key
的作用主要是为了更高效的更新虚拟DOM
vue
在patch
过程中 判断两个节点是否是相同节点是key
是一个必要条件 ,渲染一组列表时,key
往往是唯一标识,所以如果不定义key
的话,vue
只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch
过程比较低效,影响性能- 实际使用中在渲染一组列表时
key
必须设置,而且必须是唯一标识,应该避免使用数组索引作为key
,这可能导致一些隐蔽的bug
;vue
中在使用相同标签元素过渡切换时,也会使用key
属性,其目的也是为了让vue
可以区分它们,否则vue
只会替换其内部属性而不会触发过渡效果 - 从源码中可以知道,
vue
判断两个节点是否相同时主要判断两者的key
和标签类型(如div)
等,因此如果不设置key
,它的值就是undefined
,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom
更新操作,明显是不可取的
如果不使用
key
,Vue
会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key
是为Vue
中vnode
的唯一标记,通过这个key
,我们的diff
操作可以更准确、更快速
diff程可以概括为:
oldCh
和newCh
各有两个头尾的变量StartIdx
和EndIdx
,它们的2
个变量相互比较,一共有4
种比较方式。如果4
种比较都没匹配,如果设置了key
,就会用key
进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首
、尾
、旧尾新头
、旧头新尾
相关代码如下
// 判断两个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);
Vuex中actions和mutations有什么区别
题目分析
mutations
和actions
是vuex
带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。- 我们只需记住修改状态只能是
mutations
,actions
只能通过提交mutation
修改状态即可
回答范例
- 更改
Vuex
的store
中的状态的唯一方法是提交mutation
,mutation
非常类似于事件:每个mutation
都有一个字符串的类型 (type
)和一个 回调函数 (handler
) 。Action
类似于mutation
,不同在于:Action
可以包含任意异步操作,但它不能修改状态, 需要提交mutation
才能变更状态 - 开发时,包含异步操作或者复杂业务组合时使用
action
;需要直接修改状态则提交mutation
。但由于dispatch
和commit
是两个API
,容易引起混淆,实践中也会采用统一使用dispatch action
的方式。调用dispatch
和commit
两个API
时几乎完全一样,但是定义两者时却不甚相同,mutation
的回调函数接收参数是state
对象。action
则是与Store
实例具有相同方法和属性的上下文context
对象,因此一般会解构它为commit, dispatch, state
,从而方便编码。另外dispatch
会返回Promise
实例便于处理内部异步结果 - 实现上
commit(type)
方法相当于调用options.mutations[type](state)
;dispatch(type)
方法相当于调用options.actions[type](store)
,这样就很容易理解两者使用上的不同了
实现
我们可以像下面这样简单实现commit
和dispatch
,从而辨别两者不同
class Store
constructor(options)
this.state = reactive(options.state)
this.options = options
commit(type, payload)
// 传入上下文和参数1都是state对象
this.options.mutations[type].call(this.state, this.state, payload)
dispatch(type, payload)
// 传入上下文和参数1都是store本身
this.options.actions[type].call(this, this, payload)
对Vue SSR的理解
Vue.js
是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出Vue
组件,进行生成DOM
和操作DOM
。然而,也可以将同一个组件渲染为服务端的HTML
字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
SSR
也就是服务端渲染,也就是将Vue
在客户端把标签渲染成HTML
的工作放在服务端完成,然后再把html
直接返回给客户端
- 优点 :
SSR
有着更好的SEO
、并且首屏加载速度更快- 因为
SPA
页面的内容是通过Ajax
获取,而搜索引擎爬取工具并不会等待Ajax
异步完成后再抓取页面内容,所以在SPA
中是抓取不到页面通过Ajax
获取到的内容;而SSR
是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面 - 更快的内容到达时间(首屏加载更快):
SPA
会等待所有Vue
编译后的js
文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR
直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间
- 因为
- 缺点 : 开发条件会受到限制,服务器端渲染只支持
beforeCreate
和created
两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js
的运行环境。服务器会有更大的负载需求- 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的
server
更加大量占用CPU
资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略
- 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的
其基本实现原理
app.js
作为客户端与服务端的公用入口,导出Vue
根实例,供客户端entry
与服务端entry
使用。客户端entry
主要作用挂载到DOM
上,服务端entry
除了创建和返回实例,还进行路由匹配与数据预获取。webpack
为客服端打包一个Client Bundle
,为服务端打包一个Server Bundle
。- 服务器接收请求时,会根据
url
,加载相应组件,获取和解析异步数据,创建一个读取Server Bundle
的BundleRenderer
,然后生成html
发送给客户端。 - 客户端混合,客户端收到从服务端传来的
DOM
与自己的生成的 DOM 进行对比,把不相同的DOM
激活,使其可以能够响应后续变化,这个过程称为客户端激活 。为确保混合成功,客户端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到stroe
里,这样,在客户端挂载到DOM
之前,可以直接从store
里取数据。首屏的动态数据通过window.__INITIAL_STATE__
发送到客户端
Vue SSR
的实现,主要就是把Vue
的组件输出成一个完整HTML
,vue-server-renderer
就是干这事的
Vue SSR
需要做的事多点(输出完整 HTML),除了complier -> vnode
,还需如数据获取填充至 HTML
、客户端混合(hydration
)、缓存等等。相比于其他模板引擎(ejs
, jade
等),最终要实现的目的是一样的,性能上可能要差点
怎么实现路由懒加载呢
这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效
// 将
// import UserDetails from \'./views/UserDetails\'
// 替换为
const UserDetails = () => import(\'./views/UserDetails\')
const router = createRouter(
// ...
routes: [ path: \'/users/:id\', component: UserDetails ],
)
回答范例
- 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段
- 一般来说,对所有的路由都使用动态导入是个好主意
- 给
component
选项配置一个返回Promise
组件的函数就可以定义懒加载路由。例如:path: \'/users/:id\', component: () => import(\'./views/UserDetails\')
- 结合注释
() => import(/* webpackChunkName: "group-user" */ \'./UserDetails.vue\')
可以做webpack
代码分块
你觉得vuex有什么缺点
分析
相较于redux
,vuex
已经相当简便好用了。但模块的使用比较繁琐,对ts
支持也不好。
体验
使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持
const store = createStore(
modules:
a: moduleA
)
store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
store.getters[\'a/c\'] // -> 有namespaced时要加path,使用模式又和state不一样
store.commit(\'d\') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
store.commit(\'a/d\') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
回答范例
vuex
利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档- 比如:访问
state
时要带上模块key
,内嵌模块的话会很长,不得不配合mapState
使用,加不加namespaced
区别也很大,getters
,mutations
,actions
这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。 - 之前
Vue2
项目中用过vuex-module-decorators
的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia
出现之后使用体验好了很多,Vue3 + pinia
会是更好的组合
原理
下面我们来看看vuex
中store.state.x.y
这种嵌套的路径是怎么搞出来的
首先是子模块安装过程:父模块状态
parentState
上面设置了子模块名称moduleName
,值为当前模块state
对象。放在上面的例子中相当于:store.state[\'x\'] = moduleX.state
。此过程是递归的,那么store.state.x.y
安装时就是:store.state[\'x\'][\'y\'] = moduleY.state
//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
if (!isRoot && !hot)
// 获取父模块state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 获取子模块名称
const moduleName = path[path.length - 1]
store._withCommit(() =>
// 把子模块state设置到父模块上
parentState[moduleName] = module.state
)
v-if和v-show区别
v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。v-if
显示隐藏是将dom
元素整个添加或删除- 编译过程:
v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于css
切换 - 编译条件:
v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染 v-show
由false
变为true
的时候不会触发组件的生命周期v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法- 性能消耗:
v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗
v-show与v-if的使用场景
v-if
与v-show
都能控制dom
元素在页面的显示v-if
相比v-show
开销更大的(直接操作dom节
点增加与删除)- 如果需要非常频繁地切换,则使用 v-show 较好
- 如果在运行时条件很少改变,则使用
v-if
较好
v-show与v-if原理分析
v-show
原理
不管初始条件是什么,元素总是会被渲染
我们看一下在vue中是如何实现的
代码很好理解,有transition
就执行transition
,没有就直接设置display
属性
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> =
beforeMount(el, value , transition )
el._vod = el.style.display === \'none\' ? \'\' : el.style.display
if (transition && value)
transition.beforeEnter(el)
else
setDisplay(el, value)
,
mounted(el, value , transition )
if (transition && value)
transition.enter(el)
,
updated(el, value, oldValue , transition )
// ...
,
beforeUnmount(el, value )
setDisplay(el, value)
v-if
原理
v-if
在实现上比v-show
要复杂的多,因为还有else
else-if
等条件需要处理,这里我们也只摘抄源码中处理 v-if
的一小部分
返回一个node
节点,render
函数通过表达式的值来决定是否生成DOM
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) =>
return processIf(node, dir, context, (ifNode, branch, isRoot) =>
// ...
return () =>
if (isRoot)
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
else
// attach this branch\'s codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
)
)
Vue路由的钩子函数
首页可以控制导航跳转,
beforeEach
,afterEach
等,一般用于页面title
的修改。一些需要登录才能调整页面的重定向功能。
beforeEach
主要有3个参数to
,from
,next
。to
:route
即将进入的目标路由对象。from
:route
当前导航正要离开的路由。next
:function
一定要调用该方法resolve
这个钩子。执行效果依赖next
方法的调用参数。可以控制网页的跳转
vue-router 路由钩子函数是什么 执行顺序是什么
路由钩子的执行流程, 钩子函数种类有:
全局守卫
、路由守卫
、组件守卫
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+
)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+
)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发
DOM
更新。 - 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入
Composition API 与 Options API 有什么不同
分析
Vue3
最重要更新之一就是Composition API
,它具有一些列优点,其中不少是针对Options API
暴露的一些问题量身打造。是Vue3
推荐的写法,因此掌握好Composition API
应用对掌握好Vue3
至关重要
What is Composition API?(opens new window)
Composition API
出现就是为了解决Options API导致相同功能代码分散的现象
体验
Composition API
能更好的组织代码,下面用composition api
可以提取为useCount()
,用于组合、复用
compositon api提供了以下几个函数:
setup
ref
reactive
watchEffect
watch
computed
toRefs
- 生命周期的
hooks
回答范例
Composition API
是一组API
,包括:Reactivity API
、生命周期钩子
、依赖注入
,使用户可以通过导入函数方式编写vue
组件。而Options API
则通过声明组件选项的对象形式编写组件Composition API
最主要作用是能够简洁、高效复用逻辑。解决了过去Options API
中mixins
的各种缺点;另外Composition API
具有更加敏捷的代码组织能力,很多用户喜欢Options API
,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API
则可以将它们有效组织在一起。最后Composition API
拥有更好的类型推断,对ts支持更友好,Options API
在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API
时获得类型推断,然而还是没办法用在mixins
和provide/inject
上Vue3
首推Composition API
,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API
仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API
会获得更大收益
可能的追问
Composition API
能否和Options API
一起使用?
可以在同一个组件中使用两个script
标签,一个使用vue3,一个使用vue2写法,一起使用没有问题
<!-- vue3 -->
<script setup>
// vue3写法
</script>
<!-- 降级vue2 -->
<script>
export default
data() ,
methods:
</script>
子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则
,不在子组件中修改父组件是个常识问题
思路
- 讲讲单项数据流原则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
- 所有的
prop
都使得其父子之间形成了一个单向下行绑定:父级prop以上是关于前端一面常见vue面试题汇总的主要内容,如果未能解决你的问题,请参考以下文章