Skip to content

响应式

引言

如何知道一个数据变化了?

例如:this.arr.push('a'),我怎么知道a已经push进arr里了呢?


vue2.x

  • 实现模拟
js
function observe(data) {
  if (data instanceof Object) {
    if (Array.isArray(data)) {
      // 不对数组的每一项监听,只监听改变数组的方法
      data.__proto__ = arrayMiddle // 是数组则指向中间层
      data.forEach(item => observe(item))
    } else {
      for (const key in data) {
        defineProperty(data, key, data[key])
      }
    }
  }
}
// 对象的监听
function defineProperty(data, key, val) {
  observe(val) // 深度监听
  Object.defineProperty(data, key, {
    get() {
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal
        observe(val) // 如果改完后是一个对象或数组,则深度监听
        console.log('视图更新') // 模拟通知视图更新完成
      }
    }
  })
}
// 数组的监听
const arrayPrototype = Array.prototype
const arrayMiddle = Object.create(arrayPrototype); // 创建一个中间层
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(name => { // 所有会改变数组的原型上的方法
  arrayMiddle[name] = function(...arg) { // 在中间层上实现视图更新监听,同时帮你调用原本要实现的方法
    console.log('视图更新')
    arrayPrototype[name].apply(this, arg)
  }
})

// 实现 Vue 响应式
const data = {
  name: 'xiaoming',
  city: {
      first: 'beijing'
  },
  arr: ['b']
}

observe(data)
data.name = {first: 'xiaoming'} // 视图更新
data.name.first = 'zhangsan' // 视图更新
data.city.first = 'foshan' // 视图更新
data.arr.push('a')  // 视图更新
data.arr[0] = 'c' // 单独改变某个索引的值,不会出发响应式

Vue.$set

js
function set(target, key, val) {
  // 通过调用 splice 方法替换元素
  if (Array.isArray(target)) {
    target.splice(key, 1, val)
    return val
  }
  
  // 对象,则直接更新
  if (Reflect.ownKeys(target)) {
    target[key] = val
    return val
  }

  // 如果一个对象有 __ob__ 属性,则说明这个对象是响应式对象,修改对象已有属性的时候就会出发页面渲染
  const ob = target.__ob__

  // 如果本身不是响应式数据,则只改变值,不触发视图更新
  if (!ob) {
    target[key] = val
    return val
  }

  defineProperty(target, key, val) // 监听
  ob.dep.notify() // 触发视图更新
  return val
}
  • 缺点
    1. 深度监听,需要递归到底,一次性计算量大
    2. 无法监听新增属性/删除属性(Vue.setVue.delete
    3. 无法原生监听数组,需要特殊处理

问:数组也可以通过 defineProperty 监听每一项的改变,为什么不这样做呢?

答:性能消耗较大,通过监听 push、pop 等已经覆盖大多数场景了

vue3

  • 实现模拟

    js
    function observe(data = {}){
        if(!(data instanceof Object)){
            return data
        }
    
        const proxyCof = {
            get(target, key, receiver){
                // 数组,值监听本身的属性,不监听Propotype的属性,二看觉得没必要
                const keys = Reflect.ownKeys(target);
                if(keys.includes(key)){
                    console.log('监听获取')
                }
                // 返回下一层
                const result = Reflect.get(target, key, receiver)
                return observe(result) // 深度监听
            },
            set(target, key, val, receiver){
                // 判断是新增的还是修改的
                const keys = Reflect.ownKeys(target);
                if(keys.includes(key)){
                    console.log('监听修改')
                }else{
                    console.log('监听新增')
                }
                return Reflect.set(target, key, val, receiver)
            },
            deleteProperty(target, key){
                console.log('监听删除')
                return Reflect.deleteProperty(target, key)
            }
        }
    
        return new Proxy(data, proxyCof)
    }
    
    const data = {
        name: 'xiaoming',
        age: 20,
        info: {
            city: 'foshan'
        }
    }
    const proxy = observe(data)
    
    proxy.name  // 监听获取
    proxy.info.city // 监听获取 * 2
    proxy.age = 21 // 监听修改
    proxy.sex = '' // 监听新增
    delete proxy.sex // 监听删除
  • 缺点

    1. 解决了 Object.defineProperty 的问题
    2. 但是自己却有浏览器兼容问题