Vue框架核心之數據劫持

本文來自網易雲社區前端

前瞻

當前前端界空前繁榮,各類框架橫空出世,包括各種mvvm框架橫行霸道,好比Angular、Regular、Vue、React等等,它們最大的優勢就是能夠實現數據綁定,不再須要手動進行DOM操做了,它們實現的原理也基本上是髒檢查或數據劫持。那麼本文就以Vue框架出發,探索做者運用Object.defineProperty來實現數據劫持的奧祕(本文所選取的相關代碼源自於Vue v2.0.3版本的源碼)。react

回顧一下Object.defineProperty

  • 語法
     Object.defineProperty(obj,prop,descriptor)web

  • 參數
     obj:目標對象
     prop:須要定義的屬性或方法的名稱
     descriptor:目標屬性所擁有的特性數組

  • 可供定義的特性列表
     value:屬性的值
     writable:若是爲false,屬性的值就不能被重寫。
     get: 一旦目標屬性被訪問就會調回此方法,並將此方法的運算結果返回用戶。
     set:一旦目標屬性被賦值,就會調回此方法。
     configurable:若是爲false,則任未嘗試刪除目標屬性或修改屬性性如下特性(writable,    configurable, enumerable)的行爲將被無效化。
     enumerable:是否能在for...in循環中遍歷出來或在Object.keys中列舉出來。緩存

什麼是數據劫持

經過上面對Object.defineProperty的介紹,咱們不難發現,當咱們訪問或設置對象的屬性的時候,都會觸發相對應的函數,而後在這個函數裏返回或設置屬性的值。既然如此,咱們固然能夠在觸發函數的時候動一些手腳作點咱們本身想作的事情,這也就是「劫持」操做。在Vue中其實就是經過Object.defineProperty來劫持對象屬性的setter和getter操做,並「種下」一個監聽器,當數據發生變化的時候發出通知。先簡單的舉個例子:app

var data = {
    name:'lhl'}
Object.keys(data).forEach(function(key){
    Object.defineProperty(data,key,{
        enumerable:true,
        configurable:true,
        get:function(){
            console.log('get');
        },
        set:function(){
            console.log('監聽到數據發生了變化');
        }
    })
});
data.name //控制檯會打印出 「get」data.name = 'hxx' //控制檯會打印出 "監聽到數據發生了變化"

上面的這個例子能夠看出,咱們徹底能夠控制對象屬性的設置和讀取。在Vue中,做者在不少地方都很是巧妙的運用了Object.defineProperty這個方法,具體用在哪裏而且它又解決了哪些問題,下面就作詳細的介紹:框架

監聽對象屬性的變化

這個應該是Vue敲開數據綁定的前大門,它經過observe每一個對象的屬性,添加到訂閱器dep中,當數據發生變化的時候發出一個notice。 相關源代碼以下:(做者採用的是ES6+flow寫的,代碼在src/core/observer/index.js模塊裏面)mvvm

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep()//建立訂閱對象
  const property = Object.getOwnPropertyDescriptor(obj, key)//獲取obj對象的key屬性的描述  //屬性的描述特性裏面若是configurable爲false則屬性的任何修改將無效  if (property && property.configurable === false) {
    return  }

  // cater for pre-defined getter/setters  const getter = property && property.get  const setter = property && property.set
  let childOb = observe(val)//建立一個觀察者對象  Object.defineProperty(obj, key, {
    enumerable: true,//可枚舉    configurable: true,//可修改    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val//先調用默認的get方法取值      //這裏就劫持了get方法,也是做者一個巧妙設計,在建立watcher實例的時候,經過調用對象的get方法往訂閱器dep上添加這個建立的watcher實例      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value//返回屬性值    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val//先取舊值      if (newVal === value) {
        return      }
      //這個是用來判斷生產環境的,能夠無視      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)//繼續監聽新的屬性值      dep.notify()//這個是真正劫持的目的,要對訂閱者發通知了    }
  })
}

