vue-vue2和vue3响应式原理的区别

450 阅读5分钟

核心要点

  • vue2数据响应式的实现
  • vue3数据响应式的实现
  • vue2和vue3响应式原理的区别

1、vue2数据响应式

vue 2 是通过 Object.defineProperty 来实现数据 读取和更新时的操作劫持,通过更改默认的 getter/setter 函数,在 get 过程中收集依赖,在 set 过程中派发更新的;

通过下面的简易代码来分析

// 响应式数据处理,构造一个响应式对象
class Observer {
  constructor(data) {
    this.data = data
    this.walk(data)
  }

  // 遍历对象的每个 已定义 属性,分别执行 defineReactive
  walk(data) {
    if (!data || typeof data !== 'object') {
      return
    }

    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  // 为对象的每个属性重新设置 getter/setter
  defineReactive(obj, key, val) {
    // 每个属性都有单独的 dep 依赖管理
    const dep = new Dep()

    // 通过 defineProperty 进行操作代理定义
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      // 值的读取操作,进行依赖收集
      get() {
        if (Dep.target) {
          dep.depend()
        }
        return val
      },
      // 值的更新操作,触发依赖更新
      set(newVal) {
        if (newVal === val) {
          return
        }
        val = newVal
        dep.notify()
      }
    })
  }
}

// 观察者的构造函数,接收一个表达式和回调函数
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }

  // watcher 实例触发值读取时,将依赖收集的目标对象设置成自身,
     // 通过 call 绑定当前 Vue 实例进行一次函数执行,在运行过程中收集函数中用到的数据
  // 此时会在所有用到数据的 dep 依赖管理中插入该观察者实例
  get() {
    Dep.target = this
    const value = this.getter.call(this.vm, this.vm)
    // 函数执行完毕后将依赖收集目标清空,避免重复收集
    Dep.target = null
    return value
  }

  // dep 依赖更新时会调用,执行回调函数
  update() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

// 依赖收集管理者的构造函数
class Dep {
  constructor() {
    // 保存所有 watcher 观察者依赖数组
    this.subs = []
  }

  // 插入一个观察者到依赖数组中
  addSub(sub) {
    this.subs.push(sub)
  }

  // 收集依赖,只有此时的依赖目标(watcher 实例)存在时才收集依赖
  depend() {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }

  // 发送更新,遍历依赖数组分别执行每个观察者定义好的 update 方法
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Dep.target = null

// 表达式解析
function parsePath(path) {
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) {
        return
      }
      obj = obj[segments[i]]
    }
    return obj
  }
}

这里省略了数组部分,但是 数组本身的响应式监听 是通过重写数组方法来实现的,而 每个数组元素 则会再次进行 Observer 处理(需要数组在定义时就已经声明的数组元素)。

因为 Object.definePorperty 只能对 对象的已知属性 进行操作,所有才会导致 没有在 data 中进行声明的对象属性直接赋值时无法触发视图更新,需要通过($set)来处理。

而数组因为是通过重写数组的7个方法【 'push','pop','shift','unshift', 'splice','sort','reverse'】和遍历数组元素进行的响应式处理,也会导致按照数组下标进行赋值或者更改元素时无法触发视图更新

<body>
  <div id="app" class="demo-vm-1">
    <p>{{arr[0]}}</p>
    <p>{{arr[2]}}</p>
    <p>{{arr[3].c}}</p>
  </div>
</body>

<script>
  new Vue({
    el: "#app",
    data() {
      return {
        arr: [1, 2, { a: 3 },{ c: 5 }]
      }
    },
    mounted() {
      console.log("demo Instance: ", this.$data);
      setTimeout(() => {
        console.log('update')
        this.arr[0] = { o: 1 } //设置完后,发现页面展示的数据不会更新
        this.arr[2] = { a: 1 } //设置完后,发现页面展示的数据不会更新
      },2000)
    },
  })
</script>

因为数组元素的前三个元素 在定义时都是简单类型,所以即使在模板中使用了该数据,也无法进行依赖收集和更新响应

2、vue 3 的响应式实现

vue 3 采用了全新的 Proxy 对象来实现整个响应式系统基础,Proxy 是 ES6 新增的一个构造函数,用来创建一个 目标对象的代理对象,拦截对原对象的所有操作;用户可以通过注册相应的拦截方法来实现对象操作时的自定义行为;

但是 只有通过 proxyObj 进行操作的时候才能通过定义的操作拦截方法进行处理,直接使用原对象则无法触发拦截器,这也是 Vue 3 中要求的 reactive 声明的对象修改原对象无法触发视图更新的原因;

并且 Proxy 也只针对 引用类型数据 才能进行代理,所以这也是 Vue 的基础数据都需要通过 ref 进行声明的原因,内部会建立一个新对象保存原有的基础数据值;

// vue3响应式原理

let toProxy = new WeakMap() // 原对象:代理过的对象
let toRaw = new WeakSet() // 代理过的对象:原对象


function isObject(val) {
  return typeof val === 'object' && val !== 'null'
}

function reactive(target) {
  // 创建响应式对象
  return createReactiveObject(target)
}

function createReactiveObject(target) { // 创建代理后的响应式对象
  if (!isObject(target)) { // 如果不是对象,直接返回
    return target
  }

  let proxy = toProxy.get(target) // 如果对象已经被代理过了,直接返回
  if(proxy) {
    return proxy
  }

  let baseHandler = {
    //receiver:被代理后的对象
    get(target,key,receiver) { 
      console.log('获取');
      // receiver.get() ==》 new proxy().get 这会报错,也就意味着我们不能直接取到被代理对象上的属性,这时候我们需要用到Reflect,这其实也是一个对象,它只不过也含有一些明显属于对象上的方法,且和proxy上的方法一一对应
      let result = Reflect.get(target,key,receiver)
      //递归多层代理,相比于vue2的优势是,vue2默认递归,而vue3中,只要不使用就不会递归。
      return isObject(result) ? reactive(result) : result 
    },
    set(target,key,value,receiver) {
      let hadkey = target.hasOwnProperty(key)
      let oldValue = target[key]
      if(!hadkey) {
        console.log('新增');
      } else if (oldValue !== value) {
        console.log('修改');
      }
      let res = Reflect.set(target,key,value,receiver)
      return res
    },
    deleteProperty(target,key) {
      console.log('删除');
      let res = Reflect.deleteProperty(target,key)
      return res
    }
  }
  let observed = new Proxy(target, baseHandler)
  toProxy.set(target, observed)
  toRaw.add(observed,target)
  return observed
}

3、vue2和vue3响应式原理的区别

  • vue2使用 Object.defineProperty() 实现,而vue3使用 Proxy() 实现
  • vue2 Object.defineProperty 不兼容 IE8,vue3 Proxy 不兼容 IE11
  • vue2 Object.defineProperty 是劫持对象属性,vue3 Proxy是代理整个对象
  • vue2 Object.defineProperty 不能监听到数组下标变化和对象新增属性,vue3 Proxy 可以
  • vue2 Object.defineProperty 会污染原对象,修改时是修改原对象,vue3 Proxy是对原对象进行代理并会返回一个新的代理对象,修改的是代理对象
  • vue2 Object.defineProperty 局限性大,只能针对单属性监听,所以在一开始就要全部递归监听,Proxy 对象嵌套属性运行时递归,用到才代理,性能提升很大,首次渲染更快
  • vue2 数组响应式的实现是通过 重写数组的原型方法实现,而vue3通过Proxy实现
  • vue3 拦截操作更加多样,多达13种拦截方法

4、参考博客