VUE3源码阅读笔记(二)---响应式

607 阅读10分钟

上回,搞了一波vue3初始化流程的源码学习,其实那只是开胃小菜,学习初始化这部分源码能让我先了解到了vue3的基本架构,其次也让我更深入理解了vue3初始化的整个流程以及用法。学习源码的好处就是在这,之后做项目的时候用vue3在初始化碰到了啥问题就能够更快定位,这也算提升了自己的编程能力吧。这回我要学习的是vue3,也是vue这个框架最为重要的一块地方:响应式。

上期文章:vue3源码阅读笔记---初始化流程

vue2 vs vue3

首先我们要明白,数据响应式是什么。它会监听你相关数据的变化,然后和这些数据相关的内容就会更新。这就是响应式,它可以让我们的网页更加灵活,更富有灵魂。

vue2的实现

其实我并没有研究过vue2的源码,但是我知道vue2的响应式是通过Object.defineProperty实现的。

const reactive = (obj, key, val) => {
    Object.defineProperty(obj, key, {
        get() {
            return val
        },
        set(v) {
            val = v;
            // 监测到数据变化,做相关处理
            update()
        }
    })
}

他的思想是利用Object.definePropert去动态设置你的数据对象的key,之后这个数据对象的get和set就都能拿到了。由于reactive制造了一个闭包,所以当前这个val的状态会随着我们函数的调用保存起来,所以之后去取数据的时候我们就直接把闭包中的val返回。如果有人set值的话我们会把这个值保存在闭包中去做更新,之后再去做get操作的时候会有新的值返回。这就是这个基本工作原理。然后我们在set的时候在调用一个外面的函数去做其他操作,这就实现了响应式。

vue3的实现

vue3重写了响应式,它是用proxy实现的。这个es6新增的特性,但兼容性是真的差。但是因为它对于响应式是真的好用,所以vue3还是使用了proxy去重写了响应式。

const newReactive = (obj) => {
    return new Proxy(obj, {
        get(target, key) {
            return target[key]
        },
        set(target, key, val) {
            target[key] = val
            // 监测到数据变化,做相关处理
            update()
        }
    })
}

proxy的实现思路就不多讲了,它其实不止set和get还有很多其他的拦截操作,所以可以实现更多的想法。这里就跟vue2相比它的好处在哪?

  1. vue2初始化它会遍历对象所有的key,这会影响它的初始化速度。如果数据非常庞大,那么这个遍历还有之前闭包保存数据的这个内存消耗也会比较大。但是proxy就是在目标对象的外面加了一层拦截,并且用到了什么数据proxy才会去处理什么数据,所以它的内存消耗还有速度都是比较快的。
  2. vue2对于数组要特殊处理,我们知道在vue2不能直接用数组索引去修改数据。还有在一个对象里新增或删除一个对象的key,必须得用Vue.set和Vue.delete。这不符合我们的编程习惯,但是在vue3中就解决了这个问题,可以更愉快的敲代码了。

源码阅读

阅读相应式的源码前,我们要先看下vue3的响应式是怎么声明的,这样有利于我们去找源码的位置。

const state = reactive({ count: 0 })
state.count++
console.log(state.count)

由此可以看出,vue3声明响应式是用reactive这个函数的(还有其他声明响应式的方法,比如ref等,其实底层都是一样的思想,这里就以reactive为例。) 我们找到reactive这个函数在源码里的位置:vue-next/packages/reactivity/src/reactive.ts

