Vue2响应式原理三:$set

使用场景

本章节需要完成 $set的定义以及完成前两节留下的一个bug,调用数组方法修改数组时没有响应式。

vue中两个不会触发响应式的修改数据方式:

  1. 通过数组的索引修改数组
  2. 向 data中添加新的属性

原因:

  1. 由于 vue通过修改 data中数组的原型链,在原型链中修改 7个改变数组的方法来劫持数组,并未对数组中的元素设置 getter、setter,导致通过索引修改数组,并不能被监听到。
  2. 新增的属性并未在初始化时设置 getter、setter,导致属性没有被劫持。

解决办法:以上两种场景通过 $set对数据进行修改。

实现思路:为数组和对象创建一个 __ob__属性来存储 Observer实例,并为Observer实例挂载一个 Dep实例来收集 watcher,并在 $set中手动触发。

$set

如果是数组,则将其添加到数组,并观察新增的值,如果是对象,则将其进行劫持。

$set(target, key, value) {
  if (Array.isArray(target)) {
    target[key] = value
    observer(value)
  } else {
    defindReactive(target, key, value)
  }
}

在 Observer实例在挂载一个 Dep实例,并将 Observer实例挂载到属性的 __ob__上

class Observer {
  dep = new Dep()

  constructor(data) {
    // 略...
    
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false
    })
  }

  // ...
}

如果是引用数据类型,则返回 Observer实例

function observer(data) {
  const dataType = Object.prototype.toString.call(data)

  if (dataType !== '[object Object]' && dataType !== '[object Array]') {
    return
  }

  // 已经有 __ob__属性说明添加过 Observer实例,直接返回该实例
  if (data.__ob__) return data.__ob__

  return new Observer(data)
}

在 defindReactive函数中接收返回的 observer,并收集依赖。

function defindReactive(target, key, value) {
  const ob = observer(value) // 接收返回的 Observer实例
  const dep = new Dep()

  Object.defineProperty(target, key, {
    get() {
      dep.depend()
      ob?.dep.depend() // 如果 ob存在,则里面的 dep实例也对依赖进行收集
      return value
    },
    set(val) {
      // ...
    }
  })
}

手动调用

$set(target, key, value) {
  if (Array.isArray(target)) {
    target[key] = value
    observer(value)
  } else {
    defindReactive(target, key, value)
  }
  target.__ob__.dep.notify() // 手动派发任务
}

// 略 ...

const arrayMethods = methods.reduce((obj, method) => {
  obj[method] = function (...args) {
    const res = Array.prototype[method].apply(this, args)

    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
      default:
        break
    }
    inserted?.forEach(observer)
    this.__ob__.dep.notify() // 调用函数方法时,也需要手动触发

    return res
  }

  return obj
}, Object.create(Array.prototype))

完整代码

class Vue {
  constructor(options) {
    const data = options.data
    this.$options = options
    this._data = typeof data === 'function' ? data() : data

    this.initData()
    this.initWatch()
  }

  initData() {
    const data = this._data
    observer(data)

    const keys = Object.keys(data)
    for (const key of keys) {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(value) {
          data[key] = value
        }
      })
    }
  }

  initWatch() {
    const watch = this.$options.watch
    if (!watch) return

    const keys = Object.keys(watch)
    for (const key of keys) {
      this.$watch(key, watch[key])
    }
  }

  $watch(key, cb) {
    new Watcher(this, key, cb)
  }

  $set(target, key, value) {
    if (Array.isArray(target)) {
      target[key] = value
      observer(value)
    } else {
      defindReactive(target, key, value)
    }
    target.__ob__.dep.notify()
  }
}

function observer(data) {
  const dataType = Object.prototype.toString.call(data)

  if (dataType !== '[object Object]' && dataType !== '[object Array]') {
    return
  }

  if (data.__ob__) return data.__ob__

  return new Observer(data)
}

function defindReactive(target, key, value) {
  const ob = observer(value)
  const dep = new Dep()

  Object.defineProperty(target, key, {
    get() {
      dep.depend()
      ob?.dep.depend()
      return value
    },
    set(val) {
      if (val === value) return
      dep.notify()
      value = val
    }
  })
}

class Observer {
  dep = new Dep()

  constructor(data) {
    if (Array.isArray(data)) {
      data.__proto__ = arrayMethods
      this.observeArray(data)
    } else {
      this.walk(data)
    }

    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false
    })
  }

  walk(data) {
    const kyes = Object.keys(data)

    for (const key of kyes) {
      defindReactive(data, key, data[key])
    }
  }

  observeArray(arr) {
    arr.forEach(observer)
  }
}

class Dep {
  subs = []

  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target)
    }
  }

  notify() {
    for (const watcher of this.subs) {
      watcher.run()
    }
  }
}

let watcherId = 0
const watcherQueue = []

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    this.id = watcherId++

    this.get()
  }

  get() {
    Dep.target = this
    this.vm[this.key]
    Dep.target = null
  }

  run() {
    if (watcherQueue.includes(this.id)) return
    watcherQueue.push(this.id)

    Promise.resolve().then(() => {
      this.cb.call(this.vm)
      watcherQueue.splice(watcherQueue.indexOf(this.id), 1)
    })
  }
}

const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sotr', 'splice']
const arrayMethods = methods.reduce((obj, method) => {
  obj[method] = function (...args) {
    const res = Array.prototype[method].apply(this, args)

    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
      default:
        break
    }
    inserted?.forEach(observer)
    this.__ob__.dep.notify()

    return res
  }

  return obj
}, Object.create(Array.prototype))

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值