12个vue高频道理面试题(附剖析)
本文分享12道高频vue道理面试题,覆盖了 vue 中心实现道理,其实一个框架的实现道理一篇文章是不成能说完的,但愿通过这 12 道问题,让读者对本人的 Vue 把握程度有必然的认识(B 数),从而补偿本人的不足,更好的把握 Vue。
【相关引荐:vue面试题(2020)】
1. Vue 响应式道理
中心实现类:
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,在数据变更时公布新闻给订阅者,触发响应的监听回调
2. 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(懒运算)特性。)
3. computed 和 watch 有什么不同及使用场景?
不同
computed 运算属性 : 依靠其它属性值,并且 computed 的值有缓存,只要它依靠的属性值发生改动,下一次猎取 computed 的值时才会从新运算 computed 的值。
watch 侦听器 : 更多的是「视察」的作用,无缓存性,相似于某些数据的监听回调,每当监听的数据转变时都会施行回调停止后续操纵。
使用场景
使用场景:
当我们需要停止数值运算,并且依靠于其它数据时,应当使用 computed,由于可以利用 computed 的缓存特性,幸免每次猎取值时,都要从新运算。
当我们需要在数据转变时施行异步或开销较大的操纵时,应当使用 watch,使用 watch 选项同意我们施行异步操纵 ( 拜访一个 API ),限制我们施行该操纵的频率,并在我们得到终究结果前,设定中心状态。这些都是运算属性没法做到的。
4. 为什么在 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 不仅可以代理对象,还可以代理数组。还可以代理动态增添的属性。
5. 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; }
6. 谈一谈 nextTick 的道理
JS 运转机制
JS 施行是单线程的,它是基于事件轮回的。事件轮回大致分为以下几个步骤:
- 所有同步任务都在主线程上施行,构成一个施行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运转结果,就在"任务队列"之中放置一个事件。
- 一旦"施行栈"中的所有同步任务施行完毕,系统就会读取"任务队列",看看里面是什么事件。那些对应的异步任务,于是完毕等候状态,进入施行栈,开端施行。
- 主线程不竭反复上面的第三步。
主线程的施行历程就是一个 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 办法的实现道理:
- vue 用异队伍列的方式来操纵 DOM 更新和 nextTick 回调前后施行
- microtask 由于其高优先级特性,能确保队列中的微任务在一次事件轮回前被施行完毕
- 思考兼容问题,vue 做了 microtask 向 macrotask 的落级方案
7. 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
8. Vue 组件 data 为什么必需是函数 ?
new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必需是一个函数呢?
由于组件是可以复用的,JS 里对象是援用关系,假如组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,发生副作用。
所以一个组件的 data 选项必需是一个函数,因此每个实例可以保护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once
Vue 事件机制 本质上就是 一个 公布-订阅 模式的实现。
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; } }
10. 说说 Vue 的渲染历程
- 调取 compile 函数,生成 render 函数字符串 ,编译历程如下:
- parse 函数解析 template,生成 ast(抽象语法树)
- optimize 函数优化静态节点 (标志不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比力的历程,优化了 patch 的机能)
- generate 函数生成 render 函数字符串
- 调取 new Watcher 函数,监听数据的转变,当数据发生转变时,Render 函数施行生成 vnode 对象
- 调取 patch 办法,对照新旧 vnode 对象,通过 DOM diff 算法,增加、修改、删除真正的 DOM 元素
11. 聊聊 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]); } };
道理
- 猎取 keep-alive 包裹着的第一个子组件对象及其组件名
- 按照设定的 include/exclude(假如有)停止前提匹配,决议可否缓存。不匹配,直接返回组件实例
- 按照组件 ID 和 tag 生成缓存 Key,并在缓存对象中查寻可否已缓存过该组件实例。假如存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换战略的关键)
- 在 this.cache 对象中储备该组件实例并留存 key 值,之后检查缓存的实例数目可否超越 max 的设定值,超越则按照 LRU 置换战略删除比来最久未使用的实例(便是下标为 0 的阿谁 key)
- 最后组件实例的 keepAlive 属性设定为 true,这个在渲染和施行被包裹组件的钩子函数会用到,这里不细说
LRU 缓存裁汰算法
LRU(Least recently used)算法按照数据的历史拜访记载来停止裁汰数据,其中心思想是“假如数据比来被拜访过,那么未来被拜访的几率也更高”。
keep-alive 的实现正是用到了 LRU 战略,将比来拜访的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被拜访的组件,当缓存实例超越 max 设定值,删除 this.keys[0]
12. 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; }
- 假如目标是数组,使用 vue 实现的变异办法 splice 实现响应式
- 假如目标是对象,推断属性存在,即为响应式,直接赋值
- 假如 target 本身就不是响应式,直接赋值
- 假如属性不是响应式,则调取 defineReactive 办法停止响应式处置
本文转载自:https://segmentfault.com/a/1190000021407782
引荐教程:《JavaScript视频教程》
以上就是12个vue高频道理面试题(附剖析)的具体内容,更多请关注百分百源码网其它相关文章!