Skip to content
On this page

熟悉 Proxy 及其场景

概述

要想熟悉 Vue 3 源码,熟悉 Proxy 特性必不可少,本文主要内容:

  • proxy 概念
  • 相关 API
  • proxy 实现双向绑定
  • 遇到的一些问题

基于 javascript 的复杂数据类型的特点,衍生出的代理的概念,因为对于复杂的数据类型,变量存储的是引用。代理 proxy 就在引用和值之间。

另外还需要注意 Reflect,它拥有的13个方法与 proxy 一致,用来代替 Object 的默认行为。很显然,例如我们用 proxy 修改了对象属性的 getter,那如何使用原本默认行为?

例如以下代码会陷入死循环:

js
const obj = { name: '' }
new Proxy(obj, {
  get: function (target, prop) {
    // 错误的
    return target[prop]
    // 应该使用 Reflect 来得到默认行为
    return Reflect.get(target, prop)
  } 
})

相比于 Object.defineProperty ,Proxy 有 polyfill 可以 hack,兼容性会更好。

熟悉 13 个 API

  • get / set

和对象描述符中访问器属性 get/set 一样,一般在使用 Object.defineProperty 时重新定义对象属性的描述符。

js
{
  get: function (target, property, receiver) {
    return Reflect.get(target, property)
  },
  set:  function (target, property, value, receiver) {
    // 必须返回一个 Boolean
    return Reflect.set(target, property, value)
  }
}

关于 receiver,一般情况下 receiver === proxy 实例 即原对象的代理对象,例如:

js
const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    console.log(receiver === proxy)
    return receiver
  }
})

⚠️当 proxy 为一个对象的原型时,receiver 就不是 proxy 实例了,而是该对象本身。

js
const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    // 不要试图在这里获取 receiver,否则会造成死循环
    // 因为 receiver === d
    // console.log(target, receiver)
    console.log(receiver.name) // ym
    return receiver
  }
})

const d = {
  name: 'ym'
}
d.__proto__ = proxy
// const d = Object.create(proxy);
console.log(d.a === d) // true

其实也不用想那么多,receiver 就是调用对象本身,而 target 是设置 Proxy 的对象。

  • apply/construct

apply用于拦截函数调用,construct方法用于拦截new命令。

js
{
  // newTarget 和 receiver 类似
  // 这里就只有一种情况,是 proxy 的实例
  construct: function (target, args, newTarget) {
    // 必须返回一个对象
    return new target(...args)
  },
  // ctx 是函数调用的上下文
  apply: function (target, ctx, args) {
  }
}
  • has/deleteProperty

它们对应于 in/delete,将这些操作变成函数行为。

剩下一些 API 相对简单,使用时可以查看 MDN 文档,没必要刻意记忆。

  • defineProperty/ownKeys
  • getPrototypeOf/setPrototypeOf
  • isExtensible/preventExtensions
  • getOwnPropertyDescriptor

实现双向绑定

响应式的三要素:模版、观察者、事件中心。

以下是使用的例子:

js
<template>
  <div id="app"></div>
</template>

<script>
new M({
  template: `<div><p>输入框的值:{{ name }}</p><input type="text" v-model="name"></div>`,
  data () {
    return {
      name: ''
    }
  }
}).mount('#app')
</script>

我们 M 类的实现:

js
function M(opts) {
  // 为 data 设置代理
  this.data = observe(opts.data())
  // 得到模版节点
  this.node = getNodes(opts.template)
  // 解析模版节点
  this.compileElement(this.node)
}

M.prototype.mount = function (selector) {
 document.querySelector(selector).appendChild(this.node)
}

M.prototype.compileElement = function (node) {
  // 递归处理dom节点
  Array.from(node.childNodes).forEach(node => {
    let text = node.textContent
    let reg = /\{\{(.*)\}\}/

    if (node.nodeType === 1) {
      this.compile(node)
    } else if (node.nodeType === 3 && reg.test(text)) {
      this.compileText(node, reg.exec(text)[1])
    }

    if (node.childNodes && node.childNodes.length > 0) {
      this.compileElement(node)
    }
  })
}

