用了很久 Vue 全家桶开发项目,自我感觉还是挺熟练的。但是面试的时候竟然还是被 Vue 相关知识点问倒?这不应该啊,因此下决心一口气记录下以下问题,相信我搞懂了这些,Vue 再也难不倒你!
key 的作用和原理
key 是给每个 vnode 的唯一标识,也是 diff 的一种优化策略,可以根据 key 更准确,更快的找到对应的 vnode 节点,减少对 dom 的操作 提高 diff 效率。
场景
1、v-for 需要使用 key
- 如果不用 key,Vue 会采用就地复地原则:最小化 element 的移动,并且会尝试尽最大程度在同适当的地方对相同类型的 element,做 patch 或者 reuse。
- 如果使用了 key,Vue 会根据 keys 的顺序记录 element,曾经拥有了 key 的 element 如果不再出现的话,会被直接 remove 或者 destoryed
2、用 +new Date() 生成的时间戳作为 key ,手动强制触发组件重新渲染 <Comp :key="+new Date()" />
- 当拥有新值的 rerender 作为 key 时,拥有了新 key 的 Comp 出现了,那么旧 key Comp 会被移除,新 key Comp 触发重新渲染(走组件生命周期)
为什么使用 key
因为默认情况 Vue 为了高效的渲染元素,通常会复用已有元素,而不是从头开始渲染。但是这可能会导致一些数据的混乱问题,并不符合我们实际开发需求。 所以 Vue 提供了一个方式来添加具有唯一值的 key 属性告诉”这两个元素是完全独立的,不要复用它们“。
设置 key 值一定能提高 diff 效率吗?
其实不然,文档中也明确表示
当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出【查询列表】
建议尽可能在使用 v-for
时提供 key
,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升
$nextTick([callback]) 异步更新队列
记住这句话:将回调函数延迟到下次 DOM 更新循环之后执行。
通常用在 修改数据之后立即需要使用,获取更新后的 DOM 。
Vue 内部对异步队列尝试使用原生的 微任务 那几个函数去执行的。
// 修改数据
vm.msg = "Hello";
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
});
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick().then(function () {
// DOM 更新了
});
Virtual DOM
什么是虚拟 DOM
一种更轻量的纯 JavaScript 对象(树)描述 DOM 树,因为不是真实的 DOM 对象,所以叫 「虚拟 DOM」。
因原生 DOM 属性太多,操作 JS 对象比操作 DOM 效率要高很多。
透露剧情:其实就是利用牺牲 cpu 对 JS 处理的性能来替换 GPU 对页面渲染的性能。
为什么使用虚拟 DOM
例子:对一个列表做操作,新增、排序。
1、中期 模板引擎 没有解决跟踪状态变化的问题:当我们操作 DOM 的时候(新增、排序等),因为没有状态跟踪 列表 DOM 需要先被删除,然后再重建,性能可想而知。
2、新增 虚拟 DOM 只会更新发生变化的 DOM 元素,排序 虚拟 DOM 只是把元素位置调换了一下。
说白了就是以 JS 的计算性能来换取操作真实 DOM 所消耗的性能。
在数据变化前后生成真实 DOM 对应的虚拟 DOM 节点,然后对比新旧两份 VNode,更新有差异的 DOM 节点,最终达到以最少操作真实 DOM 更新视图的目的。
虚拟 DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下 提升渲染性能,简单的 纯 DOM 性能还是可以的。
- 除了渲染 DOM 以外,还可以实现跨平台 SSR(Nuxt.js\Next.js)、原生应用(Weex\React Native)、小程序(mpvue\uni-app) 等。 因为 虚拟 DOM 本身就是 JS 对象,所以可以对他做任意的编程。
主要需要解决以下问题:
1、高效的 diff 算法,即两个 Virtual DOM 比较,其实就是 patch (补丁) 过程
2、只更新需要更新的 DOM
3、数据变化检测,patch DOM 读写操作等等
snabbdom 虚拟 DOM 开源库
简介
Vue 2.x 内部使用的 Virtual DOM 就是改造的 snabbdom。
轻量的 Virtual DOM 实现,代码量少,模块化,TS 开发结构清晰。
看文档
- 学习任何一个库首先要看文档
- 通过文档了解库的作用
- 看文档中提供的示例,自己快速实现一个 demo
- 通过文档查看 API 的使用
核心事件:
- 使用 h() 函数创建 javascript 对象(VNode)描述真实 DOM
- init() 设置模块,创建 patch()
- path() 比较新旧两个 VNode, 把变化的内容更新到真实 DOM 树上
模块
模块的使用方式
1、导入模块:类似于插件
- 导入需要的模块
- init([]) 中注册模块
- 使用 h() 函数创建 Vnode 的时候,可以把第二个参数设置为对象,其他参数往后移。
import { init, h } from "snabbdom";
import { init, h, eventListenersModule, styleModule } from "snabbdom"; // 最新版
// 样式模块
// 事件监听模块
2、注册模块
let patch = init([styleModule, eventListenersModule]);
3、使用 h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h(
"div",
{
style: {
backgroundColor: "red",
},
on: {
click: eventHandler,
},
},
[
// 数组子元素
h("h1", "这是 children h1"),
h("p", "这是 p 标签"),
]
);
function eventHandler() {
console.log("点击我了");
}
let app = document.querySelector("#app");
patch(app, vnode); // 页面就渲染出来了
源码解析
h.ts
最早是 hyperscript 使用 JavaScript 创建超文本。 这里的 h() 函数增强了,创建 VNode
h( ‘sel’, data, children ) 接受一个字符串格式的标签或选择器,一个可选的数据对象,一个可选的字符串或子节点数组。
主要是利用 h 函数的重载,三个参数 三种情况判断,最后 通过 vnode 函数来创建节点,以及 svg 的处理。
(ts 重载最终还是要编译成 js 的)so 内部实现就是 针对传入的三个参数进行各种判断,children 是数组的话,遍历循环 调用 vnode 函数 创建虚拟节点
VNode 关键点
VNode 的创建
VNode 渲染真实 DOM
位于最关键 init.ts 文件 调用 init 函数,利用高阶函数 返回了 patch 函数,让 patch 更方便的访问 init 传入的两个参数,形成闭包。
发布订阅模式和观察者模式
先上个图看的更清晰
发布订阅模式
- 订阅者
- 发布者
- 事件中心
比如:Vue 的自定义事件、eventBus.js 都是利用这个模式。
// 实现一个 事件触发器
class EventEmiter {
constructor() {
// 存储事件和对应的回调函数 { 'click': [fn1,fn2] ,change: [fn3, fn4] }
this.subs = Object.create(null);
}
// 注册事件,因为可以是多个事件,所以是数组形式
$on(eventType, handle) {
this.subs[eventType] = this.subs[eventType] || []; // 初始化当前注册的事件(因为刚开始没有事件)
this.subs[eventType].push(handle); // 数组的形式存起来
}
// 触发(发布)事件
$emit(eventType) {
// 根据 事件类型 找到所有注册的事件,遍历依次执行
this.subs[eventType].forEach((handle) => {
handle();
});
}
}
// 测试
let em = new EventEmiter();
em.$on("click", () => {
console.log("em.$on");
});
em.$on("click", () => {
console.log("em.$on2222");
});
em.$emit("click", () => {
console.log("em.$emit");
});
观察者模式
- 订阅者 - 观察 Watcher
当事件发生时,具体要做的事情 update() (负责更新视图)
- 发布者 - 目标 Dep (依赖)
- subs 数组:存储所有观察者
- addSub(): 添加观察者
- notify(): 当事件发生时(数据发生变化的时候),调用所有观察者的 update() 方法
- 没有事件中心
// 定义 观察者 需要做的事情
class Watcher {
update() {
console.log("我是观察者 update 函数,我会去更新视图");
}
}
// 发布
class Dep {
constructor() {
this.subs = [];
}
// 收集
addSub(sub) {
// 确保是一个 观察者
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 通知
notify() {
// 遍历所有观察者调用自己的方法
this.subs.forEach((sub) => {
sub.update();
});
}
}
// 测试
let wac = new Watcher();
let dep = new Dep();
dep.addSub(wac);
dep.notify();
模拟 Vue 响应式原理
来我们着个简单分析一下:
- Vue: 把 data 中的所有成员注入到 Vue 实例,并且添加 getter / setter,内部会调用 Observer\Compiler
- Observer 数据劫持: 对数据对象所有属性进行监听,如有变动可拿到最新的值并通知 Dep
- Compiler: 解析每个元素中的指令以及插值表达式,并替换成相应的数据
- Dep: 添加观察者,当数据发生变化的时候通知所有的观察者
- Watcher: 定义了 update 函数,负责更新视图
Vue
Observer 数据劫持
Dep 依赖收集
Watcher 观察者
拥有属性:
vm: Vue 实例
key: 对象的属性,主要用来获取 哪个数据
cb: 回调函数,用来处理怎么更新视图的方法
oldValye:旧值,与新值比较是否去更新
方法:
update():主要处理更新视图的功能
这样子组合起来就是 Vue 响应式核心了。
Vue 数组变化检测
首先:根据索引直接改变值和改变 length
this.list[0] = 3、this.list.length = 5
两种方式都是不会触发数据双向绑定的。
最详细版本解读:https://mp.weixin.qq.com/s/FimW7pNVZZZ3Ci0fWmwwPQ
在 Vue2.x 中数组变化监听的问题,其实不是 Object.definePropertype 方法监听不到,而是为了性能和收益比例综合考虑之下,改变了监听方式,从原本的直接监听结果变化这种思路变换到监听会导致结果变化的方法上,也就上面所提到的对数组的重写。
而 Vue3.0 中利用 Proxy 的方式则完美解决了 2.x 中出现的问题,所以以后面试中如果遇到 Vue 中对于数组监听的处理的时候,一定要分清楚是哪一个版本。
父子组件生命周期顺序
- 渲染过程:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount-> 子 mounted -> 父 mounted
- 子组件更新过程:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程:
父 beforeUpdate -> 父 updated
- 销毁过程:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
/** 附上代码直接跑: **/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>父子组件生命周期</title>
</head>
<body>
<div id="app">
声明周期顺序:先父 beforeCreate ---- beforeMount --- 子组件 beforeCreate
开始 ---> mounted keep-alive 触发 activated 数据发生变化触发: updated
视图发生更新后
<!-- v-model 就是 v-bind + v-on 的语法糖 -->
<input type="text" v-model="message" />
<input
type="text"
v-bind:value="message"
v-on:input="message = $event.target.value"
/>
<p>{{message}}</p>
<keep-alive>
<my-components :msg="msg1" v-if="show"></my-components>
</keep-alive>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script>
var child = {
template: "<div>from child: {{childMsg}}</div>",
props: ["msg"],
data: function () {
return {
childMsg: "child",
};
},
beforeCreate: function () {
debugger;
},
created: function () {
debugger;
},
beforeMount: function () {
debugger;
},
mounted: function () {
debugger;
},
deactivated: function () {
alert("keepAlive停用");
},
activated: function () {
console.log("component activated");
},
beforeDestroy: function () {
console.group("beforeDestroy 销毁前状态===============》");
var state = {
el: this.$el,
data: this.$data,
message: this.message,
};
console.log(this.$el);
console.log(state);
},
destroyed: function () {
console.group("destroyed 销毁完成状态===============》");
var state = {
el: this.$el,
data: this.$data,
message: this.message,
};
console.log(this.$el);
console.log(state);
},
};
var vm = new Vue({
el: "#app",
data: {
message: "father",
msg1: "hello",
show: true,
},
beforeCreate: function () {
debugger;
},
created: function () {
debugger;
},
beforeMount: function () {
debugger;
},
mounted: function () {
debugger;
},
beforeUpdate: function () {
alert("页面视图更新前");
},
updated: function () {
alert("页面视图更新后");
console.log("1==我会先执行");
this.$nextTick(function () {
//在下次 DOM 更新循环结束之后执行这个回调。在修改数据之后立即使用这个方法,获取更新后的DOM.
console.log("3==我只能等页面渲染完了才会立即执行");
});
console.log("2==我虽然在最后但会比$nextTick先执行");
},
beforeDestroy: function () {
console.group("beforeDestroy 销毁前状态===============》");
var state = {
el: this.$el,
data: this.$data,
message: this.message,
};
console.log(this.$el);
console.log(state);
},
destroyed: function () {
console.group("destroyed 销毁完成状态===============》");
var state = {
el: this.$el,
data: this.$data,
message: this.message,
};
console.log(this.$el);
console.log(state);
},
components: {
"my-components": child,
},
});
</script>
</html>
组件之间传值/通信方式
这个算是基础这里简单列一下 关键词:
1. v-bind \ : xxx => prop
2. // 父组件
<child @handleChange="changeName"></child>
// 子组件 this.$emit('handleChange', 'Jack')
3.通过 $parent / $children 或 $refs访问组件实例
这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
4.EventBus.js 定义全局事件 EventBus.$emit("editName", this.name) //触发全局事件,并且把改变后的值传入事件函数
5. provide/inject 多层父子之间传值简单来说就是在父组件中通过 provider 来提供变量,然后在子组件中通过 inject 来注入变量,不管组件层级有多深,在父组件生效的生命周期内,这个变量就一直有效。
注:provide 和 inject 绑定并不是可响应的。即父组件的 name 变化后,子组件不会跟着变。
6. 强大的 vuex
总结:
- 父子通信: 父向子传递数据是通过 props,子向父是通过 $emit;
通过 $parent / ref 也可以访问组件实例;provide / inject ;listeners; - 兄弟通信: Bus;Vuex
- 跨级通信: Bus;Vuex;provide / inject ; listeners;
v-model 实现原理
其实说白了就是 两个语法的整合: :value + @input
- 原生 DOM : input \ select \ textarea
<input v-model="message"></input>
<input :value="message" @input="message = $event.target.value"></input>
组件中 data 为什么是一个函数
如果是一个对象的话,根据 js 原型链,当多个实例引用同一个对象的话,其中一个被修改,其他对象也都会被影响。显然这是不行的。