写点什么

阿里前端高频 vue 面试题(边面边更)

作者:bb_xiaxia1998
  • 2022-10-14
    浙江
  • 本文字数:12930 字

    阅读完需:约 42 分钟

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. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
5. unbind:只调用一次,指令与元素解绑时调用。
复制代码


原理


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


2.通过 genDirectives 生成指令代码


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


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

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

思路分析


这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态 API。


  • vuex需求分析

  • 如何实现这些需求


回答范例


  1. 官方说vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex


  • 要实现一个Store存储全局状态

  • 要提供修改状态所需 API:commit(type, payload), dispatch(type, payload)


  1. 实现Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commitdispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件

  2. commit(type, payload)方法中可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果


实践


Store的实现:


class Store {    constructor(options) {        this.state = reactive(options.state)        this.options = options    }    commit(type, payload) {        this.options.mutations[type].call(this, this.state, payload)    }}
复制代码


vuex 简易版


/** * 1 实现插件,挂载$store * 2 实现store */
let Vue;
class Store { constructor(options) { // state响应式处理 // 外部访问: this.$store.state.*** // 第一种写法 // this.state = new Vue({ // data: options.state // })
// 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更 this._vm = new Vue({ data: { $$state: options.state } })
this._mutations = options.mutations this._actions = options.actions this.getters = {} options.getters && this.handleGetters(options.getters)
this.commit = this.commit.bind(this) this.dispatch = this.dispatch.bind(this) }
get state () { return this._vm._data.$$state }
set state (val) { return new Error('Please use replaceState to reset state') }
handleGetters (getters) { Object.keys(getters).map(key => { Object.defineProperty(this.getters, key, { get: () => getters[key](this.state) }) }) }
commit (type, payload) { let entry = this._mutations[type] if (!entry) { return new Error(`${type} is not defined`) }
entry(this.state, payload) }
dispatch (type, payload) { let entry = this._actions[type] if (!entry) { return new Error(`${type} is not defined`) }
entry(this, payload) }}
const install = (_Vue) => { Vue = _Vue
Vue.mixin({ beforeCreate () { if (this.$options.store) { Vue.prototype.$store = this.$options.store } }, })}

export default { Store, install }
复制代码


验证方式


