Vue2响应式原理(二)

原理(进阶——vue2)

一.defineReactive函数——响应式操作

1.Object.defineProperty()函数

要进行响应式操作就需要使用Object对象上的一个方法defineProperty()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
//其接收三个参数
//obj
//要定义属性的对象。
//prop
//要定义或修改的属性的名称或 Symbol 。
//descriptor
//要定义或修改的属性描述符。
Object.defineProperty(obj,key,{
configurable:true,
enumerable:true,
writable:true,
//value
get(){
return;
},
set(val){
console.log(val);
}
})
//注意:set,get函数不能与value,writable同时使用
而我们的defineReactive函数则是对defineProperty进行了简单的封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function defineReactive(obj,key,value) {
//此处判断传入参数的个数 如果传入两个参数 进行value = obj[key]操作以便后面对一个对象身上含有多层属性进行递归
if(arguments.length == 2){
value = obj[key]
}
Object.defineProperty(obj,key,{
configurable:true,
enumerable:true,
get(){
console.log('访问'+key+'属性');
return value;
},
set(val){
console.log('修改'+key+'属性');
value = val
}
})
}

二.Observer类——观察者

1.observe函数

我们需要一个函数来判断一个数据是否需要为其进行响应式操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function observe(value) {
//判断传入的value是否为一个对象
if(typeof value !== 'object'){
return ;
}else{
let ob
//判断对象身上是否有一个__ob__属性 这个属性里面存放该对象的Observer类用于观察对象身上的属性
if(value.__ob__){
ob = value.__ob__
}else{
ob = new Observer(value)
}
}
}
我们需要在defineReactive函数内部去调用它 1.给子对象添加观察 2.给新的值添加观察
1
2
3
4
5
6
7
8
9
10
11
12
function defineReactive(obj,key,value) {
//给子对象添加观察
let childOb = observe(value)
Object.defineProperty(obj,key,{
get(){
},
set(val){
//给新的值添加观察
childOb = observe(val)
}
})
}

2.def函数

我们需要在Oberver中对传入的对象/数组添加一个属性,属性中存放一个Oberver实例用于观察这个对象的数据变化
1
2
3
4
5
function def(obj,key,value) {
Object.defineProperty(obj,key,{
value
})
}

3.Observer类

在Observer类中我们需要对属性进行观察 并且我们需要在observe中实例化他
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Observer{
constructor(value){
def(value,'__ob__',this)
if(Array.isArray(value)){
this.arrWalk(value)
Object.setPrototypeOf(value,arrMethods);
}else{
this.walk(value)
}
}
walk(obj){
for (const key in obj) {
defineReactive(obj,key)
}
}
arrWalk(arr){

}
}

4.数组的观察——重写方法

由于数组与对象不同   defineReactive无法监测他的增删添改 因此我们需要对数组原型上的七种方法进行重写,方便我们监听它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const methodsNeedChange = [//要重写的七种方法
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
//将Array原型绑定到arrMethods上 即arrMethods.prototype === Array.prototype
let arrMethods = Object.create(Array.prototype)
methodsNeedChange.forEach(
methods => {
//保存原来的七个方法
let oldMethods = Array.prototype[methods]
//重写方法 用def监测
def(arrMethods,methods,function () {
//当我们调用Array原型上的方法时,此时调用未重写的方法对数组进行正确的操作
let result = oldMethods.apply(this,arguments)
let newArr = []//存放改变后的数组
let ob = this.__ob__
let arr = [...arguments]//防止是类数组无法调用slice方法
switch (methods) {
case 'push':
case 'unshift':
newArr = arguments
break;
case 'slice':
newArr = arr.slice(2)
break;
}
if(newArr){
//在这里响应
console.log('使用了'+ oldMethods.name +'方法');
//如果数据变化 把新的数据更新
ob.arrWalk(newArr)
}
return result
})
}
)
而此时arrMethods上就存放了可以被监听的七种方法  我们在需要这七种方法时,也就是observe时将传数组的原型改成arrMethods,这样我们的数组调用数组的方法也就能够被监听到   如下:
1
2
3
4
5
6
arrWalk(arr){
for(let i = 0,l = arr.length;i<l;i++){
//逐项进行observe
observe(arr[i]);
}
}

三.Dep类——依赖收集

在vue中,可能我们在vue实例内部定义的数据不一定会同时使用数据,我们没必要给未使用的数据进行响应式处理.因此我们需要判断那些数据是正在被使用的,然后打上标记,这个标记要求是在全局唯一的位置进行存放.一般来说我们选择Dep类,标记为Dep.target当然你也可以选择window对象来存放.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Dep{
constructor(){
this.subs = [] //定义一个数组用来存放依赖——Watcher类的实例
}
notify(){
let sub = this.subs.slice() //浅拷贝一份依赖数组
for(let i = 0; i < sub.length; i++){
//遍历数组 通知所有观察者数据更新
sub[i].update()
}
}
depend(){
if(Dep.target){
this.addSub(Dep.target)
}
}
addSub(sub){
this.subs.push(sub)
}
}
我们需要在defineReactive函数,Observer类中实例化Dep类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function defineReactive(obj,key,val) {
let dep = new Dep()
Object.defineProperty(obj,key,{
get(){
//在getter收集依赖
if(Dep.target){
dep.depend()
if(childOb){
childOb.dep.depend()
}
}
return val
},
set(newValue){
//在setter发布更新
dep.notify()
}
})
}
class Observer{
constructor(value){
this.dep = new Dep()
}
}

四.Watcher类——订阅者

Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例回调cb进行视图的更新。

1.Watcher类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Watcher{
constructor(target,expression,callback){
this.target = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()//获取访问的属性的值
}
get(){
//将这个Watcher实例存放到Dep.target上
Dep.target = this
let obj = this.target
var value
try {
//通过getter方法获取到最新的value值
value = this.getter(obj)
} catch (error) {
throw new Error('报错了~~~')
}finally{
Dep.target = null //让这个实例从Dep.target上推下,让其他的实例上位
}
return value
}
run(){
this.getAndInvoke(this.callback)
}
update(){
this.run()
}
getAndInvoke(cb){
let value = this.get()
if(value !== this.value || typeof value == 'object'){
let oldVlaue = this.value
cb.call(this.target,value,oldVlaue)
}
}
}

2.parsePath函数——根据提供的路径寻找对象身上的属性

1
2
3
4
5
6
7
8
9
function parsePath(str) {
let arr = str.split('.')
return obj => {
for(let i = 1; i<arr.length; i++){
obj = obj[arr[i]]
}
return obj
}
}

Vue2响应式原理(二)
https://jing-jiu.github.io/jing-jiu/2021/06/05/Framework/Vue2/响应式原理-2/
作者
Jing-Jiu
发布于
2021年6月5日
许可协议