阿里前端高频 vue 面试题(边面边更)
Vue 中 computed 和 watch 有什么区别?
计算属性 computed:(1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数;(2)计算属性内不支持异步操作;(3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法;(4)计算属性是自动监听依赖值的变化,从而动态返回内容。
侦听属性 watch:(1)不支持缓存,只要数据发生变化,就会执行侦听函数;(2)侦听属性内支持异步操作;(3)侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性;(3)监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情。
vue 是如何实现响应式数据的呢?(响应式数据原理)
Vue2: Object.defineProperty
重新定义 data
中所有的属性, Object.defineProperty
可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。
具体的过程:首先 Vue 使用 initData
初始化用户传入的参数,然后使用 new Observer
对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value)
对对象进行处理,内部使用 defineeReactive
循环对象属性定义响应式变化,核心就是使用 Object.defineProperty
重新定义数据。
写过自定义指令吗 原理是什么
指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind
原理
1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
2.通过 genDirectives 生成指令代码
3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
4.当执行指令对应钩子函数时,调用对应指令定义的方法
如果让你从零开始写一个 vuex,说说你的思路
思路分析
这个题目很有难度,首先思考vuex
解决的问题:存储用户全局状态并提供管理状态 API。
vuex
需求分析如何实现这些需求
回答范例
官方说
vuex
是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
要实现一个
Store
存储全局状态要提供修改状态所需 API:
commit(type, payload), dispatch(type, payload)
实现
Store
时,可以定义Store
类,构造函数接收选项options
,设置属性state
对外暴露状态,提供commit
和dispatch
修改属性state
。这里需要设置state
为响应式对象,同时将Store
定义为一个Vue
插件commit(type, payload)
方法中可以获取用户传入mutations
并执行它,这样可以按用户提供的方法修改状态。dispatch(type, payload)
类似,但需要注意它可能是异步的,需要返回一个Promise
给用户以处理异步结果
实践
Store
的实现:
vuex 简易版
验证方式
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 中组件生命周期调用顺序说一下
组件的调用顺序都是先父后子
,渲染完成的顺序是先子后父
。
组件的销毁操作是先父后子
,销毁完成的顺序是先子后父
。
加载渲染过程
子组件更新过程
父组件更新过程
销毁过程
diff 算法
<details open=""><summary><b>答案</b></summary><p></p><p><strong>时间复杂度:</strong> 个树的完全<code> diff</code> 算法是一个时间复杂度为<code> O(n*3)</code> ,vue 进行优化转化成<code> O(n)</code> 。</p><p><strong>理解:</strong></p><ul><li><p>最小量更新,<code> key</code> 很重要。这个可以是这个节点的唯一标识,告诉<code> diff</code> 算法,在更改前后它们是同一个 DOM 节点</p><ul><li>扩展<code> v-for</code> 为什么要有<code> key</code> ,没有<code> key</code> 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加<code> key</code> 只会移动减少操作 DOM。</li></ul></li><li><p>只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。</p></li><li><p>只进行同层比较,不会进行跨层比较。</p></li></ul><p><strong>diff 算法的优化策略</strong>:四种命中查找,四个指针</p><ol><li><p>旧前与新前(先比开头,后插入和删除节点的这种情况)</p></li><li><p>旧后与新后(比结尾,前插入或删除的情况)</p></li><li><p>旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)</p></li><li><p>旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)</p></li></ol><p></p></details>
--- 问完上面这些如果都能很清楚的话,基本 O 了 ---
以下的这些简单的概念,你肯定也是没有问题的啦😉
Vue 模版编译原理知道吗,能简单说一下吗?
简单说,Vue 的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
生成 AST 树
优化
codegen
首先解析模版,生成AST语法树
(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
虚拟 DOM 的优劣如何?
优点:
保证性能下限: 虚拟 DOM 可以经过 diff 找出最小差异,然后批量进行 patch,这种操作虽然比不上手动优化,但是比起粗暴的 DOM 操作性能要好很多,因此虚拟 DOM 可以保证性能下限
无需手动操作 DOM: 虚拟 DOM 的 diff 和 patch 都是在一次更新中自动进行的,我们无需手动操作 DOM,极大提高开发效率
跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
缺点:
无法进行极致优化: 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,比如 VScode 采用直接手动操作 DOM 的方式进行极端的性能优化
参考:前端vue面试题详细解答
computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
vue3 中 watch、watchEffect 区别
watch
是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是watchEffect
不同,每次代码加载watchEffect
都会执行(忽略watch
第三个参数的配置,如果修改配置项也可以实现立即执行)watch
需要传递监听的对象,watchEffect
不需要watch
只能监听响应式数据:ref
定义的属性和reactive
定义的对象,如果直接监听reactive
定义对象中的属性是不允许的(会报警告),除非使用函数转换一下。其实就是官网上说的监听一个getter
watchEffect
如果监听reactive
定义的对象是不起作用的,只能监听对象中的属性
看一下watchEffect
的代码
改造一下代码
稍微改造一下
再看一下 watch 的代码,验证一下
监控整个
reactive
对象,从上面的图可以看到deep
实际默认是开启的,就算我们设置为false
也还是无效。而且旧值获取不到。要获取旧值则需要监控对象的属性,也就是监听一个
getter
,看下图
总结
如果定义了
reactive
的数据,想去使用watch
监听数据改变,则无法正确获取旧值,并且deep
属性配置无效,自动强制开启了深层次监听。如果使用
ref
初始化一个对象或者数组类型的数据,会被自动转成reactive
的实现方式,生成proxy
代理对象。也会变得无法正确取旧值。用任何方式生成的数据,如果接收的变量是一个
proxy
代理对象,就都会导致watch
这个对象时,watch
回调里无法正确获取旧值。所以当大家使用
watch
监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行
watch 和 watchEffect 异同总结
体验
watchEffect
立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数
watch
侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数
回答范例
watchEffect
立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch
侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数watchEffect(effect)
是一种特殊watch
,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect
就是我们需要的。watch
更底层,可以接收多种数据源,包括用于依赖收集的getter
函数,因此它完全可以实现watchEffect
的功能,同时由于可以指定getter
函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch
watchEffect
在使用时,传入的函数会立刻执行一次。watch
默认情况下并不会执行回调函数,除非我们手动设置immediate
选项从实现上来说,
watchEffect(fn)
相当于watch(fn,fn,{immediate:true})
watchEffect
定义如下
watch
定义如下
很明显watchEffect
就是一种特殊的watch
实现。
v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
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 的生命周期方法有哪些 一般在哪一步发请求
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有nextTick 来访问 Dom
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面 loading 时间;
ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
created 和 mounted 的区别
created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。
Vue 的基本原理
当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0 使用 proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
Vue 生命周期钩子是如何实现的
vue
的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法内部会对钩子函数进行处理,将钩子函数维护成数组的形式
Vue
的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
相关代码如下
原理流程图
keep-alive 中的生命周期哪些
keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。
Vue 是如何收集依赖的?
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
以上只保留了关键代码,主要就是 const dep = new Dep()
实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend()
进行依赖收集。 (1)Dep Dep 是整个依赖收集的核心,其关键代码如下:
Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
(2)Watcher
Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相关过程如下∶初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,
get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。
说说你对 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 的属性值有哪些
相关代码如下
Proxy
只会代理对象的第一层,那么Vue3
又是怎样处理这个问题的呢?
判断当前
Reflect.get的
返回值是否为Object
,如果是则再通过reactive
方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢?
我们可以判断
key
是否为当前被代理对象target
自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
评论