手抄Vue(四)—— 封裝Observer類

Vue.js 中,將數(shù)據(jù)對象轉(zhuǎn)化為響應(yīng)式數(shù)據(jù)的是 Observer 構(gòu)造函數(shù)葬毫。我準(zhǔn)備結(jié)合前面幾篇已經(jīng)整理出來的思路梗醇,實(shí)現(xiàn)一個(gè)自己的 Observer捉捅。

為了讓代碼結(jié)構(gòu)更加清晰冶匹,同時(shí)考慮到可復(fù)用性习劫,我先從前面幾篇已有的實(shí)現(xiàn)中抽一些功能較為獨(dú)立的代碼出來:

  • defineReactive 方法
function defineReactive(obj, key) {
  const dep = []
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get () {
      dep.push(target)
      return value
    },
    set (newVal) {
      if (newVal === value) return
      value = newVal
      dep.forEach(f => {
        f()
      })
    }
  })
}

該方法用來將數(shù)據(jù)對象 obj 上的數(shù)據(jù)屬性 key 轉(zhuǎn)化為響應(yīng)式屬性。

dep 是“依賴收集器”嚼隘,屬性 keygetter setter 都通過閉包引用著自己的 dep诽里。target 仍然作為全局變量存在,中轉(zhuǎn)依賴以幫助 getter 收集依賴飞蛹。setter 會(huì)執(zhí)行對應(yīng) getter 收集到的所有依賴谤狡,但如果發(fā)現(xiàn)設(shè)置的值與原值無異,則直接 return桩皿,什么也不做豌汇。

這是直接從 Vue數(shù)據(jù)響應(yīng)原理(一)—— 簡單實(shí)現(xiàn) 里拿過來的代碼,但如果要封裝一個(gè)功能完善泄隔、可復(fù)用性高的方法的話,肯定還要考慮一些邊界條件與異常場景宛徊,比如佛嬉,如果傳遞進(jìn)來的屬性本來就是不可配置的?這時(shí)就得加個(gè)判斷:

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && !property.configurable) {
  return
}

首先獲取到對象 obj 上屬性 key 的屬性描述符對象闸天,然后進(jìn)行判斷暖呕,如果屬性描述符對象存在,并且該屬性本來就不可配置苞氮,那么直接 return湾揽。

再比如,如果傳進(jìn)來的屬性本來就有 getter setter 函數(shù)對 笼吟?那就要把原來的 getter setter 緩存起來库物,在新定義的 getter 里除卻收集依賴這項(xiàng)工作以外,還要將緩存起來的 getter 執(zhí)行并將結(jié)果返回贷帮。同樣戚揭,在新定義的 setter 里,除去執(zhí)行依賴的工作以外撵枢,還要將設(shè)置的新值 newVal 與緩存的 getter 執(zhí)行之后得到的值比較民晒,如果相等則直接 return精居,什么都不做。并且要將緩存起來的 setter 執(zhí)行一遍潜必,以替代原來的賦值操作 value = newVal靴姿。

反映至代碼即:

function defineReactive(obj, key) {
  const dep = []

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && !property.configurable) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  let value = obj[key]
  Object.defineProperty(obj, key, {
    get () {
      getter && (value = getter.call(obj))
      dep.push(target)
      return value
    },
    set (newVal) {
      getter && (value = getter.call(obj))
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        value = newVal
      }
      dep.forEach(f => {
        f()
      })
    }
  })
}

上面有這么一句:

if (newVal === value || (newVal !== newVal && value !== value)) {
  return
}

其實(shí)本來是這樣的:

if (newVal === value) {
  return
}

但是考慮到 NaN 的情況:

NaN === NaN // false

這會(huì)導(dǎo)致:

newVal === value // false

所以應(yīng)該在判斷條件中加上:

newVal !== newVal && value !== value

利用 NaN 與自身不相等的特性判斷出 NaN,最后就成了:

newVal === value || (newVal !== newVal && value !== value)

值得注意的是:

Infinity === Infinity // true
-Infinity === -Infinity // true
1 / 0 === 2 / 0 // true
  • walk 方法
function walk(obj) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

該方法用于遍歷數(shù)據(jù)對象 obj 的每一個(gè)屬性磁滚,同時(shí)調(diào)用之前定義的 defineReactive 方法佛吓,將遍歷到的屬性轉(zhuǎn)化為響應(yīng)式屬性。

  • hasProto
const hasProto = '__proto__' in {}

該變量用于判斷瀏覽器是否支持 __proto__ 屬性恨旱。

  • arrayMethods 對象
const mutationMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

mutationMethods.forEach(method => {
  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args)
    console.log(`我截獲了對數(shù)組的${method}操作`)
    return result
  }
})

