vue原理面试题

Vue (40) 2024-02-10 08:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说vue原理面试题,希望能够帮助你!!!。

双向绑定原理

一句话概括,Vue 双向绑定通过数据劫持(Object.defineProperty)和发布-订阅模式实现。

下面三种解释实现过程 都可以(详细程度的却别)

实现过程说法1

vue原理面试题_https://bianchenghao6.com/blog_Vue_第1张

我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:

  • 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
  • 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
  • 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

实现过程说法2

1.通过DefineProperty劫持各个数据的setter和getter,并为每个数据添加一个订阅者列表,这个列表将会记录所有依赖这个数据 的组件。响应式后的数据相当于消息的发布者。

2.每个组件都对应一个Watcher订阅者,当组件渲染函数执行时,会将本组件的Watcher加入到所依赖的响应式数据的订阅者列表中。相当于完成了一次订阅,这个过程叫做“依赖收集”。

3.当响应式数据发生变化时,会出setter,setter负责通知数据的订阅者列表中的Watcher,Watcher触发组件重新渲染来更新视图。视图层相当于消息的订阅者。

实现过程说法3

vue原理面试题_https://bianchenghao6.com/blog_Vue_第2张

核心实现类

Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新

Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。

Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

Watcher 和 Dep 的关系 watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

依赖收集

initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集

initState 时,对侦听属性初始化时,触发 user watcher 依赖收集

render()的过程,触发 render watcher 依赖收集

re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值

派发更新

组件中对响应的数据进行了修改,触发 setter 的逻辑 调用 dep.notify() 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。

原理

当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。

一句话总结:

vue.js 采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调

为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组

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

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性

vue 响应式原理

Object.defineProperty实现响应式

监听对象,监听数组 复杂对象,深度监听

Object.defineProperty的缺点

深度监听需要递归到底,一次性计算量大 无法监听新增属性、删除属性(要使用Vue.set Vue.delete) 无法原生监听数组,需要特殊处理

虚拟dom 和diff算法

  • DOM操作非常耗费性能
  • 以前用jQuery,可以自行控制DOM操作时机,手动调整
  • vue和react都是数据驱动试图,如何有效控制DOM操作?

解决方案——vdom(虚拟dom)

  • 有一定的复杂度,想减少计算次数比较难
  • 难不能把计算,更多的转移为JS计算?因为JS执行比较快
  • vdom——用JS模拟DOM结构,计算出最小的变更,操作DOM

用js模拟虚拟dom(虚拟dom长啥样?)

vue原理面试题_https://bianchenghao6.com/blog_Vue_第3张

diff算法

两个数做diff,如这里的vdom diff

vue原理面试题_https://bianchenghao6.com/blog_Vue_第4张

过程 vnode ->patch ->new vnode

树diff的时间复杂度O(n^3)

  • 第一,遍历tree1;第二,遍历tree2
  • 第三,排序
  • 1000个节点,要计算1亿次,算法不可用

优化时间复杂度到O(n)

  • 只比较同一层级,不跨级比较
  • tag不相同,直接删掉重建,不再深度比较
  • tag和key,两者都相同,则认为是相同的节点,不再深度比较
vue原理面试题_https://bianchenghao6.com/blog_Vue_第5张

vue原理面试题_https://bianchenghao6.com/blog_Vue_第6张

diff算法总结

  • patchVnode
  • addVnodes removeVnodes
  • updateChildren(key的重要性)

vdom和diff总结

  • 细节不重要,updateChildren更新过程也不重要,不要深究
  • vnode核心概念很重要:h vnode patch diff key 等
  • vnode的存在价值更重要:数据驱动试图,控制DOM操作

模板编译原理

对于Vue来说,我们所认为的“HTML”其实都是字符串。Vue会根据其规定的模板语法规则,将其解析成AST语法树(其实就是用一个树状的大对象来描述我们所谓的“HTML”);然后会对这个大对象进行一些初步处理,比如标记没有动态绑定值的节点;最后,会把这个大对象编译成render函数,并将它绑定在组件的实例上。这样,我们所认为的“HTML”就变成了JavaScript代码,可以基于JavaScript模块规则进行导入导出,在需要渲染组件的地方,就调用render函数,根据组件当前的状态生成虚拟dom,然后就可以根据这个虚拟dom去更新视图了。

