欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue 2.0 深入源码分析(四) 基础篇 响应式原理 data属性

程序员文章站 2022-03-02 10:59:42
用法 官网对data属性的介绍如下: 意思就是:data保存着Vue实例里用到的数据,Vue会修改data里的每个属性的访问控制器属性,当访问每个属性时会访问对应的get方法,修改属性时会执行对应的set方法。 Vue内部实现时用到了ES5的Object.defineProperty()这个API, ......

 

用法


官网对data属性的介绍如下:

Vue 2.0 深入源码分析(四) 基础篇  响应式原理 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>

显示的内容为:

Vue 2.0 深入源码分析(四) 基础篇  响应式原理 data属性

当我们点击测试按钮后,hello world!变成了hello vue!:

Vue 2.0 深入源码分析(四) 基础篇  响应式原理 data属性

注:对于组件来说,需要把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做更新操作