export function reactive(target: object) {
  // 判断是否为readonly对象,是的话直接返回不做处理
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

这个函数比较简单,就先判断是否是readonly的对象,不是的话走createReactiveObject这个函数去创建响应式,我们去看createReactiveObject这个函数。

export const reactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
    // 不是对象,warn警告并直接返回
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 是否已经是proxy对象
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 这里走reactiveMap
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 这也是一个判断,判断是否为白名单(Object,Array,Map,WeakMap,Set,WeakSet类型)。
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 在对象上挂一层proxy代理(响应式的核心!)
  const proxy = new Proxy(
    target,
    // 对象走baseHandlers,数组走collectionHandlers
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy

这里我们发现,vue用WeakMap来做映射,正好去学习一波WeakMap。WeakMap的键只能是对象,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。这样有利于提升性能。 这个函数其实也不复杂,前面一部分就是一堆对于你传进来的东西的判断,最后在目标对象上挂一层proxy代理,然后返回这个proxy。所以现在重点就在于proxy的handler了,他是通过reactive函数传进来的,其中mutableHandlers处理对象和数组,mutableCollectionHandlers处理Map, WeakMap, Set, WeakSet。我们就看最常见的处理对象和数组的吧。

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

这里就只关注get和set,我们先看看get方法。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 通过Reflect拿到值
    const res = Reflect.get(target, key, receiver)
    // 如果已经是内置的方法,则不需要代理
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }
    // track依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }
    // 如果是ref对象就代理到value
    if (isRef(res)) {
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    // 这里判断是否是嵌套对象,如果是的话还要再走一遍reactive
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

其实简单来看,去除一些判断,get里就是先利用Reflect拿到值,然后通过track这个函数去收集依赖,最后返回结果。可以注意到的是如果发现是嵌套对象还会再走一遍上面的流程。实现深层次的监听。现在看看track这个函数在干啥

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  // 判断有没有被收集过,没有就收集一下
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  // 判断key有没有被收集过,没有也收集一下
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 将activeEffect塞到map中
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

这个函数做了两件事

  1. 将activeEffect塞到map里
  2. 触发onTrack 总结来说,这个track函数就是个建立映射的过程,但我们有个疑问,这个activeEffect是个啥?所以我们又要去源码里找这个activeEffect,这个activeEffect是由createReactiveEffect这个函数创建的,调用createReactiveEffect的函数是effect,这个effect在三个地方被
  3. trigger函数通过schedulerRun调用effect
  4. mountComponent通过setupRenderEffect调用effect
  5. doWatch通过调用scheduler调用effect 这里我们应该去看trigger函数,这个是核心函数
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  // 没收集过,啥都不干
  if (!depsMap) {
    return
  }
  // 去重
  const effects = new Set<ReactiveEffect>()
  
  // ...

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // 执行函数
  effects.forEach(run)
}

这个函数总结起来也只是做了一件事,执行函数。所以我们也可以知道activeEffect就是一些要执行的方法。那这个trigger函数又是在哪里被调用了呢,答案就是在之前proxy代理的set方法里

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 拿到原始值
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      // 如果原始值是ref对象,新的值不是,直接修改ref对象的value值
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    // 看下原始对象是否有这个新值得key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // 通过Reflect拿到原始的set行为
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
    // 没有这个key就添加这个属性,否则就修改原属性的值
    // 触发trigger执行函数
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

有点神奇,我们还在看get这个方法,看着看着就看到了set方法,形成了一个闭环。那我们就正好来看看这个set,set函数主要执行了以下四件事:

  1. 首先拿到原始值oldValue
  2. 然后进行判断,如果原始值是ref对象,新赋值不是ref对象,直接修改ref包装对象的value属性
  3. 然后通过Reflect拿到原始的set行为,如果原始对象里是否有新赋值的这个key,没有这个key,则是添加属性,否则是给原始属性赋值
  4. 进行对应的修改和添加属性操作,通过调用trigger通知deps更新,通知依赖这一状态的对象更新。 所以到这源码部分就看完了,作为vue最核心的部分,它没有初始化流程那样的绕,思路很清晰,但也可以看出逻辑的缜密,并且关于性能优化也有很多处理。而且还学习到了之前没接触过得WeakMap类型,之后写vue3肯定会更加得心应手。

手写简单响应式

读完源码当然还不够,我们还是得简单写一个响应式来巩固学习的知识。首先第一步先简单写一个reactive函数

const reactive = (obj) => {
    return new Proxy(obj, {
        get(target, key) {
            // 读操作的拦截
            const res = Reflect.get(target, key)
            console.log('get')
            return res
        },
        set(target, key, value) {
            // 写操作的拦截
            const res = Reflect.set(target, key, value)
            console.log('set')
            return res;
        }
    })
}

这样就能监测到一个对象里的属性读写操作,但是如果是嵌套的对象就不太行,所以还得做个递归。

const isObject = value => typeof value === 'object'

const reactive = (obj) => {
    // 进来要是个对象
    if(!isObject(obj)) {
        return
    }
    return new Proxy(obj, {
        get(target, key) {
            // 读操作的拦截
            const res = Reflect.get(target, key)
            console.log('get')
            return isObject(res) ? reactive(res) : res
        },
        set(target, key, value) {
            // 写操作的拦截
            const res = Reflect.set(target, key, value)
            console.log('set')
            return res;
        }
    })
}

首先判断一下传进来的值是不是对象,在get操作的return的时候判断读取的值是否是对象,如果是的话就再调用一下reactive函数,这样就实现了嵌套对象的响应式。 接下来我们要做的是依赖收集的功能

const effect = (fn) => {
    const e = createReactiveEffect(fn)

    // 触发依赖的track过程
    e()

    return e
}

const createReactiveEffect = (fn) => {
    const effect = function reactiveEffect() {
        // 捕获错误
        try{
            // 入栈操作
            effectStack.push(effect)
            return fn()
        }finally{
            effectStack.pop()
        }
    }

    return effect
}

这里创建了一个高阶函数,并且把这个函数存入effectStack,目的是可以捕获错误。 接下来实现track函数

// WeakMap保存映射关系
const targetMap = new WeakMap()

const track = (target, key) => {
    const effect = effectStack[effectStack.length - 1]
    if(effect) {
        // 获取target映射关系map
        const depMap = targetMap.get(target)
        if(!depMap) {
            depMap = new Map()
            targetMap.set(target, depMap)
        }
        // 继续获取key对应的set集合
        const deps = depMap.get(key)
        if(!deps) {
            deps = new Set()
            depMap.set(key, deps)
        }
        // 将当前活动的响应式函数放入deps中
        deps.add(effect)
    }
}

这个函数的作用很简单,就是把当前活动的相应式函数收集到,如果没有就创建一个结构,结构式WeakMap{Map{Set}}。这个函数是在proxy的get中执行的。 最后我们需要实现trigger函数,就是触发你收集的响应式函数

// 触发函数
const trigger = (target, key) => {
    const depMap = targetMap.get(target)
    if(!depMap) {
        return 
    }

    const deps = depMap.get(key)
    if(deps) {
        deps.forEach(dep => dep())
    }
}

这个函数就不解释了,它是在proxy的set里执行的。 到这里,一个简单的响应式就写完了,我们当然得写个页面测试一下啦 页面的源码在此,实现的是点击按钮页面上的count数字增加的效果

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <button id="button">count++</button>
    <script>
        const isObject = value => typeof value === 'object'

        const reactive = (obj) => {
            // 进来要是个对象
            if(!isObject(obj)) {
                return
            }
            return new Proxy(obj, {
                get(target, key) {
                    // 读操作的拦截
                    const res = Reflect.get(target, key)
                    // 依赖收集
                    track(target, key)
                    return isObject(res) ? reactive(res) : res
                },
                set(target, key, value) {
                    // 写操作的拦截
                    const res = Reflect.set(target, key, value)
                    trigger(target, key)
                    return res;
                }
            })
        }

        // 临时保存依赖函数
        const effectStack = []

        const effect = (fn) => {
            const e = createReactiveEffect(fn)

            // 触发依赖的track过程
            e()

            return e
        }

        const createReactiveEffect = (fn) => {
            const effect = function reactiveEffect() {
                // 捕获错误
                try{
                    // 入栈操作
                    effectStack.push(effect)
                    return fn()
                }finally{
                    effectStack.pop()
                }
            }

            return effect
        }

        // WeakMap保存映射关系
        const targetMap = new WeakMap()

        // 跟踪函数
        const track = (target, key) => {
            const effect = effectStack[effectStack.length - 1]
            if(effect) {
                // 获取target映射关系map
                let depMap = targetMap.get(target)
                if(!depMap) {
                    depMap = new Map()
                    targetMap.set(target, depMap)
                }
                // 继续获取key对应的set集合
                let deps = depMap.get(key)
                if(!deps) {
                    deps = new Set()
                    depMap.set(key, deps)
                }
                // 将当前活动的响应式函数放入deps中
                deps.add(effect)
            }
        }

        // 触发函数
        const trigger = (target, key) => {
            const depMap = targetMap.get(target)
            if(!depMap) {
                return 
            }

            const deps = depMap.get(key)
            if(deps) {
                deps.forEach(dep => dep())
            }
        }
    </script>
    <script>
        const state = reactive({
            count: 0
        })
        effect(() => {
            const app = document.querySelector('#app')
            app.innerHTML = state.count
        })
        document.querySelector('#button').onclick = () => {
            console.log(11)
            state.count++
        }
    </script>
</body>
</html>

效果呈上!

总结

比起 Vue 2.x , Vue 3.x 对于响应式方面全面拥抱了 Proxy API,通过代理初始对象默认行为来实现响应式;reactive内部利用WeakMap的弱引用性质和快速索引的特性,使用WeakMap保存了响应式代理和原始对象, readonly 代理和原始对象的互相映射;对于对象和数组类型,是通过响应式代理的相关陷阱方法实现原始对象响应式,而对于Set、Map、WeakMap、WeakSet类型,因为受到Proxy的限制,Vue 3.x 使用了劫持get、has、add、set、delete、clear、forEach等方法调用来实现响应式原理。奇怪的知识又增加了!

我是wzk-developer,欢迎跟我交流前端知识,我的邮箱是zjutwzk@163.com; github: wzk-zjut