mpvue 响应式源码分析
状态初始化
mpvue 的实例化主要执行的是 Vue.prototype._init 方法,该方法通过 initState 函数初始化各种状态,包括:props、data 和 computed,这些状态的值变化,会触发界面 UI 的更新,这是响应式的核心功能。_init 方法源码节选如下:
Vue.prototype._init = function (options) {
var vm = this;
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm);
initState(vm);
initProvide(vm);
callHook(vm, 'created');
};
在 beforeCreate 生命周期后 mpvue 会调用 initState 实例化状态,本文将统一讲解 data 这个特性,props 和 computed 与 data 是非常类似的,所以理解了 data 再看 props 和 computed 都将非常容易。我们的 mpvue 测试程序非常简单,源码如下:
<template>
<div>{{message}}</div>
</template>
<script>
export default {
data () {
return {
message: 'Hello miniprograme'
}
}
}
</script>
我们只定义了 data,里面包含一个属性 message。下面回到 initState 流程,initState 中与 data 初始化相关的源码节选如下:
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
}
data 初始化
首先,mpvue 从 vm.$options 中获取 data 属性,vm.$options.data 指向我们自己编写的 data 属性,它是一个 function,当 data 存在时会指向 initData (vm),initData 的源码如下:
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
"production" !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
if (props && hasOwn(props, key)) {
"production" !== 'production' && warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
这部分代码较长,我们逐一分析,首先 mpvue 会调用 getData 函数获取 data 的结果,如果我们的 data 是 Object 型,则会直接返回:
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
之后 mpvue 会判断返回结果是否为 Object,如果不是,则抛出警告:
if (!isPlainObject(data)) {
data = {};
"production" !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
下一步,mpvue 会判断 data 中的数据是否与 props 重名,这是因为 data 中的数据和 props 一样,都会被作为 mpvue 实例的根属性,如:
this.message = 'xxx'
如果重名了,将会出现冲突,判断逻辑如下:
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
if (props && hasOwn(props, key)) {
"production" !== 'production' && warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
可以看到,mpvue 遍历了 data 的 key,依次与 props 对比,如果不冲突,还会调用 isReserved (key) 函数,判断 data 的 key 是否包含保留关键字,具体源码如下:
function isReserved (str) {
var c = (str + '').charCodeAt(0);
return c === 0x24 || c === 0x5F
}
这里 0x24 表示 $,0x5F 表示 _,这段代码的含义是判断我们 data 中属性的第一个字符是否为 $ 或 _,因为这两个符号标识的变量为 mpvue 内置的变量,所以 mpvue 不允许我们使用这两个字符作为变量前缀,以免发生冲突。如果我们 data 的 key 与 props 不冲突,同时非 $ 和 _ 开头,那么 mpvue 将执行下面一个关键的步骤:
proxy(vm, "_data", key);
这步简单来说,就是在 mpvue 实例下添加 data 属性,并将 vm.key 的值指向 vm._data.key,这样说有点抽象。下面我们详细分析,先看下 proxy 的源码:
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
很多同学都应该听说过 Vue 的响应式核心原理是 Object.defineProperty,那么 Object.defineProperty 到底在哪里使用的呢?答案之一就是在 proxy 方法里。proxy 定义了变量的 get 和 set 方法,这说明当我们取值时将调用 proxyGetter 方法,赋值时将调用 proxySetter 方法。那么取什么值时会调用这两个方法呢?答案是取 vm.key 的值的时候,原因是下面这行代码:
Object.defineProperty(target, key, sharedPropertyDefinition);
它表示为 target 添加 key 属性,并传入自定义的属性描述对象。target 是 vm,也就是 mpvue 实例,key 是 message,因为我们在 data 下定义了一个属性 message,而 sharedPropertyDefinition 就是 get 和 set 方法。这段代码执行完毕后,将在 vm 下生成一个 message 属性: 当我们获取 vm.message 的值的时候会调用 proxyGetter 方法,该方法执行逻辑如下:
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
这里的 sourceKey 是 _data,key 是 message,所以 vm.message 实际访问的值是:vm._data.message,如上图所示。至此在 vm 下添加 message 属性的目的非常明确了,就是为了简化我们的开发。我们知道在 React 框架中状态的获取必须通过 this.state.xxx 或 this.props.xxx,这样比较麻烦。Vue 在这一点上进了一步,它设置了一个前提条件:不允许 props 和 data 重名,从而可以直接将 props 和 data 中的属性挂载到 vm 实例上,并通过 es5 新增的 Object.defineProperty 定义了其取值和赋值的代码方法。同理,我们再看一下 vm.message 的赋值方法:
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
很明显也是将值赋给了 vm._data.message。当这些步骤都执行完毕了 mpvue 会为 vm._data 下的属性添加响应式属性:
observe(data, true);
虽然上述代码只有一行,但却是整个 initData 方法中最重要的,它是 mpvue 实现响应式的精髓之一。
响应式初始化
observe 方法的源码如下:
function observe (value, asRootData, key) {
if (!isObject(value)) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value, key);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
上述代码最关键的步骤是:
ob = new Observer(value, key);
其中 value 就是 vm._data,key 为 undefined,Observer 表示一个响应对象,它的实例化源码如下:
var Observer = function Observer (value, key) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
if (key) {
this.key = key;
}
def(value, '__ob__', this);
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
};
Observer 创建了一个 Dep 对象,Dep 主要用途是完成依赖收集,当触发状态变更时,mpvue 会通知 Dep 中的所有依赖进行更新:
this.dep = new Dep();
这个我们会在后续分析中再次提到,之后通过 def 方法,为 vm._data 对象添加了一个 ob 属性,带有 ob 属性的对象,mpvue 会将其视为响应式对象:
def(value, '__ob__', this);
def 方法的源码如下:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
调用完毕后 vm._data 的属性如下:
{message: "Hello miniprograme", __ob__: Observer}
可以看到多出了一个 ob 属性。之后程序继续往下执行,由于 vm._data 不是数组,所以会调用:
this.walk(value);
该方法的主要用途是依次为 vm._data 下的所有属性定义响应式方法,walk 是 mpvue 的 data 属性具备响应式能力的最关键的一步,它的源码如下:
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i], obj[keys[i]]);
}
};
这段源码比较简单,它遍历了 vm._data 的所有属性,并调用了 defineReactive$$1 方法, defineReactive$$1 方法的源码如下:
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
var childOb = !shallow && observe(val, undefined, key);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
},
set: function reactiveSetter (newVal) {
// ...
}
});
}
这里省略了 get 和 set 的实现细节,因为这些会在下一节中重点讲解。上述代码中 obj 代表 vm._data,key 代表 message,这里的 Object.defineProperty (obj, key, {}) 对 vm._data.message 进行了重新定义,对其 get 和 set 方法进行了代理。下一节我们将重点分析,当我们对 data 中属性进行 get 和 set 操作了,具体做了哪些操作,为什么能实现界面更新。由此可见我们通过 this.message 取值时,实际调用了 this._data.message 的 reactiveGetter 方法,这一过程远比我们想象中要复杂,因为经历了两次代理:
- 第一次代理是 this.message 的 proxyGetter,它返回了 this._data.message 的值;
- 第二次代理是获取 this._data.message 值的时候,实际调用了 this._data.message 的 reactiveGetter 方法。
defineReactive 方法是 mpvue 响应式源码中非常重要的一个方法,它对 data 中的属性的 get 和 set 方法进行了代理,并实际实现了响应式功能,本节我们将深入分析 defineReactive 方法:
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
var getter = property && property.get;
var setter = property && property.set;
var childOb = !shallow && observe(val, undefined, key);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal, undefined, key);
dep.notify();
}
});
}
首先看第一行代码:
var dep = new Dep();
它完成了 Dep 对象的实例化,Dep 函数源码如下:
var Dep = function Dep () {
this.id = uid$1++;
this.subs = [];
};
每个 Dep 对象维护了一个 id 和 子 Dep 对象集合 subs 数组。接下来 mpvue 获取了对象上该属性的 PropertyDescriptor:
var property = Object.getOwnPropertyDescriptor(obj, key);
这里使用了 Object.getOwnPropertyDescriptor 方法进行获取,这里的 property 是 Object.defineProperty 的第三个参数,用于描述对象的特性,如:是否可以修改,并支持代理 get 和 set 方法,关于 property 的更多信息大家可以到 MDN 上进行学习(地址)。接着 mpvue 判断该属性是否可以改变,通过 property.configurable 进行判断,如果不能改变,将直接退出 defineReactive 方法:
if (property && property.configurable === false) {
return
}
这个非常容易理解,configurable 为 false 时,无法修改属性值,响应式也就没有意义了。之后,mpvue 从 property 中取出 get 和 set 方法,并且调用 Object.defineProperty 重新定义 vm.message 的 property:
var getter = property && property.get;
var setter = property && property.set;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
// ...
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
// ...
}
})
新的 property 定义了 enumerable 和 configurable 为 true,表示该属性是可以被枚举和修改的。同时定义了新的 get 和 set 方法:reactiveGetter 和 reactiveSetter,在这两个方法中会优先判断是否已经定义了 getter 和 setter 方法,这里的 getter 和 setter 对应上面提到的 property.get 和 property.set。至此,defineReactive 的主干逻辑已经梳理清楚,下面我们就来分析 get 和 set 的实现细节。
响应式 reactiveGetter
当我们在 mpvue 中获取 vm.message 的值时就会调用 reactiveGetter 方法,reactiveGetter 方法完整源码如下:
function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
}
if (Array.isArray(value)) {
dependArray(value);
}
}
return value
}
第一行代码判断 property 是否已经定义 get 方法,如果定义了,则会调用该 get 方法进行取值,因为有可能属性的取值逻辑被我们自定义过,如果不存在,则直接返回 val,这里的 val 即我们在 data 中定义的 function 执行后的结果:
var value = getter ? getter.call(obj) : val;
之后 mpvue 会判断 Dep.target 对象是否存在,如果存在则会调用 dep.depend,这里的 dep 我们在上小节中分析过,它是一个 Dep 实例,dep.depend 方法会调用 Dep.target.addDep 方法,并将当前的 dep 实例作为参数传入:
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
addDep 方法会维护一个 newDepIds 变量,newDepIds 是一个 Set 型的集合,它会维护所有响应式的 dep 实例的 id:
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
此步完成后,mpvue 会判断 message 属性是否存在子属性,如果存在子属性,则会调用子属性的 depend 方法,这里由于 vm.message 不存在 childOb,所以该方法会跳过:
if (childOb) {
childOb.dep.depend();
}
接着 mpvue 会判断 vm.message 的值是否为数组:
if (Array.isArray(value)) {
dependArray(value);
}
这里 vm.message 的值是 String,所以此步也会跳过。所以 reactiveGetter 方法的核心就是调用了 Dep.target.addDep 方法,将当前属性 vm.message 加入到 Watcher 维护 newDepIds 集合中,通过 Watcher 完成了实际的渲染操作。
响应式 reactiveSetter
我们再分析 reactiveSetter 方法,当我们在 mpvue 中对 vm.message 的值进行修改时会调用 reactiveSetter 方法,reactiveSetter 方法源码如下:
function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal, undefined, key);
dep.notify();
}
前四行从 getter 中获取 val 值,并判断新值和原值是否发生变化,如果未发生变化,直接直接返回,不做任何操作:
var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
然后 mpvue 会判断 property.set 方法是否存在,如果存在,则调用 property.set 方法进行赋值,否则直接将 newVal 赋值给 vm.message:
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
接下来 mpvue 获取了 childOb,这里值为 undefined。下一步 mpvue 会调用一个非常关键的方法:
dep.notify();
这里用到了观察模式,通过 notify 方法,依次调用 subs 中对象的 update 方法,notify 方法源码如下:
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
这里的 this.subs 只包含一个元素,即 Watcher 对象:
this.subs
[Watcher]
每个 mpvue 页面都包含一个 Watcher,Watcher 完成了响应式的实际更新,具体更新逻辑我们将在下节中进行分析,目前大家只要知道 mpvue 通过 dep.notify 方法触发界面更新,而该方法又调用了 Watcher 的 update 进行实际更新的操作即可。这个更新动作是异步的,所以不会阻塞程序的继续执行。