1. 前置知识

Vue.js是通过数据劫持 + 发布者-订阅者模式来实现响应式功能的。

1.1. Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

通过Object.defineProperty()给某个属性设置get和set就可以劫持该属性的读写过程,具体的api使用我们不展开(MDN传送门),现在使用Object.defineProperty写个简单的小demo,来劫持一个对象的属性赋值和取值过程👇

function Store() {
    let data = null;
    Object.defineProperty(this, 'data', {
        get: function() {
            console.log('拦截到了,你在读取数据'); // 拦截到了,你在读取数据
            return data;
        },
        set: function(val) {
            console.log('拦截到了,你在设置数据'); // 拦截到了,你在设置数据
            data = val;
        }
    });
}

let store = new Store();
store.data = 'Hi';
console.log(store.data) // Hi

1.1.1. Object.defineProperty的局限

  • Object.defineProperty只能劫持对象的某一个属性,当一个对象有嵌套的对象时,只能通过通过递归去给每一个属性设置拦截
  • Object.defineProperty并不能劫持Array.prototype上的方法。比如还是上面那个例子,当data是个数组的时候,对data调用pushpop等数组原型上的方法的时候,并不会触发setter:
function Store() {
    let list = [];
    Object.defineProperty(this, 'list', {
        get: function() {
            console.log('拦截到了,你在读取数据'); // 拦截到了,你在读取数据
            return list;
        },
        set: function(val) {
            console.log('⚠️push操作并没有被拦截到');
            list = val;
        }
    });
}

let store = new Store();
store.list.push(1);
console.log(store.list); // [1]

1.1.2. 如何劫持数组上的方法

既然setter里面拦截不到原型链上的方法,那就针对数组原型链上的需要被拦截的方法进行二次封装。

function newArrMethod() {
     // 先把数组上的方法复制一遍
    let arrExtend = Object.create(Array.prototype);
    // 然后把需要改写的方法重新封装,比如下面对push方法进行重新封装
    let newPushMethod = function(...args) {
        console.log('在这里做拦截');
        Array.prototype.push.apply(this, args);
    }
    arrExtend.push = newPushMethod; 
    return arrExtend;
}

let arr = [];
arr.__proto__ = newArrMethod();
arr.push(1); // arr的push方法被劫持到了

1.2. Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)

ProxyMDN传送门)字面意思是代理,可以理解成,在目标对象之前架设一层 "拦截",当外界对该对象访问的时候,都必须经过这层拦截,因此直接使用这个api就能够达到对象劫持的效果,vue2.*之所以不用Proxy而使用Object.defineProperty是因为Proxy是一个比较新的特性,考虑到浏览器的兼容性问题,所以没有被尤大使用。在vue3.0已经把Object.defineProperty替换为proxy,下面使用proxy来写一个劫持👇

let data = {};
data = new Proxy(data, {
    get: function(target, key) {
        console.log('拦截到了,你在读取数据');
        return target[key];
    },
    set: function(target, key, val) {
        console.log('拦截到了,你在设置数据');
        target[key] = val;
    }
})
data.name = '小明';
console.log(data.name);

1.3. 发布-订阅者模式

Vue.js里面的依赖收集是通过发布-订阅模式来实现的,这种设计模式在前端经常使用,比如document.addEventListener就是很典型发布-订阅者应用。一般来说发布-订阅模式有这几个要素:

  • 有一个指定的发布者
  • 发布者有一个缓存列表,里面存有所有订阅者的回调函数
  • 发布消息的时候,发布者遍历缓存列表,依次触发订阅者的回调

👇下面实现一个典型的发布订阅者模式

class Event {
  constructor() {
    this.subscriberList = [];
  }
  listen(key, fn) {
    if (!this.subscriberList[key]) {
      this.subscriberList[key] = [];
    }
    this.subscriberList[key].push(fn);
  }
  notify() {
    let key = Array.prototype.shift.call(arguments);
    let fns = this.subscriberList[key];
    fns && fns.forEach(fn => fn.apply(this, arguments));
  }
  remove(key, fn) {
    let fns = this.subscriberList[key];
    if (fns) {
      for (let i = fns.length - 1; i >= 0; i--) {
        let _fn = fns[i];
        if (_fn == fn) {
          fns.splice(i, 1);
        }
      }
    }
  }
}
let event = new Event();
event.listen("sayHi", function(name) {
  console.log("Hi," + name);
});
event.listen("sayHallo", function(name) {
  console.log("Hallo," + name);
});
event.notify("sayHi", "小明");
event.notify("sayHallo", "小红");

1.4. 创建一个纯净的对象

let obj1 = {};
let obj2 = Object.create(null);

console.log(obj1 instanceof Object) // true
console.log(obj1.__proto__) // {}

console.log(obj2 instanceof Object) // false
console.log(obj2.__proto__) // undefined

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。在上面的例子中创建了两个对象,其中obj1有原型链,obj2则是一个原型为null的空对象,它的原型链是纯净的。其实在很多框架源码里面我们都能找到Object.create(null)这种初始对象的写法,究竟是用{}还是Object.create(null)可以这样考量:

  • 当你需要一个非常干净的字典对象的时候
  • 当你需要自定义对象原型链上的方法的时候,不用担心覆盖其他对象的原型 上面两种情况都应该使用Object.create(null),其他情况还是使用{}
powered by Gitbook该文件修订时间: 2020-01-21 15:05:09

results matching ""

    No results matching ""