Vue3源码分析——数据侦测
Vue3.0发布beta版本了,还是来凑个热闹看看源码。本系列大概会有三篇文章,包括应用整体流程、新的响应式系统、组合式API相关内容。
Vue的一个特点就是数据响应式系统,由于这是一个比较独立的系统,因此我们先从这里开始,看看新的Proxy是如何实现数据依赖收集和通知的。
本文内容有点长,可以按目录分章节阅读。
参考:
预备知识
在学习之前需要了解一些JavaScript预备知识:Reflect
和Proxy
。
Reflect
Reflect
封装一些对象的操作,主要是为了整合之前JS中存在的一些不太规范的地方
感觉Reflect的作用主要是把一些零散的工具函数整合在Reflect
这个对象上,如in
、delete
等运算符和Function.prototype.apply
等API
一个比较有意思的功能是Reflect.get
的第三个参数receiver
var a = {
name: 'a',
get greet(){
return 'hello ' + this.name
}
}
console.log(a.greet); // "hi, I am a"
var receiver = {
name: 'receiver'
}
// receiver 修改类似于 通过函数的apply修改内部this指向一样,不过这里修改的是访问对象
// 通过这个功能,可以实现新对象借助原对象部分属性和方法的功能
var res = Reflect.get(a, 'greet', receiver); // "hi, I am receiver"
console.log(res)
JavaScript Proxy
为了收集数据依赖、以及在数据变化时更新视图,Vue2通过defineProperty
劫持set和get属性访问描述符。
defineProperty
存在一些问题,最常见的问题就是无法监听对象以及数组动态添加的属性,即使Vue2重写了数组原型相关方法,但仍旧无法监听到arr[1]=xxx
这种形式。
Vue3使用了ES6新增的Proxy
接口来替代defineProperty
。在本章节主要了解Proxy的基本使用、一些特殊用法和Proxy本身存在的缺点。
Proxy 对象用于定义基本操作的自定义行为
const p = new Proxy(target, handler)
其中handler
对象是一个容纳一批特定属性的占位符对象,它包含有 Proxy 的各个捕获器trap,如
set、
get`等
一些需要注意的细节
set方法应该返回一个布尔值,在严格模式下,如果
set
方法返回falsish
(包括undefined、false等),会抛出异常,这些细节比较麻烦,可以通过Reflect
来处理如果代理对象是数组,当调用
push
、pop
等方法时,不仅会改变数组元素,也会改变length
等属性,此时如果代理了set
,则会被触发多次。
let arr = [100,200,300]
let p = new Proxy(arr, {
get(target, key) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value', key)
target[key] = value
return true
}
})
p.push(400)
// set value 3 第一次data[3] = 400
// set value length 第二次 data.length = 4
这种多次触发handler的特性在某些场景下是冗余的,如果在set后会重新渲染视图,则多次set可能导致多次渲染(在不考虑flushQueue入队列去重的情况下)。
- proxy只能代理一层
let o = {x:{y:100},z:10}
let p = new Proxy(o, {
get(target, key) {
console.log('get value', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set value', key)
target[key] = value
return true
}
})
p.x.y =100 // 只输出了get value x, 无法监听到set value
因此,当代理对象是多层嵌套结构时,需要开发者自己实现将嵌套的属性对象转换为Proxy
对象
let handler = {
get(target, key) {
console.log('get value', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set value', key)
target[key] = value
return true
}
}
let x = {y:100}
let o = {x:new Proxy(x, handler),z:10}
let p = new Proxy(o, handler)
p.x.y =100
// get value x
// set value y
综上就是Proxy存在的一些问题
- 代理数组时可能存在多次触发trap的情况,需要实现去重
- 嵌套对象需要手动转换为Proxy,可以通过递归实现
TypeScript
Vue2使用flow
进行类型检测,Vue3全面拥抱TypeScript
,因此阅读源码需要具备一点TS相关知识,参考之前整理的
lerna
Vue3采用了lerna
组织项目源码(目前很多大型项目都采用lerna了),之前写过一篇使用verdaccio和lerna和管理npm包,这里就不过多介绍了。
开发环境
接下来搭建源码开发环境
# 如果下载比较慢的话可以从gitee上面克隆备份仓库
git clone git+https://github.com/vuejs/vue-next.git
# 安装依赖,需要一会
yarn install
# 开始rollup watch
npm run dev
cd packages/vue/examples/
在examples
下可以查看各种demo代码,这里我们直接选择composition目录下的代码查看新的语法,强烈建议在阅读源码之前先看看composition-api文档,这对了解Vue3中的API设计有非常重要的帮助!!
然后修改一下源码vue/src/index.ts
,刷新页面,就可以看见改动后的效果,接下来就开始愉快的源码时间~
rective
文档里面展示了一个关于rective
的基本用例
const { reactive, watchEffect, computed } = Vue
const state = reactive({
count: 0
})
function render() {
document.body.innerHTML = `count is ${state.count}`
}
watchEffect(render) // 初始化会调用一次render
setTimeout(() => {
state.count = 100 // state.count发生变化,会通过watchEffect重新触发render
}, 1000)
看起来reactive
方法会代理返回代理对象,而watchEffect
会在对象属性发生变化时重新执行注册的回调函数render
。
接下来就从这两个方法开始一探究竟
function reactive(target: object) {
return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 检测target是否是对象,是否已经设置__v_isReactive、__v_skip、__v_isReadonly等
// ...
const observed = new Proxy(
target,
// 根据target是否是Set, Map, WeakMap, WeakSet对象来判断使用哪一种handler
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
return observed
}
先看看普通对象的baseHandlers
,也就是传入的mutableHandlers
配置项
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(),
set: createSetter(),
// 下面三个方法均是通过`Reflect`来实现相关操作
deleteProperty,
has,
ownKeys
}
createGetter 收集依赖
const targetMap = new WeakMap<any, KeyToDepMap>()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// ... 根据 key与ReactiveFlags 返回特殊值
// ... 处理target为数组时的get操作
const res = Reflect.get(target, key, receiver)
// ... 处理res是Ref类型的操作
if (isRef(res)) {
if (targetIsArray) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays.
return res.value
}
}
// 收集依赖
!isReadonly && track(target, TrackOpTypes.GET, key)
// 判断属性值类型,递归调用reactive处理,返回新的Proxy
return isObject(res) ? reactive(res) : res
}
}
可以看见,只有当触发get
的时候,才会检测属性值的类型,然后对属性值进行reactive
操作,整体性能就Vue2在初始化时就递归劫持所有get而言,应该有不少提升。
我们知道,在触发get时需要收集依赖,可以看见track
就是处理这个工作的
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 初始化target的依赖列表,通过Map保存,每个依赖可能依赖target某个或某些属性,因此该Map的键值是target的每个属性
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
// 对于每个属性key而言,通过Set保存依赖该key的activeEffect
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// activeEffect是一个全局变量
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
在渲染视图时,在render方法中,会通过get
触发track
,然后将activeEffect
添加到数据属性的依赖列表中,这样就完成了依赖收集。
effect 变化的抽象
我们这里接触到了一个新的概念effect
,包括前面的effect
、watchEffect
等方法。类似于Watcher
对象,用于封装各种变化。
在示例中的watchEffect
注册的回调函数,就可以理解为一个effect。
// 从类型声明可以看出,effect是一个包含如下属性的函数
export interface ReactiveEffect<T = any> {
(...args: any[]): T
_isEffect: true
id: number
active: boolean
raw: () => T
deps: Array<Dep>
options: ReactiveEffectOptions
}
然后来看看watchEffect
的实现
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
const instance = currentInstance
// 根据source的类型封装getter,内部执行source的调用
let getter: () => any = () => {return callWithErrorHandling(source, instance) }
// 根据 flush字符串初始化调度器,决定何时调用getter
let scheduler: (job: () => any) => void = job => queuePostRenderEffect(job, instance && instance.suspense)
// 调用effect注册
const runner = effect(getter, {
lazy: true, // 由于下面会直接调用runner,因此lazy传入了true
computed: true,
onTrack,
onTrigger,
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
})
recordInstanceBoundEffect(runner)
runner()
// 返回一个取消effect的方法
return () => {
stop(runner)
if (instance) {
remove(instance.effects!, runner)
}
}
}
我们在这里看见了一个与mountComponent
中instance.update
类似的runner
方法,他们实际上都是一个通过effect
包装的函数。
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// 创建effect对象(函数)
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
// 在前面初始化instance.update时会先调用一次 componentEffect,从而完成页面的初始化渲染
effect()
}
return effect
}
// 下面这段源码基本是原样copy过来的,没有做删减
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect) // 清除effect.deps
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect // 将全局变量activeEffect 设置为当前运行的effect,然后调用effect
return fn(...args)
} finally {
// finally中的代码始终都能执行
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1] // 将activeEffect重置为上一个effect
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
整理一下watchEffect(cb)
的流程
- 在
doWatch
中将cb封装在getter
中, - 调用
effect(getter)
,通过createReactiveEffect
返回一个真正的ReactiveEffect
函数,赋值给runner
- 回到
doWatch
中,直接调用runner
执行- 在ReactiveEffect运行时会通过
effectStack
设置当前全局变量activeEffect
- 在ReactiveEffect运行时会通过
activeEffect是一个非常重要的全局变量,在前面的watchEffect(render)
中,会在初始化时运行render
function render() {
document.body.innerHTML = `count is ${state.count}`
}
由于内部方法内部访问了state.count
,会触发state
代理的get操作,这样就能够在track
的时候通过activeEffect
访问到封装了render
方法的这个effect,这样当state.count
发生变化时,才能够再次运行相关的effect,我们修改一下render方法
function render() {
// document.body.innerHTML = `count is ${state.count}`
console.log('render') // 只有第一次初始化的时候回打印render
document.body.innerHTML = '123'
}
watchEffect(render)
state.count // 触发一个get,然而并没有activeEffect,因此不会收集到相关的依赖
setTimeout(() => {
state.count = 100 // state.count更新时也不会触发render
}, 1000)
可以看见,如果在回调中没有触发state.count
,则无法正确track到依赖。结合get
与activeEffect
,可以精确到收集每个属性变化时对应的的effect,这是非常高效的。
另外在watchEffect
的源码中可以看见,由于runner
是同步执行的,在执行完毕后会将activeEffect
进行重置,如果我们在render
方法中通过异步的方式访问state.count
,也无法正确track依赖。
function render() {
setTimeout(()=>{
document.body.innerHTML = `count is ${state.count}` // 放在回调里面
})
}
watchEffect(render)
setTimeout(() => {
state.count = 100 // 也不会更新视图
}, 1000)
createSetter 通知变化
最后,我们来看看set代理
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
// 处理 ... ref
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 判断target === toRaw(receiver) ,不处理target原型链更新的情况
if (target === toRaw(receiver)) {
if (!hadKey) {
// 动态添加属性的情况
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 属性更新的情况
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
与get
中的track
同理,trigger
应该就是更新数据并通知依赖进行处理的逻辑了
export function trigger(
target: object,
type: TriggerOpTypes, // 表示不同的变化,如ADD、SET等
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 找到target某个key的依赖
const depsMap = targetMap.get(target)
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
// 将对应key的变化添加到effects或者computedRunners中
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
}
})
}
}
// 根据type和key找到需要处理的depsMap,这里处理了各种特殊情况
add(depsMap.get(key))
// ...
// 遍历effects和computedRunners
const run = (effect: ReactiveEffect) => {
// 如果effect自己配置了scheduler,则使用调度器运行effect
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect() // 可以看见effect实际上是一个函数
}
}
// 计算属性先运行,这样可以保证其他属性在运行时能够获取到计算属性的值
computedRunners.forEach(run)
effects.forEach(run)
}
小结:实现极简版reactive
前面的代码忽略了shallow
浅reactive、readonly
等情况,我们可以实现一个50行代码的reactive
,
let activeEffect
let targetMap = new Map()
function reactive(obj){
return new Proxy(obj, {
get(target, key){
track(target, key)
return target[key]
},
set(target, key, value){
target[key] = value
trigger(target, key)
return true
}
})
}
function track(target, key){
let depMap = targetMap.get(target)
if(!depMap) {
targetMap.set(target, (depMap = new Map()))
}
let dep = depMap.get(key)
if(!dep) {
depMap.set(key, ( dep = new Set()))
}
if(!dep.has(activeEffect)){
dep.add(activeEffect)
}
}
function watchEffect(cb){
activeEffect = cb
cb()
}
function trigger(target, key){
let depMap = targetMap.get(target)
if(!depMap) return
let effects = depMap.get(key)
if(!effects) return
effects.forEach((effect)=>{
effect()
})
}
然后测试一下
let {reactive, watchEffect} = require('./reactive')
let state = reactive({
x: 100
})
function render(){
let msg = `render template with state.x = ${state.x}`
console.log(msg)
}
watchEffect(render)
setTimeout(()=>{
state.x = 200
}, 1000)
上面的代码有意忽略了属性嵌套、数组set多次执行等很多细节问题,主要是为了展示了reactive最基本的结构。
避免effect重复执行
我们还忽略了一个比较重要的特点,当依赖的状态连续变化时,如何避免中间不比较的trigger呢?比如在上面的demo代码中
setTimeout(() => {
state.x = 100
console.log(state.x) // 100
state.x = 200
console.log(state.x) // 200
}, 1000)
// 会连续打印两次 render template with state.x = 100 | 200
在某些时候,比如渲染视图,第一次set时就触发render方法是完全没有必要的且很浪费性能的事情。
在Vue2中,Watcher会被加入到一个队列中,并在加入时进行去重,在nextTick
中统一运行队列。在上面的demo中,由于activeEffect都是render
方法,我们可以通过debounce的方式实现只调用一次render
那么Vue3是如何实现的呢?我们来研究一下
在trgger的时候可以看见
const run = (effect: ReactiveEffect) => {
// 如果effect自己配置了scheduler,则使用调度器运行effect
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect() // 可以看见effect实际上是一个函数
}
}
在doWatch
中可以看见下面的代码
scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
const runner = effect(getter, {
//... 其他配置
scheduler
})
可以看见在doWatch构造的effect中,会传入一个scheduler
配置,当effect run的时候,会调用这个scheduler
,此处就会执行queuePostRenderEffect
// 使用了两个全局队列来维护
const queue: (Job | null)[] = []
const postFlushCbs: Function[] = []
export const queuePostRenderEffect = __FEATURE_SUSPENSE__
? queueEffectWithSuspense
: queuePostFlushCb
export function queuePostFlushCb(cb: Function | Function[]) {
// 将effect放入postFlushCbs队列
if (!isArray(cb)) {
postFlushCbs.push(cb)
} else {
postFlushCbs.push(...cb)
}
queueFlush()
}
export function queueJob(job: Job) {
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
nextTick(flushJobs) // nextTick直接使用的Promise.then,在nextTick中执行flushJobs
}
}
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
let job
// 组件由父组件向子组件更新,父组件的id始终比子组件小(先构造)
queue.sort((a, b) => getId(a!) - getId(b!))
while ((job = queue.shift()) !== undefined) {
if (job === null) {
continue
}
// 依次运行effect
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
flushPostFlushCbs(seen)
isFlushing = false
// 如果在运行过程中调用了queueJob或者queuePostRenderEffect,则继续执行flushJobs
if (queue.length || postFlushCbs.length) {
flushJobs(seen)
}
}
// 依次运行postFlushCbs中的回调,在前面scheduler中添加的job就会通过queuePostRenderEffect放在postFlushCbs中
export function flushPostFlushCbs(seen?: CountMap) {
if (postFlushCbs.length) {
// 关键的异步,对postFlushCbs进行去重,这意味着即使postFlushCbs存在多个相同的effect,也只会被执行一次
const cbs = [...new Set(postFlushCbs)]
postFlushCbs.length = 0
for (let i = 0; i < cbs.length; i++) {
cbs[i]()
}
}
}
这里的代码比较简单的展示了我们的effect是如何执行的
- 首先通过注册配置项scheduler,在trigger的时候调用scheduler(effect)
- scheduler调用queuePostFlushCb将effect放入全局队列postFlushCbs中,同时将flushJobs注册到nextTick
- 在flushJobs中会清空queue和postFlushCbs,在清空
postFlushCbs
之前,会通过Set对postFlushCbs进行去重 - 这样相同的effect在同一次flushJobs中,只会被执行一次,而不论之前trigger的时候调用了多少次scheduler(effect)
Ref
前面的源码分析让我们了解了Vue3中是如何通过Proxy代理对象,并在get的时候track effect,在set
的时候trigger effect。
在某些时候需要一个依赖于其他状态的数据,可以通过计算属性来获得,计算属性作为一个函数,可以返回各种类型的值,并且当计算属性依赖的状态发生变化时,会自动重新计算并通知依赖该计算实现的地方。
官网阐述了一种最简单的实现方案,通过闭包和watchEffect
实现computed
(强烈建议大家先去看看官网这篇文章,对于理解Vue3的设计有很大帮助)
function computed(getter) {
let value
watchEffect(() => {
value = getter()
})
return value
}
那么问题来了,当计算属性返回的是基础类型的值,虽然能够重新运行watchEffect
的回调并更新值,但无法通知之前那个oldValue的依赖, 这是因为JavaScript中普通类型是按值而非引用传递的。
解决这个问题的办法就是返回一个对象,然后代理响应值收集依赖,Vue3把这种新的类型叫做Ref
。
function computed(getter) {
const ref = {
value: null,
}
watchEffect(() => {
ref.value = getter()
})
return ref
}
接下来就从computed开始,看看Ref的作用与实现
下面是基本的demo代码
const state = reactive({
count: 1
})
const doubleCount = computed(() => {
return state.count * 2
})
function render() {
document.body.innerHTML = `count is ${doubleCount.value}`
}
watchEffect(render)
setTimeout(() => {
state.count = 100
}, 1000)
computed
下面是computed
的源码
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set // 处理set computed
}
let dirty = true // 判断getter是否需要重新运算
let value: T
let computed: ComputedRef<T>
const runner = effect(getter, {
lazy: true, // 只有当用到computed的时候才运行求职
computed: true, // 将effect.computed标志为true,这样会在普通的effect之前运行
// 自定义调度器,在trigger时调用
scheduler: () => {
// 通知使用了computed.value的effect,只有当计算属性的getter已经被运行过才进行通知
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})
computed = {
__v_isRef: true,
effect: runner,
get value() {
if (dirty) {
value = runner() // 调用runner 完成activeEffect设置,这样当计算属性依赖的其他状态发生变化时,可以重新触发getter,
dirty = false // 缓存已经计算过的值
}
// runner运行完毕后会重置activeEffect为上一个effect,然后将依赖该computed的activeEffect添加到依赖中
track(computed, TrackOpTypes.GET, 'value')
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any
// 可以看见,计算属性返回的是一个特殊的对象
return computed
}
OK,看起来就比较清楚了,结合上面的例子
const doubleCount = computed(() => {
return state.count * 2
})
function render() {
document.body.innerHTML = `count is ${doubleCount.value}`
}
watchEffect(render)
把整个流程简化为
上游数据 -> computed -> 下游数据
则具体过程
- 调用
computed(getter)
时会初始化一个封装了getter
的effect - 当触发计算属性
get value
时,会运行的effect- 此时触发
getter
中计算属性依赖的上游数据的get,同时通过调用track
收集依赖于当前计算属性的下游数据 - 当上游数据发生变化时,会重新触发effect,由于这里自定义了
scheduler
,因此会使用scheduler(effect)
的方式运行effect, - 在
scheduler(effect)
中重置dirty,然后调用trigger
通知下游数据
- 此时触发
Ref
同理,我们可以看看普通的Ref的实现
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
let value = shallow ? rawValue : convert(rawValue)
const r = {
__v_isRef: true,
get value() {
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
trigger(
r,
TriggerOpTypes.SET,
'value'
)
}
}
}
return r
}
有了computed
的经验,这里这里看起来就比较容易了,在get value
的时候track
收集activeEffect,在set value
的时候,如果值发生了变化,就通过trigger
通知effect。
小结
本文主要整理了Vue3中新的响应式系统,
- 通过
effect
封装变化,在Vue2中是通过Watcher
负责的 - 通过Proxy代理对象每个属性的set和get,在get时通过track收集activeEffect,在set时通过trigger通知对应属性的所有依赖更新
- 通过Map保存每个reactive对象中每个属性的依赖
- 通过effect队列在nextTick中统一运行effect,并通过Set进行effect去重
- 为了解决普通类型按值传递的问题,Vue3实现了Ref类型对象,
computed
也可以当做是一种特殊的Ref。
至此,对于Vue3的响应式系统有了一定的了解,接下来去看组合式API应该就比较方便了。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。