Vue3响应式原理

176 阅读9分钟

1. Vue3对比Vue2的变化

  • 在Vue2的时候使用defineProperty来进行数据的劫持,需要对属性进行重写添加getter和setter性能差
  • 当新增属性和删除属性时无法监控变化。需要通过set,set,delete实现
  • 数组不采用defineProperty来进行劫持(浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理。

Vue3中使用Proxy来实现响应式数据变化。从而解决了上述问题。

CompositionAPI

  • 在vue2中采用的是OptionAPI ,用户提供的data,props,methods,computed,watch等属性(用户编写复杂业务逻辑会出现反复横跳的问题)
  • Vue2中所有的属性都是通过this访问,this 执行会不明确
  • Vue2 中很多未使用方法或属性依旧会被打包,并且所有全局API都在Vue对象上公开。Composition API 对tree-shaking更加友好,代码也更容易压缩
  • 组件逻辑共享问题,Vue2采用mixins实现组件之间的逻辑共享;但是会有数据来源不明确,命名冲突等问题,Vue3采用CompositionAPI 提取公共逻辑非常方便。

简单的组件仍然可以采用OptionsAPI进行编写,compositionAPI在复杂的逻辑中有着明显的优势~。 reactivity模块中就包含了很多我们经常使用到的API 例如:computed、reactive、ref、effect等

2. Reactive & Effect

Reactivity模块基本使用

<div id="app"></div>
<script src="./reactivity.global.js"></script>
<script>
    const { reactive, effect, shallowReactive, shallowReadonly, readonly } = VueReactivity;
    // let state = reactive({ name: 'jw', age: 30 });
    // const state = shallowReactive({ name: 'jw', age: 30 })
    // const state = readonly({ name: 'jw', age: 30 })
    const state = reactive({ name: 'jw', age: 30})
    effect(() => { // 副作用函数 (effect执行渲染了页面)
        app.innerHTML = state.name + '今年' + state.age + '岁了'
    });
    setTimeout(() => {
        state.age++;
    }, 1000)
</script>

reactive方法会将对象变成proxy对象, effect中使用reactive对象时会进行依赖收集,稍后属性变化时会重新执行effect函数~。

编写reactive函数

import { isObject } from "@vue/shared"
function createReactiveObject(target: object, isReadonly: boolean) {
    if (!isObject(target)) {
        return target
    }
}
// 常用的就是reactive方法
export function reactive(target: object) {
    return createReactiveObject(target, false)
}
// 后面的方法,不是重点我们先不进行实现... 
/*
export function shallowReactive(target: object) {
    return createReactiveObject(target, false)
}
export function readonly(target: object) {
    return createReactiveObject(target, true)
}
export function shallowReadonly(target: object) {
    return createReactiveObject(target, true)
}
*/
export function isObject(value: unknown) : value is Record<any,any> {
    return typeof value === 'object' && value !== null
}

由此可知这些方法接受的参数必须是一个对象类型。否则没有任何效果

const reactiveMap = new WeakMap(); // 缓存列表
const mutableHandlers: ProxyHandler<object> = {
    get(target, key, receiver) {
        // 等会谁来取值就做依赖收集
        const res = Reflect.get(target, key, receiver);
        return res;
    },
    set(target, key, value, receiver) {
        // 等会赋值的时候可以重新触发effect执行
        const result = Reflect.set(target, key, value, receiver);
        return result;
    }
}
function createReactiveObject(target: object, isReadonly: boolean) {
    if (!isObject(target)) {
        return target
    }
    const exisitingProxy = reactiveMap.get(target); // 如果已经代理过则直接返回代理后的对象 
    if (exisitingProxy) {
        return exisitingProxy;
    }
    const proxy = new Proxy(target, mutableHandlers); // 对对象进行代理
    reactiveMap.set(target,proxy)
    return proxy;
}

这里必须要使用Reflect进行操作,保证this指向永远指向代理对象

let school = {
    name:'zf',
    get num(){
        return this.name;
    }
}
let p = new Proxy(school,{
    get(target, key,receiver){
        console.log(key);
        // return Reflect.get(target,key,receiver)
        return target[key]
    }
})
p.num

将对象使用proxy进行代理,如果对象已经被代理过,再次重复代理则返回上次代理结果。 那么,如果将一个代理对象传入呢?

const enum ReactiveFlags {
    IS_REACTIVE = '__v_isReactive'
}
const mutableHandlers: ProxyHandler<object> = {
    get(target, key, receiver) {
        if(key === ReactiveFlags.IS_REACTIVE){ // 在get中增加标识,当获取IS_REACTIVE时返回true
            return true;
        }
    }
}
function createReactiveObject(target: object, isReadonly: boolean) {
    if(target[ReactiveFlags.IS_REACTIVE]){ // 在创建响应式对象时先进行取值,看是否已经是响应式对象
        return target
    }
}

这样我们防止重复代理就做好了~~~, 其实这里的逻辑相比Vue2真的是简单太多了。

编写effect函数

export let activeEffect = undefined;// 当前正在执行的effect

class ReactiveEffect {
    active = true;
    deps = []; // 收集effect中使用到的属性
    parent = undefined;
    constructor(public fn) { }
    run() {
        if (!this.active) { // 不是激活状态
            return this.fn();
        }
        try {
            this.parent = activeEffect; // 当前的effect就是他的父亲
            activeEffect = this; // 设置成正在激活的是当前effect
            return this.fn();
        } finally {
            activeEffect = this.parent; // 执行完毕后还原activeEffect
            this.parent = undefined;
        }

    }
}
export function effect(fn, options?) {
    const _effect = new ReactiveEffect(fn); // 创建响应式effect
    _effect.run(); // 让响应式effect默认执行
}

依赖收集

默认执行effect时会对属性,进行依赖收集

get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
    }
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);  // 依赖收集
    return res;
}
const targetMap = new WeakMap(); // 记录依赖关系
export function track(target, type, key) {
    if (activeEffect) {
        let depsMap = targetMap.get(target); // {对象:map}
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()))
        }
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = new Set())) // {对象:{ 属性 :[ dep, dep ]}}
        }
        let shouldTrack = !dep.has(activeEffect)
        if (shouldTrack) {
            dep.add(activeEffect);
            activeEffect.deps.push(dep); // 让effect记住dep,这样后续可以用于清理
        }
    }
}

