vue3 源码解析学习笔记--- 初识Proxy

什么是Proxy及其语法

Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:

var proxy = new Proxy(target, handler);

handler常用的方法

方法 描述
handler.has() in 操作符的捕捉器。
handler.get() 属性读取操作的捕捉器。
handler.set() 属性设置操作的捕捉器。
handler.deleteProperty() delete 操作符的捕捉器。
handler.ownKeys() Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
handler.apply() 函数调用操作的捕捉器。
handler.construct() new 操作符的捕捉器

举个栗子:

const originObj = {}
const proxyObj = new Proxy(originObj,{
  get: function (target, propKey, receiver) {
      return 10
   }
});
// 代理只会对proxy对象生效,如上方的origin就没有任何效果
proxyObj.a // 10;
proxyObj.b // 10;
originObj.a //  undefined
origin.b // undefined
// 使用 has 方法隐藏某些属性,不被 in 运算符发现
var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
console.log('_prop' in proxy); // false
// 又比如说 apply 方法拦截函数的调用、call 和 apply 操作。
// apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组,
var target = function () { return 'I am the target'; };
var handler = {
  apply: function () {
    return 'I am the proxy';
  }
};
var p = new Proxy(target, handler);
p();
// "I am the proxy"
//  ownKeys 方法可以拦截对象自身属性的读取操作
let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

理解defineProperty

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

这个定义说明了defineProperty只能针对修改对象上的属性

语法

  • obj 要定义属性的对象
  • prop 要定义或修改的属性的名称或 Symbol
  • descriptor 要定义或修改的属性描述符
Object.defineProperty(obj, prop, descriptor)

举个例子

const obj = {}
Object.defineProperty(obj, "a", {
  value : 1,
  writable : false, // 是否可写 
  configurable : false, // 是否可配置
  enumerable : false // 是否可枚举
})

// 上面给了三个false, 下面的相关操作就很容易理解了
obj.a = 2 // 无效
delete obj.a // 无效
for(key in obj){
  console.log(key) // 无效 
}

vue 2.0 数据双向绑定的原理

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
      if (Dep.target) {
        // 收集依赖
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // ...
      // 通知视图更新
      dep.notify()
    }
  })

对象的变异

以下的方法为什么不更新

data  () {
  return  {
    obj: {
      a: 1
    }
  }
}

methods: {
  update () {
    this.obj.b = 2
  }
}

这个我们现在就很容易理解了,因为在created的时候,我们进行了data init 方法,会对data绑定一个观察者 Observer,之后 data 中的字段更新都会通知依赖收集器Dep触发视图更新,当我们使用defineProperty 这个方法只是针对对象的属性进行监听,但是新增属性b是在之后添加的,没有相应的Observer。当然我们有很多方法去解决这个方法:
vue 全局set方法本质就是手动为新增属性添加Observer方法

// 源码位置 https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L201
function set (target: Array<any> | Object, key: any, val: any): any {
  // ....
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

数组的变异

var vm = new Vue({
  data: {
    items: ['1', '2', '3']
  }
})
vm.items[1] = '4' // 视图并未更新
vm.items.length = 6 // 视图并未更新

defineProperty 是否能监听到以上的数组变化,
例子如下:

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
      get: function defineGet() {
        console.log(`get key: ${key} val: ${val}`);
        return val;
      },
      set: function defineSet(newVal) {
        console.log(`set key: ${key} val: ${newVal}`);
        val = newVal;
      }
  })
}

function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  })
}

let test = [1, 2, 3];

observe(test);

test[0] = 4 // set key: 0 val: 4

// 以上的例子可以说明这不是defineProperty的锅,但是我们打印test

console.log(test)
(3) [(...), (...), (...)]

将鼠标悬浮于三个点上时,会提示 The property is computed with getter

所以基于性能问题不进行监听,而且新增索引的确是 defineProperty 做不到的
所以vue提供了数组的变异方法

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

methodsToPatch.forEach(function (method) {
  // 缓存原生数组
  const original = arrayProto[method]
  // def使用Object.defineProperty重新定义属性
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args) // 调用原生数组的方法

    const ob = this.__ob__  // ob就是observe实例observe才能响应式
    let inserted
    switch (method) {
      // push和unshift方法会增加数组的索引,但是新增的索引位需要手动observe的
      case 'push':
      case 'unshift':
        inserted = args
        break
      // 同理,splice的第三个参数,为新增的值,也需要手动observe
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 其余的方法都是在原有的索引上更新,初始化的时候已经observe过了
    if (inserted) ob.observeArray(inserted)
    // dep通知所有的订阅者触发回调
    ob.dep.notify()
    return result
  })
})
posted @ 2022-03-10 16:45  自在一方  阅读(166)  评论(0编辑  收藏  举报