Vue 2.0 深入源码分析(四) 基础篇 响应式原理 data属性
用法
官网对data属性的介绍如下:
意思就是:data保存着vue实例里用到的数据,vue会修改data里的每个属性的访问控制器属性,当访问每个属性时会访问对应的get方法,修改属性时会执行对应的set方法。
vue内部实现时用到了es5的object.defineproperty()这个api,也正是这个原因,所以vue不支持ie8及以下浏览器(ie8及以下浏览器是不支持ecmascript 5的object.defineproperty())。
以一个hello world为例,如下:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script> <title>document</title> </head> <body> <div id="app">{{message}}</div> <button id="b1">测试按钮</button> <script> var app = new vue({ el:'#app', data:{ //data里保存着vue实例的数据对象,这里只有一个message,值为hello world! message:"hello world!" } }) document.getelementbyid('b1').addeventlistener('click',function(){ //在b1这个按钮上绑定一个click事件,内容为修正app.message为hello vue! app.message='hello vue!'; }) </script> </body> </html>
显示的内容为:
当我们点击测试按钮后,hello world!变成了hello vue!:
注:对于组件来说,需要把data属性设为一个函数,内部返回一个数据对象,因为如果只返回一个对象,当组件复用时,不同的组件引用的data为同一个对象,这点和根vue实例不同的,可以看官网的例子:点我点我
源码分析
vue实例后会先执行_init()进行初始化(4579行),如下:
vue.prototype._init = function (options) { var vm = this; // a uid vm._uid = uid$3++; /*略*/ if (options && options._iscomponent) { //这是组件实例化时的分支,暂不讨论 /*略*/ } else { //根vue实例执行到这里 vm.$options = mergeoptions( //这里执行mergeoptions()将属性保存到vm.$options resolveconstructoroptions(vm.constructor), options || {}, vm ); } /* istanbul ignore else */ { initproxy(vm); } // expose real self vm._self = vm; initlifecycle(vm); initevents(vm); initrender(vm); callhook(vm, 'beforecreate'); initinjections(vm); // resolve injections before data/props initstate(vm); /*略*/ };
mergeoptions会为每个不同的属性定义不同的合并策略,比如data、props、inject、生命周期函数等,统一放在mergeoptions里面合并,执行完后会保存到vue实例.$options对象上,例如生命周期函数会进行数组合并处理,而data会返回一个匿名函数:
return function mergedinstancedatafn () { //第1179行,这里会做判断,如果data时个函数,则执行这个函数,当为组件定义data时会执行到这里 // instance merge var instancedata = typeof childval === 'function' ? childval.call(vm, vm) : childval; var defaultdata = typeof parentval === 'function' ? parentval.call(vm, vm) : parentval; if (instancedata) { return mergedata(instancedata, defaultdata) } else { return defaultdata } }
接下来返回到_init,_init()会执行initstate()函数对props, methods, data, computed 和 watch 进行初始化,如下:
function initstate (vm) { //第3303行 vm._watchers = []; var opts = vm.$options; if (opts.props) { initprops(vm, opts.props); } if (opts.methods) { initmethods(vm, opts.methods); } if (opts.data) { //如果定义了data,则调用initdata初始化data initdata(vm); } else { observe(vm._data = {}, true /* asrootdata */); } if (opts.computed) { initcomputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativewatch) { initwatch(vm, opts.watch); } }
initdata()对data属性做了初始化处理,如下:
function initdata (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' //先获取data的值,这里data是个函数,也就是上面说的第1179行返回的匿名函数,可以看到返回的数据对象保存到了当前实例的_data属性上了 ? getdata(data, vm) : data || {}; if (!isplainobject(data)) { data = {}; "development" !== '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); //获取data的所有键名 var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; //键的个数 while (i--) { //遍历data的每个属性 var key = keys[i]; { if (methods && hasown(methods, key)) { warn( ("method \"" + key + "\" has already been defined as a data property."), vm ); } } if (props && hasown(props, key)) { "development" !== '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); //依次执行proxy,这里对data做了代理 注1 } } // observe data observe(data, true /* asrootdata */); //这里对data做了响应式处理,来观察这个data }
****************我是分隔线****************
注1解释
initdata()函数开始的时候把的数据对象保存到了当前实例的_data属性上了,这里是给vue做了一层代码,当访问每个data属性时将从实例的_data属性上获取对应的属性,vue内部如下:
var sharedpropertydefinition = { //共享属性的一些定义 enumerable: true, configurable: true, get: noop, set: noop }; function proxy (target, sourcekey, key) { //对data、props做了代理 sharedpropertydefinition.get = function proxygetter () { //获取属性 return this[sourcekey][key] }; sharedpropertydefinition.set = function proxysetter (val) { //设置属性 this[sourcekey][key] = val; }; object.defineproperty(target, key, sharedpropertydefinition); //对target的key属性的get和set做了一层代码 }
例如:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script> <title>document</title> </head> <body> <div id="app">{{message}}</div> <button id="b1">测试按钮</button> <script> debugger var app = new vue({ el:'#app', data:{ message:"hello world!" } }) console.log(app._data.message) //浏览器会输出:(index):19 hello world! </script> </body> </html>
****************我是分隔线****************
返回到initdata()函数,最后会执行observe函数:
var observer = function observer (value) { this.value = value; this.dep = new dep(); this.vmcount = 0; def(value, '__ob__', this); if (array.isarray(value)) { //如果value是个数组 var augment = hasproto ? protoaugment : copyaugment; augment(value, arraymethods, arraykeys); this.observearray(value); } else { this.walk(value); //例子中不是数组,因此调用walk()方法 } }; observer.prototype.walk = function walk(obj) { //将obj这个对象,做响应式,处理 var keys = object.keys(obj); //获取obj对象的所有键名 for (var i = 0; i < keys.length; i++) { //遍历键名 definereactive(obj, keys[i]); //依次调用definereactive()函数对象的属性变成响应式 } };
definereactive用于把对象的属性变成响应式,如下:
function definereactive(obj, key, val, customsetter, shallow) { //把对象的属性变成响应式 var dep = new dep(); var property = object.getownpropertydescriptor(obj, key); //获取obj对象key属性的数据属性 if (property && property.configurable === false) { //如果该属性是不能修改或删除的,则直接返回 return } var getter = property && property.get; //尝试拿到该对象原生的get属性,保存到getter中 if (!getter && arguments.length === 2) { //如果getter不存在,且参数只有两个 val = obj[key]; //则直接通过obj[ke]获取值,并保存到val中 } var setter = property && property.set; //尝试拿到该对象原生的set属性,保存到setter中 var childob = !shallow && observe(val); //递归调用observe:当某个对象的属性还是对象时会进入 object.defineproperty(obj, key, { //调用object.defineproperty设置obj对象的访问器属性 enumerable: true, configurable: true, get: function reactivegetter() { var value = getter ? getter.call(obj) : val; if (dep.target) { //这里就是做依赖收集的事情 dep.depend(); //调用depend()收集依赖 if (childob) { //如果childob存在 childob.dep.depend(); //则调用childob.dep.depend()收集依赖 if (array.isarray(value)) { dependarray(value); } } } return value }, set: function reactivesetter(newval) { //做派发更新的事情 var value = getter ? getter.call(obj) : val; //如果之前有定义gvetter,则调用getter获取值,否则就赋值为val if (newval === value || (newval !== newval && value !== value)) { //如果value没有改变 return //则直接返回,这是个优化错误,当data值修改后和之前的值一样时不做处理 } if ("development" !== 'production' && customsetter) { customsetter(); } if (setter) { setter.call(obj, newval); } else { val = newval; } childob = !shallow && observe(newval); //再调用observe,传递newval,这样如果新值也是个对象也会是响应式的了。 dep.notify(); //通知订阅的watcher做更新 } }); }
当render函数执行转换成虚拟vnode的时候就会执行with(this){}函数,内部访问到某个具体的data时就会执行到这里的访问器控制get函数了,此时会收集对应的渲染watcher作为订阅者,保存到对应属性的dep里面
当修改了data里某个属性时就会除法对应的set访问器控制属性,此时会执行对应的访问其控制的set函数,会执行notify()通知订阅的watcher做更新操作
上一篇: HTML5 canvas画布(三)
下一篇: Jquery的使用
推荐阅读
-
Vue 2.0 深入源码分析(七) 基础篇 侦听器 watch属性详解
-
Vue 2.0 深入源码分析(五) 基础篇 methods属性详解
-
Vue 2.0 深入源码分析(六) 基础篇 computed 属性详解
-
Vue 2.0 深入源码分析(四) 基础篇 响应式原理 data属性
-
Vue 2.0 深入源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解
-
Vue 2.0 深入源码分析(七) 基础篇 侦听器 watch属性详解
-
Vue 2.0 深入源码分析(六) 基础篇 computed 属性详解
-
Vue 2.0 深入源码分析(五) 基础篇 methods属性详解
-
Vue 2.0 深入源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解
-
Vue 2.0 深入源码分析(四) 基础篇 响应式原理 data属性