参考:Vue 源码解析系列课程
源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/Data_Reactive_Study
课程目的:彻底弄懂 Vue2 的数据更新原理
数据响应式
MVVM 模式:
侵入式 和 非侵入式:
Object.defineProperty()
参考文档:Object.defineProperty() - JavaScript | MDN (mozilla.org)
Object.defineProperty() 用于数据劫持 / 数据代理,利用 JavaScript 引擎赋予的功能,检测对象属性变化。
该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
var obj = {}
Object.defineProperty(obj, 'a', {
value: 3,
writable: false,
enumerable: true,
})
console.log(obj.a)
obj.a++
console.log(obj.a);
该方法的 getter / setter 需要变量周转才能工作:
var obj = {}
var temp
Object.defineProperty(obj, 'a', {
get() {
console.log('get a');
return temp
},
set(newVal) {
console.log('set a', newVal);
temp = newVal
}
})
obj.a = 1
obj.a++
console.log(obj.a)
自定义一个 defineReactive 函数,使用闭包,就不需要设置临时变量:
export default function defineReactive(obj, key, val) {
if (arguments.length == 2) {
val = obj[key]
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('get', key);
return val
},
set(newVal) {
console.log('set', key, newVal);
if (val === newVal) return
val = newVal
}
})
}
let obj = {}
defineReactive(obj, 'a', 1)
console.log(obj.a);
obj.a = 10
console.log(obj.a);
递归检测对象全部属性
对于如下对象,使用上面的 defineReactive 是无法监听 obj.a.m.n 的属性的(只能监听一层,无法监听多层)
let obj = {
a: {
m: {
n: 1
}
},
b: 1
}
想要达到的效果:通过 observe 使 obj 所有属性都变成响应式的
observe(obj)
obj.b++
obj.a.m.n++
程序流程图:(通过各级函数之间的调用实现了递归的效果)
observe.js :
export default function observe(obj) {
if (!obj || typeof obj !== 'object') return
var ob;
if (typeof obj.__ob__ != 'undefined') {
ob = obj.__ob__
} else {
ob = new Observer(obj)
}
return ob
}
Observer.js :
export default class Observer {
constructor(obj) {
console.log('Observer constructor', obj);
def(obj, '__ob__', this, false)
this.walk(obj)
}
walk(obj) {
console.log('walk', obj);
for (let k in obj) {
defineReactive(obj, k)
}
}
}
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
defineReactive.js :
export default function defineReactive(obj, key, val) {
console.log('defineReactive', key);
if (arguments.length == 2) {
val = obj[key]
}
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return val
},
set(newVal) {
console.log('set', key, newVal);
if (val === newVal) return
val = newVal
childOb = observe(newVal)
}
})
}
效果:
let obj = {
a: {
m: {
n: 1
}
},
b: 1
}
observe(obj)
obj.b++
obj.a.m.n++
数组的响应式原理
对于数组对象,以上实现是无法监听其 push 、pop 等元素修改方法的:
let obj = {
c: [1, 2, 3, 4]
}
改写 7 个方法:push 、pop 、shift 、unshift 、splice 、sort 、reverse
array.js :
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsNeedChange.forEach(methodName => {
const originMethod = arrayPrototype[methodName]
def(arrayMethods, methodName, function () {
console.log('arrayMethods', methodName);
const result = originMethod.apply(this, arguments)
const args = [...arguments]
const ob = this.__ob__
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break;
case 'splice':
inserted = args.slice(2)
break;
}
if (inserted) ob.observeArray(inserted)
return result
}, false)
})
修改 Observer.js 中构造 Observer 的代码:
export default class Observer {
constructor(obj) {
console.log('Observer constructor', obj);
def(obj, '__ob__', this, false)
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayMethods)
this.observeArray(obj)
} else {
this.walk(obj)
}
}
walk(obj) {
console.log('walk');
for (let k in obj) {
defineReactive(obj, k)
}
}
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
使用效果:
let obj = {
c: [1, 2, 3, 4]
}
observe(obj)
obj.c.push(55, 66, 77)
obj.c.splice(1, 1, [1, 2])
console.log(obj.c);
依赖收集
什么是依赖?需要用到数据的地方,称为依赖
- Vue 1.x 中,依赖是细粒度的,用到数据的 DOM 都是依赖
- Vue 2.x 中,依赖是中等粒度的,用到数据的 组件 是依赖
- 在 getter 中收集依赖,在 setter 中触发依赖
Dep 类 和 Watcher 类:
- Dep 类封装了依赖收集的代码,专用用来管理依赖,每个 Observer 的实例,成员中都有一个 Dep 的实例
- Watcher 是一个中介,数据发生变化时通过 Watcher 中转,通知组件
- 依赖就是 Watcher,只有 Watcher 触发的 getter 才会收集依赖,哪个 Watcher 触发了 getter,就会把哪个 Watcher 收集到 Dep
- Dep 使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的 Watcher 都通知一遍
- 代码实现的巧妙之处:Watcher 把自己设置到全局的一个指定位置,然后读取数据,因为读取到了数据,所以会触发这个数据的 getter。在 getter 中就能得到当前正在读取数据的 Watcher,并把这个 Watcher 收集到 Dep 中。
参考文章:Vue深入响应式原理
使用效果:
let obj = {
a: 1,
b: {
m: {
n: 1
}
},
c: [1, 2, 3, 4]
}
observe(obj)
new Watcher(obj, 'b.m.n', val => {
console.log('Watcher 在监控 b.m.n', val)
})
obj.b.m.n++
console.log(obj)
Dep.js :
export default class Dep {
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
notify() {
console.log('Dep notify');
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher.js :
var uid = 0
export default class Watcher {
constructor(target, expression, callback) {
this.id = uid++
this.target = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()
}
update() {
this.run()
}
get() {
Dep.target = this
const obj = this.target
let value
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
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)
}
}
}
function parsePath(str) {
var segments = str.split('.');
return obj => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]];
}
return obj
}
}
其余文件代码省略…
完整源码参考:https://gitee.com/szluyu99/vue-source-learn/tree/master/Data_Reactive_Study
|