Vue 數據劫持

什麼是數據劫持?

定義: 數據劫持,指的是在訪問或者修改對象的某個屬性時,經過一段代碼攔截這個行爲,進行額外的操做或者修改返回結果。react

簡單地說,就是當咱們 觸發函數的時候 動一些手腳作點咱們本身想作的事情,也就是所謂的 "劫持"操做git

數據劫持的兩種方案:

  • Object.defineProperty
  • Proxy

1).Object.defineProperty

  • 語法:

Object.defineProperty(obj,prop,descriptor)數組

  • 參數:緩存

    • obj:目標對象
    • prop:須要定義的屬性或方法的名稱
    • descriptor:目標屬性所擁有的特性
  • 可供定義的特性列表:bash

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

例子

在Vue中其實就是經過Object.defineProperty來劫持對象屬性的settergetter操做,並「種下」一個監聽器,當數據發生變化的時候發出通知,以下:app

var data = {name:'test'}
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這個方法,具體用在哪裏而且它又解決了哪些問題,下面就簡單的說一下:dom

監聽對象屬性的變化

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

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep()//建立訂閱對象  
  const property = Object.getOwnPropertyDe述  //屬性的描述特性裏面若是configurable爲false則屬性的任何修改將無效  
  if (property && property.configurable === false) { return }scriptor(obj, key)//獲取obj對象的key屬性的描
    // 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//返回屬性值    
    },
    setfunction 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方便更好的理解:

let arrayMethod = Object.create(Array.prototype);
['push','shift'].forEach(function(method){
    Object.defineProperty(arrayMethod,method,{
        value:function(){
            let i = arguments.length
            let args = new Array(i)
            while (i--) {
              args[i] = arguments[i]
            }
            let original = Array.prototype[method];
            let result = original.apply(this,args);
            console.log("已經控制了,哈哈");
            return result;
        },
        enumerable: true,
        writable: true,
        configurable: true    
    })
})
let 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]
      },
      setfunction proxySetter (val) {
        vm._data[key] = val;
      }
    });
  }
}
複製代碼

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

Object.defineProperty()的缺點

1).不能監聽數組的變化

let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj, 'arr', {
  get () {
    console.log('get arr')
    return arr
  },
  set (newVal) {
    console.log('set', newVal)
    arr = newVal
  }
})
obj.arr.push(4) // 只會打印 get arr, 不會打印 set
obj.arr = [1,2,3,4] // 這個能正常 set
複製代碼

數組的如下幾個方法不會觸發 set: pushpopshiftunshiftsplicesortreverse

Vue 把這些方法定義爲變異方法 (mutation method),指的是會修改原來數組的方法。與之對應則是非變異方法 (non-mutating method),例如 filter, concat, slice 等,它們都不會修改原始數組,而會返回一個新的數組。

2).必須遍歷對象的每一個屬性

使用 Object.defineProperty() 多數要配合 Object.keys() 和遍歷,因而多了一層嵌套。如:

Object.keys(obj).forEach(key => {
  Object.defineProperty(obj, key, {
    // ...
  })
})
複製代碼

3).必須深層遍歷嵌套的對象

所謂的嵌套對象,是指相似

let obj = {
  info: {
    name: 'eason'
  }
}
複製代碼

若是是這一類嵌套對象,那就必須逐層遍歷,直到把每一個對象的每一個屬性都調用 Object.defineProperty() 爲止。

給出完整版的數據劫持代碼:

const arrayProto = Array.prototype;// 獲得原型上的方法
const proto = Object.create(arrayProto)  // 複製一份原型上的方法
    ;['push', 'shift', 'pop', 'splice'].forEach(method => {
        // console.log(method)
        // 重寫'push','shift','pop','splice',固然也能夠多加幾個方法,想加什麼就加什麼
        proto[method] = function (...args) {
            // console.log(this)  // [ 1, 2, 3, { age: [Getter/Setter] } ]
            updateView();
            arrayProto[method].call(this, ...args)
        }
    })
