核心原理
数据劫持
vue2: 通过 Object.defineProperty
劫持对象
js
const span = document.createElement("span");
document.body.appendChild(span);
const updateNode = (key, val) => {
if (key === "span") {
span.innerHTML = val;
}
};
const data = { span: "" };
const copyData = JSON.parse(JSON.stringify(data));
for (let key in copyData) {
Object.defineProperty(copyData, key, {
get() {},
set(val) {
data[key] = val;
updateNode(key, val);
},
});
}
copyData.span = "666";劫持数组:通过覆写数组原生方法进行接持
js
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
// hack 以下几个函数
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach(function (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);
// 触发更新
ob.dep.notify();
return result;
});
});vue3: 通过 Proxy
使用 Proxy 进行代理,相比 Object.defineProperty 的方式,具有以下优点
- 可以直接监听对象而非属性
- 可以直接监听数组
- 具有更多的拦截方法,扩展性高
- 无需在操作前拷贝原对象
js
const span = document.createElement("span");
document.body.appendChild(span);
const updateNode = (key, val) => {
if (key === "span") {
span.innerHTML = val;
}
};
const data = { span: "" };
const proxy = new Proxy(data, {
set(target, prop, value, receiver) {
Reflect.set(target, prop, value, receiver);
updateNode(prop, value);
},
});
proxy.span = "777";组件渲染
模板编译
模板是对组件渲染结构的描述, 相比于直接编写 render,对开发者更加友好, 且易读性更高 vue 和 react 都支持 jsx, 相比于通过 h 函数描述的 render 可读性更高, vue 同时还独立出 template, 其较 jsx 更接近 html vue 内部是通过 vnode 描述组件的, 因而 template 在使用前还需要经过编译流程
- 解析: 将模板解析为 AST
- 优化: 对静态内容进行优化(静态结点渲染后不会经过 patch 流程)
- 标记静态和非静态结点
- 标记静态根节点
- 转化: 对处理后的 AST 转换为 render 函数
虚拟 DOM
虚拟 DOM 是对真实 DOM 的抽象, 相比于真实 DOM 有以下特点
- 本质为数据对象, 因而可以独立于平台
- 由于是真实 DOM 的数据抽象, 因而性能开支较小
- 在响应式数据变动时,多次同步的数据变动只触发一次真实 DOM 更新
vue 中使用 VNode 指代 虚拟 DOM, 其包含的信息包括
- 当前标签: 元素标签或组件名称
- 相关数据:
class、style、attrs、props、on(事件回调)、directives、ref等 - 子节点: 文本节点、VNode
真实映射
渲染器负责将 虚拟 DOM 渲染为真实 DOM,其工作分为两个阶段
- 挂载(
mount)元素: 实例或 app 挂载到指定 容器 上 - 更新(
patch)元素: 响应式数据变动导致真实 DOM 变动
diff 流程
diff算法 是 patch 阶段, 为了尽可能减少元素变动使用的算法, vue 相比 react 采用了双端比较, 实际性能略高
- 信息记录
- 原列表:使用 oldHead 与 oldTail 指向头尾节点
- 现列表:使用 newHead 与 newTail 指向头尾节点
- 双端比较:在一次比较中,按顺序进行以下最多四次比较,若节点相同则更新,并修改节点指向
- oldHead 与 newHead 比较,若相同则更新,oldHead 与 newHead 分别指向下一个节点
- oldTail 与 newHead 比较,若相同则更新,oldTail 与 newHead 分别指向上一个节点
- oldHead 与 newTail 比较,若相同则更新,oldHead 节点移动到 oldTail 之后,oldHead 后移,newTail 前移
- oldTail 与 newHead 比较,若相同则更新,oldTail 节点移动到 oldHead 之前,oldTail 前移,newTail 后移
- 若以上都不满足,则遍历原列表查看 newHead 是否存在
- 若存在,则更新节点,并将该节点移动到 oldHead 之前;
- 若不存在,则添加到 oldHead 之前, newHead 后移
- 双端比较结束后,判断新增或删除节点
- 若 oldTail < oldHead,loop: newHead -> newTail 新增节点
- 若 newTail < newTail,loop: oldHead -> oldTail 移除节点
code
[a, b, c, d] // 原列表
[b, a, d, c] // 现列表
b, [a, c, d] // 不满双端比较(5),b 移动到 a 之前,oldHead 指向 a,newHead 指向 a
b, a, [c, d] // 满足双端比较(1),oldHead 指向 c,newHead 指向 d
b, a, d, [c] // 满足双端比较(4),d 移动到 c 之前,oldTail 指向 c,newTail 指向 c
b, a, d, c // 满足双端比较(1),遍历结束vue3 的优化
- 静态标记: diff 中跳过静态节点的比较和更新
- 静态提升: 编译阶段对静态节点打上标记, 渲染时只创建一次
- 事件缓存: 更新时不再解绑和重新绑定
nextTick
在响应式数据变动时,DOM 的更新是非同步的, nextTick 可在 DOM 更新后立即执行
- vue2 为
this.$nextTick(cb) - vue3 还提供了独立的
nextTick()
v2.6+ 使用内部会优先使用微任务处理(并按浏览器支持程度降序)
Promise>MutationObserver>setImmediate>setTimeout
vue3 因为不再兼容低端浏览器则默认使用 Promise
路由监听
hash 模式
- 监听
popstate事件, 对 历史记录条目 变动进行响应 - 若不支持
popstate则监听hashchange事件,对url中#(包括)后的哈希值变化进行响应
history 模式
- 服务端部分: 当页面刷新时,请求路径未匹配资源时,服务器返回 index.html 即可
- 浏览器部分
- 导航部分: 浏览器导航 和
router导航- 浏览器导航: 该方式发生页面加载, 通过监听
popstate事件, 对历史记录变动进行响应- 用户操作: 浏览器前进后退跳转
- 代码操作:
history.go()、.back()、.forward()、location.href等
router导航: 该方式页面不加载<router-link>: 禁用默认事件ev.preventDefault, 调用router.push()或router.replace()router.push()、router.replace(): 对history对应的方法进行了包装
- 浏览器导航: 该方式发生页面加载, 通过监听
- 渲染部分:
<router-view>对路由变动进行监听并响应
- 导航部分: 浏览器导航 和