概要
本文通过分析Vue 2.0 源码,探讨一下在Vue 2.0的初始化过程中,如何生成响应式数据。最后我们将关键的代码抽取出来,模拟出具体的实现过程。
代码调用关系
- 调用src\core\instance\index.js _init方法。该方法定义在src\core\instance\init.js中的initMixin方法中。
 - 调用src\core\instance\state.js中initState方法
 - 调用src\core\instance\state.js中的initData方法
 - 调用src\core\observer\index.js中的observe方法
 所有的响应式数据均在observe方法中创建。
关键代码分析
生成响应式数据过程中都做了什么?
在使用Vue过程中,我们通常会将要绑定到页面HTML元素中的数据作为data方法的返回值。Vue实例会将这些数据转换为响应式数据,以支持其单项或双向的数据绑定。
observe方法
考虑到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
}
- 如果是Primitive值,例如数字,字符串等直接返回。
- 生成响应式数据后,用户数据的每个子对象包含一个Observer对象,key为__ob__。
- 由于生成响应式数据的过程是一个函数递归调用的过程,为了避免在复杂数据结构情况下重复生成Observer,所以增加当前子对象是否包含__ob__的检查,如果包含,则直接赋值。
- 如果当前对象是用户数据,并且不包含__ob__,则调用Observer的构造方法,为用户数据创建Observer对象。
Observer对象的构造方法
该构造方法主要用于区分对象和数组两种数据结构,每种数据结构对应不同的处理方法。
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)
}
}
- 当前用户数据中的子对象作为参数,传入Observer的构造器。
- 实例化依赖收集对象Dep。
- 调用def方法为当前子对象生成__ob__。
- 如果是数组调用protoAugment方法和observeArray方法
- 如果不是数组,调用walk方法。
walk方法/defineReactive方法
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
- 遍历当前子对象。
- 调用defineReactive,为每个属性生成响应式数据。
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
}
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
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
- 如果该属性存在,但是不可以删除,即不能通过delete进行删除并重新定义,则直接返回,不进行响应式处理。
- 获取对应属性的值。
- 递归调用observe方法,如果属性值为对象,则重新为其生成Observer,否则退出observe方法。
- 为该属性创建getter和setter,其他代码为依赖收集相关,请参看依赖收集的章节。
- 在setter方法中考虑到原有属性值可能被一个对象修改,所以要重新调用observe方法,生成 响应式数据。
protoAugment方法/observeArray方法
protoAugment方法用于修改数组对象的原型属性__proto__,对数组对象的七个方法进行重新定制,从而到达监控数组变化的需求。
function protoAugment (target, src: Object) {
target.__proto__ = src
}
传入的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'
]
methodsToPatch.forEach(function (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)
ob.dep.notify()
return result
})
})
- 备份数组对象的原型。
- 基于备份的原型链,创建一个新的Array.prototype原型对象。
- Vue监控的数组操作包括:‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’,其他任何数组操作,Vue对象都无法监控到。
- 通过defineProperty重新定义上述七个方法,每个key值对应一个既完成原有数组操作,又可以监控数组变化的新方法。
- 新的方法处理包括:
- 利用备份的数组原型链,调用原生数组操作方法,完成具体数组操作。
- 如果是新的元素插入到了数组中,调用observeArray方法,将新的数据转化成生成响应式数据。
- 返回数组操作结果。
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
- 遍历数组中的每个元素
- 调用observe方法为数组的每个元素生成响应式数据。
FAQ
数组内数据是否可以通过索引值进行修改? 不可以,只有通过’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" },
]
}
}
});
})();
附录
isObject方法
src\shared\util.js
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
def方法
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
})
}
|