Vue的模板编译就是将“HTML”模板编译成render函数的过程。这个过程大致可以分成三个阶段:

  • 解析阶段:将“HTML”模板解析成AST语法树;
  • 优化阶段:从AST语法树中找出静态子树并进行标记(被标记的静态 子树在虚拟dom比对时会被忽略,从而提高虚拟dom比对的性能);
  • 代码生成阶段:通过AST生成代码字符串,并最终生成render函数。

组件渲染更新过程

vue原理的三大知识点

  • 响应式:监听data属性 getter setter(包括数组)
  • 模版编译:模版到render函数,再到vnode
  • vdom:patch(elem,vnode)和patch(vnode,newVnode) **组件渲染/更新过程
  • 解析模版为render函数(或在开发环境已完成,vue-loader)
  • 触发响应式,监听data属性getter setter
  • 执行render函数,生成vnode,patch(elem,vnode)

第二步是因为,执行render函数会触发getter操作

vue原理面试题_https://bianchenghao6.com/blog_Vue_第7张

更新过程

  • 修改data,触发setter(此前在getter中已被监听)
  • 重新执行render函数,生成newVnode
  • patch(vnode,newVnode)打补丁

触发setter,看是修改的data是否在getter中已经被监听,如果是,就执行render函数

patch的diff算法,会计算出最小差异,更新在DOM上

完整流程图

vue原理面试题_https://bianchenghao6.com/blog_Vue_第8张

模板编译完,生成render函数,执行render函数生成vnode (虚拟DOM的树)

执行render函数的时候会touch getter,即执行函数的时候回触发Data里的getter

触发的时候就会收集依赖,即在模板中出发了哪个变量的getter就会把哪个给观察起来(watcher)

在修改Data的时候,看这个Data是否是之前作为依赖被观察起来的

如果是,就重新出发re-render,重新渲染,重新生成vdom tree,重新touch

vue 渲染过程 (如果上面解释不理解)

vue原理面试题_https://bianchenghao6.com/blog_Vue_第9张

render

调用 compile 函数,生成 render 函数字符串 ,编译过程如下: parse 函数解析 template,生成 ast(抽象语法树)

optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)

generate 函数生成 render 函数字符串

调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象

调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

路由原理

(1)网页url组成部分

vue原理面试题_https://bianchenghao6.com/blog_Vue_第10张

(2)hash的特点

vue原理面试题_https://bianchenghao6.com/blog_Vue_第11张

(3)h5 history

  • 用url规范的路由,但跳转时不刷新页面
  • history.pushState
  • window.onpopstate~~

总结:

hash 模式 (默认)

  • 工作原理: 监听网页的hash值变化 —> onhashchange事件, 获取location.hash
  • 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
  • 会给用户好像跳转了网页一样的感觉, 但是实际上没有跳转
  • 主要用在单页面应用(SPA)

history 模式

  • 工作原理: 主要利用 history.pushState() API 来改变URL, 而不刷新页面.
  • 其实一共有五种模式可以实现改变URL, 而不刷新页面.
  • 需要后台配置支持, 如果输入一个并不存在的url, 需要后端配置做 “兜底配置”, 不是粗暴的返回404, 而是返回首页

$nextTick 异步渲染

vue是异步渲染,$nextTick会待Dom渲染完之后调用

页面渲染时会将data的修改做整合,多次data修改只会渲染一次

2.汇总data的修改,一次性更新试图

3.减少DOM操作次数,提高性能

$nextTick原理

js运行机制 JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

1.所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

2.主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

3.一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

4.主线程不断重复上面的第三步。

vue原理面试题_https://bianchenghao6.com/blog_Vue_第12张

event-loop 主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

