Vue 2.0 源碼分析(四) 基礎篇 響應式原理 data屬性

 

用法css


官網對data屬性的介紹以下:html

意思就是:data保存着Vue實例裏用到的數據,Vue會修改data裏的每一個屬性的訪問控制器屬性,當訪問每一個屬性時會訪問對應的get方法,修改屬性時會執行對應的set方法。vue

Vue內部實現時用到了ES5的Object.defineProperty()這個API,也正是這個緣由,因此Vue不支持IE8及如下瀏覽器(IE8及如下瀏覽器是不支持ECMASCRIPT 5的Object.defineProperty())。react

以一個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!:app

注:對於組件來講,須要把data屬性設爲一個函數,內部返回一個數據對象,由於若是隻返回一個對象,當組件複用時,不一樣的組件引用的data爲同一個對象,這點和根Vue實例不一樣的,能夠看官網的例子:點我點我ide

 

 源碼分析函數

 


 Vue實例後會先執行_init()進行初始化(4579行),以下:oop

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

相關文章
相關標籤/搜索