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