Vue响应式原理解析(文字篇)

109 阅读10分钟

响应式原理文章总结

vue源码阅读计划:
1.熟悉vue原理,面试能回答深层源码问题
2.熟悉代码原理,写业务代码更能理解,找bug能力更上一层楼
3.Vue二次开发
4.在面试官同事朋友装逼~ 4.gif

响应式原理

响应式原理问题

问题概况

  • 代码1.1是响应式原理代码,为啥这样写?
    答:暂略
  • vue3为什么要用Proxy?vue2用Object.defineProperty有啥缺点? 那么Proxy也有啥缺点?
    答:暂略
//代码1.1
const data = {
    text: '123'
}

let activeEffect = void 0;

let effectMap = new Set()
const obj = new Proxy(data, {
    get (target, key) {
        //收集依赖
        effectMap.add(activeEffect)
        return target[key]
    },
    set (target, key, newval) {
        target[key] = newval
        //通知依赖变化
        effectMap.forEach(fn => fn())
    }
})

function effect (fn) {
    activeEffect = fn
    fn();
}

effect(() => {
    console.log(obj.text)
    document.getElementById('app').textContent = obj.text
})
setTimeout(() => {
    obj.text = 'Hello World'
}, 1000)

问题答案

  • 代码1.1是响应式原理代码,为啥这样写?
    答:get收集依赖,set通知依赖,get是读取数据,set是设置数据,要通知到对应数据的副作用函数执行

  • vue3为什么要用Proxy?vue2用Object.defineProperty有啥缺点? 那么Proxy也有啥缺点? 答:Proxy能代理对象的任何操作,然后能监听到数组下标等操作,而Object.defineProperty不能,虽然vue2对数组原型方法进行扩展来通知数组触发副作用函数,但是还是不能通过修改下标来触发副作用函数,Proxy则有一个this指向错误的缺点,可以使用Reflect来解决this这个问题

响应式代码数据结构设计

问题概况

  • WeakMap和Map区别是什么?
    答:暂略
  • 代码1.2中设计WeakMap => Map => Set数据结构,为什么这样设计? 答:暂略
//代码1.2
const data = {
    text: '123'
}

let activeEffect = void 0;
let bucket = new WeakMap();

const obj = new Proxy(data, {
    get (target, key) {
        //收集依赖
        let targetMap = bucket.get(target)
        if (!targetMap) {
            bucket.set(target, (targetMap = new Map()))
        }
        let effectMap = targetMap.get(key)
        if (!effectMap) {
            targetMap.set(key, (effectMap = new Set()))
        }
        effectMap.add(activeEffect)

        return target[key]
    },
    set (target, key, newval) {
        target[key] = newval

        let targetMap = bucket.get(target)
        if (!targetMap) {
            bucket.set(target, (targetMap = new Map()))
        }
        let effectMap = targetMap.get(key)
        if (!effectMap) {
            targetMap.set(key, (effectMap = new Set()))
        }
        effectMap.forEach(fn => fn())
    }
})

function effect (fn) {
    activeEffect = fn
    fn();
}

effect(() => {
    console.log(obj.text)
    document.getElementById('app').textContent = obj.text
})
setTimeout(() => {
    obj.text = 'Hello World'
}, 1000)

问题答案

  • WeakMap和Map区别是什么?
    答:WeakMap是弱引用,而Map是强引用

    1. WeakMap只能用对象作为键值,Map可以用对象或基本数据类型作为键值
    2. WeakMap对对象进行引用计数,如没引用,会对其进行垃圾回收,防止内存泄漏
    3. Map对对应值会强引用,只能手动清除,如不手动清除,则一直保存其内存,即使没引用,也不会进行垃圾回收,会导致内存泄漏
  • 代码1.2中设计WeakMap => Map => Set数据结构,为什么这样设计? 答:WeakMap是代表data一定是个对象,vue代码的data数据结构设计是 { key: value },key有可能是基础数据类型,所以是Map,value则是副作用数据函数,防止重复添加,所以是Set,

    组件的data() { return () => {} }, 该函数返回一个对象,则作为WeakMap,所以data的return () => {}会进行垃圾回收

分支切换和cleanup

预期行为 如果当下面伪代码这种情况,预期行为应该是?

const obj = {ok: false, text: '123'}
effect(() => {
    body.innerHTML = obj.ok ? obj.text : 'default'
})
obj.text = '456'; //不应该触发副作用函数执行,因为没效果~

解决思路 重新清空cleanup副作用函数依赖,并重新增加副作用函数依赖~~

  1. effectFn中清空依赖关系 - 删除依赖关系
  2. Proxy的get增加依赖关系 - 重新获取依赖关系
  3. proxy的set遍历副作用函数 - 遍历中会获取依赖关系,导致爆栈~

