1. 双向绑定详解
2.computed 的实现原理
1)为什么需要computed?
template 使用大量复杂的逻辑表达式处理数据,使得代码维护性差,且相同数据重复计算对性能开销大
2)计算属性的实现原理?
1.初始化 data 和 computed,分别代理其 set 和 get 方法,对 data 中的所有属性生成唯一的 dep 实例
2.对 computed 中的 属性生成唯一的 watcher,并保存在 vm._computedWatchers 中
3.访问计算属性时,设置 Dep.target 指向 计算属性的 watcher,调用该属性具体方法
4.方法中访问 data 的属性,即会调用 data 属性的 get 方法,将 data 属性的 dep 加入到 计算属性的 watcher , 同时该 dep 中的 subs 添加这个 watcher
5.设置 data 的这个属性时,调用该属性代理的 set 方法,触发 dep 的 notify 方法
6.因为时 computed 属性,只是将 watcher 中的 dirty 设置为 true
7.最后,访问计算属性的 get 方法时,得知该属性的 watcher.dirty 为 true,则调用 watcher.evaluate() 方法获取新的值
综合以上:也可以解释了为什么有些时候当computed没有被访问(或者没有被模板依赖),当修改了this.data值后,通过vue-tools发现其computed中的值没有变化的原因,因为没有触发到其get方法。
3.为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
1)Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性,导致通过数组下标添加属性无法实时响应
2) Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象
3)Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性
补救:push();pop();shift();unshift();splice();sort();reverse(); Vue重写了数组这个7个方法,称为变异数组,添加了响应式,使得这7个方法添加的属性可以实时响应
4.Vue中的key有什么作用?
1)key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位) .diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
2)更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug
3)更快速 : 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;
}
5.谈一谈nextTick 的原理
vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更;Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替
6.vue 是如何对数组方法进行变异的 ?
1 const arrayProto =Array.prototype;2 export const arrayMethods =Object.create(arrayProto);3 const methodsToPatch =[4 "push",5 "pop",6 "shift",7 "unshift",8 "splice",9 "sort",10 "reverse"
11 ];12 /** * Intercept mutating methods and emit events*/
13 methodsToPatch.forEach(function(method) {14 //cache original method
15 const original =arrayProto[method];16 def(arrayMethods, method, functionmutator(...args) {17 const result = original.apply(this, args);18 const ob = this.__ob__; let inserted;19 switch(method) {20 case "push":21 case "unshift":22 inserted =args;23 break;24 case "splice":25 inserted = args.slice(2);26 break;27 }28 if(inserted)29 ob.observeArray(inserted);30 //notify change
31 ob.dep.notify();32 returnresult;33 });34 });35 /** * Observe a list of Array items.*/
36 Observer.prototype.observeArray = functionobserveArray(items) {37 for (var i = 0, l = items.length; i < l; i++) {38 observe(items[i]);39 }40 };
简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update
7.Vue 组件 data 为什么必须是函数 ?
new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。
所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
8.聊聊 keep-alive 的实现原理和缓存策略?
1 export default{2 name: "keep-alive",3 abstract: true,4 //抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
5 props: {6 include: patternTypes,7 //被缓存组件
8 exclude: patternTypes,9 //不被缓存组件
10 max: [String, Number] //指定缓存大小
11 },12 created() {13 this.cache = Object.create(null); //缓存
14 this.keys = []; //缓存的VNode的键
15 },16 destroyed() {17 for (const key in this.cache) {18 //删除所有缓存
19 pruneCacheEntry(this.cache, key, this.keys);20 }21 },22 mounted() {23 //监听缓存/不缓存组件
24 this.$watch("include", val =>{25 pruneCache(this, name =>matches(val, name));26 });27 this.$watch("exclude", val =>{28 pruneCache(this, name => !matches(val, name));29 });30 },31 render() {32 //获取第一个子元素的 vnode
33 const slot = this.$slots.default;34 const vnode: VNode =getFirstComponentChild(slot);35 const componentOptions: ?VNodeComponentOptions = vnode &&vnode.componentOptions;36 if(componentOptions) {37 //name不在inlcude中或者在exlude中 直接返回vnode
38 //check pattern
39 const name: ?string =getComponentName(componentOptions);40 const { include, exclude } = this; if(41 //not included
42 (include && (!name || !matches(include, name))) ||
43 //excluded
44 (exclude && name &&matches(exclude, name))45 ) { returnvnode; }46 const { cache, keys } = this;47 //获取键,优先获取组件的name字段,否则是组件的tag
48 const key: ?string = vnode.key == null ?
49 //same constructor may get registered as different local components
50 //so cid alone is not enough (#3269)
51 componentOptions.Ctor.cid +
52 (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key;53 //命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
54 if(cache[key]) {55 vnode.componentInstance =cache[key].componentInstance;56 //make current key freshest
57 remove(keys, key); keys.push(key);58 }59 //不命中缓存,把 vnode 设置进缓存
60 else{61 cache[key] =vnode; keys.push(key);62 //prune oldest entry
63 //如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
64 if (this.max && keys.length > parseInt(this.max)) {65 pruneCacheEntry(cache, keys[0], keys, this._vnode);66 }67 }68 //keepAlive标记位
69 vnode.data.keepAlive = true;70 } return vnode || (slot && slot[0]);71 }72 };
原理
获取 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]
9.vm.$set()实现原理是什么?
受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。
那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?
1 export function set(target: Array |Object, key: any, val: any): any {2 //target 为数组
3 if (Array.isArray(target) &&isValidArrayIndex(key)) {4 //修改数组的长度, 避免索引>数组长度导致splice()执行有误
5 target.length =Math.max(target.length, key);6 //利用数组的splice变异方法触发响应式
7 target.splice(key, 1, val); returnval;8 }9 //target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
10 if (key in target && !(key inObject.prototype)) {11 target[key] = val; returnval;12 }13 //以上都不成立, 即开始给target创建一个全新的属性
14 //获取Observer实例
15 const ob =(target: any).__ob__;16 //target 本身就不是响应式数据, 直接赋值
17 if (!ob) { target[key] = val; returnval; }18 //进行响应式处理
19 defineReactive(ob.value, key, val);20 ob.dep.notify();21 returnval;22 }
如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
如果目标是对象,判断属性存在,即为响应式,直接赋值
如果 target 本身就不是响应式,直接赋值
如果属性不是响应式,则调用 defineReactive 方法进行响应式处理
10.Object.defineProperty和Proxy的区别?
Object.defineProperty
不能监听到数组length属性的变化;
不能监听对象的添加;
只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
Proxy
可以监听数组length属性的变化;
可以监听对象的添加;
可代理整个对象,不需要对对象进行遍历,极大提高性能;
多达13种的拦截远超Object.defineProperty只有get和set两种拦截
11.你认为Vue的核心是什么?
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统。
以上是官方原话,从中可以得知Vue的核心是模板语法和数据渲染。
12.Vue为什么要求组件模板只能有一个根元素?
当前的virtualDOM差异和diff算法在很大程度上依赖于每个子组件总是只有一个根元素。
13.ajax、fetch、axios这三都有什么区别?
ajax是最初出现的发送临时请求的技术,属于原生js标准,核心是使用XMLHttpRequest对象,使用并存并有先后顺序的话,容易产生地狱。
fetch号称可以代替ajax的技术,是基于es6中的Promise对象设计的,参数和jQuery中的ajax类似,它并不是对ajax的进一步封装,它属于原生js尺寸。没有使用XMLHttpRequest对象。
axios不是原生js,使用时需要进行进行安装,客户端和服务器端都可以使用,可以在请求和相应阶段进行拦截,基于promise对象。
14.如果想扩展某个现有的Vue组件时,怎么做呢?
用mixins混入
用extends,比mixins先触发
用高阶组件HOC封装
注意:extends使得组件能像面向对象变成一样便于拓展
extends会比mixins先执行。执行顺序:extends > mixins > 组件
extends只能暴露一个extends对象,暴露多个extends不会执行。
mixins能暴露多个
15.vue组件和插件的区别
组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue。
插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身。
简单来说,插件就是指对Vue的功能的增强或补充。
比如说,让你在每个单页面的组件里,都可以调用某个方法,或者共享使用某个变量,或者在某个方法之前执行一段代码等
就可以写一个插件,在Vue原型上扩展方法,要实现这个需求绝对没法写成组件。
1 let whatever ={2 install: function(Vue, options) {3 Vue.prototype.$whatever = function(){4 //do something
5 };6 }7 }
16.为什么Vue使用异步更新组件?
批量更新 收集当前的改动一次性更新 节省diff开销;关联this.$nextTick使用微任务方式得到更新后的dom
17.用vue怎么实现一个换肤的功能?
1.base.scss: 一些通用样式文件(使用scss定义颜色变量,通过@include 引入mixin中的变量)
#content{@include bg_color()}
2.varibale.scss: 颜色,字体,背景的配置文件
$background-color-them1:red
$background-color-them2:blue
3.mixin.scss: 定义mixin方法的文件(通过判断HTML上属性,识别加载模板)
@import "./variable";/*引入配置*/@mixin font_size($size){/*通过该函数设置字体大小,后期方便统一管理;*/@include font-dpr($size);}@mixin bg_color($color){/*通过该函数设置主题颜色,后期方便统一管理;*/background-color:$color;[data-theme="theme1"] & {
background-color:$background-color-theme1;
}[data-theme="theme2"] &{background-color:$background-color-theme2;
}[data-theme="theme3"] &{background-color:$background-color-theme3;
}}
主要原理:
通过设置html的attribute属性在封装的函数中进行判断,进行相应的设置不同的颜色
css中 [ ] 可以识别到在html标签上设置的属性,所以在html上对应属性发生变化时,就会执行相应的样式,
这一步有点类似于平时给div添加一个.active属性,css自动执行相应样式
18.对于 vue3.0 特性你有什么了解的吗?
1.使用porxy替换object.defineProperty
只能监测属性,不能监测对象
检测属性的添加和删除;
检测数组索引和长度的变更;
支持 Map、Set、WeakMap 和 WeakSet。
2.模板
模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能
同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
3.对typescript结合使用更容易
4.其他改变:
• 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
• 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
• 基于 treeshaking 优化,提供了更多的内置功能
19.key除了在v-for中使用,还有什么作用?
还可以强制替换元素/组件而不是重复使用它。在以下场景可以使用
完整地触发组件的生命周期钩子
触发过渡
{{ text }}
当 text 发生改变时,会随时被更新,因此会触发过渡
不要使用对象或数组之类的非基本类型值作为key,请用字符串或数值类型的值;
不要使用数组的index作为key值,因为在删除数组某一项,index也会随之变化,导致key变化,渲染会出错