将属性和对应的effect维护成映射关系,后续属性变化可以触发对应的effect函数重新run

触发更新

set(target, key, value, receiver) {
    // 等会赋值的时候可以重新触发effect执行
    let oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver);
    if (oldValue !== value) {
        trigger(target, 'set', key, value, oldValue)
    }
    return result;
}
export function trigger(target, type, key?, newValue?, oldValue?) {
    const depsMap = targetMap.get(target); // 获取对应的映射表
    if (!depsMap) {
        return
    }
    const effects = depsMap.get(key);
    effects && effects.forEach(effect => {
        if (effect !== activeEffect) effect.run(); // 防止循环
    })
}

分支切换与cleanup

在渲染时我们要避免副作用函数产生的遗留

const state = reactive({ flag: true, name: 'jw', age: 30 })
effect(() => { // 副作用函数 (effect执行渲染了页面)
    console.log('render')
    document.body.innerHTML = state.flag ? state.name : state.age
});
setTimeout(() => {
    state.flag = false;
    setTimeout(() => {
        console.log('修改name,原则上不更新')
        state.name = 'zf'
    }, 1000);
}, 1000)
function cleanupEffect(effect) {
    const { deps } = effect; // 清理effect
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect);
    }
    effect.deps.length = 0;
}
class ReactiveEffect {
    active = true;
    deps = []; // 收集effect中使用到的属性
    parent = undefined;
    constructor(public fn) { }
    run() {
        try {
            this.parent = activeEffect; // 当前的effect就是他的父亲
            activeEffect = this; // 设置成正在激活的是当前effect
+           cleanupEffect(this);
            return this.fn(); // 先清理在运行
        }
    }
}

这里要注意的是:触发时会进行清理操作(清理effect),在重新进行收集(收集effect)。在循环过程中会导致死循环。

let effect = () => {};
let s = new Set([effect])
s.forEach(item=>{s.delete(effect); s.add(effect)}); // 这样就导致死循环了

停止effect

export class ReactiveEffect {
    stop(){
        if(this.active){ 
            cleanupEffect(this);
            this.active = false
        }
    }
}
export function effect(fn, options?) {
    const _effect = new ReactiveEffect(fn); 
    _effect.run();

    const runner = _effect.run.bind(_effect);
    runner.effect = _effect;
    return runner; // 返回runner
}

调度执行

trigger触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式