问题概况

  • 代码1.3中为啥要单独增加deps来管理副作用函数依赖呢? 答:暂略
  • 代码1.3中Proxy的set为啥要new Set一个数据结构来处理副作用函数呢? 答:暂略
//代码1.3
const data = {
    text: '123',
    ok: true
}

let activeEffect = void 0;
let bucket = new WeakMap();

const obj = new Proxy(data, {
    get (target, key) {
        //收集依赖
        let targetMap = bucket.get(target)
        if (!targetMap) {
            bucket.set(target, (targetMap = new Map()))
        }
        let effectMap = targetMap.get(key)
        if (!effectMap) {
            targetMap.set(key, (effectMap = new Set()))
        }
        effectMap.add(activeEffect) //添加副作用函数 - new Set的代码就是用来处理这个
        activeEffect.deps.push(effectMap) //重新添加作用函数依赖

        return target[key]
    },
    set (target, key, newval) {
        target[key] = newval

        let targetMap = bucket.get(target)
        if (!targetMap) {
            bucket.set(target, (targetMap = new Map()))
        }
        let effectMap = targetMap.get(key)
        if (!effectMap) {
            targetMap.set(key, (effectMap = new Set()))
        }

        const effectsToRun = new Set(effectMap) //重新获取一个依赖副作用数组
        effectsToRun.forEach(fn => fn()) //执行,防止执行的时候----获取副作用添加
    }
})

function cleanup (effectFn) {
    //清空依赖关系
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i];
        deps.delete(effectFn) //删除当前副作用函数依赖
    }
    effectFn.deps.length = 0;
}

function effect (fn) {
    const effectFn = () => {
        cleanup(effectFn) //清除副作用依赖关系
        activeEffect = effectFn
        fn() //副作用函数执行, 追踪或触发函数,追踪就增加当前副作用函数依赖
    }
    effectFn.deps = [] //初始化当前副作用函数的依赖关系
    effectFn();
}

effect(() => {
    console.log(obj)
    document.getElementById('app').textContent = obj.ok ? obj.text : 'default' //触发track依赖
})

setTimeout(() => {
    obj.text = 'Hello World'
    // obj.ok = true
}, 1000)

问题答案

  • 代码1.3中为啥要单独增加deps来管理副作用函数依赖呢? 答:因为要单独管理副作用函数依赖,实现分支切换(依赖关系重新收集)
  • 代码1.3中Proxy的set为啥要new Set一个数据结构来处理副作用函数呢? 答:因为副作用函数里面可能增加依赖,到Deps里面,导致死循环,因为是引用关系~

嵌套effect和effect栈

预期行为 如果当下面伪代码这种情况,预期行为应该是?

const obj = {foo: true, bar: true}
effect(() => {
    effect(() => {
        console.log(obj.bar)
    })
   console.log(obj.foo) 
})
effect.foo = false; //但是执行obj.bar的副作用函数了

解决思路 通过栈来管理effect的嵌套关系,子函数嵌套就会进栈,等子函数执行完毕就出栈,直到栈顶为空~~类似javascript的函数栈

//代码1.4
const data = {
    foo: "foo component render 1",
    bar: "bar component render 1"
}

let activeEffect = void 0;
let bucket = new WeakMap();

const obj = new Proxy(data, {
    get (target, key) {
        track(target, key)
        return target[key]
    },
    set (target, key, newval) {
        target[key] = newval

        let targetMap = bucket.get(target)
        if (!targetMap) {
            bucket.set(target, (targetMap = new Map()))
        }
        let effectMap = targetMap.get(key)
        if (!effectMap) {
            targetMap.set(key, (effectMap = new Set()))
        }

        const effectsToRun = new Set(effectMap)
        effectsToRun.forEach(fn => fn()) 
    }
})

const effectStack = [] //副作用函数栈
function effect (fn) {
    const effectFn = () => {
        cleanup(effectFn) 
        activeEffect = effectFn
        effectStack.push(effectFn) //进栈
        fn() 
        effectStack.pop() //出栈
        activeEffect = effectStack[effectStack.length - 1] //重新获取上一个activeEffect
    }
    effectFn.deps = []
    effectFn();
}

var fooComponent, barComponent;
effect(() => {
    console.log(obj.foo) //先执行
    effect(() => {
        barComponent = obj.bar
        console.log(obj.bar)
    })
    fooComponent = obj.foo //后执行不符合预期,被子组件的副作用函数覆盖了
})

setTimeout(() => {
    obj.foo = 'foo component render 2' //不符合预期,输出obj.bar了
    // obj.bar = 'bar component render 2'
}, 500)

防止爆栈

