引言
Vue2的响应式系统是核心之一,它使得Vue.js能够实现数据驱动的视图变化。其实现主要基于Object.defineProperty API,通过在数据对象上添加属性监听来实现数据变化时对视图进行更新。
vue3实现主要基于Proxy API和Reactive,Reactive函数负责将一个普通的JavaScript对象转换成响应式对象。它通过递归遍历对象的所有属性,并使用Proxy代理对象来实现对属性的拦截。
Vue2.x响应式系统
在Vue.js中,响应式系统主要分为两部分:数据劫持和发布订阅。
数据劫持:通过使用Object.defineProperty API来对数据对象的属性进行劫持,在属性get和set时添加钩子函数,在get时记录依赖,在set时通知观察者更新视图。
发布订阅:Vue.js通过实现一个自己的发布订阅模型来实现响应式系统,通过依赖收集器来收集所有依赖,并在依赖变化时触发通知器进行视图更新。
具体来说,Vue2.x的响应式原理主要是通过Observer、Dep和Watcher三个核心组件来实现的。
Vue2.x源码解析
下面是Vue2.x响应式原理源码解析:
1. Observer:
用于收集数据属性的依赖,并在数据发生变化时通知订阅者进行更新。
Observer负责将一个普通的JavaScript对象转换成响应式对象。它通过递归遍历对象的所有属性,并使用Object.defineProperty
方法为每个属性设置getter和setter。在getter中,Observer会收集当前正在执行的Watcher作为依赖。在setter中,Observer会触发依赖更新,并通知相关的Watcher进行更新。
class Observer {constructor(value) {// this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)if (isArray(value)) {// 数组处理// ...} else {// 对象处理const keys = Object.keys(value)for (let i = 0; i < keys.length; i++) {const key = keys[i]defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)}}}}
在Observer构造函数中,创建一个Dep实例作为依赖收集器。然后,通过
def
函数将Observer实例添加到value对象的__ob__
属性上,这样可以在后续操作中方便地获取到Observer实例。接下来,根据value的类型进行不同的处理。如果是数组,则调用数组处理逻辑;如果是对象,则调用对象处理逻辑。
在对象处理逻辑中,通过
Object.keys
方法获取对象的所有属性,并遍历每个属性,调用defineReactive
函数为每个属性设置getter和setter。
2. Dep(依赖收集器)
用于存储一个或多个依赖关系,在数据发生变化时通知订阅者进行更新。
Dep是一个用于收集依赖和触发更新的类。每个响应式对象都会有一个对应的Dep实例,用于管理该对象所有属性的依赖关系。在getter中,Watcher会将自身添加到Dep实例中,表示该Watcher依赖于该属性。在setter中,Dep实例会通知所有依赖于该属性的Watcher进行更新。
class Dep {...constructor() {this.subs = []}addSub(sub) {this.subs.push(sub)}removeSub(sub) {remove(this.subs, sub)}depend() {if (Dep.target) {Dep.target.addDep(this)}}notify() {const subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {const sub = subs[i]// ...subs.update()}}}
在Dep类中,subs数组用于存储所有依赖(即Watcher)。
addSub
方法用于将一个依赖添加到subs数组中。removeSub
方法用于从subs数组中移除一个依赖。depend
方法用于将当前正在执行的Watcher添加到Dep实例中。notify
方法用于触发所有依赖(即Watcher)进行更新。
3. Watcher(观察者)
用于订阅一个或多个依赖关系,在依赖发生变化时执行相应的回调函数。
Watcher是一个用于订阅和接收属性变化通知的类。它负责创建一个订阅者,并将自身添加到当前正在执行的Dep实例中。当属性发生变化时,Dep实例会通知所有订阅者进行更新。
class Watcher {constructor(vm, expOrFn, cb) {if ((this.vm = vm) && isRenderWatcher) {vm._watcher = this}if (isFunction(expOrFn)) {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = noop}}this.value = this.lazy ? undefined : this.get()}get() {pushTarget(this)let valueconst vm = this.vmtry {value = this.getter.call(vm, vm)} catch (e) {// ...} finally {if (this.deep) {traverse(value)}popTarget()this.cleanupDeps()}return value}addDep(dep) {const id = dep.idif (!this.newDepIds.has(id)) {this.newDepIds.add(id)this.newDeps.push(dep)if (!this.depIds.has(id)) {dep.addSub(this)}}}update() {if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}run() {if (this.active) {const value = this.get()if (value !== this.value ||isObject(value) ||this.deep) {// ...this.cb.call(this.vm, value, oldValue)}}}}
在Watcher构造函数中,首先将传入的vm、expOrFn和cb保存到实例的对应属性上。expOrFn可以是一个函数或一个字符串,如果是字符串,则会通过parsePath
方法将其解析为一个函数。
get
方法用于获取属性的值。在get方法中,会将当前Watcher添加到全局的targetStack中,并将Dep.target设置为当前Watcher。然后通过调用getter方法获取属性的值,并在过程中收集依赖。最后,将Dep.target恢复为上一个Watcher,并返回属性的值。
addDep
方法用于将依赖(即Dep实例)添加到当前Watcher中。在addDep方法中,会判断该依赖是否已经被添加过,如果没有,则将其添加到newDeps数组和newDepIds集合中,并判断是否已经被订阅过,如果没有,则调用dep.addSub(this)将当前Watcher添加到依赖(即Dep实例)中。
update
方法用于触发更新操作。在update方法中,会调用run方法进行更新。
run
方法用于执行更新操作。首先获取最新的属性值,并与旧值进行比较。如果不相等或新值是对象或this.deep为true,则调用回调函数cb进行更新操作。
Vue3源码解析
在Vue3的源码中,createReactiveObject
函数是reactive.ts
文件中的核心部分,负责创建响应式对象。路径packages/reactivity/src/reactive.ts
createReactiveObject
function createReactiveObject(target: Target,isReadonly: boolean,baseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>,proxyMap: WeakMap<Target, any>): any {// 检查目标对象是否为非对象类型,如果是则直接返回if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)}return target}// 如果目标对象已经是一个Proxy,则直接返回它// 例外情况:在一个响应式对象上调用readonly()函数if (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// 检查目标对象是否已经存在对应的代理对象,如果存在则直接返回缓存的代理对象const existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// 只有特定类型的值可以被观察const targetType = getTargetType(target)if (targetType === TargetType.INVALID) {return target}// 创建一个新的代理对象proxyconst proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)// 将代理对象proxy缓存到proxyMap中proxyMap.set(target, proxy)return proxy}
在这个函数中,首先会检查目标对象是否为非对象类型,如果是则直接返回。然后会检查目标对象是否已经存在对应的代理对象,如果存在则直接返回缓存的代理对象。
接下来,会根据传入的参数选择相应的处理器(baseHandlers或collectionHandlers),并使用new Proxy
创建一个代理对象proxy。
最后,将代理对象proxy缓存到proxyMap中,并返回该代理对象。
通过这个函数,Vue3实现了对目标对象的响应式转换,并缓存了代理对象以避免重复创建。
effect
文件路径packages/reactivity/src/effect.ts
在Vue3的源码中,effect.ts
文件包含了effect
、trigger
和track
这些核心源码,它们在Vue3的响应式系统中扮演着重要的角色。下面是对这些核心源码及其作用的大致讲解:
export function effect<T = any>(fn: () => T,options?: ReactiveEffectOptions): ReactiveEffectRunner {if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {fn = (fn as ReactiveEffectRunner).effect.fn}const _effect = new ReactiveEffect(fn)if (options) {extend(_effect, options)if (options.scope) recordEffectScope(_effect, options.scope)}if (!options || !options.lazy) {_effect.run()}const runner = _effect.run.bind(_effect) as ReactiveEffectRunnerrunner.effect = _effectreturn runner}
effect
函数用于创建一个副作用函数,该副作用函数会自动追踪其依赖,并在依赖变化时自动重新执行。effect(fn, options)
接受一个函数fn
和一个可选的选项对象options
。- 在内部,它通过调用
createReactiveEffect(fn, options)
创建了一个ReactiveEffect
实例_effect
。 - 如果选项中没有设置
lazy: true
(即立即执行),则会调用_effect.run()
来执行副作用函数。 - 最后,创建并返回一个运行器
runner = _effect.run.bind(_effect)
,并将_effect
实例赋值给运行器的属性runner.effect = _effect
。
track
track(target, key)
函数用于收集当前正在执行的副作用函数的依赖。- 它通过调用
track(target, key)
来进行依赖收集。 - 在内部,它使用了一个名为
targetMap
的 WeakMap 来存储依赖关系。它以目标对象为键,以属性的依赖集合为值。 - 当访问响应式对象的属性时,会获取当前正在执行的副作用函数,并将其添加到对应属性的依赖集合中。
trigger
trigger
函数用于触发依赖更新,即执行所有依赖该属性的副作用函数。- 在内部,它使用了一个名为
targetMap
的 WeakMap 来获取存储在追踪阶段收集到的依赖关系。 - 它遍历所有相关联的副作用函数,并执行它们。
通过这些核心源码,Vue3实现了响应式系统中的副作用追踪和依赖更新。effect
函数用于创建副作用函数,track
函数用于收集依赖,trigger
函数用于触发更新。它们共同协作,实现了Vue3的响应式原理。
总结
Vue2和Vue3在响应式系统的实现上有一些重要的区别,下面是它们之间的主要区别:
实现方式:
- Vue2使用Object.defineProperty来实现响应式。它通过在对象上定义getter和setter来拦截对属性的访问和修改,从而实现依赖收集和触发更新。
- Vue3使用Proxy来实现响应式。Proxy是ES6中新增的特性,它可以拦截对象上的各种操作,包括属性访问、修改、删除等。Vue3利用Proxy的强大拦截能力来追踪依赖并触发更新。
性能优化:
- Vue2在每个组件实例化时都会为数据对象进行递归遍历,并为每个属性设置getter和setter。这样会导致初始化时的性能开销较大。
- Vue3通过Proxy实现了惰性创建副作用函数(effect),只有当副作用函数被真正使用时才会进行依赖收集。这样可以减少不必要的依赖收集和更新操作,提高了性能。
依赖追踪:
- Vue2使用全局变量Dep来追踪依赖关系,并将Watcher与Dep进行关联。每个属性都有一个对应的Dep实例,当属性被访问时,Watcher会将自身添加到Dep中,当属性发生变化时,Dep会通知所有关联的Watcher进行更新。
- Vue3使用WeakMap来存储依赖关系,将对象作为键,将属性的依赖集合作为值。这样可以避免内存泄漏,并且不需要全局变量来追踪依赖。
嵌套属性和数组:
- Vue2对于嵌套属性和数组的处理较为复杂。对于嵌套属性,需要递归调用Observer进行响应式转换;对于数组,需要重写数组的一些方法来拦截变更操作。
- Vue3通过Proxy的拦截能力可以直接处理嵌套属性和数组。无需递归调用Observer或重写数组方法。
TypeScript支持:
- Vue3对TypeScript提供了更好的支持,并且在源码中使用了大量的TypeScript类型定义,提高了开发效率和代码可靠性。
总体而言,Vue3在响应式系统上进行了一系列改进和优化,提升了性能、可维护性和开发体验。同时引入Composition API以及对TypeScript的支持也使得开发更加灵活和可靠。