vue 响应原理
下面的 vue 响应原理图和总结参考于 vue 官网教程的深入响应式原理章节。本文为了大家能够快速理解 vue 响应式原理的代码实现,对基本原理做了适当的总结。若想详细了解,可以到官网查看教程。
vue 的响应式基本原理:
- vue 会遍历
data
选项中的所有 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。 - 每个组件实例都对应一个
watcher
实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。 - 当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
上述图文只是对 vue 的响应式原理做了概括总结(当然,说得并不是很清楚),若想了解其具体实现还是得结合源码来看。为了便于阅读和调试源码,下面简单叙述一下大概实现步骤。
明确核心:
vue 采用数据劫持结合发布者-订阅者的方式,通过Object.defineProperty()
来劫持各个属性的setter 和 getter,且在数据发生变动时将消息发布给订阅者,以触发相应的监听回调。
大概步骤:
-
首先,observe(观察者)会对数据对象进行递归遍历(包括子属性对象的属性),为它们设置 setter 和 getter。这样,当给某个属性赋值时,就会触发 setter,从而能够监听到数据变化(通过Object.defineProperty()实现)。
-
然后,compile(编译者) 进行模板指令解析。主要工作:
- 将模板中的变量替换成数据,然后初始化并渲染页面视图。
- 将每个指令对应的节点绑定更新函数并添加监听数据的订阅者。
- 一旦数据发生变动,通知订阅者更新视图。
-
接着,Watcher(订阅者)进行订阅。它是 Observer 和 Compile 之间通信的桥梁,主要工作:
- 当自身实例化时,往订阅器(Dep)中添加自己。
- 自身要拥有一个 update() 更新方法。
- 当属性发生变动,调用 dep.notice() 进行通知,也就是去调用自身的 update() 方法,并触发 Compile 中绑定的回调。
以上步骤最终的实现:就是数据的双向绑定,也就是我们常提到的 MVVM 模式(Model-View-ViewModel)。看下面两张图(此图来源于网上,若侵权,请联系本人,立删)。
核心代码
本文 demo 参考于 vue 源码(只关注于主要功能实现,对细节部分做了删除),目录结构和函数名基本一致,且已实现模版编译和数据劫持。感兴趣的同学们可以点击链接去看看。
下面我们一起看一下核心代码的实现。
observe 观察者
initState(vm)
初始化状态时,会调用 observe
观察者函数,对数据进行观测,以便在其发生改变时,做出响应。也就是说,它会遍历 data
选项中的所有 property,并使用 Object.defineProperty
把这些 property 全部转为 getter/setter。
function initState (vm) {
vm._watchers = []; // 监听者列表
const options = vm.$options;
if (options.data) {
initData(vm); // 初始化 data
}
}
function initData (vm) {
let data = vm.$options.data;
// Vue 中的 data 可以是函数(Vue 中建议将 data 作为一个函数来使用),也可以是 Object --> {}
data = vm.$data = typeof data === 'function' ? data.call(vm) : data || {};
for (var key in data) {
// proxy 实现数据代理,vm.name --> vm.$data.name
proxy(vm, '$data', key);
}
// observe 观察者,对数据进行观测,以便在其发生改变时,做出响应。
observe(vm.$data);
}
import Dep from './dep';
import { arrayMethods } from './array';
import {
isObject,
def,
hasProto,
hasOwn,
isPlainObject,
} from '../../shared/util';
// 返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
// done: 尝试为某个值创建一个观察实例,
// 如果成功地观察到,返回新的观察者,如果值已经有观察者,则返回现有的观察者。
export function observe(value, asRootData) {
// 检查 value 是否为对象(注意:在 js 中,数组也是对象,isObject 方法并不排除数组)。
if (!isObject(value)) return;
let ob;
// 检查对象是否具有 '__ob__' 属性且是一个观察者实例
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__; // 返回现有的观察实例
} else if (
// isPlainObject 判断值是否是普通对象,指其原始类型字符串是不是 [object object]
(Array.isArray(value) || isPlainObject(value)) &&
// Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
Object.isExtensible(value) &&
// _isVue 是一个要被观察的标志
!value._isVue
) {
ob = new Observer(value); // 返回新的观察实例
}
if (asRootData && ob) {
ob.vmCount++; // 记录实例个数
}
return ob;
}
// done: 附加到每个被观察对象的观察者类。
// 一旦附加,观察者将目标对象的属性键转换为收集依赖项和分派更新的 getter/setter。
export class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 为当前 value 定义 __ob__ 属性,其值为 this(即当前 Observer 类)
def(value, '__ob__', this);
if (Array.isArray(value)) {
// 以是否存在 __proto__ 来判断使用何种方法增加扩充目标对象或数组
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
// 观察数组(Array)
this.observeArray(value);
} else {
// 观察对象(Object)
this.walk(value);
}
}
// done: 遍历所有属性并将它们转换为 getter/setter。
// 仅当值类型为 Object 时才应调用此方法
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i]; // 属性
const value = obj[key]; // 属性值
defineReactive(obj, key, value);
}
}
// done: 观察数组(Array)的每一项
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
// done: 定义响应式属性
function defineReactive(obj, key, val, customSetter, shallow) {
// 创建订阅器
const dep = new Dep();
// Object.getOwnPropertyDescriptor 方法返回指定对象上一个自有属性对应的属性描述符(自有属性指的是
// 直接赋予该对象的属性,不需要从原型链上进行查找的属性)。
const property = Object.getOwnPropertyDescriptor(obj, key);
// 属性存在且不可配置,则阻止运行
if (property && property.configurable === false) {
return;
}
// 预定义 getter/setter
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
// 递归观察 val, 它可能是一个对象(shallow,控制是否递归)
let childOb = !shallow && observe(val);
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
// 它是实现数据劫持的关键所在。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// Dep.target 目标监视器
if (Dep.target) {
// 添加到订阅器
dep.depend();
// 子观察实例
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value); // 收集数组元素上的依赖项
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
// 同名属性,不需要重新赋值或观察
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
// 自定义setter
if (customSetter) {
customSetter();
}
// 对于没有 setter 的访问器属性,则阻止运行
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 递归观察 newVal,它可能是一个对象
childOb = !shallow && observe(newVal);
dep.notify(); // 通知更新
},
});
}
// done: 通过使用 __proto__ 截取原型链来增加目标对象或数组
function protoAugment(target, src) {
target.__proto__ = src;
}
// done: 通过定义隐藏属性来扩充目标对象或数组
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
// 当数组被接触时,收集数组元素上的依赖项,因为我们不能像属性getter那样截取数组元素访问。
function dependArray(value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e); // 递归
}
}
}
Dep 订阅器
一个存储可观察对象的对象(俗称订阅器)。这些可观察的对象会被 watcher 记录为依赖项,当它们的 setter
触发时,就会通知 watcher,从而使它关联的组件重新渲染。
import { remove } from '../../shared/util';
let uid = 0;
/**
* dep 是一个存储可观察对象的对象(俗称订阅器)。
*/
export default class Dep {
static target;
constructor() {
this.id = uid++;
this.subs = [];
}
// 添加
addSub(sub) {
this.subs.push(sub);
}
// 删除
removeSub(sub) {
remove(this.subs, sub);
}
// 添加依赖项
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
// 通知更新
notify() {
// 考虑到数据安全和稳定性,这里获取订阅列表的一个副本
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update(); // 更新
}
}
}
// 当前正在处理的目标监视器。且同一时间,只有一个监视器可以被计算,所以这是全局唯一的,
Dep.target = null;
const targetStack = [];
export function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
export function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
watcher 订阅者
每个组件实例都对应一个 watcher
实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。
// 挂载组件
export function mountComponent(vm) {
// 更新组件
updateComponent = () => {
// 将 vm._render() 返回的 vnode 虚拟节点对象传递给 vm._update,它会调用 patch 函数生成文档树
vm._update(vm._render());
};
new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */);
}
import { isObject, _Set as Set } from '../../shared/util';
import { queueWatcher } from './scheduler';
import { pushTarget, popTarget } from './dep';
let uid = 0;
/**
* 订阅者,收集依赖项,并在表达式值发生变化时触发回调。这用于 $watch() api 和指令。
*/
export default class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
this.cb = cb;
this.id = ++uid;
this.deps = [];
this.newDeps = [];
this.depIds = new Set(); // 用于判断dep是否已存在
this.newDepIds = new Set();
this.getter = expOrFn;
this.value = this.get();
}
/**
* 获取值并收集依赖项。
*/
get() {
pushTarget(this); // 添加订阅者到栈中并设置为当前正在处理的订阅者
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
throw e;
} finally {
popTarget(); // 移除当前订阅者
this.cleanupDeps(); // 清理依赖项集合。
}
return value;
}
/**
* 添加依赖项
*/
addDep(dep) {
const id = dep.id;
// 根据id,判断依赖项是否已存在
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
/**
* 清理依赖项集合。
*/
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this); // 删除
}
}
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
/**
* 订阅更新接口。
* 将在依赖项更改时调用。
*/
update() {
queueWatcher(this);
}
/**
* Scheduler(调度器)作业接口。
* 将被 scheduler 调用。
*/
run() {
const value = this.get();
if (
value !== this.value ||
// 即使值相同,对象/数组上的订阅也应该触发,因为值可能已经发生了变化。
isObject(value)
) {
// 设置新的值
const oldValue = this.value;
this.value = value;
this.cb.call(this.vm, value, oldValue);
}
}
/**
* 依赖当前观察者收集的所有数据.
*/
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
}
小结
核心代码的展示,不仅是为了向大家说明 vue 响应式基本原理实现的关键步骤,更是为了方便大家调试源码。感兴趣的小伙们,可以下载本例 demo 进行调试,毕竟实践出真知。最后,大家在调试时,要结合者响应式基本原理图进行,这能够帮你快速掌握源码。