「深入」Vue源码学习(三)- 手写耳熟能详的数据响应式原理

(封面图片来源于网络,侵删)
「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

Vue源码学习系列已更新两篇,可参阅。

「深入」Vue源码学习(一)- 运行与调试

「深入」Vue源码学习(二)- 实例选项对象初始化

使用过Vue的前端开发者,都对其数据实时变化的这一特性爱不释手。Vue将这一特性称之为数据响应式,数据响应式作为Vue最独特的特性之一,给前端开发提供了很大的便利,但同时它又存在一定的缺陷,以至于在Vue3中,响应式原理相较于Vue2做了很大的更改。Vue3的响应式原理后面再探讨。那到底什么是响应式原理呢?

1、什么叫做响应

百度百科中响应的意思,指其如回响的应答声,如“响应你的要求”。具体意思是:1、回声响应。2、比喻应答敏捷。3、反应。可以看出,响应一词本身就是指某种反应,某种应答。结合Vue实践,我们可以发现,当操作数据时,页面中对应数据也发生了变化,这种变化在用户角度来说,是实时的、无感知的。

那数据会发生变化,这种变化如何追踪?

1.1 追踪数据变化

Vue在设计时,巧妙的想到了使用Object.defineProperty()这个方法,我们知道该方法可以直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。这个方法中有两种方式可以修改现有对象的属性,一种是通过设置Writable属性,进行更改。一种则是通过属性的setter函数进行修改。设置Writable属性的方式,只是单纯更改当前属性的值,而不能做其他复杂逻辑,显然,自定义setter函数最合适。

当访问对象某个属性时,会调用get函数,当修改对象某个属性值时,会调用set函数。Vue对data选项对象中的每个属性,利用Object.defineProperty()增加getter/setter属性,就能轻易追踪到数据变化了。这里我们先不谈依赖注入模式。仅仅看一下源码中如何使用Object.defineProperty()的。下面的代码中,我抽取了主要的内容,描述主要流程。

初始化选项对象一文已经提到,在initData中,主要对数据进行初始化处理,初始化过程里有一个很重要的内容,就是为data对象属性增加getter/setter,因此定位源码可以看到,从initData()-> observe() -> new Observer() -> walk() -> defineReactive() -> Object.defineProperty(),属性的getter/setter也就成功增加进来了。

// 初始化数据函数
function initData (vm) {
    // 获取传入的data对象
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    observe(data, true);
}

// observe函数
function observe (value, asRootData) {
    ob = new Observer(value);
}

// Observer
var Observer = function Observer (value) {
    this.walk(value);
}

// walk方法
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
};

// defineReactive中定义了具体的get/set操作
function defineReactive (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    return value
  },
  set: function reactiveSetter (newVal) {
    var value = getter ? getter.call(obj) : val;
  }
});
}
复制代码
1.2 追踪过程中的特殊性
  • data对象属性外的设置:由于Vue,只能对data对象上存在的属性增加getter/setter转化,对于其他新设置的属性,如给vue对象直接增加某个属性(vm.prototype1 = ''),这时vue是无法追踪该属性变化情况的。如果想要把该属性增加到响应式系统中,则需要使用Vue.set()或者vm.$set()方法,通过源码,其实我们可以发现该set方法的本质也是去调用了defineReactive()方法。
// Vue.set方法中,通过调用defineReactive为属性增加setter/getter属性
function set (target, key, val) {
    defineReactive(ob.value, key, val);
}
复制代码
  • 数组:Object.defineProperty()仅仅对对象有效,那么如果data某个属性是数组形式该如何响应呢。这里Vue通过数据劫持将能改变数组的方法push、pop、shift、unshift、splice、sort、reverse,进行了重写。同时在后续为其增加依赖监听,完成数组的响应式。
var arrayProto = Array.prototype;  // 获取数组的所有方法和属性
var arrayMethods = Object.create(arrayProto); // 创建新的对象
// 需要重写的方法,这些方法都有一个共同点是能够改变原始数组
var methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