該對象用于代理數(shù)組的變異方法以實(shí)現(xiàn)攔截辈毯。

  • def 方法
function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

該方法是 Object.defineProperty 的簡單封裝,用于定義一個(gè)屬性搜贤,可以控制該屬性是否可枚舉谆沃。

  • protoAugment 方法
function protoAugment(target, src) {
  target.__proto__ = src
}

該方法用于在瀏覽器支持 __proto__ 屬性時(shí),通過修改原型鏈仪芒,讓 __proto__ 指向 src唁影,來增強(qiáng)目標(biāo)對象或數(shù)組。

  • copyAugment 方法
function copyAugment(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

該方法用來遍歷 keys掂名,并在目標(biāo)對象 target 上定義不可枚舉的屬性据沈,該屬性的鍵為 keys 中的元素饺蔑,值為該元素在 src 中對應(yīng)的屬性值猾警。

  • isPlainObject 方法
function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'
}

該方法用于判斷給定的變量是否為純對象发皿。

有了以上這些方法和屬性之后穴墅,Observer 類也就應(yīng)運(yùn)而生了:

class Observer {
  constructor (value) {
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, mutationMethods)
    } else {
      this.walk(value)
    }
  }
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

但現(xiàn)在有兩個(gè)問題玄货,一個(gè)是這個(gè)類沒有實(shí)現(xiàn)深度觀測鹅士,再一個(gè)是沒有對調(diào)用 Observer 時(shí)傳進(jìn)來的參數(shù)做檢測惩坑,以防止傳進(jìn)來 undefined null 100 'kobe' 等等不能被觀測的數(shù)據(jù)類型。并且我希望調(diào)用 Observer 的時(shí)候傳進(jìn)來的只能是數(shù)組或者純對象慢哈。綜合這些因素,再封裝一層出來會(huì)比較好:

function observe(value) {
  if (Array.isArray(value) || isPlainObject(value)) {
    return new Observer(value)
  }
}

observe 會(huì)判斷給定的 value 如果是數(shù)組或者純對象的話再去 new 出來 Observer键俱,并將結(jié)果返回编振。

有了 observe踪央,深度觀測就可以這樣來實(shí)現(xiàn):在 defineReactive 方法中畅蹂,對給定的 obj[key] 以及 setter 中的 newVal 調(diào)用 observe 方法進(jìn)行觀測液斜,因?yàn)檫@兩者都可能是數(shù)組或者純對象,如果不是,observe 方法內(nèi)部已經(jīng)統(tǒng)一做了判斷祷嘶,外部調(diào)用時(shí)無需特殊處理论巍。即:

function defineReactive(obj, key) {
  const dep = []

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && !property.configurable) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  let value = obj[key]
  // 這里
  observe(value)
  Object.defineProperty(obj, key, {
    get () {
      getter && (value = getter.call(obj))
      dep.push(target)
      return value
    },
    set (newVal) {
      getter && (value = getter.call(obj))
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        value = newVal
      }
      // 這里
      observe(newVal)
      dep.forEach(f => {
        f()
      })
    }
  })
}

但其實(shí)發(fā)現(xiàn)還有一個(gè)問題,現(xiàn)在數(shù)組鞋怀、純對象以及純對象內(nèi)嵌套數(shù)組密似、純對象內(nèi)嵌套純對象這幾種情形都已經(jīng)實(shí)現(xiàn)了(深度)觀測村斟,但數(shù)組內(nèi)嵌套純對象以及數(shù)組內(nèi)嵌套數(shù)組還沒有實(shí)現(xiàn)蟆盹,所以要再寫這么一個(gè)方法:

function observeArray(items) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

該方法用來遍歷給定的數(shù)組逾滥,即 items匣距,再分別對每一個(gè)元素 items[i] 執(zhí)行 observe 方法毅待,即可對數(shù)組里面的嵌套情形進(jìn)行深度觀測。同時(shí) Observer 類要做以下改造:

class Observer {
  constructor (value) {
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, mutationMethods)
      // 二
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // 一
  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

注釋一的地方外里,給 Observer 類添加一個(gè)實(shí)例方法,也就是我剛寫的 observeArray墩莫。

注釋二的地方狂秦,調(diào)用 observeArray 方法,并將數(shù)組 value 作為參數(shù)傳入堪簿。

那么最終戴甩,代碼就是這個(gè)樣子:

const mutationMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

const hasProto = '__proto__' in {}
function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'
}

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

