手抄Vue(四)—— 封装Observer类

Vue.js 中,将数据对象转化为响应式数据的是 Observer 构造函数。我准备结合前面几篇已经整理出来的思路,实现一个自己的 Observer

为了让代码结构更加清晰,同时考虑到可复用性,我先从前面几篇已有的实现中抽一些功能较为独立的代码出来:

  • 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()
      })
    }
  })
}

该方法用来将数据对象 obj 上的数据属性 key 转化为响应式属性。

dep 是“依赖收集器”,属性 keygetter setter 都通过闭包引用着自己的 deptarget 仍然作为全局变量存在,中转依赖以帮助 getter 收集依赖。setter 会执行对应 getter 收集到的所有依赖,但如果发现设置的值与原值无异,则直接 return,什么也不做。

这是直接从 Vue数据响应原理(一)—— 简单实现 里拿过来的代码,但如果要封装一个功能完善、可复用性高的方法的话,肯定还要考虑一些边界条件与异常场景,比如,如果传递进来的属性本来就是不可配置的?这时就得加个判断:

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

首先获取到对象 obj 上属性 key 的属性描述符对象,然后进行判断,如果属性描述符对象存在,并且该属性本来就不可配置,那么直接 return

再比如,如果传进来的属性本来就有 getter setter 函数对 ?那就要把原来的 getter setter 缓存起来,在新定义的 getter 里除却收集依赖这项工作以外,还要将缓存起来的 getter 执行并将结果返回。同样,在新定义的 setter 里,除去执行依赖的工作以外,还要将设置的新值 newVal 与缓存的 getter 执行之后得到的值比较,如果相等则直接 return,什么都不做。并且要将缓存起来的 setter 执行一遍,以替代原来的赋值操作 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
}

其实本来是这样的:

if (newVal === value) {
  return
}

但是考虑到 NaN 的情况:

NaN === NaN // false

这会导致:

newVal === value // false

所以应该在判断条件中加上:

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])
  }
}

该方法用于遍历数据对象 obj 的每一个属性,同时调用之前定义的 defineReactive 方法,将遍历到的属性转化为响应式属性。

  • 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(`我截获了对数组的${method}操作`)
    return result
  }
})

该对象用于代理数组的变异方法以实现拦截。

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

该方法是 Object.defineProperty 的简单封装,用于定义一个属性,可以控制该属性是否可枚举。

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

该方法用于在浏览器支持 __proto__ 属性时,通过修改原型链,让 __proto__ 指向 src,来增强目标对象或数组。

  • 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,并在目标对象 target 上定义不可枚举的属性,该属性的键为 keys 中的元素,值为该元素在 src 中对应的属性值。

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

该方法用于判断给定的变量是否为纯对象。

有了以上这些方法和属性之后,Observer 类也就应运而生了:

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])
    }
  }
}

但现在有两个问题,一个是这个类没有实现深度观测,再一个是没有对调用 Observer 时传进来的参数做检测,以防止传进来 undefined null 100 'kobe' 等等不能被观测的数据类型。并且我希望调用 Observer 的时候传进来的只能是数组或者纯对象。综合这些因素,再封装一层出来会比较好:

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

observe 会判断给定的 value 如果是数组或者纯对象的话再去 new 出来 Observer,并将结果返回。

有了 observe,深度观测就可以这样来实现:在 defineReactive 方法中,对给定的 obj[key] 以及 setter 中的 newVal 调用 observe 方法进行观测,因为这两者都可能是数组或者纯对象,如果不是,observe 方法内部已经统一做了判断,外部调用时无需特殊处理。即:

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()
      })
    }
  })
}

但其实发现还有一个问题,现在数组、纯对象以及纯对象内嵌套数组、纯对象内嵌套纯对象这几种情形都已经实现了(深度)观测,但数组内嵌套纯对象以及数组内嵌套数组还没有实现,所以要再写这么一个方法:

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

该方法用来遍历给定的数组,即 items,再分别对每一个元素 items[i] 执行 observe 方法,即可对数组里面的嵌套情形进行深度观测。同时 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 类添加一个实例方法,也就是我刚写的 observeArray

注释二的地方,调用 observeArray 方法,并将数组 value 作为参数传入。

那么最终,代码就是这个样子:

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(`我截获了对数组的${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}个球衣号码,${data.otherInfo.numbers[0]}和${data.otherInfo.numbers[1]},他的队友有${data.teammates[0]}和${data.teammates[1].name},其中,${data.teammates[1].name}在湖人时期穿的球衣号码为${data.teammates[1].numbers[1]}号`
}

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)

执行以后发现,无论嵌套关系如何对属性的赋值操作均触发了 render 函数,对两个数组data.otherInfo.numbersdata.teammates[1].numberspush 操作也执行了扩展的功能即打印 '我截获了对数组的push操作'这句信息。但是数组的 push 操作没有触发页面重新渲染,这是因为对数组变异方法的整个代理过程中没有收集依赖也没有触发依赖,这个问题先留下,等我写到 Dep 类的时候再回过头来写这个问题。但其实站在这篇博客的角度来看,Observer 类的封装就算是初步完成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容