为Vue3.0作铺垫之defineProperty & Proxy

前言

最近看代码发现Object.defineProperty和Proxy这两个东西出现的比较频繁,包括Vue源码,包括即将到来的vue3,包括一些库,因此咱们这里将这两个东西抽取出来作一个规整。
本篇参考MDN。虽然是在炒现饭,更多的是本身养成写blog的习惯。javascript

Object.defineProperty

语法:

Object.defineProperty(obj,prop,descriptor)vue

参数:

obj -> 须要劫持的对象
prop -> 劫持的属性
descriptor -> 属性描述符java

介绍:

obj和prop咱们没必要多说,就像定义一个对象同样,键值对使用。咱们主要重点介绍一下descriptor:react

数据描述符

value

表示该属性对应的值,能够是任意js值,默认为undefinedgit

writable

表示该属性是否可写,只有为true的时候该属性才能被赋值运算符改变,默认为falsegithub

存取描述符

get
一个给属性提供getter的方法,默认为undefined。
当使用obj.xxx时将会调用该方法,并将返回值做为取得的值,该方法不会传参。
this指向的是被定义的对象(obj)
复制代码
set
一个给属性提供setter的方法,默认为undefined。  
当对属性赋值时会触发该函数。 
该函数传入惟一一个参数,就是newVal
复制代码

公共描述符

上述两个描述符是互斥的,若是你定义了get又定义了value,将会报错,而公共描述符是指能够为该属性定义公共的描述符数组

enumerable
只有该属性enumerable为true时该属性才会出如今对象的可枚举属性中。
默认为false。
(可枚举属性决定了该属性是否会被for in循环找到。 
for in会找到继承的可枚举属性,想要找到自身的用Object.keys)
复制代码
configurable
只有该属性的configurable为true时,该属性才能修改描述符,才能使用delete删除该属性值。  
不然删除会返回false并删除失败,默认为false复制代码
ps:以上这些描述符不必定指自身属性,继承来的属性也须要考虑在内,因此须要经过Object.create(null)建立一个原型指向null的对象做为继承对象。
复制代码

做用

