熟悉 Proxy 及其場景

本文同步發表在我的博客:熟悉 Proxy 及其場景javascript

概述

本文主要內容:java

  • proxy 概念
  • 相關 API
  • proxy 實現雙向綁定
  • 遇到的一些問題

基於 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

熟悉 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 實例 即原對象的代理對象,例如: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

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]
}
複製代碼

問題

遍歷對象屬性的方法有哪些?以及它們的區別

在 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) // []
複製代碼
相關文章
相關標籤/搜索