Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

熟悉 Proxy 及其场景 #39

Open
Jmingzi opened this issue Oct 11, 2019 · 0 comments
Open

熟悉 Proxy 及其场景 #39

Jmingzi opened this issue Oct 11, 2019 · 0 comments

Comments

@Jmingzi
Copy link
Owner

Jmingzi commented Oct 11, 2019

本文同步发表在个人博客 熟悉 Proxy 及其场景

概述

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

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

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

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

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

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 时重新定义对象属性的描述符。

{
  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 实例 即原对象的代理对象,例如:

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

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

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命令。

{
  // 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

实现双向绑定

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

以下是使用的例子:

<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 类的实现:

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
}

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

// 简单的事件中心
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 才能被遍历到。

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 源码,敬请关注。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant