关于响应式原理分析
Vue响应式
前言
数据响应式系统是Vue最显著的一个特性,我们通过Vue官方文档回顾一下。
数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。
现在是时候深入一下了!
本文针对响应式系统的原理进行一个详细介绍。
响应式是什么
我们先来看一个例子
<div id="app">
<p>{{color}}</p>
<button @click="changeColor">change color!</button>
</div>
new Vue({
el: '#app',
data() {
color: 'blue'
},
methods: {
changeColor() {
this.color = 'yellow';
}
}
})
当我们点击按钮的时候,视图的p标签文本就会从 blue改变成yellow。
Vue要完成这次更新,其实需要做两件事情:
- 监听数据
color的变化。 - 当数据
color更新变化时,自动通知依赖该数据的视图。
换成专业那么一点点点的名词就是利用数据劫持/数据代理去进行依赖收集、发布订阅模式。
我们只需要记住一句话:在getter中收集依赖,在setter中触发依赖
如何追踪侦测数据的变化
首先有个问题,如何侦测一个对象的变化?
目前来说,侦测对象变化有两种方法。大家都知道的!
Object.defineProperty
vue 2.x就是使用Object.defineProperty来数据响应式系统的。但是用此方法来来侦测变化会有很多缺陷。例如:
- Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。
- …
本文也是利用Object.defineProperty来介绍响应式系统。
Proxy
vue3就是通过proxy实现响应式系统的。而且在国庆期间已经发布pre-alpha版本。
相比旧的Object.defineProperty, proxy可以代理整个对象,并且提供了多个traps,可以实现诸多功能。此外,Proxy支持代理数组的变化等等
当然proxy也有一个致命的缺点,就是无法通过polyfill模拟,兼容性较差。
依赖收集的重要角色 Dep Watcher
Dep、Watcher是数据响应式中两个比较重要的角色。
收集依赖的地方 Dep
因为在视图模板上可能有很多地方引用相同的数据,所以有一个存储数据的地方,这个地方就是Dep。
Dep主要维护一个依赖的数组,当我们使用render函数生成VNode时,它会触发数据的getter,然后将依赖push转移到Dep的依赖数组中。
依赖是Watcher!
我们可以把Watcher理解为中介角色。当数据发生变化时,它会触发数据的setter,然后通过Dep中对Watcher的依赖,然后Watcher通知额外的操作。额外的操作可能是更新视图和computed、更新watch等等...
原理实现
还是那句话:在getter中收集依赖,在setter中触发依赖。
下面我们看看代码:
代码有点长,下面会有步骤来讲解一次。
/*
* 劫持数据的getter、setter
*/
function defineReactive(data, key, val) {
// 1-1:把color数据变成响应式
const dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 3. 因为模板编译watcher访问到了color,从而触发get方法,触发了收集依赖的方法
dep.depend();
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
// 4-1. 假设我们通过 `this.color = 'yellow';`去更改`color`的值,就会触发set方法。
dep.notify();
}
});
}
/*
* dep类,收集依赖,和触发依赖
*/
class Dep {
constructor() {
this.subs = []; // 收集依赖的数组
}
// 收集依赖
depend() {
// 3-1. 通过外部的变量来添加到color的依赖中.
if (window.target && !this.subs.includes(window.target)) {
this.subs.push(window.target);
}
}
// 通知依赖更新
notify() {
// 4-2. 遍历
this.subs.forEach(watcher => {
watcher.update();
});
}
}
/**
* 数据与外部的中介
*/
class Watcher {
constructor(expr, cb) {
this.cb = cb;
this.expr = expr;
// 2-1. 这里触发了get方法
this.value = this.get();
}
get() {
// 2-2. 这里把自己(watcher)赋值给了外部其中的一个变量
window.target = this;
// 2-3. data[this.expr]触发了color的get
const value = data[this.expr];
window.target = undefined;
return value;
}
update() {
this.value = this.get();
this.cb(this.value);
}
}
下面我们来走一次流程。
括号里面的1-1,2-2是对应代码的执行点。
1. 把数据变成响应式
利用defineReactive把color数据变成响应式(1-1),执行过这个方法后,我们调用console.log(this.color)的时候可以触发get方法。同理当我们this.color = 'yellow'。
注意:在Object.defineProperty上面初始化一个存放依赖的dep,这里其实是把dep作为数据color的一个私有变量,让get和set的方法可以访问到,也是我们经常说的闭包。
2. 编译模板创建watcher
假设我们现在编译模板遇到{{color}}。
Vue就会创建一个Watchter,伪代码如下:
new Watcher('color', () => {
// 当color发生变化的时候,会触发这里的方法。
});
这里高能!!
Watcher的构造函数里面调用了get()方法(2-2),把自己(watcher)赋值给了一个外部变量。
然后再触发get方法(2-3)。
3. get中收集依赖
因为模板编译watcher访问到了color,从而触发get方法,触发了收集依赖的方法。
进入到dep.depend方法中(3-1),这里因为在Watcher中把自己存到了外部变量中,所以在dep.depend方法中可以收集到依赖。
现在,依赖就被收集了。
4. 通过setter触发依赖
假设我们通过 this.color = 'yellow';去更改color的值,就会触发set方法,执行dep.notify(4-1)。
会遍历依赖数组,从而去触发Watcher的cb方法。 cb就是上面伪代码new Watcher的那个回调函数。
只要回调函数里面运行了操作dom方法,或者触发了diff算法更新dom,都可以把视图进行更新。
响应式简易流程大概就是这样了…
侦测数据变化的类型
事实上,数据监控有两种变化,一种是“推”(push),另一种是“拉”(pull)。
React和Angular中的变化检测属于“拉”,也就是说,当数据发生变化时,它不知道哪些数据发生了变化,然后向框架发送信号。框架收到信号后,会进行暴力比较,找出需要重新渲染的dom节点。Angular使用脏检查,React使用虚拟domdiff。
vue的数据监控属于“推”。当数据发生变化时,你就会知道哪些数据发生了变化,从而更新这些数据所依赖的视图。
因此,框架知道的数据更新信息越多,粒度就越细。例如,通过domm直接更新。 api操作dom。
相对“拉”的力度是最粗的。看到这里,你觉得vue的更新效率最快吗?
让我们来看看下面的例子
<template>
<div>{{a}}</div>
<div>{{b}}</div>
</template>
这里我们看出模板只有a、b两个数据依赖,也就是说我们要创建两个闭包dep去存放两个watcher依赖,我们知道闭包的缺点就是内存泄露。如果有1000个数据依赖在模板上,每个数据所绑定的依赖就越多,依赖追踪在内存上的开销就会越大。
所以,从Vue.js2.0开始,它引入了虚拟dom,一个状态所绑定的依赖不再是具体的dom节点,而是一个组件,即一个组件一个Watcher。 这样状态变化后,会通知到组件,组件内部再使用虚拟 dom进行比对。这可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。但并不是引入虚拟dom后,渲染速度变快了。准确的来说,应该是80%的场景下变得更快了,而剩下的20%反而变慢了。
个人觉得,鱼和熊掌不可兼得。
“推”是牺牲内存来换更新速度。
“拉”则是牺牲更新速度来获取内存。
总结
Vue响应式的灵魂:在getter中收集依赖,在setter中触发依赖。

我们再看看图,回顾一下整个流程。
- 通过
defineReactive,遍历data里面的属性,把数据的getter/setter劫持,用来收集和触发依赖。 - 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到Dep依赖中。
- 当数据发生变化时,会触发setter,从而Dep会向依赖(Wachter)发送通知。
- Watcher收到通知后,会向外界发送通知,变化通知到外界后可能接触视图更新,也有可能触发用户的某个回调函数等等。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)