本文同步發表在我的博客:熟悉 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) // []
複製代碼