Vue2响应式原理|环球视点

来源:博客园2023-03-24 14:57:14

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

检测变化注意事项

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

由于 JavaScript 的限制,这个 Object.defineProperty() api 没办法监听数组长度的变化,也不能检测数组和对象的新增变化。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__有两大用处:

如果一个对象被劫持过了,那就不需要再被劫持了,要判断一个对象是否被劫持过,可以通过__ob__来判断
// 数据观测export function observe(data) {  // 只对对象进行劫持  if (typeof data !== "object" || data == null) {    return  }  // 如果一个对象被劫持过了,那就不需要再被劫持了 (要判断一个对象是否被劫持过,可以在对象上增添一个实例,用实例的原型链来判断是否被劫持过)  if (data.__ob__ instanceof Observer) {    return data.__ob__  }  return new Observer(data)}
我们重写了数组的7个变异方法,其中 push、unshift、splice 这三个方法会给数组新增成员。此时需要对新增的成员再次进行观测,可以通过__ob__调用 Observer 实例上的 observeArray 方法

标签:

  • 路线
天津新增本土确诊病例10例、本土无症状感染者8例
东北华北黄淮等地有雷阵雨 湖南江西福建等地降水减弱