// 处理文本节点
M.prototype.compileText = function (node, expression) {
  let reg = /\{\{.*\}\}/
  expression = expression.trim()
  let value = this.data[expression]
  const oldText = node.textContent
  value = typeof value === 'undefined' ? '' : value
  node.textContent = oldText.replace(reg, value)

  // 添加事件处理
  add(expression, (value, oldValue) => {
    console.log(value, oldValue)
    value = typeof value === 'undefined' ? '' : value
    node.textContent = oldText.replace(reg, value)
  })
}

// 简单的处理 v-model
M.prototype.compile = function (node) {
  Array.from(node.attributes).forEach(attr => {
    if (attr.name === 'v-model') {
      node.value = this.data[attr.value]
      node.addEventListener('input', e => {
        this.data[attr.value] = e.target.value
      })
      node.removeAttribute(attr.name)
    }
  })
  return node
}

简单的观察者与事件中心:

js
// 简单的事件中心
const observeMap = {}
function add(k, cb) {
  observeMap[k] = cb
}

// 观察者,我们使用 proxy
function observe(tar) {
  const handler = {
    get: function (target, property, receiver) {
      return Reflect.get(target, property)
    },
    set: function (target, property, value, receiver) {
      const oldValue = Reflect.get(target, property)
      const setResult = Reflect.set(target, property, value)
      // 只是简单的处理下存在与否的判断
      if (observeMap[property]) {
        Reflect.apply(observeMap[property], receiver, [value, oldValue])
      }
      return setResult
    }
  }
  return new Proxy(tar, handler)
}

function getNodes(str) {
  const div = document.createElement('div')
  div.innerHTML = str
  return div.childNodes[0]
}

问题

Proxy 本身的特性所带来的问题

1. 对于嵌套的对象,只会代理第一层

var obj = { a: { b: 3 } }
// var arr = [1]
const proxy = new Proxy(obj, {
  get: function(target, prop, receiver) {
    console.log('get', prop)
    return Reflect.get(target, prop)
  },
  set: function(target, prop, value, receiver) {
    console.log('set', prop, value)
    return Reflect.set(target, prop, value)
  }
})
// proxy.push(2)
proxy.a.b = 1

// 只会打印 get a

所以如果我们要实现代理嵌套对象需要做递归处理。

2. 由于数组本身操作的特点,proxy 的 get 和 set 会被多次触发。

var arr = [1]
const proxy = new Proxy(arr, {
  get: function(target, prop, receiver) {
    console.log('get', prop)
    return Reflect.get(target, prop)
  },
  set: function(target, prop, value, receiver) {
    console.log('set', prop, value)
    return Reflect.set(target, prop, value)
  }
})
proxy.push(2)
// get push
// get length
// set 1 2
// set length 2

对于 push 操作,会先去get push 方法,再 set 数组下标 1 的值为 2,然后修改数组 length 属性值为 2,最后返回数组的长度 length。

也就是说,对于代理数组对象去触发回调需要考虑到触发的类型。

遍历对象属性的方法有哪些?以及它们的区别

在 es5 中,我们常用的获取对象属性的方式:

  • in 操作符
  • for ... in
  • Object.keys
  • Object.getOwnPropertyNames()

在熟悉了 Reflect 之后,我们要使用新的方式 Reflect.ownKeys()

⚠️它们的区别在于是否自身拥有这个属性、或者该属性存在与原型中。当我们在讨论一个对象是否存在某个属性时,已经是在讨论一个实例本身了。

以上提到的 es5 中的4种方法中,只有 in 操作符不区分属性所在的位置,其它都要求对象实例本身拥有该属性。

同时,该对象的属性描述符 enumerable 为 true 才能被遍历到。

js
function Person(name, age) {
}
Person.prototype.name = 'cjh'
const person = new Person('ym', 18)

'name' in person // true
Object.keys(person) // []
Reflect.ownKeys(person) // []

person.name = 'ym'
Reflect.ownKeys(person) // ['ym']

delete person.name
Reflect.ownKeys(person) // []

后续会学习 Vue 3 源码,敬请关注。