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 代替。

相关推荐
Irene19918 小时前
Vue 官方推荐:kebab-case(短横线命名法)
javascript·vue.js
一只小阿乐10 小时前
vue-web端实现图片懒加载的方
前端·javascript·vue.js
+VX:Fegn089510 小时前
计算机毕业设计|基于springboot + vue小型房屋租赁系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
萌萌哒草头将军10 小时前
Node.js 存在多个严重安全漏洞!官方建议尽快升级🚀🚀🚀
vue.js·react.js·node.js
这个图像胖嘟嘟11 小时前
前端开发的基本运行环境配置
开发语言·javascript·vue.js·react.js·typescript·npm·node.js
北辰alk11 小时前
Vue 自定义指令生命周期钩子完全指南
前端·vue.js
是小崔啊11 小时前
03-vue2
前端·javascript·vue.js
北辰alk13 小时前
Vue 路由跳转完全指南:8种跳转方式深度解析
vue.js
北辰alk13 小时前
Vue v-for 遍历对象顺序完全指南:从混乱到可控
vue.js
m0_4711996313 小时前
【场景】如何快速接手一个前端项目
前端·vue.js·react.js