1. 程式人生 > 實用技巧 >Vue中陣列變動監聽

Vue中陣列變動監聽

Vue中陣列變動監聽

Vue的通過資料劫持的方式實現資料的雙向繫結,即使用Object.defineProperty()來實現對屬性的劫持,但是Object.defineProperty()中的setter是無法直接實現陣列中值的改變的劫持行為的,想要實現對於陣列下標直接訪問的劫持需要使用索引對每一個值進行劫持,但是在Vue中考慮效能問題並未採用這種方式,所以需要特殊處理陣列的變動。

描述

Vue是通過資料劫持的方式來實現資料雙向資料繫結的,其中最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,該方法允許精確地新增或修改物件的屬性,對資料新增屬性描述符中的getter

setter存取描述符實現劫持。

var obj = { __x: 1 };
Object.defineProperty(obj, "x", {
    set: function(x){ console.log("watch"); this.__x = x; },
    get: function(){ return this.__x; }
});
obj.x = 11; // watch
console.log(obj.x); // 11

而如果當劫持的值為陣列且直接根據下標處理陣列中的值時,Object.defineProperty()中的setter是無法直接實現陣列中值的改變的劫持行為的,所以需要特殊處理陣列的變動,當然我們可以對於陣列中每一個值進行迴圈然後通過索引同樣使用Object.defineProperty()

進行劫持,但是在Vue中尤大解釋說是由於效能代價和獲得的使用者體驗收益不成正比,所以並沒有使用這種方式使下標訪問實現響應式,具體可以參閱githubVue原始碼的#8562

var obj = { __x: [1, 2, 3] };
Object.defineProperty(obj, "x", {
    set: function(x){ console.log("watch"); this.__x = x; },
    get: function(){ return this.__x; }
});
obj.x[0] = 11;
console.log(obj.x); // [11, 2, 3]
obj.x = [1, 2, 3, 4, 5, 6]; // watch
console.log(obj.x); // [1, 2, 3, 4, 5, 6]
obj.x.push(7);
console.log(obj.x); // [1, 2, 3, 4, 5, 6, 7]
// 通過下標對每一個值進行劫持
var obj = { __x: [1, 2, 3] };
Object.defineProperty(obj, "x", {
    set: function(x){ console.log("watch"); this.__x = x; },
    get: function(){ return this.__x; }
});
obj.x.forEach((v, i) => {
    Object.defineProperty(obj.x, i,{
        set:function(x) { console.log("watch"); v = x; },
        get: function(){ return v; }
    })
})
obj.x[0] = 11; // watch
console.log(obj.x); // [11, 2, 3]

Vue中對於資料是經過特殊處理的,對於下標直接訪問的修改同樣是不能觸發setter,但是對於push等方法都進行了重寫。

<!DOCTYPE html>
<html>
<head>
    <title>Vue中陣列變動監聽</title>
</head>
<body>
    <div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data: {
            msg: [1, 2, 3]
        },
        template:`
            <div>
                <div v-for="item in msg" :key="item">{{item}}</div>
                <button @click="subscript">subscript</button>
                <button @click="push">push</button>
            </div>
        `,
        methods:{
            subscript: function(){
                this.msg[0] = 11;
                console.log(this.msg); // [11, 2, 3, __ob__: Observer]
            },
            push: function(){
                this.msg.push(4, 5, 6);
                console.log(this.msg); // [1, 2, 3, 4, 5, 6, __ob__: Observer]
            }
        }
    })
</script>
</html>

Vue中具體的重寫方案是通過原型鏈來完成的,具體是通過Object.create方法建立一個新物件,使用傳入的物件來作為新建立的物件的__proto__,之後對於特定的方法去攔截對陣列的操作,從而實現對運算元組這個行為的監聽。

// dev/src/core/observer/array.js
/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

處理方法

重賦值

Object.defineProperty()方法無法劫持對於陣列值下標方式訪問的值的改變,這樣的話就需要避免這種訪問,可以採用修改後再賦值的方式,也可以採用陣列中的一些方法去形成一個新陣列,陣列中不改變原陣列並返回一個新陣列的方法有sliceconcat等方法以及spread操作符,當然也可以使用map方法生成新陣列,此外在Vue中由於重寫了splice方法,也可以使用splice方法進行檢視的更新。

var obj = { __x: [1, 2, 3] };
Object.defineProperty(obj, "x", {
    set: function(x){ console.log("watch"); this.__x = x; },
    get: function(){ return this.__x; }
});
obj.x[0] = 11;
obj.x = obj.x; // watch
console.log(obj.x); // [11, 2, 3]

Proxy

Vue3.0使用Proxy實現資料劫持,Object.defineProperty只能監聽屬性,而Proxy能監聽整個物件,通過呼叫new Proxy(),可以建立一個代理用來替代另一個物件被稱為目標,這個代理對目標物件進行了虛擬,因此該代理與該目標物件表面上可以被當作同一個物件來對待。代理允許攔截在目標物件上的底層操作,而這原本是Js引擎的內部能力,攔截行為使用了一個能夠響應特定操作的函式,即通過Proxy去對一個物件進行代理之後,我們將得到一個和被代理物件幾乎完全一樣的物件,並且可以從底層實現對這個物件進行完全的監控。

var target = [1, 2, 3];
var proxy = new Proxy(target, {
    set: function(target, key, value, receiver){ 
        console.log("watch");
        return Reflect.set(target, key, value, receiver);
    },
    get: function(target, key, receiver){ 
        return target[key];
    }
});
proxy[0] = 11; // watch
console.log(target); // [11, 2, 3]

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://zhuanlan.zhihu.com/p/50547367
https://juejin.im/post/6844903699425263629
https://juejin.im/post/6844903597591773198
https://segmentfault.com/a/1190000015783546
https://cloud.tencent.com/developer/article/1607061
https://www.cnblogs.com/tugenhua0707/p/11754291.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy