【vue】🔥vue2.x底层的问题,你能答多少
有句老话说,在父母那里我们永远是孩子,同样在各位大佬这里,我永远是菜鸡 🐣🐣🐣。不管怎样,学习的激情永远不可磨灭。答案如有错误,感谢指教 🌚
# 首发个人博客
种一棵树,最好的时机是十年前,其次是现在 (opens new window)
# vue 源码中值得学习的点
科里化
: 一个函数原本有多个参数, 只传入一个
参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构偏函数
: 一个函数原本有多个参数, 只传入一部分
参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构高阶函数
: 一个函数参数是一个函数
, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数- ...
# vue 响应式系统
简述:
vue 初始化时会用Object.defineProperty()
给 data 中每一个属性添加getter
和setter
,同时创建dep
和watcher
进行依赖收集
与派发更新
,最后通过diff
算法对比新老 vnode 差异,通过patch
即时更新 DOM
# 简易图解:
# 详细版本
可以参考下图片引用地址: 图解 Vue 响应式原理 (opens new window)
# Vue 的数据为什么频繁变化但只会更新一次
- 检测到数据变化
- 开启一个队列
- 在同一事件循环中缓冲所有数据改变
- 如果同一个
watcher (watcherId相同)
被多次触发,只会被推入到队列中一次
不优化,每一个数据变化都会执行: setter->Dep->Watcher->update->run
优化后:执行顺序update -> queueWatcher -> 维护观察者队列(重复id的Watcher处理) -> waiting标志位处理 -> 处理$nextTick(在为微任务或者宏任务中异步更新DOM)
# vue 使用 Object.defineProperty() 的缺陷
数组的 length 属性被初始化configurable false
,所以想要通过 get/set 方法来监听 length 属性是不可行的。
vue 中通过重写了七个
能改变原数组的方法来进行数据监听
对象还是使用 Object.defineProperty()添加 get 和 set 来监听
参考
# Vue.nextTick()原理
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
源码实现:Promise > MutationObserver > setImmediate > setTimeout
参考文章:浅析 Vue.nextTick()原理 (opens new window)
# computed 的实现原理
computed 本质是一个惰性求值的观察者computed watcher
。其内部通过 this.dirty
属性标记计算属性是否需要重新求值。
- 当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher
通过this.dep.subs.length
判断有没有订阅者, - 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性
最终计算的值
发生变化时才会触发渲染 watcher
重新渲染,本质上是一种优化。) - 没有的话,仅仅把
this.dirty = true
(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
# watch 的理解
watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中
的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听
注意:Watcher : 观察者对象 , 实例分为渲染 watcher
(render watcher),计算属性 watcher
(computed watcher),侦听器 watcher
(user watcher)三种
# vue diff 算法
- 只对比父节点相同的新旧子节点(比较的是 Vnode),时间复杂度只有 O(n)
- 在 diff 比较的过程中,循环从两边向中间收拢
新旧节点对比过程
1、先找
到 不需要移动的相同节点,借助 key 值找到可复用的节点是,消耗最小
2、再找相同但是需要移动
的节点,消耗第二小
3、最后找不到,才会去新建删除
节点,保底处理
注意:新旧节点对比过程,不会对这两棵 Vnode 树进行修改,而是以比较的结果直接对 真实DOM 进行修改
Vue 的 patch 是即时的
,并不是打包所有修改最后一起操作 DOM(React 则是将更新放入队列后集中处理)
参考文章:Vue 虚拟 dom diff 原理详解 (opens new window)
# vue 渲染过程
- 调用
compile
函数,生成 render 函数字符串 ,编译过程如下:
- parse 使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。
模板 -> AST (最消耗性能)
- optimize 遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,
优化runtime的性能
- generate 将最终的 AST 转化为 render 函数字符串
- 调用
new Watcher
函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象 - 调用
patch
方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素
# 结合源码,谈一谈 vue 生命周期
vue 生命周期官方图解 (opens new window)
# Vue 中的 key 到底有什么用?
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速
更准确 : 因为带 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;
}
2
3
4
5
6
7
8
9
# vue-router 路由模式有几种
默认值: "hash"
(浏览器环境) | "abstract"
(Node.js 环境)
可选值: "hash"
| "history"
| "abstract"
配置路由模式:
hash
: 使用 URL hash 值来作路由。支持所有浏览器
,包括不支持 HTML5 History Api 的浏览器。history
: 依赖HTML5 History API
和服务器配置。abstract
: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
# 说一说 keep-alive 实现原理
# 定义
keep-alive
组件接受三个属性参数:include
、exclude
、max
include
指定需要缓存的组件name
集合,参数格式支持String, RegExp, Array。
当为字符串的时候,多个组件名称以逗号隔开。exclude
指定不需要缓存的组件name
集合,参数格式和 include 一样。max
指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持 String、Number。
# 原理
keep-alive
实例会缓存对应组件的 VNode,如果命中缓存,直接从缓存对象返回对应 VNode
LRU(Least recently used)
算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)
# 对对象属性访问的解析方法
eg:访问 a.b.c.d
函数柯里化 + 闭包 + 递归
function createGetValueByPath(path) {
let paths = path.split("."); // [ xxx, yyy, zzz ]
return function getValueByPath(obj) {
let res = obj;
let prop;
while ((prop = paths.shift())) {
res = res[prop];
}
return res;
};
}
let getValueByPath = createGetValueByPath("a.b.c.d");
var o = {
a: {
b: {
c: {
d: {
e: "正确了",
},
},
},
},
};
var res = getValueByPath(o);
console.log(res);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# vue 中针对 7 个数组方法的重写
Vue 通过原型拦截
的方式重写了数组的 7 个方法,首先获取到这个数组的Observer
。如果有新的值,就调用 observeArray
对新的值进行监听,然后调用 notify
,通知 render watcher
,执行 update
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
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;
});
});
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# vue 处理响应式 defineReactive 实现
// 简化后的版本
function defineReactive(target, key, value, enumerable) {
// 折中处理后, this 就是 Vue 实例
let that = this;
// 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
if (typeof value === "object" && value != null && !Array.isArray(value)) {
// 是非数组的引用类型
reactify(value); // 递归
}
Object.defineProperty(target, key, {
configurable: true,
enumerable: !!enumerable,
get() {
console.log(`读取 ${key} 属性`); // 额外
return value;
},
set(newVal) {
console.log(`设置 ${key} 属性为: ${newVal}`); // 额外
value = reactify(newVal);
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# vue 响应式 reactify 实现
// 将对象 o 响应式化
function reactify(o, vm) {
let keys = Object.keys(o);
for (let i = 0; i < keys.length; i++) {
let key = keys[i]; // 属性名
let value = o[key];
if (Array.isArray(value)) {
// 数组
value.__proto__ = array_methods; // 数组就响应式了
for (let j = 0; j < value.length; j++) {
reactify(value[j], vm); // 递归
}
} else {
// 对象或值类型
defineReactive.call(vm, o, key, value, true);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 为什么访问 data 属性不需要带 data
vue 中访问属性代理 this.data.xxx 转换 this.xxx 的实现
/** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
function proxy(target, prop, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return target[prop][key];
},
set(newVal) {
target[prop][key] = newVal;
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
# 参考文章(除文章已指出的)
- 01
- 2023/07/03 00:00:00
- 02
- 2023/04/22 00:00:00
- 03
- 2023/02/16 00:00:00