
数据变化,视图会自动变化。
侵入式就是调用一些api使得当数据变化的时候,视图会跟着变化。然后Vue使用的是非侵入式。

数据劫持
基础使用:
js
//数据劫持,数据变化都是由defineProperty 内部的成员控制
Object.defineProperty(data,key, {
// writable:true, // writable 会与下面的get和set冲突
// value:3, // 也不可以同时指定 value 属性
enumerable:true, // 是否可以被枚举
configurable: true, // 是否可以被删除
// 访问 key 属性的时候会触发get
get () {
//return 值就是 key属性的值
},
// 修改 key 属性的时候会触发set
set(){
// 将newValue赋值给临时变量,然后get再将临时变量返回
}
})
封装:
js
function defineReactive(data, key, val){
//数据劫持,数据变化都是由defineProperty 内部的成员控制
Object.defineProperty(data,key, {
// writable:true, // writable 会与下面的get和set冲突
// value:3, // 也不可以同时指定 value 属性
enumerable:true, // 是否可以被枚举
configurable: true, // 是否可以被删除
// 访问 key 属性的时候会触发get
get () {
console.log('访问属性'+key)
//return 值就是 key属性的值
return val
},
// 修改 key 属性的时候会触发set
set(newVal){
console.log('修改属性'+key)
// 将newValue赋值给临时变量,然后get再将临时变量返回
if (newVal === val) {
return
}
val = newVal;
}
})
}
let obj = {}
defineReactive(obj,'a',1)
console.log(obj.a)
obj.a++
console.log(obj.a)

递归侦听对象全部属性

接下来就要上强度了(😥呜呜呜~)。

defineReactive.js
这个方法像上面讲的一样,就是用来做数据劫持的。用observe方法监视子节点的变化(数据劫持),就是为子节点设置__ob__
对象属性,而这个属性其实就是Observer类的实例对象。
js
import observe from "./observe.js";
export default function defineReactive(data, key, val) {
console.log('我是defineReactive',key)
// 当有两个参数参入,val就是data子元素(下一层嵌套)
if (arguments.length === 2) {
val = data[key]
}
// 子元素进行 observe,形成递归函数和类
let childOb = observe(val)
//数据劫持,数据变化都是由defineProperty 内部的成员控制
Object.defineProperty(data, key, {
// writable:true, // writable 会与下面的get和set冲突
// value:3, // 也不可以同时指定 value 属性
enumerable: true, // 是否可以被枚举
configurable: true, // 是否可以被删除
// 访问 key 属性的时候会触发get
get() {
console.log('访问属性' + key)
//return 值就是 key属性的值
return val
},
// 修改 key 属性的时候会触发set
set(newVal) {
console.log('修改属性' + key)
// 将newValue赋值给临时变量,然后get再将临时变量返回
if (newVal === val) {
return
}
val = newVal;
// 当设置了新值,这个新值也要被observe
childOb = observe(newVal)
}
});
}
index.js
js
import observe from "./observe.js";
let obj = {
a: {
b: {
c:2
},
d: 1
},
g: 4
}
observe(obj)
obj.a.b = 20
observe.js
为传入的对象配置监听(劫持)属性,其实是配置一个告诉其他人这个是被数据劫持的对象属性 的属性。而这个配置标志是__ob__
,并在内部创建Observer实例。
js
import Observer from "./Observer.js";
export default function observe(value) {
if (typeof value !== 'object') return
let ob
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
Observer.js
这是设置响应式的观察者,他主要观察所有的对象属性,为他们添加响应式。
可以发现有一个def方法,她存在的一个原因是为了添加上面的__ob__
属性时,保证这个属性不可被遍历,而创建的一个工具函数(配置部分属性);另一个原因就是我们熟知的数据劫持。
js
//将Object内部的每一个属性都进行数据劫持,使他们都具有响应式
import {def} from "./utils.js";
import defineReactive from "./defineReactive.js";
export default class Observer {
constructor(value) {
// 给实例添加__ob__属性,值是 new 的一个实例(构造函数中的this不是表示类本身,而是表示实例本身)
def(value, '__ob__', this, false)
console.log('我是Observer构造器', value)
this.walk(value)
}
//遍历每一个成员属性,将每一个属性都设置为defineReactive
walk(value) {
for (let valueKey in value) {
defineReactive(value, valueKey)
}
}
}
utils.js
配置不可遍历属性和绑定数据劫持。
js
//对单个需要进行数据劫持的数据 配置部分属性
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
};

