Vue2响应式原理

  • Vue2响应式原理已关闭评论
  • 109 次浏览
  • A+
所属分类:Web前端
摘要

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

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 方法