methodsToPatch.forEach(function (method) {
// 重写上面定义的7种方法
    def(arrayMethods, method, function mutator () {
    }
}
复制代码

2、响应后的反馈

我们知道,数据会在多个组件中进行共享,同时会存在多种触发数据变化的方式。当数据变化后,如何做到所有用到该数据的地方都能实时更新呢,这就必须考虑到依赖收集与通知更新的概念了。

2.1 依赖收集

依赖收集,顾名思义就是将所有改变数据的东西都放在一起,在vue当中相当于Watcher实例。即在执行get函数时,会做依赖收集的动作,将Watcher实例增加到依赖收集器Dep中。

Object.defineProperty(obj, key, {
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend() // 将Watcher增加到Dep依赖收集器中
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  },
 }

// Dep依赖收集器中新增Watcher实例
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

// Watcher实例
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};
复制代码
2.2 更新数据

数据变化后会执行set函数,通知上面提到的依赖收集器中的所有Watcher实例,更新数据。调用dep.notify()方法,该方法通知watcher实例,实例调用update()函数就完成了数据更新。

Object.defineProperty(obj, key, {
    set: function reactiveSetter (newVal) {
        dep.notify()  // 依赖收集器中的notify方法,通知收集的watcher实例更新数据
    }
 }
 
 // Dep中的notify方法
subs: Array<Watcher>;
notify () {
    // 调用watcher中的update更新数据
    subs[i].update()
}

// Wwatcher去更新数据
Watcher.prototype.update = function update () {
}
 
复制代码

从上面的流程梳理中可以看出,依赖收集-通知更新这个流程,刚好和观察者模式相符,Watcher相当于观察者、data属性相当于被观察者、Dep作为两者中间的桥梁将两者结合起来。

3、手写实践

3.1 观察者模式

观察者模式指的是对象间存在一对多关系,当一个对象被修改时,会自动通知依赖他的对象。其属于一种行为模式。详情可在此参考。 观察者模式在代码上如何体现呢?

// 被观察者
class Subject  {
    constructor(name) {
        this.name = name,
        this.observers = []
        this.state = '奔跑速度50km/h'
    }
    // 被观察者上绑定观察者
    attach(o) {
        this.observers.push(o)
    }
    // 被观察者状态发生变化
    setState(newState) {
        this.state = newState
        this.observers.forEach(o => o.update(this))
    }

}

// 观察者
class Observe{
    constructor (name) {
        this.name = name
    }
    update(s) {
        console.log(`我是${this.name},速度提升为`,s.state)
    }
}

const sub = new Subject('千里马')
const observe = new Observe('伯乐')
sub.attach(observe)
sub.setState('60km/h')
// 定是函数模拟状态变化
setTimeout(function() {
    sub.setState('70km/h')
}, 1000)
复制代码
结果打印:
我是伯乐,速度提升为 60km/h
我是伯乐,速度提升为 70km/h
复制代码
3.2 vue中如何体现

这里写出大概实现逻辑,具体的后续再优化。

  • 被观察者
// 被观察者
let horse = {}
speed= 60;
Object.defineProperty(horse, 'speed', {
    enumerable: true,
    configurable: true,
    set(newValue) {    
        speed = newValue
        dep.notify(horse); // 更新数据时,进行通知
    },
    get() {
        dep.depend(watcher) // 读取数据时,进行依赖收集
        return speed
    }
})
console.log("设置速度值以前,为"+horse.speed)
horse.speed = 70;
复制代码
  • 观察者
// 观察者
class Watcher{
    constructor (name) {
        this.name = name
    }
    update(horse) {
        // 更新后读取数据
        console.log(`我是${this.name},速度提升为${horse.speed}`)
    }
}
复制代码
  • 依赖收集器
// 依赖收集器
class Dep  {
    constructor(name) {
        this.name = name,
        this.watchers = []
    }
    depend(o) {
        // 依赖收集watcher,这里还可以优化成调用watcher中的addWatcher方法
        this.watchers.push(o)
    }
    notify(horse) {
        if (this.watchers.length > 0) {
        // update更新watcher
            this.watchers.forEach(o => o.update(horse))
        }
    }
}
复制代码
结果打印:
设置速度值以前,为60km/h
我是伯乐,速度提升为70km/h
复制代码
3.3 总结
  • 通过Object.defineProperty()方法将data属性转变成具有getter/setter属性的被观察者。
  • 通过Watcher观察属性变化,同时更新最新值。
  • 通过Dep依赖收集器,连接观察者与被观察者,提供依赖收集和通知方法。
  • 被观察者只要变化,就可自动更新。

4、弊端

响应式原理虽然对数据变化提供了很大的便利,但同时也存在弊端。

  • 使用Object.defineProperty()对复杂数据结构会进行深度遍历,这种深度监听,会消耗大量性能。
  • 对数组的响应式不支持,需要重写数组方法。
  • 除了data上定义的属性之外,其他的属性都无法具备响应式特点。

今天这篇文章,侧重用最简便的方式描述vue的响应式原理,所有源码的分析与手写实践上都只提供了主要流程,试图用最简便的方式让大家能看懂。有的在自己的理解上可能存在偏差,欢迎大家指正。

猜你喜欢

转载自juejin.im/post/7063384773539921957