以上是Vue監聽對象屬性的變化,那麼問題來了,咱們常常在傳遞數據的時候每每不是一個對象,頗有多是一個數組,那是否是就沒有辦法了呢,答案顯然是不然的。那麼下面就看看做者是如何監聽數組的變化:函數

監聽數組的變化

咱們還看先看這段源碼:性能

const arrayProto = Array.prototype//原生Array的原型export const arrayMethods = Object.create(arrayProto)

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse']
.forEach(function (method) {
  const original = arrayProto[method]//緩存元素數組原型  //這裏重寫了數組的幾個原型方法  def(arrayMethods, method, function mutator () {
    //這裏備份一份參數應該是從性能方面的考慮    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    const result = original.apply(this, args)//原始方法求值    const ob = this.__ob__//這裏this.__ob__指向的是數據的Observer    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break      case 'unshift':
        inserted = args
        break      case 'splice':
        inserted = args.slice(2)
        break    }
    if (inserted) ob.observeArray(inserted)
    // notify change    ob.dep.notify()
    return result
  })
})

...//定義屬性function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true  });
}

上面的代碼主要是繼承了Array自己的原型方法,而後又作了劫持修改,能夠發出通知。Vue在observer數據階段會判斷若是是數組的話,則修改數組的原型,這樣的話,後面對數組的任何操做均可以在劫持的過程當中控制。結合Vue的思想,我簡單的寫個小demo方便更好的理解:

var arrayMethod = Object.create(Array.prototype);
['push','shift'].forEach(function(method){
    Object.defineProperty(arrayMethod,method,{
        value:function(){
            var i = arguments.length
            var args = new Array(i)
            while (i--) {
              args[i] = arguments[i]
            }
            var original = Array.prototype[method];
            var result = original.apply(this,args);
            console.log("已經控制了,哈哈");
            return result;
        },
        enumerable: true,
        writable: true,
        configurable: true    })
})var bar = [1,2];
bar.__proto__ = arrayMethod;
bar.push(3);//控制檯會打印出 「已經控制了,哈哈」;而且bar裏面已經成功的添加了成員 ‘3’

整個過程看起來好像沒有什麼問題,彷佛Vue已經作到了完美,其實否則,Vue仍是不能檢測到數據項和數組長度改變的變化,例以下面的調用:

vm.items[index] = "xxx";
vm.items.length = 100;

咱們儘可能避免這樣的調用方式,若是確實須要,做者也幫咱們實現了一個$set操做,這裏就不作介紹了。

實現對象屬性代理

正常狀況下咱們是這樣實例化一個Vue對象:

var VM = new Vue({    data:{        name:'lhl'    },    el:'#id'})

按理說咱們操做數據的時候應該是VM.data.name = ‘hxx’纔對,可是做者以爲這樣不夠簡潔,因此又經過代理的方式實現了VM.name = ‘hxx’的可能。 相關代碼以下:

function proxy (vm, key) {
  if (!isReserved(key)) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return vm._data[key]
      },
      set: function proxySetter (val) {
        vm._data[key] = val;
      }
    });
  }
}

 

表面上看起來咱們是在操做VM.name,實際上仍是經過Object.defineProperty()中的get和set方法劫持實現的。

總結

Vue框架很好的利用了Object.defineProperty()這個方法來實現了數據的雙向綁定,同時也達到了很好的模塊間解耦,在平常開發中,你也能夠用好這個方法來優化對象獲取和修改屬性方式,或者本身實現一個MVVM的雙向數據綁定等。

 

本篇文章是我對Vue的淺薄之悟,若有理解不足之處,還請你們批評指正,Thank you ~

 

本文來自網易雲社區,經做者黎浩梁受權發佈。

原文:Vue框架核心之數據劫持

 

新用戶大禮包:https://www.163yun.com/gift

 

更多網易研發、產品、運營經驗分享請訪問網易雲社區

相關文章
相關標籤/搜索