预期行为 如果当下面伪代码这种情况,预期行为应该是? effect(() => { obj.n++; 或 obj.n = obj.n + 1; //死循环,先读取后执行 })

解决思路

  1. 防止执行等于activeEffect变量,因为在里面的时候不需要trigger,而外面可以执行trigger(set),但是不会track(get)
  2. 有效防止n++,死循环

问题概况

  1. i++在副作用函数(effect回调函数)会爆栈,为什么?
//代码1.5
const data = {
    n: 1
}

//省略代码
function trigger (target, key) {
    let targetMap = bucket.get(target)
    if (!targetMap) {
        bucket.set(target, (targetMap = new Map()))
    }
    let effectMap = targetMap.get(key)
    if (!effectMap) {
        targetMap.set(key, (effectMap = new Set()))
    }

    const effectsToRun = new Set()
    effectMap && effectMap.forEach(effectFn => {
        if (effectFn !== activeEffect) { //保护机制,如果副作用函数里面set值,那么不触发当前activeEffect函数,防止死循环,副作用函数外面读取不到
            effectsToRun.add(effectFn)
        }
    })
    effectsToRun.forEach(fn => fn()) 
}

effect(() => {
    console.log(obj.n)
    obj.n = 5;
})
obj.n++; //先get后set

// obj.n = obj.n + 1; 

问题答案

  1. i++在副作用函数(effect回调函数)会爆栈,为什么? i++在副作用函数里面(effect回调函数),先读取后设置,读取的时候没有activeEffect不执行,但是设置的时候会执行对应的副作用函数,副作用函数里面死循环 i++,如此类推,所以只需要排除fn不是activeEffect即可。如果解决了在副作用函数(effect回调函数)里面设置值会怎样?是不会触发自身的effect重新渲染的。 所以副作用函数先设置再读取。读取再设置就是获取到oldVal旧值

调度器

调度器可以做?

  1. 调度器需要改变数据的时候触发,可合并执行队列,多次改变数据只执行一次
  2. 调度器可以异步执行副作用函数
  3. 调度器可以当数据改变的生命周期(update)
//代码1.6调度器
const data = {
    n: 1
}
function trigger (target, key) {
    //...省略代码
    const effectsToRun = new Set()
    effectsToRun.forEach(fn => {
        if (fn.options.scheduler) {
            //核心代码
            fn.options.scheduler(fn)
        } else {
            fn()
        }
    }) 
}

const jobQueue = new Set();
const p = Promise.resolve();
const isFlushing = false;
function flushJob () {
    if (isFlushing) return //如果正在执行,那么退出
    isFlushing = true

    p.then(() => {
        jobQueue.forEach(fn => fn())
    }).finally(() => {
        isFlushing = false
    })
}

effect(() => {
    console.log(obj.n)
}, {
    scheduler: (fn) => {
        console.log('数据改变前')
        // setTimeout(fn) //把任务放到setTimeout中执行

        jobQueue.add(fn)
        flushJob() //如果处于刷新队列,那么不执行
    } //调度器
})
obj.n++;

计算属性和lazy

问题概况

  1. 计算属性可以根据layz和调度器来实现,追踪对应数据
//代码1.7
const data = {
    n: 1
}
//...省略代码
function computed (gettter) {
    //缓存值
    let result, dirty = true;
    const effectFn = effect(gettter, {
        lazy: true,
        scheduler () {
            if (!dirty) {
                dirty = true
                trigger(obj, 'value') //触发依赖值
            }
        }
    })

    const obj = {
        get value () {
            if (dirty) {
                result = effectFn()
                dirty = false
                track(obj, 'value') //追踪依赖值
            }
            return result
        }
    }
    return obj
}

//计算属性和lazy
const sum = computed(() => {
    console.log('sum')
    return obj.n + obj.n
})
const forSum = computed(() => {
    console.log('forSum')
    return sum.value
})
console.log(sum.value)
console.log(forSum.value)
obj.n = obj.n + 1;
console.log(sum.value)
console.log(forSum.value)

watch

watch可以做?

  1. 深度监听数据变化,做出改变后,执行副作用函数
  2. 数据竞态,只获取最后的finalData数据,丢弃过期数据
function watch (source, cb, options = {}) {
    let getter;

    if (typeof source === 'function') {
        getter = source()
    } else {
        getter = () => traverse(source);
    }
    
    let cleanup
    const onInvalidate = (fn) => {
        cleanup = fn
    }
    const job = () => {
        newval = effectFn();
        if (cleanup) {
            cleanup()
        }
        cb(newval, oldval, onInvalidate)
        oldval = newval
    }

    let oldval, newval;
    const effectFn = effect(() => getter(), {
        scheduler () {
            if (options.flush === 'post') {
                const p = Promise.resolve()
                p.then(job)
            } else {
                job()
            }
        },
        lazy: true
    })

    if (options.immediate) {
        job()
    } else {
        oldval = effectFn()
    }
}
//递归读取全部值,并且不读取相同的值,防止循环引用
function traverse (value, seen = new Set()) {
    if (seen.has(value) || typeof value !== 'object' || value === null) return;
    seen.add(value)
    for (let key in value) {
        traverse(value[key], seen)   
    }
    return value
}

//watch实现一个竞态请求代码
let finalData;
watch(obj, async (newval, oldval, onInvalidate) => {
    console.log(newval, oldval, 'update...')
    
    let expired = false
    onInvalidate(() => {
        expired = true
    })

    const res = await fetch('/api/xxx')
    if (!expired) {
        finalData = res;
    }
},{
    // immediate: true,
    flush: 'pre', //pre | post | sync
})
obj.n++

相关对应代码函数

Proxy劫持对象

proxy代理对象操作

let activeEffect = void 0;
let bucket = new WeakMap();

const obj = new Proxy(data, {
    get (target, key) {
        track(target, key)
        return target[key]
    },
    set (target, key, newval) {
        target[key] = newval
        trigger(target, key)
    }
})

track

追踪数据信息~,添加对应副作用函数

function track (target, key) {
    if (!activeEffect) return;
    let targetMap = bucket.get(target)
    if (!targetMap) {
        bucket.set(target, (targetMap = new Map()))
    }
    let effectMap = targetMap.get(key)
    if (!effectMap) {
        targetMap.set(key, (effectMap = new Set()))
    }
    effectMap.add(activeEffect) 
    activeEffect.deps.push(effectMap) 
}

trigger

触发副作用函数

function trigger (target, key) {
    let targetMap = bucket.get(target)
    if (!targetMap) {
        bucket.set(target, (targetMap = new Map()))
    }
    let effectMap = targetMap.get(key)
    if (!effectMap) {
        targetMap.set(key, (effectMap = new Set()))
    }

    const effectsToRun = new Set()
    effectMap && effectMap.forEach(effectFn => {
        if (effectFn !== activeEffect) { //保护机制,如果副作用函数里面i++值,那么不触发当前activeEffect函数,防止死循环
            effectsToRun.add(effectFn)
        }
    })
    effectsToRun.forEach(fn => {
        if (fn.options.scheduler) {
            //调度器
            fn.options.scheduler(fn)
        } else {
            fn()
        }
    }) 
}

jobQueue调度函数之防止多次执行队列

const jobQueue = new Set();
const p = Promise.resolve();
const isFlushing = false;
function flushJob () {
    if (isFlushing) return //如果正在执行,那么退出
    isFlushing = true

    p.then(() => {
        jobQueue.forEach(fn => fn())
    }).finally(() => {
        isFlushing = false
    })
}

cleanup和effect

/**
    目的:防止不必要的渲染
    作用:每次执行effect清空依赖关系,防止obj.foo为false,obj.bar改变都渲染一次~,没必要
    案例:effect(() => {
        document.body.innerHTML = obj.foo ? obj.bar : 'default'
    })
 */
function cleanup (effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i];
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0;
}

/**
    目的:实现一个effect嵌套的副作用函数栈
    作用:可以实现组件的层级渲染
    案例:effect(() => {
            effect(() => {
                temp1 = obj.foo
            })
            temp1 = obj.bar
        })
 */
const effectStack = [] //副作用函数栈
function effect (fn, options = {}) {
    const effectFn = () => {
        cleanup(effectFn) 
        activeEffect = effectFn
        effectStack.push(effectFn) //进栈
        const res = fn() 
        effectStack.pop() //出栈
        activeEffect = effectStack[effectStack.length - 1] //重新获取上一个activeEffect
        return res
    }
    effectFn.deps = []
    effectFn.options = options

    if (!options.lazy) {
        effectFn();
    }
    return effectFn
}

结语

  • 1.响应式数据结构 该数据结构能合理存储对应的effect
  • 2.封装响应式数据
  • 3.分支切换和cleanup清除 切换分支需要清除依赖,并且重新增加依赖
  • 4.嵌套effect 实现一个effects栈来实现组件的树级渲染,祖先级渲染必然导致子孙级渲染
  • 5.避免无限递归 避免清空依赖Deps,重新收集的时候,收集activeEffect,不等于当前activeEffect就可以执行,同样这个要配合effect栈来使用,每次执行完,清空栈顶
  • 6.调度执行,调度器实现多次改变数据,只会更新一次副作用函数,刷新队列,还可以将副作用函数放到不同的时机执行
  • 7.计算属性computed和lazy, 通过lazy实现计算属性,并用一个对象的value实现依赖追踪函数,并缓存数据
  • 8.watch的利用调度函数监听数据变化,以及立即执行、数据变化后的函数执行时机、过期副作用函数