import Vue from 'vue'import Vuex from './vuex'// this.$storeVue.use(Vuex)
export default new Vuex.Store({ state: { counter: 0 }, mutations: { // state从哪里来的 add (state) { state.counter++ } }, getters: { doubleCounter (state) { return state.counter * 2 } }, actions: { add ({ commit }) { setTimeout(() => { commit('add') }, 1000) } }, modules: { }})
复制代码

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 中组件生命周期调用顺序说一下

组件的调用顺序都是先父后子,渲染完成的顺序是先子后父


组件的销毁操作是先父后子,销毁完成的顺序是先子后父


加载渲染过程


父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
复制代码


子组件更新过程


父beforeUpdate->子beforeUpdate->子updated->父updated
复制代码


父组件更新过程


父 beforeUpdate -> 父 updated
复制代码


销毁过程


父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
复制代码

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的代码


<template><div>  请输入firstName:  <input type="text" v-model="firstName"></div><div>  请输入lastName:  <input type="text" v-model="lastName"></div><div>  请输入obj.text:  <input type="text" v-model="obj.text"></div> <div> 【obj.text】 {{obj.text}} </div></template>
<script>import {ref, reactive, watch, watchEffect} from 'vue'export default { name: "HelloWorld", props: { msg: String, }, setup(props,content){ let firstName = ref('') let lastName = ref('') let obj= reactive({ text:'hello' }) watchEffect(()=>{ console.log('触发了watchEffect'); console.log(`组合后的名称为:${firstName.value}${lastName.value}`) }) return{ obj, firstName, lastName } }};</script>
复制代码




改造一下代码


watchEffect(()=>{  console.log('触发了watchEffect');  // 这里我们不使用firstName.value/lastName.value ,相当于是监控整个ref,对应第四点上面的结论  console.log(`组合后的名称为:${firstName}${lastName}`)})
复制代码



watchEffect(()=>{  console.log('触发了watchEffect');  console.log(obj);})
复制代码



稍微改造一下


let obj = reactive({  text:'hello'})watchEffect(()=>{  console.log('触发了watchEffect');  console.log(obj.text);})
复制代码



再看一下 watch 的代码,验证一下


let obj= reactive({  text:'hello'})// watch是惰性执行, 默认初始化之后不会执行,只有值有变化才会触发,可通过配置参数实现默认执行watch(obj, (newValue, oldValue) => {  // 回调函数  console.log('触发监控更新了new',  newValue);  console.log('触发监控更新了old',  oldValue);},{  // 配置immediate参数,立即执行,以及深层次监听  immediate: true,  deep: true})
复制代码



  • 监控整个reactive对象,从上面的图可以看到 deep 实际默认是开启的,就算我们设置为false也还是无效。而且旧值获取不到。

  • 要获取旧值则需要监控对象的属性,也就是监听一个getter,看下图




总结


  • 如果定义了reactive的数据,想去使用watch监听数据改变,则无法正确获取旧值,并且deep属性配置无效,自动强制开启了深层次监听。

  • 如果使用 ref 初始化一个对象或者数组类型的数据,会被自动转成reactive的实现方式,生成proxy代理对象。也会变得无法正确取旧值。

  • 用任何方式生成的数据,如果接收的变量是一个proxy代理对象,就都会导致watch这个对象时,watch回调里无法正确获取旧值。

  • 所以当大家使用watch监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行


watch 和 watchEffect 异同总结


体验


watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数


const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0count.value++// -> logs 1
复制代码


watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数


const state = reactive({ count: 0 })watch(  () => state.count,  (count, prevCount) => {    /* ... */  })
复制代码


回答范例


  1. watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数

  2. watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch

  3. watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项

  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})


watchEffect定义如下


export function watchEffect(  effect: WatchEffect,  options?: WatchOptionsBase): WatchStopHandle {  return doWatch(effect, null, options)}
复制代码


watch定义如下


export function watch<T = any, Immediate extends Readonly<boolean> = false>(  source: T | WatchSource<T>,  cb: any,  options?: WatchOptions<Immediate>): WatchStopHandle {  return doWatch(source as any, cb, options)}
复制代码


很明显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 表单元素为例:


<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
复制代码


如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:


父组件:<ModelChild v-model="message"></ModelChild>
子组件:<div>{{value}}</div>
props:{ value: String},methods: { test1(){ this.$emit('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 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)


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


原理流程图


keep-alive 中的生命周期哪些

keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。


如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。


当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。

Vue 是如何收集依赖的?

在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶


function defieneReactive (obj, key, val){  const dep = new Dep();  ...  Object.defineProperty(obj, key, {    ...    get: function reactiveGetter () {      if(Dep.target){        dep.depend();        ...      }      return val    }    ...  })}
复制代码


以上只保留了关键代码,主要就是 const dep = new Dep()实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend() 进行依赖收集。 (1)Dep Dep 是整个依赖收集的核心,其关键代码如下:


class Dep {  static target;  subs;
constructor () { ... this.subs = []; } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.sub, sub) } depend () { if(Dep.target){ Dep.target.addDep(this) } } notify () { const subs = this.subds.slice(); for(let i = 0;i < subs.length; i++){ subs[i].update() } }}
复制代码


Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶


(2)Watcher


class Watcher {  getter;  ...  constructor (vm, expression){    ...    this.getter = expression;    this.get();  }  get () {    pushTarget(this);    value = this.getter.call(vm, vm)    ...    return value  }  addDep (dep){        ...    dep.addSub(this)  }  ...}function pushTarget (_target) {  Dep.target = _target}
复制代码


Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。


(3)过程


在实例化 Vue 时,依赖收集的相关过程如下∶初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,


updateComponent = () => {  vm._update(vm._render())}new Watcher(vm, updateComponent)
复制代码


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 种拦截方:不限于applyownKeysdeletePropertyhas等等是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

用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
阿里前端高频vue面试题(边面边更)_Vue_bb_xiaxia1998_InfoQ写作社区