带你从源码分析关于vue(v2.7.10)的面试题

我们在面试的时候经常会被问到vue框架的原理类问题,我今天整理了一些常见问题和答案,希望有不正确之处还请指正。

1.new Vue时发生了什么

首先实例化一个对象,该对象执行init方法初始化生命周期等等,随后执行$mount方法开始生成vnode和dom。其中el会作为根dom,然后会根据render生成vnode。
在这里插入图片描述
执行init方法
在这里插入图片描述
开始挂载
在这里插入图片描述
判断有无render函数,没有则会生成
在这里插入图片描述

h函数就是$createElement,调用该方法生成vnode
在这里插入图片描述
总结:new Vue时会触发vue的_init方法去初始化数据,将el作为根dom,然后调用$mount方法去挂载组件,挂载的过程中会执行_render方法生成vnode。

2.双向数据绑定的原理

所谓的双向数据绑定就是当数据改变时视图也跟着变化,接下来我们看看关键代码。首先在get方法中会判断Dep.target是否存在,target是watcher实例,在视图中或者函数中获取该变量就会触发变量的get方法。然后该变量的dep实例就会放入当前的watcher(建立变量和watcher的联系)。
在这里插入图片描述
在这里插入图片描述
在set的时候,即数据发生修改的时候会执行当前变量的dep实例的notify方法,该方法会遍历执行当前观察的视图(所有的watcher)的update方法去异步更新视图。
在这里插入图片描述
在这里插入图片描述
总结:双向绑定的原理就是利用Object.defineProperty去设置变量的get和set方法,在该函数中会实例化dep数组。在获取数据的时候会触发变量的get方法去收集watcher,当变量发生改变时会触发变量的set方法去遍历dep数组,并执行watcher的update方法去刷新视图。

3.Computed 和 Watch 的区别

computed

首先在init过程中会执行initProxy,在渲染的时候获取getA会从vm实例中获取getA:
在这里插入图片描述

我们再来看看computed的实现过程:
在这里插入图片描述
我们进入watcher看看,其中getter函数就是我们定义的computed函数getA。
在这里插入图片描述
在执行渲染函数的时候触发我们开头设置的proxy
在这里插入图片描述
createComputedGetter函数在init过程中定义
在这里插入图片描述
执行watcher的evaluate方法,此时调用get方法,该方法就是getA函数。
在这里插入图片描述
此时会触发变量的get方法去收集当前的Dep.target(computed的watcher实例)。所以当变量发生变化时会执行watcher的update方法。
在这里插入图片描述

缓存

computed还有缓存功能,当依赖不发生变化时是不会重新执行的,看如下代码dirty为false不会重新执行computed函数而是直接返回值。
在这里插入图片描述

不支持异步

从上面的分析可知会执行computed函数然后通知变量收集依赖,如果是异步函数那么返回的就是一个Promise,此时变量无法收集依赖,所以不支持异步也很好解释。

watch

先看看实现过程,首先watch也会实例化一个watcher,handler就是watcher函数。
在这里插入图片描述

支持异步监听

之所以支持异步是因为是根据获取函数名去监听的,函数名是从变量中获取到的,不涉及到函数内部实现,所以函数内部是否异步并无关联。
在这里插入图片描述
通过执行watcher的get方法获取初始值
在这里插入图片描述
在这里插入图片描述

由于expOrFn是字符串,所以会执行parsePath函数,obj是vm对象,从vm对象中获取变量的。在获取对象的值时,变量会收集当前的watcher。

在这里插入图片描述
当变量改变时,会触发watcher的update方法,从而执行queneWatcher函数,异步执行flushSchedulerQueue方法
在这里插入图片描述
这里又会执行watcher的run方法
在这里插入图片描述

不支持缓存

通过get方法获取变量的最新值,通过判断当前值是否改变或者值是个对象,或者有deep属性从而记录oldValue。然后调用用户定义的watch函数并传入新值和旧值,在实现方法中并没有缓存这么一说,因为没有返回值。
在这里插入图片描述
总结:
相同点:

  1. 初始化过程都会生成watcher实例。
  2. 监听的过程都是触发变量的get方法收集watcher实例

不同点:

  1. computed会对结果缓存(是因为有返回值), watch没有缓存(是因为没有返回值)。
  2. computed不支持异步(因为要函数执行结果只能拿到promise,无法触发变量的get方法),watch支持异步(因为不依赖函数内部实现)

4. 说说once事件修饰符原理

带once和不带的区别是渲染的时候click函数不一样,带once的是~click还有arguments参数和$event事件。
在这里插入图片描述
我们来看看怎么处理~click的,在createElm的时候执行invokeCreateHooks:
在这里插入图片描述
cbs.create对象中包含了updateDOMListeners函数:
在这里插入图片描述
在这里插入图片描述
这里先执行normalizeEvent解析name,
在这里插入图片描述
执行createOnceHandler返回onceHandler函数,如果当前执行函数返回不是null则会移除该事件。
在这里插入图片描述
所以函数返回值为null时是不会移除监听事件的

在这里插入图片描述
总结
在render函数createEle的时候会通过addEventListener给当前dom添加事件,once就是通过判断返回值是否为null来决定是否移除监听事件

5.说说v-show的原理

v-show时vue的内部指令,会被解析为如下格式
在这里插入图片描述
createEle时调用如下函数
在这里插入图片描述

在这里插入图片描述
这里会执行updateDirectives函数的_update
在这里插入图片描述
这里执行了
在这里插入图片描述
在这里插入图片描述

在执行bind方法的时候会先获取当前的指令的value值,然后根据该值设置是否设置当前el的display是否为none。

在这里插入图片描述
总结
v-show只是在创建el的时候通过获取判断条件的值去设置当前dom的display属性,所以v-show不会影响到vnode的形成。

