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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM