响应式原理-响应式对象
在前面的文章中,我们主要介绍了 Vue
的初始化过程,原始的数据是怎么映射到 DOM
并展示在页面上的,但没有涉及到数据变化到 DOM
变化的部分,从本文开始我们就来了解数据变更触发 DOM
变化的流程。
先从一个示例开始:
<div id="app" @click="changeMsg">{{ message }}</div>
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue!",
},
methods: {
changeMsg() {
this.message = "Hello World!";
},
},
});
示例中我们在 HTML
中定义了一个 id
为 app
的根 DOM
,并且绑定了一个 click
事件,里面绑定了 message
变量。
在 JS
中实例化了一个 Vue
,并在 data
中定义了 message
,在 methods
中定义了 changeMsg
事件。
这些在我们开发中总是熟悉不过的流程,但是当我们去修改 this.message
的时候,模板对应的插值也会渲染成新的数据,这在 Vue
中是如何实现的呢?
Object.defineProperty
没错,首先介绍的是大家熟悉的响应式对象,在 Vue 2+
中就是利用 Object.defineProperty 方法来实现响应式核心的。
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor);
他接受三个入参:
obj
是要定义的对象prop
是要定义或修改的属性的名称descriptor
是要定义或修改的属性描述符:
- configurable:为
false
时不可修改和删除该属性 - enumerable:是否可被枚举
- value:该属性对应的值。可以是任何有效的
JavaScript
值(数值,对象,函数等) - writable:是否可写
- get:当访问该属性时,会调用此函数
- set:当属性值被修改时,会调用此函数
其中 descriptor
中的 getter
和 setter
比较重要,当我们给属性提供 getter
方法,访问该属性就会触发,在给属性设置值时 setter
同理。当对象拥有 getter
和 setter
后,我们可以把这个对象称为响应式对象。那么 Vue
把哪些对象变成了响应式对象呢,我们接着看。
initState
在 Vue
初始化执行 _init
方法时,会执行 initState
方法,它定义在 src/core/instance/state.js
中:
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
initState
方法主要是对 props
、methods
、data
、computed
、watcher
等做初始化操作,在这里我们只关注 initProps
和 initData
。
initProps
定义在 src/core/instance/state.js
中:
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {};
const props = (vm._props = {});
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = (vm.$options._propKeys = []);
const isRoot = !vm.$parent;
// root instance props should be converted
if (!isRoot) {
toggleObserving(false);
}
for (const key in propsOptions) {
keys.push(key);
const value = validateProp(key, propsOptions, propsData, vm);
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
// ...
} else {
defineReactive(props, key, value);
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key);
}
}
toggleObserving(true);
}
initProps
的主要过程就是遍历我们定义的 props
配置,然后做了两件事:
- 调用
defineReactive
把每个prop
对应的值变为响应式,可以通过vm._props.xxx
访问到定义在props
中的属性 - 通过
proxy
把vm._props.xxx
代理到vm.xxx
initData
也是定义在 src/core/instance/state.js
中:
function initData(vm: Component) {
let data = vm.$options.data;
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
if (!isPlainObject(data)) {
data = {};
// ...
}
// proxy data on instance
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
while (i--) {
const key = keys[i];
if (process.env.NODE_ENV !== "production") {
// ...
}
if (props && hasOwn(props, key)) {
// ...
} else if (!isReserved(key)) {
// 代理属性到 vm 上
proxy(vm, `_data`, key);
}
}
// observe data
// 调用 observe 为 data 对象上的数据设置响应式
observe(data, true /* asRootData */);
}
initData
的主要过程还是遍历我们定义的 data
配置,然后做了两件事:
- 通过
proxy
把每一个值vm.data.xxx
都代理到vm.xxx
上 - 调用
observe
方法监测 整个data
的变化,把data
也变成响应式,可以通过vm._data.xxx
访问到data
中定义的属性
proxy
在 initProps
和 initData
中都调用了 proxy
方法,他的作用是把 props
和 data
上的属性都代理到 vm
实例上,这也就是我们明明在 props
中定义的属性,在 this.xx
就可以访问到:
let comP = {
props: {
msg: "hello",
},
methods: {
say() {
console.log(this.msg);
},
},
};
export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
proxy
方法内部逻辑很简单,就是通过 Object.defineProperty
把 target[sourceKey][key]
的读写变成了对 target[key]
的读写。
比如 props,对 vm._props.xxx
的读写变成了 vm.xxx
的读写,而 vm._props.xxx
又已经代理到 props
中的属性上了。所以我们就是相当于 vm.xxx = props.xxx
。
对于 data
同理。
observe
observe
的功能就是用来监测数据的变化,它的定义在 src/core/observer/index.js
中:
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe(value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob: Observer | void;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}
observe
主要做了下面几件事:
- 判断不是对象或者是
VNode
类型,则什么也不做,直接返回 - 如果对象拥有
__ob__
属性,则直接返回value.__ob__
(十秒之后会讲到这个) - 如果对象未拥有
__ob__
属性,则在满足一定条件时,实例化一个Observer
实例并返回
Observer
Observer
是一个类,作用是给对象的属性添加 getter
和 setter
,用于依赖收集和派发更新,定义在 src/core/observer/index.js
中:
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor(value: any) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
/**
* Observe a list of Array items.
*/
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
Observer
主要做了下面几件事:
- 首先实例化一个
Dep
实例 - 接着执行
def
方法,把自身实例添加到数据对象value
的__ob__
属性上,如果value.__ob__
存在的话说明是已经绑定过的,所以在observe
中会有对__ob__
的判断 - 接下来对
value
做判断,如果是数组则执行observeArray
,否则执行walk
方法
observeArray
是遍历执行 observe
,而 walk
方法是遍历对象的 key
调用 defineReactive
方法。
defineReactive
defineReactive
的作用就是定义一个响应式对象,给对象动态增加 getter
和 setter
,定义在 src/core/observer/index.js
中:
/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
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;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
defineReactive
先实例化一个 Dep
实例,接着拿 obj
的属性描述符,对子对象递归调用 observe
方法,这样就保证无论 obj
层次多复杂,所有的子属性也都能变成响应式。当我们访问或修改 obj
对象中一个属性时,就能触发 getter
和 setter
。
关于 getter
和 setter
具体实现,我们后文在讲。
总结
这里主要介绍了响应式对象,核心是利用 Object.defineProperty
给数据添加 getter
和 setter
,目的是让我们在写入或读取数据时自动执行逻辑。
getter
负责依赖收集,setter
负责派发更新。
参考:Vue.js 技术揭秘