zoukankan      html  css  js  c++  java
  • Vue.js 源码分析(四) 基础篇 响应式原理 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行),如下:

    writer by:大沙漠 QQ:22969969

      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:
    ' +
          '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做更新操作

  • 相关阅读:
    C# Lambda表达式 (转)
    用C#读取txt文件的方法(转)
    c#中stringbuilder的使用(转)
    c# 日期和时间的获取(转)
    C# List<T>用法 泛型 (转)
    indent format codes
    格式化输入输出 小结
    putty connection manager 一些问题的整理
    linux 网络的一些书籍
    Oracle学习笔记
  • 原文地址:https://www.cnblogs.com/greatdesert/p/11022130.html
Copyright © 2011-2022 走看看