function updateView() {
    console.log("更新視圖成功了...")
}
function observer(obj) {
    if (typeof obj !== "object" || obj == null) {
        return obj
    }
    if (Array.isArray(obj)) {
        // 若是是一個數組要重寫數組上原型上的方法 
        Object.setPrototypeOf(obj, proto)
        for (let i = 0; i < obj.length; i++) {
            let item = obj[i];
            observer(item)
        }
    } else {
        for (let key in obj) {
            definedReactive(obj, key, obj[key])
        }
    }
}

function definedReactive(obj, key, value) {
    observer(value)
    Object.defineProperty(obj, key, {
        get() {
            console.log("獲取數據成功了...")
            return value;
        },
        set(newValue) {
            if (value !== newValue) {
                observer(newValue)
                value = newValue;
                updateView();
            }
        }
    })
}

let data = { name: [1, 2, 3, { age: 888 }] }
observer(data)
// 數據改變了
// data.name[3].age = 666;  
// push shift unshift pop 也能改變數組中的數組
data.name.push({ address: "xxx" })  // 目的是:更新視圖

// 思路:重寫Push方法  這些方法在Array的原型上
//      不要把Array原型上的方法直接重寫了
//      先把原型上的方法copy一份,去重寫(加上視圖更新的操做)
//      再去調用最原始的push方法

複製代碼

接下來講一下Object.defineProperty()的升級版 Proxy

2).Proxy數據代理

在數據劫持這個問題上,Proxy 能夠被認爲是 Object.defineProperty() 的升級版。外界對某個對象的訪問,都必須通過這層攔截。所以它是針對 整個對象,而不是 對象的某個屬性。

proxy即代理的意思。我的理解,創建一個proxy代理對象(Proxy的實例),接受你要監聽的對象和監聽它的handle兩個參數。當你要監聽的對象發生任何改變,都會被proxy代理攔截來知足需求。

var arr = [1,2,3]
var handle = {
    //target目標對象 key屬性名 receiver實際接受的對象
    get(target,key,receiver) {
        console.log(`get ${key}`)
        // Reflect至關於映射到目標對象上
        return Reflect.get(target,key,receiver)
    },
    set(target,key,value,receiver) {
        console.log(`set ${key}`)
        return Reflect.set(target,key,value,receiver)
    }
}
//arr要攔截的對象,handle定義攔截行爲
var proxy = new Proxy(arr,handle)
proxy.push(4) //能夠翻到控制檯測試一下會打印出什麼
複製代碼

優勢:
1.使用proxy能夠解決defineProperty不能監聽數組的問題,避免重寫數組方法;
2.不須要再遍歷key
3.Proxy handle的攔截處理器除了getset外還支持多種攔截方式。
4.嵌套查詢。實際上proxy get()也是不支持嵌套查詢的。解決方法:

let handler = {
  get (target, key, receiver) {
    // 遞歸建立並返回
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return Reflect.get(target, key, receiver)
  }
}
複製代碼

依賴管理方案

說完了上面的,簡單說一下 依賴管理方案

Object.defineProperty 只是解決了狀態變動後,如何觸發通知的問題,那要通知誰呢?誰會關心那些屬性發生了變化呢?在 Vue 中,使用 Dep 解耦了依賴者與被依賴者之間關係的肯定過程。簡單來講:

  • 第一步,經過 Observer 提供的接口,遍歷狀態對象,給對象的每一個屬性、子屬性都綁定了一個專用的 Dep 對象。這裏的狀態對象主要指組件當中的data屬性。
  • 第二步,建立三中類型的watcher
    1.調用 initComputedcomputed 屬性轉化爲 watcher 實例
    2.調用 initWatch 方法,將watch 配置轉化爲 watcher 實例
    3.調用 mountComponent 方法,爲 render 函數綁定 watcher 實例
  • 第三步,狀態變動後,觸發 dep.notify() 函數,該函數再進一步觸發 Watcher 對象 update 函數,執行watcher的從新計算。

對應下圖:

圖片加載失敗!

注意,Vue 組件中的 render 函數,咱們能夠單純將其視爲一種特殊的 computed 函數,在它所對應的 Watcher 對象發生變化時,觸發執行render,生成新的 virutal-dom 結構,再交由 Vue 作diff,更新視圖。

OK 本章就到此了!


^_<

相關文章
相關標籤/搜索