【Vue2.x源码系列01】响应式原理
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 方法