Vue 核心——响应式系统

Vue 核心------响应式系统

今天分享 Vue 的核心之一:响应式系统。文章内容尽量保持简单易懂,并模仿 Copilot 使用 emoji 来增添色彩。

希望大家多提意见,多点赞,感谢。

Vue 2

Vue 2 的响应式系统基于 Object.defineProperty,通过 访问器描述符 对对象的属性进行拦截,从而实现 依赖收集和触发更新

下面是根据自己的理解输出的代码,并不与源代码一致。

javascript 复制代码
// 在 src/core/instance 里面执行 initMixin -> initState -> initData
function initData(vm) {
    let data: any = vm.$options.data
    observe(data)
}

function observe(data) {
    return new Observer(data)
}

class Observer{
    constructor(data) {
        if(isArray(data)){
            observeArray(data)
        }else{
            const keys = Object.keys(data)
            for(let i = 0; i < keys.length; i++){
                defineReactive(data, key)
            }
        }
    }
}

function defineReactive(target, key, value = undefined) {
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(target, key)
    if(property && property.configurable === false) return
    
    for(let key in obj){
        if(Object.hasOwn(obj, key)){
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    dep.depend({target, type: TrackOpTypes.GET, key})
                    return target[key]
                },
                set(newVal) {
                    dep.notify({
                        type: TriggerOpTypes.SET,
                        target: obj,
                        key,
                        newValue: newVal,
                        oldValue: value
                    })
                    target[key] = newVal
                }
            })
        }
    }
}

class Dep{
    constructor() {
        this.subs:Watcher[] = []
    }
    
    depend(sub: Subscriber) {
        this.subs.push(sub)
    }
    
    notify() {
        for(let i = 0; i < this.subs.length; i++) {
            subs[i].update()
        }
    }
}

Vue 3

Vue 3 中抛弃了 Object.defineProperty,转头 Proxy 的怀抱,但是 Proxy 并不能拦截基础类型,所以 Vue 3 中对基础类型和对象类型的处理有区别。

ref

ref 通过将基本类型转换成带 .value 的对象,然后拦截对象的 getter 和 setter,从而实现 依赖收集和触发更新

javascript 复制代码
function ref(value) {
    return new RefImpl(value);
}

class RefImpl{
    constructor(value) {
        this._value = value
        this.dep = new Dep()
    }

    get value() {
        this.dep.track(this._value)
        return this._value
    }

    set value(newVal) {
        this._value = newVal
        this.dep.trigger()
    }
}

class Dep{
    constructor() {
        this.head = null;
        this.tail = null;
		this.subs: Link = null;
    }

    track(value) {
        const link = new Link(value)
        addSub(link) // 双向链表操作
    }

    trigger() {
        this.notify()
    }

    notify() {
        const result =  traverse(this.subs) // 遍历双向链表
        for(let i = 0; i < result.length; i++){
            this.subs[i].notify()
        }
    }

}

class Link {
    constructor() {
        this.prevDep = this.nextDep = this.prevSub = this.nextSub = null
    }
}

ref 案例说明

javascript 复制代码
const name = ref('zhangsan')
const age = ref(12)

const upperName = computed(() => name.value.toUpperCase())

watchEffect(() => {
  console.log(name.value, age.value)
})
ref 链表

name.dep.subs 是一个链表,里面挂着所有依赖 name 的副作用

age.dep.subs 是一个链表,里面挂着所有依赖 age 的副作用

每个 Link.sub 指向一个副作用函数

所以当 name.value 改变时,Vue 会遍历 dep.subs,执行每个 Link.sub.run()

effect 链表

每个副作用函数也维护一个 deps 链表,里面挂着所有它依赖的响应式数据

当副作用失效(比如组件卸载或 stop())时,Vue 会遍历 effect.deps,从每个 dep.subs 中移除对应的 Link

text 复制代码
┌──────────────┐																  ┌──────────────┐
│   name (ref) │																  │   age (ref)  │
└──────┬───────┘																  └──────┬───────┘
       │       																			 │
       ▼      																			 ▼
┌────────────────────────────┐												┌────────────────────────────┐
│         dep (Dep)          │												│         dep (Dep)          │
│ subs: Link A               │												│ subs: Link C               │
└──────┬─────────────────────┘												└──────┬─────────────────────┘
       │      																	   │
       ▼  																		   ▼
┌────────────────────────────┐       ┌────────────────────────────┐			┌────────────────────────────┐
│        Link A              │──────▶│     watchEffect (effect)   │◀────── │        Link C              │
│ sub       → watchEffect    │       │ deps: Link A Link C        │			│ sub       → watchEffect    │
│ dep       → name.dep       │◀──────│                            │──────▶ │ dep       → age.dep        │
│ nextSub   → Link B         │       └────────────────────────────┘			│ nextSub   → null           │
│ prevSub   → null           │												│ prevSub   → null           │
└────────────────────────────┘												└────────────────────────────┘
       │
       ▼
