【Vue2源码】响应式原理
文章目录
- 【Vue2源码】响应式原理
- `Vue响应式`的核心设计思路
- 整体流程
- 响应式中的关键角色
- 检测变化注意事项
- 响应式原理
- 数据观测
- 重写数组7个变异方法
- 增加__ob__属性
- __ob__有两大用处:
Vue.js 基本上遵循 MVVM(Model–View–ViewModel)架构模式,数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。 本文讲解一下 Vue 响应式系统的底层细节。
Vue响应式
的核心设计思路
当创建Vue
实例时,vue
会遍历data
选项的属性,利用Object.defineProperty
为属性添加getter
和setter
对数据的读取进行劫持(getter
用来依赖收集,setter
用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。
每个组件实例会有相应的watcher
实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有computed watcher
,user watcher
实例),之后依赖项被改动时,setter
方法会通知依赖与此data
的watcher
实例重新计算(派发更新),从而使它关联的组件重新渲染。
整体流程
作为一个前端的MVVM
框架,Vue
的基本思路和Angular
、React
并无二致,其核心就在于: 当数据变化时,自动去刷新页面DOM
,这使得我们能从繁琐的DOM
操作中解放出来,从而专心地去处理业务逻辑。
这就是Vue
的数据双向绑定(又称响应式原理)。数据双向绑定是Vue
最独特的特性之一。此处我们用官方的一张流程图来简要地说明一下Vue
响应式系统的整个流程:
在Vue
中,每个组件实例都有相应的watcher
实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter
被调用时,会通知watcher
重新计算,从而致使它关联的组件得以更新。
这是一个典型的观察者模式。
响应式中的关键角色
在 Vue 数据双向绑定的实现逻辑里,有这样三个关键角色:
Observer
: 它的作用是给对象的属性添加getter
和setter
,用于依赖收集和派发更新Dep
: 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep
实例(里面subs
是Watcher
实例数组),当数据有变更时,会通过dep.notify()
通知各个watcher
。Watcher
: 观察者对象 , 实例分为渲染 watcher (render watcher)
,计算属性 watcher (computed watcher)
,侦听器 watcher(user watcher)
三种
检测变化注意事项
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 来实现一下对数组和对象的劫持。
\src\observe\index.js
import { newArrayProto } from "./array"
class Observe {
constructor (data) {
//Object.defineReactive只能劫持已经存在的属性(vue俩民回为此单独写一些api)
//data.__ob__ = this //这里的this指的是Observe,把这个实例附到了ob上,还可以用于检测是否被劫持过
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false //将__ob__编程不可枚举,这样循环的时候就无法获取到,不会进入死循环
})
if(Array.isArray(data)) {
data.__proto__ = newArrayProto
this.observeArray(data) //如果数组中放置的是对象,也可以被监控到
//这里我们可以重写数组中的方法,7个变异方法,是可以修改到数组本身的
}else {
this.walk(data)
}
}
walk (data) { //循环对象,对属性依次劫持
//“重新定义”属性 性能差
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
observeArray(data) {
data.forEach(item=> observe(item))
}
}
export function defineReactive (target, key, value) { //闭包
observe(value) //对所有的对象都进行属性劫持 深度劫持
Object.defineProperty(target, key, {
get () { //取值的时候会执行get
console.log(key,"key");
return value
},
set (newValue) { //修改的时候会执行set
if (newValue === value) return
observe(newValue)
value = newValue
}
})
}
export function observe (data) {
if (typeof data !== 'object' || data == null) {
//只对对象进行劫持
return
}
if(data.__ob__ instanceof Observe) { //如果存在data.__ob__就说明这个被代理过了
return data.__ob__
}
//如果一个对象被劫持过了,那就不需要再被劫持了
//要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过
return new Observe(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 调用原来的方法,并对新增数据进行劫持观测。
\src\observe\array.js
let oldArrayProto = Array.prototype //获取数组的原型
export let newArrayProto = Object.create(oldArrayProto)
let methods = [ //通过遍历寻找到所有变异方法
'push',
'pop',
'shift',
'reverse',
'sout',
'splice'
] //concat slice不会改变原数组
methods.forEach(method => {
newArrayProto[method] = function (...args) { //这里重写了数组的方法
const result = oldArrayProto[method].call(this,...args) //再内部调用原来的方法,函数的劫持,切片编程
//我们需要对新增的数据进行劫持
let inserted
let ob = this.__ob__
switch (method) {
case 'push':
case 'unshift':
inserted = args
break;
case 'splice' : //arr.splice(0,1,{a:1},{b:2})
inserted = args
break
}
console.log("xinzeng ");
if(inserted) {
//对新增的内容再次进行观测
ob.observeArray(inserted)
}
return result
}
})
增加__ob__属性
这是一个恶心又巧妙的属性,我们在 Observer 类内部,把 this 实例添加到了响应式数据上。相当于给所有响应式数据增加了一个标识,并且可以在响应式数据上获取 Observer 实例上的方法
class Observe {
constructor (data) {
//Object.defineReactive只能劫持已经存在的属性(vue俩民回为此单独写一些api)
//data.__ob__ = this //这里的this指的是Observe,把这个实例附到了ob上,还可以用于检测是否被劫持过
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false //将__ob__编程不可枚举,这样循环的时候就无法获取到,不会进入死循环
})
if(Array.isArray(data)) {
data.__proto__ = newArrayProto
this.observeArray(data) //如果数组中放置的是对象,也可以被监控到
//这里我们可以重写数组中的方法,7个变异方法,是可以修改到数组本身的
}else {
this.walk(data)
}
}
walk (data) { //循环对象,对属性依次劫持
//“重新定义”属性 性能差
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
observeArray(data) {
data.forEach(item=> observe(item))
}
}
__ob__有两大用处:
1、如果一个对象被劫持过了,那就不需要再被劫持了,要判断一个对象是否被劫持过,可以通过 ob 来判断
export function observe (data) {
if (typeof data !== 'object' || data == null) {
//只对对象进行劫持
return
}
if (data.__ob__ instanceof Observe) { //如果存在data.__ob__就说明这个被代理过了
return data.__ob__
}
//如果一个对象被劫持过了,那就不需要再被劫持了
//要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过
return new Observe(data)
}
2、我们重写了数组的7个变异方法,其中 push、unshift、splice 这三个方法会给数组新增成员。此时需要对新增的成员再次进行观测,可以通过 ob 调用 Observer 实例上的 observeArray 方法