手写 Vue 的响应式

124 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情

图片.png

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

对需要 observe 的数据对象进行递归遍历,包括子对象的属性,都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

let obj = {
  a: 1
}

当 obj.a = 2 的时候,触发渲染,调用 render 函数。

const render = (key, value) => {
  console.log(`${key}: ${value}`)
}

const defineReative = (obj, key, value) => {
  // 递归遍历,包括子对象的属性
  reactive(value)
  Object.defineProperty(obj, key, {
    get() {
      return value
    },
    set(newVal) {
      if (value === newVal) {
        return
      }
      value = newVal
      render(key, value)
    }
  })
}

const reactive = (obj) => {
  if (typeof obj === "object") {
    for (let key in obj) {
      defineReative(obj, key, obj[key])
    }
  }
}

let obj = {
  a: 1
}

reactive(obj)

obj.a = 2

需要通过Object.create实现一个Array.prototype继承者arraymethods。它访问的方法和Array.prototype上的是一样的。我们不能直接在Array.prototype上对方法进行监听,因为这样会影响到正常方法的调用。

const render = (key, value) => {
  console.log(`${key}: ${value}`)
}

const arrPrototype = Array.prototype
const newArrayPrototype = Object.create(arrPrototype);

['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(methodName => {
  newArrayPrototype[methodName] = function () {
    arrPrototype[methodName].call(this, ...arguments)

    render(methodName, ...arguments)
  }
})

const reactive = (obj) => {
  if (Array.isArray(obj)) {
    obj.__proto__ = newArrayPrototype
  }
}

const data = [1, 2, 3, 4]
reactive(data)
data.push(5)

为什么 Vue3.0 要使用 Proxy

  1. Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性
  2. 对象上新增属性,Proxy 可以监听到,Object.defineProperty 不能
  3. 数组新增修改,Proxy 可以监听到,Object.defineProperty 不能
  4. 若对象内部属性要全部递归代理,Proxy 可以只在调用的时候递归,而 Object.definePropery 需要一次完成所有递归,性能比 Proxy
const render = (key, val) => {
  console.log(`${key}: ${val}`)
}

const reactive = (obj, setBind, getLogger) => {
  return new Proxy(obj, {
    get(target, key, recevier) {
      getLogger(target, key)
      return Reflect.get(target, key, recevier)
    },
    set(target, key, value) {
      setBind(key, value)
      return Reflect.set(target, key, value)
    }
  })
}

let obj = {
  a: 1
}

let p = reactive(obj, (key, value) => {
  render(key, value)
}, (target, key) => {
  render(target, key)
})

p.a = 2