function defineReactive(obj, key) {
  const dep = []

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && !property.configurable) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  let value = obj[key]
  observe(value)
  Object.defineProperty(obj, key, {
    get () {
      getter && (value = getter.call(obj))
      dep.push(target)
      return value
    },
    set (newVal) {
      getter && (value = getter.call(obj))
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        value = newVal
      }
      observe(newVal)
      dep.forEach(f => {
        f()
      })
    }
  })
}

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args)
    console.log(`我截獲了對數(shù)組的${method}操作`)
    return result
  }
})

function observe(value) {
  if (Array.isArray(value) || isPlainObject(value)) {
    return new Observer(value)
  }
}

class Observer {
  constructor (value) {
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, mutationMethods)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

function protoAugment(target, src) {
  target.__proto__ = src
}
function copyAugment(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

function myWatch(exp, fn) {
  target = fn
  if (typeof exp === 'function') {
    exp()
    return
  }
  let pathArr,
      obj = data
  if (/\./.test(exp)) {
    pathArr = exp.split('.')
    pathArr.forEach(p => {
      target = fn
      obj = obj[p]
    })
    return
  }
  data[exp]
}

添加以下測試代碼:

const data = {
  name: 'kobe bryant',
  otherInfo: {
    height: 198,
    numbers: [8, 24]
  },
  teammates: [
    'paul gasol',
    {
      name: 'shaq',
      numbers: [32, 34, 33]
    }
  ]
}

function render() {
  document.body.innerText = `我最喜歡的NBA球員是${data.name}茉稠,他身高${data.otherInfo.height}cm而线,穿過${data.otherInfo.numbers.length}個(gè)球衣號(hào)碼,${data.otherInfo.numbers[0]}和${data.otherInfo.numbers[1]}誓竿,他的隊(duì)友有${data.teammates[0]}和${data.teammates[1].name}筷屡,其中毙死,${data.teammates[1].name}在湖人時(shí)期穿的球衣號(hào)碼為${data.teammates[1].numbers[1]}號(hào)`
}

observe(data)
myWatch(render, render)

data.name = 'michael'
data.otherInfo.height = 198.1
data.otherInfo.numbers.push(23)
data.teammates[1].name = 'scott pippen'
data.teammates[1].numbers.push(33)

執(zhí)行以后發(fā)現(xiàn),無論嵌套關(guān)系如何對屬性的賦值操作均觸發(fā)了 render 函數(shù),對兩個(gè)數(shù)組data.otherInfo.numbersdata.teammates[1].numberspush 操作也執(zhí)行了擴(kuò)展的功能即打印 '我截獲了對數(shù)組的push操作'這句信息。但是數(shù)組的 push 操作沒有觸發(fā)頁面重新渲染,這是因?yàn)閷?shù)組變異方法的整個(gè)代理過程中沒有收集依賴也沒有觸發(fā)依賴候址,這個(gè)問題先留下匹耕,等我寫到 Dep 類的時(shí)候再回過頭來寫這個(gè)問題稳其。但其實(shí)站在這篇博客的角度來看,Observer 類的封裝就算是初步完成了嘱蛋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疙驾,一起剝皮案震驚了整個(gè)濱河市荆萤,隨后出現(xiàn)的幾起案子偏竟,更是在濱河造成了極大的恐慌,老刑警劉巖殖蚕,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛤育,死亡現(xiàn)場離奇詭異瓦糕,居然都是意外死亡亥揖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門旁趟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橙困,“玉大人,你說我怎么就攤上這事。” “怎么了猫态?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵义辕,是天一觀的道長夺巩。 經(jīng)常有香客問我续镇,道長摸航,這世上最難降的妖魔是什么擂涛? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮棋蚌,結(jié)果婚禮上蒿往,老公的妹妹穿的比我還像新娘。我一直安慰自己坷备,他們只是感情好熄浓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著省撑,像睡著了一般赌蔑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上竟秫,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天娃惯,我揣著相機(jī)與錄音,去河邊找鬼肥败。 笑死趾浅,一個(gè)胖子當(dāng)著我的面吹牛愕提,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播皿哨,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼浅侨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了证膨?” 一聲冷哼從身側(cè)響起如输,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎央勒,沒想到半個(gè)月后不见,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崔步,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年稳吮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片井濒。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡灶似,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出眼虱,到底是詐尸還是另有隱情喻奥,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布捏悬,位于F島的核電站撞蚕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏过牙。R本人自食惡果不足惜甥厦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寇钉。 院中可真熱鬧刀疙,春花似錦、人聲如沸扫倡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撵溃。三九已至疚鲤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缘挑,已是汗流浹背集歇。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留语淘,地道東北人诲宇。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓际歼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親姑蓝。 傳聞我的和親對象是個(gè)殘疾皇子鹅心,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容