Vue.$data、this._data源码解析
Vue.$data源码解析$data是Vue实例中的实例属性,表示Vue实例观察的数据对象。官网给出的解释:vm.$data,类型:Object,详细:Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。在了解vm.$data之前我们先来复习一下原型,然后再了解一下Object.defineProperty()。正是Vue内部实...
Vue.$data、this._data源码解析
$data是Vue实例中的实例属性,表示Vue实例观察的数据对象。官网给出的解释:vm.$data
类型:Object
详细:Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。
先看一个栗子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
{{ message }}
</div>
</body>
<script>
var vm = new Vue({
el: "#app",
data: {
message: "hello vue."
}
});
console.log(vm.$data) //{__ob__: Observer}
console.log(vm._data); //{__ob__: Observer}
console.log(vm.$data == vm._data); //true
//三种方式都可以访问到message
console.log(vm.$data.message); //hello vue
console.log(vm._data.message); //hello vue
console.log(vm.message); //hello vue
console.log(Vue.prototype)
</script>
</html>
在了解vm.$data之前我们先来复习一下原型及对象的属性,然后再了解一下Object.defineProperty()。正是Vue内部实现时用到了ES5的Object.defineProperty()方法,所以Vue不支持IE8及以下浏览器(IE8及以下浏览器是不支持ECMASCRIPT 5的Object.defineProperty())。
一、原型__proto__ 、prototype属性
1、__proto__、prototype属性
在js中,对象可谓贯穿一生。对象可以分为两类。一是普通对象(Object);二是函数对象(Function)。prototype和__proto__都指向原型对象。
任意一个函数(包括构造函数)都有一个prototype属性,指向该函数的原型对象。函数本身是一种对象,所以函数既有prototype属性也有__proto__属性。
任意一个构造函数实例化的对象,都有一个__proto__属性(__proto__并非标准属性,ECMA-262第5版将该属性或指针称为[[Prototype]],可通过Object.getPrototypeOf()标准方法访问该属性),指向构造函数的原型对象。
举个栗子:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
//给自己构造一个女朋友
function GirlFriend () {
this.name = "zhiling";
}
//设置GirlFriend()这个函数的prototype属性
var hand = {
whoName: "right hand",
someFunction: function(){
console.log("女朋友="+this.whoName);
}
};
//将函数的原型prototype指向hand
GirlFriend.prototype = hand;
//打印hand
console.log(hand);
//创建一个myGirlFriend对象
var myGirlFriend = new GirlFriend();
//GirlFriend的原型prototype是hand对象
console.log(GirlFriend.prototype);
//myGirlFriend的原型__proto__是hand对象
console.log(myGirlFriend.__proto__);
//prototype 与__proto__ 的关系就是:对象的__proto__==对象的构造函数的prototype
console.log(myGirlFriend.__proto__ === GirlFriend.prototype) //true
console.log(Object.prototype==hand.__proto__);//true
console.log(Object.prototype); //最顶层的原型
console.log(hand.__proto__.__proto__)//null
</script>
</html>
案例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
//给自己构造一个女朋友
function GirlFriend () {
this.name = "zhiling";
}
//第一条:普通函数GirlFriend.__proto__->GirlFriend.__proto__.__proto__->GirlFriend.__proto__.__proto__.__proto__
console.log(GirlFriend.__proto__); //function Empty() {}
console.log(GirlFriend.__proto__.__proto__); //[object Object]
console.log(GirlFriend.__proto__.__proto__.__proto__); //null
//构造函数:GirlFriend.constructor.__proto__
console.log(GirlFriend.constructor.__proto__);//function Empty() {}
//构造函数:GirlFriend.constructor.prototype
console.log(GirlFriend.constructor.prototype);//function Empty() {}
//Object
console.log(GirlFriend.__proto__.__proto__.constructor.__proto__);//function Empty() {}
console.log(GirlFriend.__proto__.__proto__.constructor.prototype);//[object Object]
console.log(GirlFriend.__proto__.__proto__.prototype);//对象,undefined
</script>
</html>
原型链的关系图:
二、Object属性简介
在js中,我们有很多种方式给对象定义属性和赋值。其中最常见的就是:1)对象.属性 = 值;2)对象['属性'] = 值;
除了上述的方式之外,还可以使用Object.defineProperty()
方法来定义和修改对象的属性。Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
注:应当直接在 Object
构造器对象上调用此方法,而不是在任意一个 Object
类型的实例上调用。
1、Object属性的类型
在了解Object.defineProperty()之前,先看看ECMAScript5中对对象属性的一个描述:
Object 是一个属性的集合。每个属性既可以是一个命名的数据属性,也可以是一个命名的访问器属性,或是一个内部属性:
- 命名的数据属性(named data property)由一个名字与一个 ECMAScript 语言值和一个 Boolean 属性集合组成(拥有一个确定的值的属性。这也是最常见的属性)
- 命名的访问器属性(named accessor property)由一个名字与一个或两个访问器函数,和一个 Boolean 属性集合组成。访问器函数用于存取一个与该属性相关联的 ECMAScript 语言值(通过
getter
和setter
进行读取和赋值的属性) - 内部属性(internal property)没有名字,且不能直接通过 ECMAScript 语言操作。内部属性的存在纯粹为了规范的目的。可以通过
Object.getPrototypeOf()
方法间接的读取到它的值。
2、Object属性特性
2.1、命名的数据属性/命名的访问器属性
每个属性(property)都拥有4个特性(attribute).两种类型的属性一种有6种属性特性,也被叫做属性描述符:
属性描述符通常使用在下面的这些函数中:Object.defineProperty, Object.getOwnPropertyDescriptor, Object.create.如果省略了属性描述符对象中的某个属性,则该属性会取一个默认值:
名称 | 默认值 |
---|---|
[[Value]] | undefined |
[[Get]] | undefined |
[[Set]] | undefined |
[[Writable]] | false |
[[Enumerable]] | false |
[[Configurable]] | false |
2.2、内部属性
这些内部属性不是 ECMAScript 语言的一部分。纯粹是以说明为目的定义它们。ECMAScript 实现需要保持和这里描述的内部属性产生和操作的结果一致。所有对象(包括宿主对象)必须实现 表8 中列出的所有内部属性(他们可以看成是一种规范)。
1)所有对象都有一个叫做 [[Prototype]] 的内部属性。此对象的值是 null 或一个对象,并且它用于实现继承。它可以通过Object.getPrototypeOf(obj)
访问
2)内置对象都定义了 [[Class]] 内部属性的值。宿主对象的 [[Class]] 内部属性的值可以是除了 "Arguments"、"Array"、"Boolean"、"Date"、"Error"、"Function"、"JSON"、"Math"、"Number"、"Object"、"RegExp"、"String" 的任何字符串。[[Class]] 内部属性的值用于内部区分对象的种类。它可以通过Object.prototype.toString(obj)访问
3)所有 ECMAScript对象 都有一个 Boolean 类型的 [[Extensible]] 内部属性,它决定了 是否可以给对象添加命名属性。如果 [[Extensible]] 内部属性的值是 false 就不得给对象添加命名属性。它可以通过Object.isExtensible(obj)访问
4)[[Get]],对象默认的内置[[Get]]操作首先是在对象中查找是否有名称相同的属性,有则返回这个属性的值,如果没有找到,则会遍历可能存在的[[Prototype]]链,如果无论如何都没找到,name就返回一个undefined值。
5)[[Put]],此操作并不仅仅只是给对象设置或者创建一个属性,[[Put]]会检查以下内容:a)属性是否是访问描述符,如果是并存在setter就调用setter;b)属性的数据描述符中的writable参数,如果writable不是false,则将该值设定为属性的值。
内部属性 | 值的类型范围 | 说明 |
---|---|---|
[[Prototype]] | Object 或 Null | 此对象的原型 |
[[Class]] | String | 说明规范定义的对象分类的一个字符串值 |
[[Extensible]] | Boolean | 如果是 true,可以向对象添加自身属性。 |
[[Get]] | SpecOp(属性名) → 任意 | 返回命名属性的值 |
[[GetOwnProperty]] | SpecOp(属性名) → Undefined 或 属性描述 | 返回此对象的自身命名属性的属性描述,如果不存在返回 undefined |
[[GetProperty]] | SpecOp(属性名) → Undefined 或 属性描述 | 返回此对象的完全填入的自身命名属性的属性描述,如果不存在返回 undefined |
[[Put]] | SpecOp(属性名, 任意, Boolean) | 将指定命名属性设为第二个参数的值。flag 控制失败处理。 |
[[CanPut]] | SpecOp(属性名) → Boolean | 返回一个 Boolean 值,说明是否可以在 属性名 上执行 [[Put]] 操作。 |
[[HasProperty]] | SpecOp (属性名) → Boolean | 返回一个 Boolean 值,说明对象是否含有给定名称的属性。 |
[[Delete]] | SpecOp(属性名, Boolean) → Boolean | 从对象上删除指定的自身命名属性。flag 控制失败处理。 |
[[DefaultValue]] | SpecOp(暗示) → 原始类型 | 暗示 是一个字符串 。返回对象的默认值 |
[[DefineOwnProperty]] | SpecOp(属性名, 属性描述, Boolean) → Boolean | 创建或修改自身命名属性为拥有属性描述里描述的状态。flag 控制失败处理。 |
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
var user = {name:"老王",age:18,sex:"男"};
//[[Prototype]]
console.log(Object.prototype.toString(user));//[object Object]
//[[Extensible]]
console.log(Object.isExtensible(user)); //true
//[[GetOwnProperty]]
console.log(Object.getOwnPropertyNames(user));//Array(3):"name,age,sex"
//[[GetProperty]]
console.log(Object.getPrototypeOf(user));//[object Object]
//[[HasProperty]]
console.log(Object.hasOwnProperty("name"));//true
</script>
</html>
3、常见的对象创建与赋值
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
//使用构造函数方式创建对象
var obj = new Object();
obj.name = "老王";
obj.age = 18;
obj.sex = "男";
//使用对象字面量方式创建对象
var user = {name:"老王",age:18,sex:"男"};
//分别将其属性描述符打印出来
console.log("obj属性描述:",Object.getOwnPropertyDescriptor(obj,"name"));
console.log("user属性描述:",Object.getOwnPropertyDescriptor(user,"name"));
// 数据属性:[[Configurable]]、[[Enumerable]]、[[Writable]]、[[Value]]
// Configurable是否可以通过delete删除属性
// Enumerable可否for-in
// Writable能否修改属性值
// Value读取这个属性的数据值
</script>
</body>
</html>
效果:
使用这种方式创建的对象,我们对一个Object对象设置属性时,一般是通过对象的.
操作符或者[]
操作符直接赋值的,通过这种方式添加的属性后续可以更改属性值,并且默认该属性是可枚举的(他的数据属性描述符默认都是true)。如果我们想要修改他的属性默认的特性呢?那么Object.defineProperty()他来了。
三、Object.defineProperty()方法的使用
Object.defineProperty()
的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。详细文档可以看:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
1、Object.defineProperty()语法
语法:Object.defineProperty(obj, prop, descriptor)
参数:
obj
要定义属性的对象。
prop
要定义或修改的属性的名称或 Symbol
。
descriptor 要定义或修改的属性描述符。
返回值:被传递给函数的对象。
注:在ES6中,由于 Symbol类型的特殊性,用Symbol类型的值来做对象的key与常规的定义或修改不同,而Object.defineProperty
是定义key为Symbol的属性的方法之一。
该方法允许精确地添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,在枚举对象属性时会被枚举到(for...in
或 Object.keys
方法),可以改变这些属性的值,也可以删除
这些属性。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty()
添加的属性值是不可修改(immutable)的。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
var user = {};
Object.defineProperty(user, 'age', {
value: 24,
writable: false
});
console.log(user);
console.log(user.age);//24
user.age = 28; // throws an error in strict mode
console.log(user.age);//24
</script>
</html>
2、参数descriptor 属性描述符
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
2.1、数据描述符value 和 writable
栗子:value -值、writable -是否可写
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
var user = {};
Object.defineProperty(user, 'age', {
value: 24,
writable:true //默认为false,不可修改
});
console.log(user); //[object Object]
console.log(user.age);//24
user.age = 28;
console.log(user.age);//28
</script>
</html>
2.2、存取描述符getter 和 setter
和数据属性不同,存取器属性不具有可写性(writable attribute)。如果属性同时具有getter和setter方法,那么它是一个读/写属性。如果它只有getter方法,那么它是一个只读属性。如果它只有setter方法,那么它是一个只写属性(数据属性中有一些例外),读取只写属性总是返回undefined。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get: function() {
console.log('get!');
return temperature;
},
set: function(value) {
temperature = value;
archive.push({
val: temperature
});
}
});
this.getArchive = function() {
return archive;
};
}
var arc = new Archiver();
arc.temperature; // 'get!' 访问时被调用
arc.temperature = 11; //设置时调用set--archive
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
var obj = {};
Object.defineProperty(obj, 'x', {
set: function(val){
this.x = "要设置我吗?";
}
});
console.log(obj.x);//undefined
</script>
</html>
2.3、共有属性configurable 和 enumerable
栗子:configurable - 是否可删除
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
var obj = {}
Object.defineProperty(obj, 'name', {
value: '老王',
configurable: true,
writable: true
});
delete obj.name
console.log(obj.name); // undefined
Object.defineProperty(obj, 'name', {
value: '老李',
configurable: false, //不能被删除
writable: true
});
delete obj.name
console.log(obj.name); //老李
</script>
</html>
栗子:enumerable - 是否可枚举
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script>
var obj = {}
Object.defineProperty(obj, 'name', {
value: '老王',
enumerable:true
});
obj.sex = "男";
Object.defineProperty(obj, 'age', {
value: '28',
enumerable:false
});
console.log(Object.keys(obj)); //name,sex
for(var keys in obj){
console.log(keys+"="+obj[keys]);//name=老王 sex=男
}
console.log(obj.propertyIsEnumerable('age')); //false
</script>
</html>
了解完了上诉内容,现在开始进入源码模式,探究一下Vue.$data、this._data和this.property 为何都能取到data里面的数据。
四、Vue.$data、this._data源码解析
1、找到Vue函数
找到源码,Vue原来是一个函数。首先process.env.NODE_ENV
是判断你启动时候的参数的,如果不符合的话,就发出警告,否则执行_init
方法。值得一提的是一般属性名前面加_
默认代表是私有属性,不对外展示。
function Vue(options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
2、初始化_init
这个_init
是哪来的呢?可以看到下面有很多初始化的函数,我们先看第一个initMixin,然后去查看他的定义。
initMixin(Vue) //定义 _init(初始化就已经加载了,里面定义了Vue的原型方法_init)
stateMixin(Vue) //定义 $set $get $delete $watch 等
eventsMixin(Vue) // 定义事件 $on $once $off $emit
lifecycleMixin(Vue) // 定义 _update $forceUpdate $destroy
renderMixin(Vue) // 定义 _render 返回虚拟dom
3、initMixin初始化
initMixin中定义了原型方法_init,并初始化options参数,对生命周期变量初始化,初始化渲染Render,初始化 vm的状态,prop/data/computed/method/watch都在这里完成初始化,这里就有对data的初始化。initState(vm)
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
//..
// a flag to avoid this being observed-一个避免被观察到的标志
vm._isVue = true
// merge options - 合并opt信息
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
//优化内部组件实例化,因为动态选项合并非常慢,而且没有一个内部组件选项需要特殊处理。
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
//..
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm); // 定义 vm.$parent vm.$root vm.$children vm.$refs 等(生命周期变量初始化)
initEvents(vm); // 定义 vm._events vm._hasHookEvent 等(事件监听初始化)
initRender(vm); // 定义 $createElement $c (初始化渲染)
callHook(vm, 'beforeCreate'); // 回调 beforeCreate 钩子函数
initInjections(vm); // resolve injections before data/props (初始化注入)
initState(vm); // 初始化 props methods data computed watch 等方法 (状态初始化)
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // 回调 created 钩子函数
// 如果有el选项,则自动开启模板编译阶段与挂载阶段
// 如果没有传递el选项,则不进入下一个生命周期流程
// 用户需要执行vm.$mount方法,手动开启模板编译阶段与挂载阶段
if (vm.$options.el) {
vm.$mount(vm.$options.el); // 实例挂载渲染dom
}
}
}
4、initState初始化
将该对象赋值给 vm._data 属性,是函数也会转为变量。isReserved 函数通过判断一个字符串的第一个字符是不是 $ 或_来决定其是否是保留的,Vue 是不会代理那些键名以 $ 或 _ 开头的字段的,因为Vue自身的属性和方法都是以 $ 或 _ 开头的,所以这么做是为了避免与 Vue 自身的属性和方法相冲突。 如果 key 既不是以 $ 开头,又不是以 _ 开头,那么将执行 proxy 函数,实现实例对象的代理访问
function initData (vm: Component) {
let data = vm.$options.data //先通过$options获取到data
data = vm._data = typeof data === 'function' //将对象赋值给vm._data
? getData(data, vm) //判断data是不是通过返回函数对象的方式建立的,如果是,那么则执行getdata方法,getdata的方法主要操作就是 data.call(vm, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== '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
//检测data中的key是不是与props、methods中有重名
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') { //在非生产环境下如果发现在 methods 对象上定义了同样的key,打印一个警告
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {//判断starts with $ or _
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
5、代理data->_data
proxy(vm, `_data`, key);proxy 函数的原理是通过 Object.defineProperty 函数在实例对象 vm 上定义与 data 数据字段同名的访问器属性,并且这些属性代理的值是 vm._data 上对应属性的值。假如访问:vm.message,就会触发sharedPropertyDefinition
的get,然后返回vm._data.message。
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key] //如:访问vm.message = 访问vm._data.message
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
6、在原型上绑定$data
$data的数据劫持是在stateMixin函数中处理的,因为$data被定义为一个getter,实际上它仍然访问的是this._data。
function stateMixin (Vue) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
var dataDef = {};
dataDef.get = function () { return this._data };
var propsDef = {};
propsDef.get = function () { return this._props };
{
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
);
};
propsDef.set = function () {
warn("$props is readonly.", this);
};
}
Object.defineProperty(Vue.prototype, '$data', dataDef); //将$data绑定到原型上
Object.defineProperty(Vue.prototype, '$props', propsDef);
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
}
注:数据劫持最著名的应用当属双向绑定,比较典型的是Object.defineProperty()和 ES2016 中新增的Proxy对象。Vue 2.x
使用的是Object.defineProperty()(Vue 在 3.x
版本之后改用 Proxy
进行实现)。
更多推荐
所有评论(0)