for (macroTask of macroTaskQueue) {
  // 1. Handle current MACRO-TASK
  handleMacroTask();
 
  // 2. Handle all MICRO-TASK
  for (microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}

复制代码

在浏览器环境中 :

常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate

常见的 micro task 有 MutationObsever 和 Promise.then

异步更新队列

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 方法的实现原理:

1.vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行

2.microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

3.考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

computed 和 watch 有什么区别及运用场景?

computed

computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

watch

watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

应用场景

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

computed的实现原理

computed 本质是一个惰性求值的观察者。

computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。

其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,

computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

组件生命周期

1.有哪些生命周期

1. beforeCreate(创建前)
2. created (创建后)
3. beforeMount (载入前)
4. mounted (载入后)
5. beforeUpdate (更新前)
6. updated   (更新后)
7. beforeDestroy( 销毁前)
8. destroyed (销毁后)
复制代码

2.每个生命周期的介绍

beforeCreate(){}:Vue创建前,此阶段为实例初始化之后,this指向创建的实例,数据观察,数据监听事件机制都未形成,不能获得DOM节点。data,computed,watch,methods 上的方法和数据均不能访问,注:date和methods的数据都还未初始化。

Created(){}: Vue创建后,此阶段为实例初始化之后,data、props、computed的初始化导入完成, 注:要调用methods中的方法,或者操作data中的数据,最早只能在Created中操作

能访问 data computed watch methods 上的方法和数据,初始化完成时的事件写这个里面,

此阶段还未挂载DOM。

beforeMount(){}: Vue载入前,阶段执行时, 模板已经在内存中编译好了,但是未挂载到页面中,(页面还是旧的)

注:这个阶段是过渡性的,一般一个项目只能用到一两次。

Mounted(){}:Vue载入后,(完成创建vm.$el,和双向绑定); 只要执行完mounted,就表示整个Vue实例已经初始化完成了,此时组件已经脱离里了创建阶段, 进入到了运行阶段。

beforeUpdate(){}:Vue更新前, 当执行beforeUpdate的时候,页面中显示的数据还是旧的,此时date数据是最新的,页面尚未和最新数据数据保持同步。但是DOM中的数据会改变,这是vue双向数据绑定的作用,可在更新前访问现有的DOM,如手动移出添加的事件监听器。

Updated(){}:Vue更新后, Updated执行时数据已经保持同步了,都是最新的,

完成虚拟DOM的重新渲染和打补丁。 组件DOM已完成更新,可执行依赖的DOM操作。

注意:不要在此函数中操作数据(修改属性),否则就会陷入死循环。

beforeDestroy(){}:(Vue销毁前,可做一些删除提示,比如:您确定删除****吗?)

当执行beforeDestroy的时候,Vue实例就已经从运行阶段进入到销毁阶段了。实例上的所有date和methods以及过滤器和指令都是处于可用状态,此时还没有真正的执行销毁过程。

Destroyed(){}:Vue销毁后, 当执行到destroted函数的时候,组件已经完全销毁(渣都不剩),此时组件中的所有的数据,方法,指令,过滤器...都已经销毁(不可用了)。

Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : 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;
}

复制代码

key是虚拟DOM对象的标识,当状态中的数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】,随后Vue进行【新虚拟DOM】的差异比较,比较规则如下:

1、旧虚拟DOM中找到了与新虚拟DOM相同的key:

  • 若虚拟DOM中内容没变,直接使用之前的真实DOM
  • 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM

2、旧虚拟DOM中未找到与新虚拟DOM相同的key

  • 创建新的真实DOM,随后渲染到页面

用index作为key可能会引发的问题:

1、若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ===> 界面效果没问底,但效率低

2、如果结构中还包含输入类的DOM:会产生错误DOM更新 ===> 界面有问题

vue 是如何对数组方法进行变异的 ?

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
 "push",
 "pop",
 "shift",
 "unshift",
 "splice",
 "sort",
 "reverse"
];

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
 // cache original method
 const original = arrayProto[method];
 def(arrayMethods, method, function mutator(...args) {
   const result = original.apply(this, args);
   const ob = this.__ob__;
   let inserted;
   switch (method) {
     case "push":
     case "unshift":
       inserted = args;
       break;
     case "splice":
       inserted = args.slice(2);
       break;
   }
   if (inserted) ob.observeArray(inserted);
   // notify change
   ob.dep.notify();
   return result;
 });
});

/**
* Observe a list of Array items.
*/
Observer.prototype.observeArray = function observeArray(items) {
 for (var i = 0, l = items.length; i < l; i++) {
   observe(items[i]);
 }
};

复制代码

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢? 因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

谈谈 Vue 事件机制,手写on,on,on,off,emit,emit,emit,once

class Vue {
 constructor() {
   //  事件通道调度中心
   this._events = Object.create(null);
 }
 $on(event, fn) {
   if (Array.isArray(event)) {
     event.map(item => {
       this.$on(item, fn);
     });
   } else {
     (this._events[event] || (this._events[event] = [])).push(fn);
   }
   return this;
 }
 $once(event, fn) {
   function on() {
     this.$off(event, on);
     fn.apply(this, arguments);
   }
   on.fn = fn;
   this.$on(event, on);
   return this;
 }
 $off(event, fn) {
   if (!arguments.length) {
     this._events = Object.create(null);
     return this;
   }
   if (Array.isArray(event)) {
     event.map(item => {
       this.$off(item, fn);
     });
     return this;
   }
   const cbs = this._events[event];
   if (!cbs) {
     return this;
   }
   if (!fn) {
     this._events[event] = null;
     return this;
   }
   let cb;
   let i = cbs.length;
   while (i--) {
     cb = cbs[i];
     if (cb === fn || cb.fn === fn) {
       cbs.splice(i, 1);
       break;
     }
   }
   return this;
 }
 $emit(event) {
   let cbs = this._events[event];
   if (cbs) {
     const args = [].slice.call(arguments, 1);
     cbs.map(item => {
       args ? item.apply(this, args) : item.call(this);
     });
   }
   return this;
 }
}
复制代码