export function effect(fn, options:any = {}) {
    const _effect = new ReactiveEffect(fn,options.scheduler); // 创建响应式effect
    // if(options){
    //     Object.assign(_effect,options); // 扩展属性
    // }
    _effect.run(); // 让响应式effect默认执行
    const runner = _effect.run.bind(_effect);
    runner.effect = _effect;
    return runner; // 返回runner
}

export function trigger(target, type, key?, newValue?, oldValue?) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return
    }
    let effects = depsMap.get(key);
    if (effects) {
        effects = new Set(effects);
        for (const effect of effects) {
            if (effect !== activeEffect) { 
                if(effect.scheduler){ // 如果有调度函数则执行调度函数
                    effect.scheduler()
                }else{
                    effect.run(); 
                }
            }
        }
    }
}

深度代理

 get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
    }
    // 等会谁来取值就做依赖收集
    const res = Reflect.get(target, key, receiver);
    track(target, 'get', key);

    if(isObject(res)){
        return reactive(res);
    }
    return res;
}

当取值时返回的值是对象,则返回这个对象的代理对象,从而实现深度代理

Computed实现原理

接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。

import { isFunction } from "@vue/shared";
import { activeEffect, ReactiveEffect, trackEffects, triggerEffects } from "./effect";

class ComputedRefImpl {
    public effect;
    public _value;
    public dep;
    public _dirty = true;
    constructor(getter,public setter) {
        this.effect = new ReactiveEffect(getter,()=>{ 
            if(!this._dirty){ // 依赖的值变化更新dirty并触发更新
                this._dirty = true;
                triggerEffects(this.dep)
            }
        });
    }
    get value(){ // 取值的时候进行依赖收集
        if(activeEffect){
            trackEffects(this.dep || (this.dep = new Set));
        }
        if(this._dirty){ // 如果是脏值, 执行函数
            this._dirty = false;
            this._value = this.effect.run(); 
        }
        return this._value; 
    }
    set value(newValue){
        this.setter(newValue)
    }
}
export function computed(getterOrOptions) {
    const onlyGetter = isFunction(getterOrOptions); // 传入的是函数就是getter
    let getter;
    let setter;
    if (onlyGetter) {
        getter = getterOrOptions;
        setter = () => { }
    } else {
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
    }
    // 创建计算属性
    return new ComputedRefImpl(getter, setter)
}

创建ReactiveEffect时,传入scheduler函数,稍后依赖的属性变化时调用此方法!

export function triggerEffects(effects) { 
    effects = new Set(effects);
    for (const effect of effects) {
        if (effect !== activeEffect) { // 如果effect不是当前正在运行的effect
            if (effect.scheduler) {
                effect.scheduler()
            } else {
                effect.run(); // 重新执行一遍
            }
        }
    }
}
export function trackEffects(dep) { // 收集dep 对应的effect
    let shouldTrack = !dep.has(activeEffect)
    if (shouldTrack) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep); 
    }
}

WatchAPI实现原理

watch的核心就是观测一个响应式数据,当数据变化时通知并执行回调 (那也就是说它本身就是一个effect)

watch(state,(oldValue,newValue)=>{ // 监测一个响应式值的变化
    console.log(oldValue,newValue)
})

监测响应式对象

function traverse(value,seen = new Set()){
    if(!isObject(value)){
        return value
    }
    if(seen.has(value)){
        return value;
    }
    seen.add(value);
    for(const k in value){ // 递归访问属性用于依赖收集
        traverse(value[k],seen)
    }
    return value
}
export function isReactive(value){
    return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
export function watch(source,cb){
    let getter;
    if(isReactive(source)){ // 如果是响应式对象
        getter = () => traverse(source)// 包装成effect对应的fn, 函数内部进行遍历达到依赖收集的目的
    }
    let oldValue;
    const job = () =>{
        const newValue = effect.run(); // 值变化时再次运行effect函数,获取新值
        cb(newValue,oldValue);
        oldValue = newValue
    }
    const effect = new ReactiveEffect(getter,job) // 创建effect
    oldValue = effect.run(); // 运行保存老值
}

监测函数

export function watch(source,cb){
    let getter;
    if(isReactive(source)){ // 如果是响应式对象
        getter = () => traverse(source)
    }else if(isFunction(source)){
        getter = source // 如果是函数则让函数作为fn即可
    }
    // ...
}

watch中回调执行时机

export function watch(source,cb,{immediate} = {} as any){
	const effect = new ReactiveEffect(getter,job) // 创建effect
    if(immediate){ // 需要立即执行,则立刻执行任务
        job();
    }
    oldValue = effect.run(); 
}

watch中cleanup实现

连续触发watch时需要清理之前的watch操作

const state = reactive({ flag: true, name: 'jw', age: 30 })
let i = 2000;
function getData(timer){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(timer)
        }, timer);
    })
}
watch(()=>state.age,async (newValue,oldValue,onCleanup)=>{
    let clear = false;
    onCleanup(()=>{
        clear = true;
    })
    i-=1000;
    let r =  await getData(i); // 第一次执行1s后渲染1000, 第二次执行0s后渲染0, 最终应该是0
    if(!clear){document.body.innerHTML = r;}
},{flush:'sync'});
state.age = 31;
state.age = 32;
let cleanup;
let onCleanup = (fn) =>{
    cleanup = fn;
}
const job = () =>{
    const newValue = effect.run(); 
    if(cleanup) cleanup(); // 下次watch执行前调用上次注册的回调
    cb(newValue,oldValue,onCleanup); // 传入onCleanup函数
    oldValue = newValue
}

