深入响应式原理
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
1.Object.defineProperty(obj, prop, descriptor)方法
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
//默认为 false。
let obj = {};
Object.defineProperty(obj,'a',{
value:3,
enumerable:true,//是否可枚举。默认false
writable: false,//可读写 默认为 false
configurable:true, //true该属性也能从对应的对象上被删除。默认false
})
Object.defineProperty(obj,'b',{
//getter
get(){
console.log('试图访问obj的b属性')
}
//setter
set(val){
console.log('试图改变obj的b属性')
}
})
console.log(obj)
console.log(obj.a,obj.b);
2.defineReactive函数
defineReactive.js
import Dep from './Dep'
import { observe } from './observe'
export default function defineReactive (data, key, val) {
// console.log('我是defineReactive', key)
const dep = new Dep()
if (arguments.length == 2) {
val = data[key]
}
//子元素要进行observe,至此形成了递归,这个递归不是函数自己调用自己,而是多个函数、类循环调用
let childOb = observe(val)
Object.defineProperty(data, key, {
// writable: false,//是否可写
enumerable: true, //是否可枚举
configurable: true,//是否可配置 如delete
//getter
get () {
console.log('你正在访问' + key + '属性')
if (Dep.target) {
dep.depend()
}
return val
},
//setter
set (newValue) {
console.log('你正在改变' + key + '属性', newValue)
if (newValue == val) {
return
}
val = newValue
childOb = observe(newValue)
dep.depend()
//发布订阅模式,通知dep
dep.notify()
}
})
}
3.递归侦测对象全部属性
utils.js
export const def = function (obj, key, value, enumerable) {
// console.log('我是utils')
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
observe.js
// 创建Observe 函数 注意没有r
import Observer from './Observer.js'
export default function observe (value) {
// console.log('我是observe')
// 如果value不是对象 什么都不做
if (typeof value !== 'object') return
// 定义ob
let ob
if (typeof value.__ob__ !== 'undefined') {
// console.log('0')
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
新建 Observer.js(
将一个正常的Object转换为每个层级的属性都是响应式的object)
import { def } from './utils'
import defineReactive from './defineReactive.js'
import { arrayMethods } from './array'
import { observe } from './observe'
import Dep from './Dep'
// Observer 类 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的object)
export default class Observer {
constructor(value) {
this.dep = new Dep()
//给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)
// value.__ob__ = this
def(value, '__ob__', this, false)
// console.log('我是Observer构造器', value)
//不要忘记初心,Observer类的目的是:将一个正常的Object转换为每个层级的属性都是响应式(可以被侦测的)的object
//检查它是数组还是对象
if (Array.isArray(value)) {
//如果是数组,要非常强行的蛮干,将这个数组的原型,指向arrayMethods
Object.setPrototypeOf(value, arrayMethods)
this.observerArray(value)
} else {
this.walk(value)
}
}
//对象遍历
walk (value) {
for (let k in value) {
defineReactive(value, k)
}
}
//数组遍历
observerArray (arr) {
for (let i = 0, l = arr.length; i < l; i++) {
//逐项进行observe
observe(arr[i])
}
}
}
4.数组的响应式处理(array.js)
import { def } from './utils'
// 得到Array.prototype
const arrayPrototype = Array.prototype
// 以Array.prototype为原型创建arrayMethods对象 并暴露
export const arrayMethods = Object.create(arrayPrototype)
//要被改写的7个数组方法
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsNeedChange.forEach(methodName => {
//备份原来的方法 因为改写的7个函数的功能不能被剥夺
const original = arrayPrototype[methodName]
//定义新的方法
def(arrayMethods, methodName, function () {
//恢复原来的功能
const result = original.apply(this, arguments)
const args = [...arguments]
// 把这个数组身上的__ob__属性取出来
// __ob__已经被添加了? 因为数组肯定不是最高层,比如obj.g属性是数组,第一次遍历obj这个对象的第一层的时候
// 已经给g属性 就是这个数组 添加了__ob__属性
const ob = this.__ob__
// 有三种方法push,unshift,splice能够插入新项,现在要把插入的新项也要observe的
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
// splice格式是(下标,数量,新项)
inserted = args.slice(2)
break
}
// 判断有没有要插入的新项,让新项也变为响应的
if (inserted) {
console.log('判断有没有要插入的新项')
ob.observerArray(inserted)
}
console.log('啦啦啦')
ob.dep.notify()
return result
}, false)
})
5.依赖收集(Dep.js)
1什么是依赖。
2 Dep类和Watcher类
var uid = 0
export default class Dep {
constructor() {
console.log('我是Dep类的构造函数')
this.id = uid++
// 用数组存储自己的订阅者,这个数组里放的是Watcher的实例
this.subs = []
}
// 添加订阅
addSub (sub) {
this.subs.push(sub)
}
// 添加依赖
depend () {
// Dep.target 实际上就是我们自己指定的一个全局位置
if (Dep.target) {
this.addSub(Dep.target)
}
}
// 通知更新
notify () {
console.log('我是notify')
// 浅克隆一份
const subs = this.subs.slice()
// 遍历
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher.js
import Dep from "./Dep.js"
var uid = 0
export default class Watcher {
constructor(target, expression, callback) {
console.log('我是watcher构造函数')
this.id = uid++
this.target = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.getter()
}
update () {
this.run()
}
run () {
this.getAndInvoke(this.callback)
}
getAndInvoke (cb) {
const value = this.get()
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value
this.value = value
cb.call(this.target, value, oldValue)
}
}
get () {
// 进入依赖收集阶段
// 让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this
const obj = this.target
var value
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
}
function parsePath (str) {
let segments = str.split('.')
console.log(segments)
return (obj) => {
for (let i = 0; i < segments.length; i++) {
obj = obj[segments[i]]
}
return obj
}
}
github 仓库地址 webpack5环境
https://github.com/pitersu/vue2.x-