按照原理来讲他是做为一个拦截层同样,拦截对象属性的get或者set或者value,好比Vue中的对响应式数据的建立。浏览器

Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      // 在编译模板时会触发属性的get方法,将依赖添加到dep里
      get: function reactiveGetter() {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      // 在设置值时 dep.notify将当前全部依赖触发更新
      set: function reactiveSetter(newVal) {
        var value = getter ? getter.call(obj) : val;
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        if ("development" !== 'production' && customSetter) {
          customSetter();
        }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
复制代码

proxy

语法

let p = new Proxy(target,handler)bash

参数

target

使用proxy包装的目标对象(能够是任意类型的对象,包括原生数组,函数甚至是另外一个代理)app

handler

一个对象,操做代理时的函数

示例

  1. get
let handler = {
  // 两个参数 target,name 对应obj 和 key
  // 此处代理了obj的get方法,当调用get不存在时返回默认值default
  get: function (target, name) {
    return target[name] ? target[name] : 'default'
  }
}
let obj = {}
let objProxy = new Proxy(obj, handler)
obj.a = 1
obj.b = 2
console.log(objProxy.a,objProxy.b,objProxy.c)
复制代码
  1. set
let handler = {
  // 与get不同的是,set多了一个value值,是指你新设置的值
  set: function (target, name, value){
    if (name === 'age') {
      if (!Number.isInteger(value)){
        throw new Error('age must be a Number')
      } else if (value > 100) {
        throw new Error('age cant over then 1000')
      }
    }
    target[name] = value
  }
}
let setProxy = new Proxy({}, handler)
setProxy.age = '1'
复制代码
  1. 扩展构造函数
function extend(sup,base) {
  // 获取base下原有的descriptor
  var descriptor = Object.getOwnPropertyDescriptor(
    base.prototype,"constructor"
  );
  base.prototype = Object.create(sup.prototype);
  var handler = {
    // 拦截new指令
    construct: function(target, args) {
      // 此时base已经链接到sup原型上了
      var obj = Object.create(base.prototype);
      // apply方法也被拦截了
      this.apply(target,obj,args);
      return obj;
    },
    apply: function(target, that, args) {
      // 这个that指向的是base
      sup.apply(that,args);
      base.apply(that,args);
    }
  };
  var proxy = new Proxy(base,handler);
  descriptor.value = proxy;
  Object.defineProperty(base.prototype, "constructor", descriptor);
  return proxy;
}

var Person = function(name){
  this.name = name
};

var Boy = extend(Person, function(name, age) {
  this.age = age;
});

Boy.prototype.sex = "M";

var Peter = new Boy("Peter", 13);
console.log(Peter.sex);  // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age);  // 13
复制代码
  1. 查找数组特定对象
var arr = [
  { name: 'Firefox', type: 'browser' },
  { name: 'SeaMonkey', type: 'browser' },
  { name: 'Thunderbird', type: 'mailer' }
]
// 给定数组,想经过name,下标,type不一样方式查找
let products = new Proxy(arr,{
  get: function(target, key) {
    let types = {}
    let result
    if (Number.isInteger(+key)){
      return target[key]
    } else {
      for (item of target) {
       
        if (item.name === key) {
          result = item
        }
        if (types[item.type]){
          types[item.type].push(item)
        } else {
          types[item.type] = [item]
        }
      }
    }
    if (result) {
      return result
    }
    if (key === 'types') {
      return types
    }
    if (key === 'number') {
      return target.length
    }
    if (key in types) { 
      return types[key]
    }
    
  }
})
复制代码

固然Proxy能够劫持的属性多达13种,咱们这里只是作一个简单的介绍

对比

proxy是即将到来的vue3代替Object.definePrototype的实现,至于为何要用proxy代替咱们大概能够阐述出如下几个观点:
1.

proxy劫持的是整个对象,而不须要对对象的每个属性进行拦截。  
这样将减小以前对于为了实现总体对象响应式而递归对对象每个属性进行拦截的操做,大大优化了性能
复制代码
对于defineProperty有一个致命的弱点,就是他没有办法监听数组的变化。  
为了解决这个问题,vue在底层对数组的方法进行了hack,监听了每一次数组特定的操做,并为操做后的数组实现响应式。
复制代码
methodsToPatch.forEach(function(method) {
    // cache original method
    // 获取原方法
    var original = arrayProto[method];
    // def方法从新定义arrayMethods的method方法,而后将新的取值方法赋值
    def(arrayMethods, method, function mutator() {
      var args = [],
        len = arguments.length;
      while (len--) args[len] = arguments[len];
      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          // [].push(1),[].unshift(1)
          // arg = [1]
          inserted = args;
          break
        case 'splice':
          // [1,2,3].splice(0,1,1)
          // 第三个参数为插入的值
          inserted = args.slice(2);
          break
      }
      if (inserted) { ob.observeArray(inserted); }
      // 若是是插入操做则对插入的数组进行响应式观察
      // 其余操做将手动触发一次响应收集
      // notify change
      ob.dep.notify();
      return result
    });
  });
复制代码

虽然在vue底层对数组进行了hack,因为defineProperty是没有办法进行监听数组角标而致使的变化的,迫不得已下只能提供了一个$set方法进行响应收集,而在proxy里是不存在这个问题的。

let arr = [1,2,3]
let arr1 = new Proxy(arr,{
	set:function(target,key,newVal) {
        target[key] = newVal
		console.log(1)
	}
})
arr1[0] = 2 // 1 arr1 = [2,2,3]
复制代码

兼容

虽然proxy很好用,可是他存在最大的问题就是兼容性,根据MDN所给出的兼容来看,对于edge如下的全部ie浏览器都不支持(MDN浏览器兼容)。可是当初Vue刚出来的时候defineProperty实际上也是存在兼容问题的,实践证实优秀的东西是不会被淘汰的。 拒绝IE从你我作起。

后记

若是文章出现问题欢迎小伙伴一块儿指出,共同进步~
该篇文章收录到个人github中,有兴趣小伙伴能够给个star,近期将对文档库作一个规整~ 最后求一个深圳内推 130985264@qq.com

相关文章
相关标签/搜索