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 语言值(通过gettersetter进行读取和赋值的属性)
  • 内部属性(internal property)没有名字,且不能直接通过 ECMAScript 语言操作。内部属性的存在纯粹为了规范的目的。可以通过Object.getPrototypeOf()方法间接的读取到它的值。

2、Object属性特性

2.1、命名的数据属性/命名的访问器属性

    每个属性(property)都拥有4个特性(attribute).两种类型的属性一种有6种属性特性,也被叫做属性描述符:

    属性描述符通常使用在下面的这些函数中:Object.defineProperty, Object.getOwnPropertyDescriptor, Object.create.如果省略了属性描述符对象中的某个属性,则该属性会取一个默认值:

表7:默认的特性
名称默认值
[[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,则将该值设定为属性的值。

表8 - 所有对象共有的内部属性
内部属性值的类型范围说明
[[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 进行实现)。

Logo

前往低代码交流专区

更多推荐