vue
1. v-show与v-if的区别
控制手段
v-show 通过 css display: none 来控制,实际上元素一直都在;v-if 通过将整个 dom 元素添加或删除。
编译过程
v-if 会重新编译销毁子孙组件,并触发子孙组件完整的生命周期函数,而 v-show 由于元素一直存在,所以不会触发生命周期函数。
性能消耗
在首次渲染时,如果条件为假,v-if 不会渲染,而 v-show 会渲染节点,只是看不到,因此这种情况下 v-show 更耗性能;
而 v-if 每次切换都会涉及到组件的销毁和重新编译,所以在频繁切换时他更消耗性能。
2. Vue实例挂载的过程 / new Vue()这个过程中究竟做了些什么?
new Vue()的时候会调用内部的_init方法- 定义
$set、$get、$delete、$watch等方法 - 定义
$on、$off、$emit、$off等事件 - 定义
_update、$forceUpdate、$destroy生命周期
- 定义
- 调用
$mount进行页面挂载 - 挂载的时候主要通过
mountComponent方法 - 定义
updateComponent更新函数 - 执行
render生成虚拟DOM _update将虚拟DOM生成真实DOM结构,并且渲染到页面中
3. Vue中如何给对象添加新属性?
Vue.set()
Vue.set( target, propertyName/index, value )
Vue 内部通过 defineReactive 方法实现新增属性的响应式,这个方法内部还是通过 Object.defineProperty 实现属性拦截。
Object.assign()
直接使用Object.assign()添加到对象的新属性不会触发更新,应创建一个新的对象,合并原对象和混入对象的属性:
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
$forceUpdate
$forceUpdate 迫使 Vue 实例重新渲染,仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
4. Vue中为什么data属性是一个函数而不是一个对象?
● 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
● 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData 时会将其作为工厂函数返回全新data对象
5. Vue组件之间的通信方式都有哪些?
有8种常规通信方式
父子组件之间的通信
props父向子传递数据$emit父监听子组件派发的事件和数据ref父组件通过给子组件设置ref属性来操作子组件的数据
兄弟组件之间的通信
EventBus// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// Children1.vue
this.$bus.$emit('foo')
// Children2.vue
this.$bus.$on('foo', this.handle)$parent或$root
通过共同祖辈$parent或者$root搭建通信桥连this.$parent.on('add',this.add)
this.$parent.emit('add')
祖孙与后代组件之间的通信
$attrs与$listenersprovide与inject
非关系组件间之间的通信
vuexState设置默认的初始状态Getter允许组件从 Store 中获取数据Mutation是唯一更改 store 中状态的方法,且必须是同步函数Action用于提交 mutation,而不是直接变更状态,可以异步操作Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
6. Vue双向绑定的原理是什么?
Vue2 的双向绑定主要是通过 数据劫持和发布订阅模式 来实现的,核心是实现监听器、解析器、订阅者和订阅器(依赖收集)
- 实现一个监听器
Observer
对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()对属性都加上setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。 - 实现一个解析器
Compile
解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者Watcher,一旦数据有变动,收到通知,调用更新函数进行数据更新。 - 实现一个订阅者
Watcher
Watcher订阅者是Observer和Compile之间通信的桥梁 ,主要的任务是订阅监听器Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile中对应的更新函数。 - 实现一个订阅器/依赖收集
Dep
订阅器采用 发布-订阅 设计模式,用来收集订阅者Watcher,对监听器Observer和 订阅者Watcher进行统一管理
7. vue2中NextTick是什么?什么场景使用?原理是什么?为什么 Vue 要异步更新 DOM?
nextTick 在 Vue2 里是一个异步执行回调函数的方法,它的作用是:在下一次 DOM 更新循环结束之后执行回调。
因为在 Vue 中,数据变化不会立刻反映到 DOM 上,Vue 会对数据更新做异步队列合并,把同一事件循环内的多次数据修改合并成一次 DOM 更新。
所以有时候我们改完数据,立刻去访问 DOM,会拿到旧的内容。
这时候就需要把操作放到 nextTick 里,等 DOM 更新完成后再执行。
举个常见的例子,比如我们在修改数据之后,需要马上获取更新后的元素高度,就需要在 this.$nextTick 里获取。
Vue2 的 nextTick 底层是通过微任务优先(Promise、MutationObserver),
如果环境不支持再降级到宏任务(setTimeout),确保在 DOM 更新完成后再调用回调。
Vue 异步更新 DOM 可以避免重复渲染,提升性能。
如果每次数据变动都更新 DOM,会造成性能浪费。
Vue 通过异步队列把多次修改合并成一次批量更新。
8. Vue2 中 key 的原理
在 Vue2 里,key 的作用是 给虚拟 DOM 的每个节点做唯一标识,它的核心原理是:在虚拟 DOM diff 过程中,通过比较新旧节点的 key 来判断节点是否相同。
如果两个节点的 key 相同,Vue 会复用旧节点,只更新必要的内容; 如果 key 不同,就会认为是全新的节点,销毁旧的、创建新的,从而触发完整的渲染。
这样做的好处是提高 DOM 更新效率,减少不必要的重绘和重排。
比如在 v-for 循环中必须加 key,否则 Vue 会默认按顺序复用旧节点,导致组件状态混乱或动画异常。
另外,key 最好使用具有业务唯一性的值,比如 id,不建议用 index,因为当列表顺序变化时,index 会变化,导致 DOM 复用错误或重新渲染。
总结来说,key 是虚拟 DOM diff 的优化标识,通过唯一标识来让 Vue 更精准地复用节点,提高性能并保持状态正确。
9. Vue2 中的自定义指令
自定义指令中的钩子函数
-
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
-
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中) 。
-
update:所在组件的
VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。 -
componentUpdated:指令所在组件的
VNode及其子VNode全部更新后调用 -
unbind:只调用一次,指令与元素解绑时调用
所有的钩子函数的参数都有以下:
- el:指令所绑定的元素,可以用来直接操作 DOM
- binding:一个对象,包含以下属性:
- name:指令名,不包括
v-前缀。 - value:指令的绑定值,例如:
v-my-directive="1 + 1"中,绑定值为 2 - oldValue:指令绑定的前一个值,仅在
update和componentUpdated钩子中可用。无论值是否改变都可用。 - expression:字符串形式的指令表达式。例如
v-my-directive="1 + 1"中,表达式为"1 + 1" - arg:传给指令的参数,可选。例如
v-my-directive:foo中,参数为"foo" - modifiers:一个包含修饰符的对象。例如:
v-my-directive.foo.bar中,修饰符对象为{ foo: true, bar: true }
- name:指令名,不包括
- vnode:Vue 编译生成的虚拟节点
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用
使用场景 - 一键复制
const vCopy = { //
/*
bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置
el: 作用的 dom 对象
value: 传给指令的值,也就是我们要 copy 的值
*/
bind(el, { value }) {
el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到
el.handler = () => {
// 动态创建 textarea 标签
const textarea = document.createElement('textarea');
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
// textarea.setSelectionRange(0, textarea.value.length);
const result = document.execCommand('Copy');
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener('click', el.handler);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener('click', el.handler);
},
};
export default vCopy;
10. vue2中什么是虚拟DOM?为什么需要虚拟DOM?如何实现虚拟DOM?虚拟DOM的优缺点?
什么是虚拟DOM
在 Vue2 里,虚拟 DOM(Virtual DOM) 其实就是用 JavaScript 对象 来描述真实 DOM 结构的一种抽象表示。
Vue 会在内存中根据组件的模板生成一棵虚拟 DOM 树,每个节点都是一个普通的 JS 对象, 比如包含标签名、属性、子节点等信息。
为什么需要
我们之所以需要虚拟 DOM,是因为 直接操作真实 DOM 的性能开销很大,DOM 是浏览器提供的接口,每次修改都会引起重绘和回流。
而 Vue 通过虚拟 DOM,可以在内存中先做批量的 diff 对比,找出真正变化的部分,再有选择地更新到页面上,这样能显著提升性能、同时让跨平台渲染更灵活(比如可以渲染到小程序或 Native)。
如何实现
至于实现原理,大致分三步:
- 把模板编译成 render 函数;
- 执行 render 生成虚拟 DOM(VNode);
- 当数据变化时,用
diff算法 比较新旧虚拟 DOM,只更新变化的节点到真实 DOM。
简单来说,虚拟 DOM 就是“用 JS 模拟 DOM,用 diff 算法找不同,再高效更新”,它不是比真实 DOM 快,而是让 DOM 更新变得“更聪明、更可控”。
优缺点
优点
-
保证性能下限
框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普遍适应的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限 -
无需手动操作 DOM
我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率 -
跨平台
虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等
缺点
- 无法进行极致优化
虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化
11. Vue 的 diff 算法具体怎么比较虚拟 DOM?
Vue 的 diff 算法主要是在虚拟 DOM 更新阶段用来比较新旧两棵虚拟 DOM 树,目的是找出最小的差异,然后只更新必要的部分。
Vue2 的 diff 算法采用的是一种 同层比较、深度优先、双端对比 的策略。
它不会把整棵树重新对比,而是:
- 从根节点开始,只比较同一层级的节点;
- 如果节点类型(比如标签名)不同,直接替换整个节点;
- 如果节点类型相同,就复用旧节点,只比较属性和子节点;
- 在子节点比较时,使用双端指针法——同时从头尾两边往中间对比,通过 key 快速定位、移动或复用节点,从而提高性能。
最终,diff 算法会生成一系列最小化的 DOM 操作补丁(patch),再由 patch 函数把这些改动一次性应用到真实 DOM 上。
总结来说,Vue 的 diff 算法不是暴力全量比较,而是在保持正确性的前提下尽可能减少操作量,
用 key 辅助节点复用,最大化性能。
12. computed 和 watch 的区别和运用的场景?
区别
- computed: 是计算属性,依赖多个响应式数据计算出一个新值,并且 computed 的值有缓存,只有当依赖的数据发生变化时才会重新计算,否则会直接返回缓存的结果,性能更高。
- watch:则用于观察某个数据的变化并执行副作用逻辑,比如发送请求、操作 DOM、手动更新其他数据等。它更像一个监听回调函数。
场景
- computed:据用户输入动态计算显示结果
- watch: 在数据变化时调用接口或写入本地存储
13. 父组件可以监听到子组件的生命周期吗?
子组件使用 $emit
父组件使用 @hook 监听
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
14. 谈谈你对 keep-alive 的了解
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件
- 提供
include和exclude属性,两者都支持字符串或正则表达式,include表示只有名称匹配的组件会被缓存,exclude表示任何名称匹配的组件都不会被缓存 ,其中exclude的优先级比include高 - 对应两个钩子函数
activated和deactivated,当组件被激活时,触发钩子函数activated,当组件被移除时,触发钩子函数deactivated