本文同步发表在我的博客:熟悉 Proxy 及其场景javascript
本文主要内容:java
基于 javascript 的复杂数据类型的特色,衍生出的代理的概念,由于对于复杂的数据类型,变量存储的是引用。代理 proxy 就在引用和值之间。node
另外还须要注意 Reflect,它拥有的13个方法与 proxy 一致,用来代替 Object 的默认行为。很显然,例如咱们用 proxy 修改了对象属性的 getter,那如何使用本来默认行为?express
例如如下代码会陷入死循环:app
const obj = { name: '' }
new Proxy(obj, {
get: function (target, prop) {
// 错误的
return target[prop]
// 应该使用 Reflect 来获得默认行为
return Reflect.get(target, prop)
}
})
复制代码
相比于 Object.defineProperty
,Proxy 有 polyfill 能够 hack,兼容性会更好。dom
和对象描述符中访问器属性 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 实例
即原对象的代理对象,例如:ui
const proxy = new Proxy({}, {
get: function(target, property, receiver) {
console.log(receiver === proxy)
return receiver
}
})
复制代码
⚠️当 proxy 为一个对象的原型时,receiver 就不是 proxy 实例了,而是该对象自己。this
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 的对象。es5
apply用于拦截函数调用,construct方法用于拦截new命令。
{
// newTarget 和 receiver 相似
// 这里就只有一种状况,是 proxy 的实例
construct: function (target, args, newTarget) {
// 必须返回一个对象
return new target(...args)
},
// ctx 是函数调用的上下文
apply: function (target, ctx, args) {
}
}
复制代码
它们对应于 in/delete
,将这些操做变成函数行为。
剩下一些 API 相对简单,使用时能够查看 MDN 文档,不必刻意记忆。
响应式的三要素:模版、观察者、事件中心。
如下是使用的例子:
<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]
}
复制代码
在 es5 中,咱们经常使用的获取对象属性的方式:
在熟悉了 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) // []
复制代码