数组的响应式处理
Vue2对数组的七个方法进行了重写。所有我们自己定义的数组将不再直接调用原型Array.prototype上的这些方法。而是走一条新的原型链:arr --__proto__
-> arrayMethods --__proto__
-> Array.prototype。
对数组的操作只能通过该响应式处理,直接修改操作都会使其丢失响应式。
array.js
js
import {def} from "./utils";
//要改写的七个方法
const methodsNeedChange = [
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
]
const arrayPrototype = Array.prototype
// arrayMethods.__proto__ = Array.prototype
// 创建新的原型,重写Array 的七个方法
export const arrayMethods = Object.create(arrayPrototype)
methodsNeedChange.forEach(methodName => {
// 备份原来的方法
const original = arrayPrototype[methodName]
//定义新的的方法
def(arrayMethods, methodName, function () {
// 把类数组对象变为数组
const args = [...arguments]
// 将数组身上的__ob__属性取出来(数组肯定是对象内部的非第一层的某一层的属性,所以__ob__一定已经被添加到了该数组身上)
const ob = this.__ob__
// push unshift splice 会插入新项,现在需要把这些新项也变成响应式的
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
//slice(下标,数量,插入的新项)
inserted = args.slice(2)
break
}
//判断有没有要插入的新项, 将新项变成响应式
if (inserted) {
ob.observeArray(inserted)
}
// 恢复原来功能
const res = original.apply(this, arguments)
return res
}, false);
})
Observer.js
js
//将Object内部的每一个属性都进行数据劫持,使他们都具有响应式
import {def} from "./utils.js";
import defineReactive from "./defineReactive.js";
import {arrayMethods} from "./array.js";
import observe from "./observe";
export default class Observer {
constructor(value) {
// 给实例添加__ob__属性,值是 new 的一个实例(构造函数中的this不是表示类本身,而是表示实例本身)
def(value, '__ob__', this, false)
console.log('我是Observer构造器', value)
// 判断是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,将数组的原型指向arrayMethods
Object.setPrototypeOf(value, arrayMethods)
// 对数组进行 observe
this.observeArray(value)
} else {
this.walk(value);
}
}
//遍历每一个成员属性,将每一个属性都设置为defineReactive
walk(value) {
for (let valueKey in value) {
defineReactive(value, valueKey)
}
}
//数组遍历,使每一个方法都被劫持
observeArray(){
//l = arr.length防止数组的长度在遍历图中变化
for (let i = 0,l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
依赖收集


从这里往后又到了另一个难点🥲
Watcher.js
每个Watcher
实例订阅一个或者多个数据,这些数据也被称为wacther
的依赖;当依赖发生变化,Watcher
实例会接收到数据发生变化这条消息,之后会执行一个回调函数来实现某些功能。
js
import Dep from "./Dep";
let uid = 0
export default class Watcher {
constructor(target, expression, callback) {
// target: 数据对象 obj
// expression:表达式,如b.c,根据target和expression就可以获取watcher依赖的数据
// callback:依赖变化时触发的回调
console.log('Watcher类的构造器')
this.id = uid++
this.target = target
this.getter = parsePath(expression)
this.callback = callback
// 订阅数据
this.value = this.get()
}
update() {
this.value = parsePath(this.target, this.getter) // 对存储的数据进行更新
this.callback()
}
get() {
//进入依赖收集 让全局的Dep.target设置为Watch本身,那么就是进入了依赖收集
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(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
那么,Watcher 如何与前面劫持的数据发生关系呢?我们进行了如下操作:
- 有一个数组来存储
watcher
实例 watcher
实例需要订阅数据,也就是收集依赖watcher
的依赖发生变化时,触发watcher
的回调函数,也就是派发更新。
然后,就可以引入dep的概念了。
当我们实例化watcher
时,会执行get
方法,get
方法的作用就是获取自己依赖的数据,也就是触发了getter
。我们把watcher
收集起来那不就是依赖的收集吗?那么这些依赖收集到哪里呢?答案是收集到dep
中。
所以此时应该是watcher
收集了依赖,而dep
收集了watcher
!
当我们实例化watcher 的时候,getter读取不到这个实例。解决方法是将watcher放到全局,比如window.target上。
defineReactive.js
js
import observe from "./observe.js";
import Dep from "./Dep";
export default function defineReactive(data, key, val) {
const dep = new Dep()
console.log('我是defineReactive',key)
if (arguments.length === 2) {
val = data[key]
}
// 子元素进行 observe,形成递归函数和类
let childOb = observe(val)
//数据劫持,数据变化都是由defineProperty 内部的成员控制
Object.defineProperty(data, key, {
// writable:true, // writable 会与下面的get和set冲突
// value:3, // 也不可以同时指定 value 属性
enumerable: true, // 是否可以被枚举
configurable: true, // 是否可以被删除
// 访问 key 属性的时候会触发get
get() {
console.log('访问属性' + key)
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
//return 值就是 key属性的值
return val;
},
// 修改 key 属性的时候会触发set
set(newVal) {
console.log('修改属性' + key)
// 将newValue赋值给临时变量,然后get再将临时变量返回
if (newVal === val) {
return
}
val = newVal;
// 当设置了新值,这个新值也要被observe
childOb = observe(newVal)
//发布订阅模式,通知 dep
dep.notify()
}
});
}
说了这么多,是时候将dep放出来了。
Dep.js
js
let 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()
}
}
}
总结一下