┌────────────────────────────┐        ┌────────────────────────────┐
│        Link B              │──────▶ │   computedEffect (effect)  │
│ sub       → computedEffect │        │ deps: Link B               │
│ dep       → name.dep       │◀────── │                            │
│ nextSub   → null           │		  └────────────────────────────┘
│ prevSub   → Link A         │
└────────────────────────────┘

reactive

reactive 通过 Proxy 来实现 依赖收集和触发更新,但是对普通对象和集合对象有点区别。

javascript 复制代码
const targetMaps = new Map()

function reactive(target) {
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap,)
}
function createReactiveObject(target, isReadOnly, baseHandler, collectionHandler, proxyMap) {
    const handler = TargetType.COLLECTION ? collectionHandler : baseHandler
    const proxy = new Proxy(target, handler)
    return proxy
}

const mutableHandlers = new MutableReactiveHandler()

class MutableReactiveHandler extends BaseReactiveHandler{
    set() {
        const result = Reflect.set(target, key, value)
        if (!hadKey) {
        	trigger(target, TriggerOpTypes.ADD, key, value)
        } else if (hasChanged(value, oldValue)) {
        	trigger(target, TriggerOpTypes.SET, key, value, oldValue)
        }
        return result
    }
    deleteProperty() {
        const result = Reflect.deleteProperty(target, key)
        trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
        return result
    }
    has() {
        const result = Reflect.has(target, key)
        track(target, TrackOpTypes.HAS, key)
        return result
    }
    ownKeys() {
		track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
    	return Reflect.ownKeys(target)
    }
}

class BaseReactiveHandler {
    get() {
		const res = Reflect.get(target, key, isRef(target) ? target : receiver)
        track(target, TrackOpTypes.GET, key)
        return res
    }
}

const mutableCollectionHandler = { 
     // Map 和 Set 的所有方法都需要经过 get trap,所以这里只需要定义 get trap
	 get: createInstrumentationGetter(false, false),
}

function createInstrumentationGetter() {
    const instrumentations = createInstrumentations(isReadonly, shallow)
	return Reflect.get(hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver,
    )
}

function createInstrumentations() {
    const instrumentations = {
        get() {
            track(rawTarget, TrackOpTypes.GET, rawKey)
            return wrap(target.get(key))
        }
        get size() {
            track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
      		return target.size
        }
    	has() {
            track(rawTarget, TrackOpTypes.HAS, rawKey)
            return key === rawKey
        		? target.has(key)
        		: target.has(key) || target.has(rawKey)
        }
    	forEach() {
            track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
            return target.forEach((value: unknown, key: unknown) => {
        		return callback.call(thisArg, wrap(value), wrap(key), observed)
      		})
        }
    	extend(instrumentations, {
            add() {
                trigger(target, TriggerOpTypes.ADD, value, value)
                return this
            }
            set() {
            	trigger(target, TriggerOpTypes.ADD, key, value)
            	return this
        	}
    		delete() {
                const result = target.delete(key)
                trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
                return result
            }
    		clear() {
                const result = target.clear()
				trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
                return result
            }
        })
    }
    return instrumentations
}

Vue 2 和 Vue 3 的响应式异同

1️⃣ Vue 2 基于 Object.defineProperty,Vue 3 基于 对象的 getter/setter 和 Proxy/Reflect。

2️⃣ Vue 2 的依赖收集在一个 数组,Vue 3 的 ref 依赖收集在 双向链表,reactive 的依赖收集在 Map 集合对象。

3️⃣ Vue 2 中 Watcher 用于处理 渲染、自定义 watch、computed。Vue 3 中使用 ReactiveEffect 代替。

相关推荐
passer98119 小时前
基于Vue的场景解决
前端·vue.js
游荡de蝌蚪20 小时前
快速打造Vue后台管理系统
前端·javascript·vue.js
我是日安1 天前
从零到一打造 Vue3 响应式系统 Day 10 - 为何 Effect 会被指数级触发?
前端·vue.js
艾小码1 天前
还在硬邦邦跳转页面?Vue这3招让应用丝滑如德芙!
前端·javascript·vue.js
Olrookie1 天前
ruoyi-vue(十五)——布局设置,导航栏,侧边栏,顶部栏
前端·vue.js·笔记
召摇1 天前
API 设计最佳实践 Javascript 篇
前端·javascript·vue.js
码间舞1 天前
文件太大怎么上传?【分组分片上传大文件】-实战记录
前端·vue.js·程序员
鹏多多1 天前
基于Vue3+TS的自定义指令开发与业务场景应用
前端·javascript·vue.js
gnip2 天前
企业级配置式表单组件封装
前端·javascript·vue.js