【Vue2.x源码系列01】响应式原理

Vue.js 基本上遵循 MVVM(Model–View–ViewModel)架构模式,数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。 本文讲解一下 Vue 响应式系统的底层细节。

检测变化注意事项

Vue 2.0中,是基于 Object.defineProperty 实现的响应式系统 (这个方法是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因)
vue3 中,是基于 Proxy/Reflect 来实现的

  1. 由于 JavaScript 的限制,这个 Object.defineProperty() api 没办法监听数组长度的变化,也不能检测数组和对象的新增变化。
  2. Vue 无法检测通过数组索引直接改变数组项的操作,这不是 Object.defineProperty() api 的原因,而是尤大认为性能消耗与带来的用户体验不成正比。对数组进行响应式检测会带来很大的性能消耗,因为数组项可能会大,比如1000条、10000条。

响应式原理

响应式基本原理就是,在 Vue 的构造函数中,对 options 的 data 进行处理。即在初始化vue实例的时候,对data、props等对象的每一个属性都通过 Object.defineProperty 定义一次,在数据被set的时候,做一些操作,改变相应的视图。

数据观测

让我们基于 Object.defineProperty 来实现一下对数组和对象的劫持。

import { newArrayProto } from './array'

class Observer {
  constructor(data){
    if (Array.isArray(data)) {
      // 这里我们可以重写可以修改数组本身的方法 7个方法,切片编程:需要保留数组原有的特性,并且可以重写部分方法
      data.__proto__ = newArrayProto
      this.observeArray(data) // 如果数组中放的是对象 可以监控到对象的变化
    } else {
      this.walk(data)
    }
  }
  // 循环对象"重新定义属性",对属性依次劫持,性能差
  walk(data) {
    Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
  }
  // 观测数组
  observeArray(data) {
    data.forEach(item => observe(item))
  }
}

function defineReactive(data,key,value){
  observe(value)  // 深度属性劫持,对所有的对象都进行属性劫持

  Object.defineProperty(data,key,{
    get(){
      return value
    },
    set(newValue){
      if(newValue == value) return
      observe(newValue) // 修改属性之后重新观测,目的:新值为对象或数组的话,可以劫持其数据
      value = newValue
    }
  })
}

export function observe(data) {
  // 只对对象进行劫持
  if(typeof data !== 'object' || data == null){
    return
  }
  return new Observer(data)
}

重写数组7个变异方法

7个方法是指:push、pop、shift、unshift、sort、reverse、splice。(这七个都是会改变原数组的)

实现思路:面向切片编程!!!

不是直接粗暴重写 Array.prototype 上的方法,而是通过原型链继承与函数劫持进行的移花接木。

利用 Object.create(Array.prototype) 生成一个新的对象 newArrayProto,该对象的 __proto__指向 Array.prototype,然后将我们数组的 __proto__指向拥有重写方法的新对象 newArrayProto,这样就保证了 newArrayProto 和 Array.prototype 都在数组的原型链上。

arr.__proto__ === newArrayProto;newArrayProto.__proto__ === Array.prototype

然后在重写方法的内部使用 Array.prototype.push.call 调用原来的方法,并对新增数据进行劫持观测。

let oldArrayProto = Array.prototype // 获取数组的原型

export let newArrayProto = Object.create(oldArrayProto)

// 找到所有的变异方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']

methods.forEach(method => {
  // 这里重写了数组的方法
  newArrayProto[method] = function (...args) {
    // args reset参数收集,args为真正数组,arguments为伪数组
    const result = oldArrayProto[method].call(this, ...args) // 内部调用原来的方法,函数的劫持,切片编程

    // 我们需要对新增的数据再次进行劫持
    let inserted
    let ob = this.__ob__

    switch (method) {
      case 'push':
      case 'unshift': // arr.unshift(1,2,3)
        inserted = args
        break
      case 'splice': // arr.splice(0,1,{a:1},{a:1})
        inserted = args.slice(2)
      default:
        break
    }

    if (inserted) {
      // 对新增的内容再次进行观测
      ob.observeArray(inserted)
    }
    return result
  }
})

增加__ob__属性

这是一个恶心又巧妙的属性,我们在 Observer 类内部,把 this 实例添加到了响应式数据上。相当于给所有响应式数据增加了一个标识,并且可以在响应式数据上获取 Observer 实例上的方法

class Observer {
  constructor(data) {
    // data.__ob__ = this // 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false, // 将__ob__ 变成不可枚举 (循环的时候无法获取到,防止栈溢出)
    })

    if (Array.isArray(data)) {
      // 这里我们可以重写可以修改数组本身的方法 7个方法,切片编程:需要保留数组原有的特性,并且可以重写部分方法
      data.__proto__ = newArrayProto
      this.observeArray(data) // 如果数组中放的是对象 可以监控到对象的变化
    } else {
      this.walk(data)
    }
  }

}

__ob__有两大用处:

  1. 如果一个对象被劫持过了,那就不需要再被劫持了,要判断一个对象是否被劫持过,可以通过__ob__来判断
// 数据观测
export function observe(data) {
  // 只对对象进行劫持
  if (typeof data !== 'object' || data == null) {
    return
  }

  // 如果一个对象被劫持过了,那就不需要再被劫持了 (要判断一个对象是否被劫持过,可以在对象上增添一个实例,用实例的原型链来判断是否被劫持过)
  if (data.__ob__ instanceof Observer) {
    return data.__ob__
  }

  return new Observer(data)
}
  1. 我们重写了数组的7个变异方法,其中 push、unshift、splice 这三个方法会给数组新增成员。此时需要对新增的成员再次进行观测,可以通过__ob__调用 Observer 实例上的 observeArray 方法
posted @ 2023-03-24 14:33  柏成  阅读(458)  评论(0编辑  收藏  举报