Ref的概念

proxy代理的目标必须是非原始值,所以reactive不支持原始值类型。所以我们需要将原始值类型进行包装。

const flag = ref(false)
effect(()=>{
    document.body.innerHTML = flag.value ? 30:'姜文'
});
setTimeout(()=>{
    flag.value = true
},1000);

Ref & ShallowRef

function createRef(rawValue, shallow) {
    return new RefImpl(rawValue,shallow); // 将值进行装包
}
// 将原始类型包装成对象, 同时也可以包装对象 进行深层代理
export function ref(value) {
    return createRef(value, false);
}
// 创建浅ref 不会进行深层代理
export function shallowRef(value) {
    return createRef(value, true);
}
function toReactive(value) { // 将对象转化为响应式的
    return isObject(value) ? reactive(value) : value
}
class RefImpl {
    public _value;
    public dep;
    public __v_isRef = true;
    constructor(public rawValue, public _shallow) {
        this._value = _shallow ? rawValue : toReactive(rawValue); // 浅ref不需要再次代理
    }
    get value(){
        if(activeEffect){
            trackEffects(this.dep || (this.dep = new Set)); // 收集依赖
        }
        return this._value;
    }
    set value(newVal){
        if(newVal !== this.rawValue){
            this.rawValue = newVal;
            this._value = this._shallow ? newVal : toReactive(newVal);
            triggerEffects(this.dep); // 触发更新
        }
    }
}

toRef & toRefs

响应式丢失问题

const state = reactive({name: 'jw', age: 30 })
let person = {...state}
effect(()=>{
    document.body.innerHTML = person.name +'今年' + person.age +'岁了'
})
setTimeout(()=>{
    person.age = 31;
},1000)

如果将响应式对象展开则会丢失响应式的特性

class ObjectRefImpl {
    public __v_isRef = true
    constructor(public _object, public _key) { }
    get value() {
        return this._object[this._key];
    }
    set value(newVal) {
        this._object[this._key] = newVal;
    }
}
export function toRef(object, key) { // 将响应式对象中的某个属性转化成ref
    return new ObjectRefImpl(object, key);
}
export function toRefs(object) { // 将所有的属性转换成ref
    const ret = Array.isArray(object) ? new Array(object.length) : {};
    for (const key in object) {
        ret[key] = toRef(object, key);
    }
    return ret;
}
let person = {...toRefs(state)}; // 解构的时候将所有的属性都转换成ref即可
effect(()=>{
    document.body.innerHTML = person.name.value +'今年' + person.age.value +'岁了'
})
setTimeout(()=>{
    person.age.value = 31;
},1000)

自动脱ref

let person = proxyRefs({...toRefs(state)})
effect(()=>{
    document.body.innerHTML = person.name +'今年' + person.age +'岁了'
})
setTimeout(()=>{
    person.age = 31;
},1000)
export function proxyRefs(objectWithRefs){ // 代理的思想,如果是ref 则取ref.value
    return new Proxy(objectWithRefs,{
        get(target,key,receiver){
            let v = Reflect.get(target,key,receiver);
            return v.__v_isRef? v.value:v; 
        },
        set(target,key,value,receiver){ // 设置的时候如果是ref,则给ref.value赋值
            const oldValue = target[key];
            if(oldValue.__v_isRef){
                oldValue.value = value;
                return true
            }else{
                return Reflect.set(target,key,value,receiver)
            }
        }
    })
}