聊聊 keep-alive 的实现原理和缓存策略

export default {
  name: "keep-alive",
  abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
  props: {
    include: patternTypes, // 被缓存组件
    exclude: patternTypes, // 不被缓存组件
    max: [String, Number] // 指定缓存大小
  },
 
  created() {
    this.cache = Object.create(null); // 缓存
    this.keys = []; // 缓存的VNode的键
  },
 
  destroyed() {
    for (const key in this.cache) {
      // 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },
 
  mounted() {
    // 监听缓存/不缓存组件
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },
 
  render() {
    // 获取第一个子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name不在inlcude中或者在exlude中 直接返回vnode
      // check pattern
      const name: ?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode;
      }
 
      const { cache, keys } = this;
      // 获取键,优先获取组件的name字段,否则是组件的tag
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }
      // 不命中缓存,把 vnode 设置进缓存
      else {
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // keepAlive标记位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};
复制代码

原理

1.获取 keep-alive 包裹着的第一个子组件对象及其组件名

2.根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

3.根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

4.在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)

5.最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法 LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

vue原理面试题_https://bianchenghao6.com/blog_Vue_第13张

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

vm.$set()实现原理是什么

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

export function set(target: Array<any> | Object, key: any, val: any): any {
  // target 为数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
    target.length = Math.max(target.length, key);
    // 利用数组的splice变异方法触发响应式
    target.splice(key, 1, val);
    return val;
  }
  // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__;
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 进行响应式处理
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}
复制代码
  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值
  3. 如果 target 本身就不是响应式,直接赋值
  4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

说出至少 4 种 vue 指令和它的用法

  • v-if:判断是否隐藏;
  • v-for:数据循环;
  • v-bind:class:绑定一个属性;
  • v-model:实现双向绑定;

v-if和v-show的区别

  • v-if通过控制dom节点的方式,添加和删除元素,进而实现显示或隐藏元素,v-show通过设置dom元素的display来实现显示或隐藏的操作,并不会删除dom
  • v-if隐藏会将组件销毁,显示时会将其内部的监听事件重建,v-show只是设置display,并不会阻止子组件内部的监听事件
  • v-if有着更高的切换消耗,v-show有着更高的初始渲染消耗

MVVM 模型

数据驱动视图

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到ViewModel层并自动将数据渲染到页面中,视图变化的时候通知viewModel层更新数据

vuex

有哪几种属性

有五种,分别是 State、 Getter、Mutation 、Action、 Module。

每个属性介绍 1.state

Vuex就是一个仓库,仓库里面放了很多对象。其中state就是数据源存放地,对应于一般Vue对象里面的data。

state里面存放的数据是响应式的,Vue组件从store中读取数据,若是store中的数据发生改变,依赖这个数据的组件也会发生更新。

通过mapState把全局 state 和 getters 映射到当前组件的 computed 计算属性中。

2.getter

  • getters 可以对State进行计算操作,它就是Store的计算属性。
  • 虽然在组件内也可以做计算属性,但是getters 可以在多组件之间复用。
  • 如果一个状态只在一个组件内使用,可以不用getters。

3.mutation和actions

Action 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。 不使用vuex会带来什么问题

  • 可维护性会下降,想修改数据要维护三个地方;
  • 可读性会下降,因为一个组件里的数据,根本就看不出来是从哪来的;
  • 增加耦合,大量的上传派发,会让耦合性大大增加,Vue用Component本意就是为了减少耦合,现在这么用,和组件化的初衷相背

组件之间通信

详细内容可参考:blog.csdn.net/Albert_weik…

(1)props/ $emit(父子间传递常用方式)

(2) attrs/attrs/ attrs/listeners

(3) parent/parent/ parent/children与ref(直接得到组件实例)

(4)EventBus(针对安卓优化的发布/订阅事件总线

作者:6块
链接:https://juejin.cn/post/7125462961501503519
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

发表回复