本文通过分析Vue 2.0 源码,探讨一下在Vue 2.0的初始化过程中,如何生成响应式数据。最后我们将关键的代码抽取出来,模拟出具体的实现过程。
在使用Vue过程中,我们通常会将要绑定到页面HTML元素中的数据作为data方法的返回值。Vue实例会将这些数据转换为响应式数据,以支持其单项或双向的数据绑定。
考虑到Vue使用者的业务数据结构可能非常复杂,例如对象中包含数组,数组中每项又是一个js对象,如下代码中的情况:
data(){
return {
stulist:[
{id: 1, name: 'Tom'},
{id: 2, name: 'Jack'}
]};
}
Observe方法通过递归调用的方式,为数据中的每个属性逐个生成getter和setter方法,为每个子对象逐一生成依赖收集中使用到的数据。
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
}
该构造方法主要用于区分对象和数组两种数据结构,每种数据结构对应不同的处理方法。
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
protoAugment方法用于修改数组对象的原型属性__proto__,对数组对象的七个方法进行重新定制,从而到达监控数组变化的需求。
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
传入的scr内容来自src\core\observer\array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
数组内数据是否可以通过索引值进行修改?
不可以,只有通过’push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, 'reverse’这七个方法对数组的修改,Vue才能监控到。
通过splice方法修改数组,为什么从要忽略前两个参数, 通过args.slice(2)来取得新添加的元素?
splice方法包含两个required参数,第一个是原数组中要添加的新元素的位置,第二个是要添加多少个新元素,从第三个参起才是要添加的新元素。所以要slice方法的参数是2,即忽略前两个参数。
在defineReactive 方法中为每个属性定义getter和setter方式的时候,为什么要通过一个中间变量value或val来获取或设置属性值,不能直接通过obj[key]方式完成具体操作?
这样做会触发死循环,在getter中,如果通过obj[key]返回属性值,会再次触发obj的getter方法,从而形成死循环调用。setter方法同理。
模拟代码并不需要nodejs环境,可以直接在Chrome浏览器的Console中运行。
(function () {
var uid = 0;
function Vue(optioanl) {
if (!(this instanceof Vue)) {
console.error('Vue is constructor and should be called with new keyword');
}
const vm = this;
const { isPlainObject } = Utils();
const initData = function (optioanl) {
let data = vm._data = typeof optioanl.data === 'function'
? optioanl.data.call(this) : {};
if (!isPlainObject(data)) {
data = {};
console.error('data function should return an object.');
}
var keys = Object.keys(data);
for (let key of keys) {
proxy(vm, '_data', key);
}
const { observe } = Reactive();
observe(data);
};
const proxy = function (target, sourceKey, key) {
const handler = {
get: function () {
return this[sourceKey][key];
},
set: function (val) {
this[sourceKey][key] = val;
}
}
Object.defineProperty(target, key, handler);
};
initData(optioanl);
}
function Observer(target) {
const { walk, observeArray } = Reactive();
const { def } = Utils();
this.dep = new Dep();
this.value = target;
this.observeArray = observeArray.bind(this);
def(target, "__ob__", this);
if (Array.isArray(target)) {
target = setArrayProto(target);
observeArray(target);
} else {
console.log(target);
walk(target);
}
}
function Dep() {
this.id = uid++;
this.notity = function(){
console.log("Notify");
};
this.subs = [];
this.addSub = function(sub){
this.subs.push(sub);
}.bind(this);
this.depend = function(){
if (window.__target != null){
this.addSub(window.__target);
}
}.bind(this);
}
function setArrayProto(target) {
const arrayProto = {};
const { def } = Utils();
const methods = ["splice", "push", "pop", "reverse", "sort", "shift", "unshift"];
methods.forEach(method => {
def(arrayProto, method, function (...args) {
const protoMethod = Array.prototype[method];
const ob = this.__ob__;
const result = protoMethod.apply(this, args);
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
default:
break;
}
if (inserted) {
ob.observeArray(inserted);
ob.dep.notity();
}
return result;
}, false);
});
target.__proto__ = arrayProto;
return target;
}
function Reactive() {
const { hasOwn, isObject } = Utils();
const defineReactive = function (obj, key) {
let value = obj[key];
let childObj = observe(value);
let dep = new Dep();
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: function () {
console.log(`Get ${key}'s value`);
return value;
},
set: function (newVal) {
if (value === newVal) {
return;
}
console.log(`${key}'s value is updated from ${value} to ${newVal}`);
value = newVal;
childObj = observe(newVal);
dep.notity();
}
});
}
const walk = function (target) {
const keys = Object.keys(target);
for (let key of keys) {
defineReactive(target, key);
}
}
const observe = function (obj) {
if (!isObject(obj)) {
return;
}
let ob;
if (hasOwn(obj, "__ob__") && obj["__ob__"] instanceof Observer) {
ob = obj["__ob__"];
}
ob = new Observer(obj);
return ob;
};
const observeArray = function (target) {
for (var i = 0, l = target.length; i < l; ++i) {
observe(target[i]);
}
}
return { defineReactive, walk, observe, observeArray };
}
function Utils() {
const hasOwn = function (target, key) {
const hasOwnProperty = Object.prototype.hasOwnProperty;
return hasOwnProperty.call(target, key);
}
const isObject = function (target) {
if (target != null && typeof target === "object") {
return true;
}
return false;
}
const isPlainObject = function (obj) {
return (Object.prototype.toString.call(obj) === "[object Object]");
}
const def = function (target, key, val, enumerable) {
Object.defineProperty(target, key, {
value: val,
configurable: true,
enumerable: !!enumerable,
writable: true
})
}
return { hasOwn, isObject, isPlainObject, def };
}
var v = new Vue({
data() {
return {
name: "ly",
age: 21,
address: {
district: "ABC",
street: "DEF"
},
cards: [
{ id: 1, title: "credit card" },
{ id: 2, title: "visa card" },
]
}
}
});
})();
src\shared\util.js
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
src\core\util\lang.js
function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}