6.说下v-model的实现原理

1.在组件上的用法

从源码看vue(v2.7.10)中的v-model(双向绑定)之组件的原理

2.在表单上的用法

从源码看vue(v2.7.10)中的v-model(双向绑定)之input的原理

7.data为什么是一个函数而不是对象

在引用组件的时候如果data是一个对象的话那么就会出现多个对象指向同一个内存地址,导致一处改变多个引用组件都会变化。所以只要返回的对象指向不同的内存地址即可,不一定就要返回一个函数。
在这里插入图片描述

8.对keep-alive的理解,它是如何实现的,具体缓存的是什么?

从源码看vue(v2.7.10)中的keep-alive的原理
那么keep-alive 中的生命周期哪些呢?keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。

9.$nextTick 原理

执行nextTick函数
在这里插入图片描述
执行timerFunc函数
在这里插入图片描述

1.Promise.resolve

首先考虑Promise.resolve,因为兼容性好
在这里插入图片描述
在这里插入图片描述

2.MutationObserver

MutationObserver是微任务
在这里插入图片描述

3.setImmediate和setTimeout

setImmediate和setTimeout是宏任务
在这里插入图片描述
总结
this.$nextTick方法是一个异步更新策略,主要是防止变量频繁变化导致视图频繁刷新。使用异步更新就可以在执行完所有的同步任务后再执行宏任务,做到最后刷新视图。执行的主要策略是:
Promise.resolve(微任务)=>MutationObserve(微任务)r=>setImmediate和setTimeout(宏任务)

10.Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?

<template> 
   <div>
      <ul>
         <li v-for="value in obj" :key="value"> {{value}} </li> 
      </ul> 
      <button @click="addObjB">添加 obj.b</button> 
   </div>
</template>

<script>
    export default { 
       data () { 
          return { 
              obj: { 
                  a: 'obj.a' 
              } 
          } 
       },
       methods: { 
          addObjB () { 
              this.obj.b = 'obj.b' 
              console.log(this.obj) 
          } 
      }
   }
</script>

当给一个对象添加新值时视图没有刷新,是因为vue无法监听对象的增删,只能在对象已经监听的原有的值上进行修改才会触发视图刷新。要想解决这个问题只要给新设置的对象设置响应式并刷新视图不就好了,vue是使用$set方法解决的。
在这里插入图片描述
通过defineReactive给当前对象新增属性添加响应式
在这里插入图片描述
通过notify通知watcher刷新视图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这个时候视图已经刷新了。如果只是想刷新视图不想生成响应式那么只需要执行this.obj.__ob__.dep.notify()就可以手动刷新视图。
总结
由于vue无法监听对象的属性增删,所以得手动将新增的属性设置为响应式并通知watcher刷新视图。

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

vue是没法监听数组的增删的,所以vue单独监听了部分数组操作。
在这里插入图片描述
这里监听的方法放在methodsToPatch数组里,当执行这些操作时会触发ob.dep.notify函数去刷新视图。并会对push/unshift/splice等新添加或删除的值进行监听。
在这里插入图片描述

12.Vue template 到 render 的过程

如果没有render函数,那么就会经过template -> ast -> render函数,我们来看看具体怎么实现的。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
首先通过parse将template转换为ast
在这里插入图片描述
标记静态节点
在这里插入图片描述

如果child不为static,那么父节点也会被设置为非静态节点

在这里插入图片描述

如果是text节点或者没有动态绑定/没有v-if v-for v-else/tag不为component或slot/是html标签或svg/不为直接for循环/node对象属性在下面属性等条件就是静态节点
在这里插入图片描述
在这里插入图片描述
这里由于子节点不是静态节点,所以这整个都不是静态节点。

<template> 
	// 静态节点
   <div>
      // 静态节点
      <ul>
      	 // 非静态节点,不满足node.for
         <li v-for="value in obj" :key="value">
         	// 非静态节点,不满足isPlatformReservedTag和staticKey
          	{{value}} 
          </li> 
      </ul> 
      <button @click="addObjB">添加 obj.b</button> 
   </div>
</template>

我们来看看第二趴静态根节点的判断。

在这里插入图片描述
判断是否静态根节点首先是静态节点,并且有子节点而且子节点个数不为1类型不为3。
在这里插入图片描述
这两个过程会分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化。静态节点生成的DOM永远不会改变,在运行时模板更新起到了极大的优化作用。最后一步就是将ast节点转成code并返回一个对象。
在这里插入图片描述
code.render是个with函数并传入this对象,这样我们在模块中也可以使用this。
在这里插入图片描述
patchVnode的时候如果是静态节点那么在渲染的时候会执行_vm._m方法。
在这里插入图片描述
在这里插入图片描述
然后执行staticRenderFns函数,执行结果会被缓存

在这里插入图片描述

生成了render函数后会缓存起来。

在这里插入图片描述
总结
template会被转换成ast,然后经过optimize标记静态节点,静态节点在渲染的时候会被单独拿出来。在updateChildren的patchVnode过程中静态节点的新旧节点一致不会进行子节点的比对,最后返回render渲染函数。

13.说下$emit的原理

$emit方法会带入一个event,然后从vm._events中查找该event并执行。
从_events中获取事件
在这里插入图片描述
我们看看vm._events是如何生成的,首先组件init的时候执行initEvents。
在这里插入图片描述
获取组件
在这里插入图片描述
添加事件
在这里插入图片描述

最后是执行了vm的$on方法注册事件

在这里插入图片描述

在这里插入图片描述
总结
$emit本质就是一个数组,该数组在initEvents的时候根据model的event在$on中注册事件(当前vm的_event数组下放入回调函数),在$emit的时候从_event中查找该函数并执行。

未完待续,持续更新。。。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Young soul2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值