0
点赞
收藏
分享

微信扫一扫

深入浅出 Vue —— 第一篇、变化侦测

栖桐 2022-03-11 阅读 38


序、Vue.js 简介


时至今日,Vue.js 就像曾经的 JQuery,已经成为前端工程师必备的技能。不可否认,它可以极大的提高我们的开发效率,并且很容易学习。

这就造成了一个很普遍的现象,大部分前端工程师对框架以及第三方周边插件的关注程度越来越高,甚至把自己全部的关注点都放在了框架上。

在我看来,这多少有点亚健康,不是很利于前端工程师的技术成长。因为我发现大家关注框架时,更多的是关注其用法(包括框架自身、第三方插件和 UI 组件库等)、奇淫技巧和最佳实践等。

大家在使用 Vue.js 开发项目时,不免总会遇到一些奇奇怪怪的问题,而我们是否能很快解决这些问题以及理解这些问题为什么会发生,主要取决于对 Vue.js 原理的理解是否足够深入。

Vue 是一款渐进式的 JavaScript 框架。渐进式即:假如现已有一个现成的非单页应用,可以将 Vue 作为该应用的一部分嵌入其中,带来更加丰富的交互体验。


第一篇、变化侦测

变化侦测就是侦测数据的变化,当数据变化时,要能侦测到并发出通知。

Angular 和 React 中的变化侦测属于“拉”,即状态变化时,它不知道哪个状态变了,只知道状态有可能变了,然后会发信号给框架,框架会进行暴力对比找出需要重新渲染的 DOM。在 Angular 中是脏检查的流程,在 React 中使用的是虚拟 DOM。

而 Vue 中属于“推”。状态变化时,Vue 在一定程度上知道哪些状态变了,可进行更细粒度的更新。

但粒度越细,每个状态绑定的依赖越多,依赖追踪在内存上开销就越大。因此 Vue 2.0 开始引入虚拟 DOM,调整为中等粒度,即 ​一个状态所绑定的依赖不再是具体的 DOM 节点,而是一个组件。状态变化后,会通知组件,组件内部再使用虚拟 DOM 对比。可大大降低依赖数量,从而降低依赖追踪所消耗的内存。

变化侦测是响应式系统的核心,Vue 2.x 中 Object 和 Array 的变化侦测采用不同的处理方式。


  • Object.defineProperty 能够监听对象的变化 (Vue 2.x — 当时ES6再浏览器支持度不理想),对于数组要进行重写 Array 原型方法(push、pop…)
  • ES6 的 Proxy 可监听对象和数组的变化 (Vue 3.x)

第 1 章、Object 的变化侦测

Object 通过封装 ​​Object.defineProperty​​​ 方法得到的 ​​defineReactive​​​ 函数来劫持属性的​​getter​​​和​​setter​​,以此来追踪数据变化。


  • 读取数据时触发​​getter​​​来收集依赖(​​Watcher​​​)到​​Dep​​,
  • 修改数据时触发​​setter​​​,并遍历依赖列表,通知所有相关依赖(​​Watcher​​)
  • ​Dep​​​ 类用来收集和管理依赖,在​​getter​​​中​​depned​​​,在​​setter​​​中​​notify​
  • ​Watcher​​​ 类就是收集的依赖,实际上是一个订阅器,​​Watcher​​​会将自己的实例赋值给​​window.target​​​(全局变量)上,然后去主动访问属性,触发属性的​​getter​​​,​​getter​​​中会将此​​Watcher​​​收集到​​Dep​​​中,​​Watcher​​​的​​update​​​方法会在​​Dep​​​的​​notify​​方法中被调用,触发更新。
  • ​Observer​​​ 类用来将一个对象的所有属性和子属性都变成响应式的,通过递归调用​​defineReactive​​来实现。
  • 由于无法侦测对象上新增/删除属性,所以提供 ​​$set​​​ 和 ​​$delete​​ API

1.1 如何追踪变化

使用​​Object.defineProperty​​​劫持对象的​​get​​​和​​set​​​属性,封装成为​​defineReactive(data, key, value)​​这样一个函数

const defineReactive = (data, key, value) => {
// 递归子属性, 都转换成响应式
if (typeof value === 'object') {
new Observer(value);
}
let dep = new Dep(); // 实例化 Dep(管理依赖)类
// 拦截 get 和 set
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend(); // 收集依赖
return value;
},
set(newVal) {
if (newVal === value) {
return;
}
value = newVal;
dep.notify(); // 触发依赖更新
}
});
};


衍生实战题:如何使变量 a 同时满足:​​(a === 1) && (a === 2) && (a === 3)​​ ?

​let current = 0 Object.defineProperty(window, 'a', { get() { current ++ return current } }) console.log((a === 1) && (a === 2) && (a === 3)) // true ​

1.2 如何收集依赖

在​​getter​​​中收集依赖,在​​setter​​中触发更新

1.3 依赖收集在哪里

首先将依赖保存在一个全局变量上(比如​​window.target​​),然后用一个数组来保存收集的依赖。

下面代码封装为​​Dep​​​类,专门用来管理依赖,它包括​​addSub、removeSub、depend、notify​​方法。

​Dep​​​会在​​defineReactive​​​中实例化,在​​get​​​中​​depend​​​,在​​set​​​中​​notify​

class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
if (this.subs.length) {
const index = this.subs.indexOf(sub);
if (index > -1) {
arr.splice(index, 1)
}
}
}
// 将依赖添加到 sub 数组中收集起来
depend() {
if (window.target) {
this.addSub(window.target);
}
}
// 触发set时,循环触发收集到的依赖
notify() {
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}

1.4 依赖(Watcher)是什么

​Watcher​​类,它是抽象出来,用来集中处理各种情况的类。依赖收集阶段只收集这个封装好的类的实例,通知只通知它一个,再由它通知其他地方。

class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.cb = cb; // callback 回调函数
this.getter = parsePath(expOrFn); // 将字符串转成 Keypath
this.value = this.get();
}

get() {
window.target = this; // 先把 Watcher 实例赋值给 window.target
this.value = this.getter.call(this.vm, this.vm); // 触发属性值的 getter 收集依赖
window.target = undefined;
return this.value;
}
// 在 Dep 的 notify 中调用,数据更新时,触发真正要执行的方法
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}

无论是用户执行的​​vm.$watch('a.b.c', (nv, ov) => {})​​​,还是模板中用到的​​data​​​,都是通过​​Watcher​​来通知自己是否需要发生变化。

1.5 递归侦测所有key(Observer)

上面只能侦测单个属性 ,所以封装了​​Observer​​​类来递归侦测数据所有的属性(包括子属性),将所有的属性都转换为​​getter/setter​​的形式,追踪其变化

class Observer {
constructor(value) {
this.value = value;

if (!Array.isArray(value)) {
// 遍历所有属性
this.walk(value);
}
}

walk(value) {
const keys = Object.keys(value);
for (let i = 0; i < keys.length; i++) {
defineReactive(value, keys[i], value[keys[i]]);
}
}
}


const defineReactive = (data, key, value) => {
// 递归子属性
if (typeof value === 'object') {
new Observer(value);
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend();
return value;
},
set(newVal) {
if (newVal === value) {
return;
}
value = newVal;
dep.notify();
}
});
};

码字不易,觉得有帮助的小伙伴点个赞支持下~

深入浅出 Vue —— 第一篇、变化侦测_拦截

扫描上方二维码关注我的订阅号~



举报

相关推荐

0 条评论