【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 方法
![[oeasy]python0135_变量名与下划线_dunder_声明与赋值](https://img-blog.csdnimg.cn/img_convert/15722c320d3e77ffd05b08adac0b0a69.jpeg)

















