vue3源码学习02-响应式原理、依赖收集

84 阅读1分钟

本节主要内容

  1. 数据响应式的实现
  2. 依赖收集过程
  3. 手写实现网页响应式

响应式源码学习

vue3响应式的核心是reactive。这部分源码参见vue/packages/reactivity。

reactive会对对象所有嵌套属性做转换,返回对象的响应式副本。 ref本质上也是reactive实现,ref('test') = reactive({value: 'test'}),所以我们访问ref时都要使用.value

// 源码中reactive 由 createReactiveObject实现。
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // reactive不支持直接传入原始数据类型的target。如果target不是引用类型,则直接返回原始值。
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // xxxx
  
  // 核心:使用proxy代理
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}  
const mutableHandlers = {
  get:(target: Target, key: string | symbol, receiver: object)=>{ 
      // 1、拿到target上key对应的值
      // 2、track跟踪一下,将activeEffect存储到target-key对应的副作用函数序列里
      // 3、判断返回结果是否是对应类型,是的话往下递归,否则直接返回。
     const res = Reflect.get(target, key, receiver)
     track(target, key)
     if (isObject(res)) {
         // 如果是对象类型,则转换为Proxy返回。
         // 这里的精妙之处在于,不在初始化的时候做拦截,等用户访问到了再做响应式处理,
         // 一个lazy reactive,减少不必要的递归。有专业人士称之为渐进式响应式。
          return isReadonly ? readonly(res) : reactive(res)
    }

    return res 
  },
  set: ( target: object, key: string | symbol, value: unknown, receiver: object)=>{
      // 1、设置target的key值,
      // 2、trigger一下target.key对应的副作用函数
      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)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  },
  has: function(target: object, key: string | symbol): boolean {
      const result = Reflect.has(target, key)
      if (!isSymbol(key) || !builtInSymbols.has(key)) {
        track(target, TrackOpTypes.HAS, key)
      }
      return result
    },
  deleteProperty: function(target: object, key: string | symbol): boolean {
      // 删除属性,流程同set,相当于给set了一个undefined,delete target.key
      const hadKey = hasOwn(target, key)
      const oldValue = (target as any)[key]
      const result = Reflect.deleteProperty(target, key)
      if (result && hadKey) {
        trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
      }
      return result
  }, 
} 
const mutableCollectionHandlers = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
// 存储target与副作用函数之前的映射函数
// 这里使用WeakMap,是因为WeakMap的key值可以是对象类型
const reactiveMap = new WeakMap<Target, any>() 
 
const reactive = function(target) {
    return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers,
        reactiveMap
    )
} 

依赖收集源码学习

上节我们介绍初始化流程的时候,提到setupComponent时,里面调用了一个函数setupRenderEffect(依赖收集),这里我们就来详细看下源码是怎么实现这部分功能的。

1、创建副作用更新函数


  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => { 
    
    const componentUpdateFn = () => {
       // 副作用更新函数,这里只摘取关键代码
       if(!intance.isMounted) {
         // 组件初次渲染
         // 根据组件根结点的渲染函数拿到组件的children。renderComponentRoot函数主要逻辑为:
         const {
             type: Component, 
             vnode, 
             proxy, 
             withProxy, 
             props, 
             slots,
             render, 
             renderCache, 
             data, 
             setupState, 
             ctx 
         } = instance  
         const proxyToUse = withProxy || proxy
         // 1、触发beforeMount生命周期钩子
         
         //根据实例的上下文,调用render函数,拿到子组件vnode
         const subTree = (instance.subTree = normalizeVNode(
            render!.call(
              proxyToUse,
              proxyToUse!,
              renderCache,
              props,
              setupState,
              data,
              ctx
            )
          ))
         // 2、调用patch,初始化子组件 
         patch(null, subTree,container, ...) 
         // 3、触发mounted生命周期钩子
         
       }else {
         // 更新组件
         /** 组件更新有两种场景:
         一是组件自身state发生变化,需要重新渲染;
         另一种是父组件更新过程中遇到子组件,需要判断子组件是否需要更新,
         如果需要更新则主动执行子组件的重新渲染方法,
         这种情况下,next就是新的子组件vnode
         */
         let { next, bu, u, parent, vnode } = instance
         if (next) {
          // 设置更新后vnode的el,因为下面第一次渲染新的组件vnode没有设置;
          next.el = vnode.el
          updateComponentPreRender(instance, next, optimized)
        } else {
          // 如果没有next,直接指向vnode
          next = vnode
        }
        // 1、触发beforeUpdate或onBeforeUpdate  
        if (bu) {
          invokeArrayFns(bu)
        } 
        if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
          invokeVNodeHook(vnodeHook, parent, next, vnode)
        }
        // 2、根据上下文、render函数拿到子组件新的vnode,执行patch更新
        const nextTree = renderComponentRoot(instance)
        // 更新实例的subTree
        const prevTree = instance.subTree
        instance.subTree = nextTree
        // 执行patch,更新的时候patch再根据节点类型,执行不同的更新方法。
        // 是数组类型的则遍历更新,是树结构的则递归遍历....
        patch(
          prevTree,
          nextTree,
          // parent may have changed if it's in a teleport
          hostParentNode(prevTree.el!)!,
          // anchor may have changed if it's in a fragment
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          isSVG
        )
        
        // 3、触发update hook 或 onUpdated
       }
        
    }

    // 创建副作用函数并缓存下来
    
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))

    const update: SchedulerJob = (instance.update = () => effect.run())
    update.id = instance.uid
    // xxx

    // 缓存副作用函数后,立即执行一次
    update()
  }
  

瞧瞧上边的effect咋来的,new了一个ReactiveEffect。

// here it comes: packages/reactivity/src/effect.ts。这里仅摘取关键代码
  
  const targetMap = new WeakMap<any, KeyToDepMap>()
  export let activeEffect: ReactiveEffect | undefined
  export class ReactiveEffect<T = any> {
      // 注意下此处的写法
      constructor(
        public fn: () => T,
        public scheduler: EffectScheduler | null = null,
        scope?: EffectScope
      ) {
        recordEffectScope(this, scope)
      }

      run() {
        /**
        
        */
         if (!this.active) {
         // 当前effect不是激活状态,不需要进行依赖收集,直接调用this.fn()
            return this.fn()
         }
        try {
          // this.parent指向全局变量activeEffect,如果当前ReactiveEffect对象调用run方法时,是在其他ReactiveEffect的run方法里的,那么this.parent就会指向activeEffect,再将activeEffect指向当前effect,等当前的依赖收集完了,activeEffect再指回到this.parent
         
          this.parent = activeEffect 
          activeEffect = this // 
          shouldTrack = true

          trackOpBit = 1 << ++effectTrackDepth

          if (effectTrackDepth <= maxMarkerBits) {
            initDepMarkers(this)
          } else {
            cleanupEffect(this)
          }
          return this.fn()
        } finally {
          if (effectTrackDepth <= maxMarkerBits) {
            finalizeDepMarkers(this)
          }

          trackOpBit = 1 << --effectTrackDepth

          activeEffect = this.parent // 指回“依赖”层
          shouldTrack = lastShouldTrack
          this.parent = undefined 

          if (this.deferStop) {
            this.stop()
          }
        }
      }
  }

effect函数嵌套:vue3使用树形结构解决该问题。

// 这里在parent effect执行的时候,会触发child effect执行
effect(()=> {
    console.log('parent执行')
    effect(()=> {
        console.log('child 执行') 
        /** 当前effect执行的时候,将activeEffect赋值给child.parent, 
        activeEffect指向child,child执行完了,再将activeEffect指向parent。
        不然等proxy.name执行的时候,activeEffect是指向child effect的
        */
        proxy.id
    })
    proxy.name
})
    

2、track:收集target.key对应的副作用函数

// 我们上面提到的targetMap是一个存储target-key-effectFnSets的WeakMap
// 这部分目的就是将effectFn存储起来
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) { 
    // 获取target对应的depsMap,没有则新建
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 获取key对应的effectSets,没有则新建
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}

3、trigger:触发target.key对应的副作用函数集

// 从targetMap中一次查找target->key->effect set
// 遍历effect set,依次run()
export function trigger

总结

  • effect(fn):传入fn,返回响应式副作用函数。fn内部引用到了reactive的数据,proxy发生变化的时候,副作用函数会再次执行
  • track(target, key): 建立target-key和副作用函数之间的映射关系
  • trigger(target, key): 根据track建立的映射关系,找到对应的副作用函数并执行。

手写简易版数据响应式和依赖收集

<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <div id="app"></div>
    <script>
      const isObject = v => typeof v === 'object' && v !== null
      const reactive = target => { 
        if(!isObject(target)) return target
        return new Proxy(target, {
          get: (target, key, receiver) => {
            const res = Reflect.get(target, key, receiver)
            track(target, key)
            return isObject(res) ? reactive(res) : res
          },
          set: (target, key, value, receiver) => {
            Reflect.set(target, key, value, receiver)
            trigger(target, key)
            return target
          },
          has: (target, key) => {
            track(target, key)
            return Reflect.has(target, key)
          },
          deleteProperty: (target, key) => {
            const res = Reflect.deleteProperty(target, key)
            trigger(target, key)
            return res
          }
        })
      }
      const targetMap = new WeakMap()
      const effectStack = []
      const track = (target, key) => {
        let dep = effectStack[effectStack.length - 1]
        if (!dep) return
        let depsMap = targetMap.get(target)
        if (!depsMap) {
          depsMap = new Map()
          targetMap.set(target, depsMap)
        }
        let deps = depsMap.get(key)
        if (!deps) {
          deps = new Set()
          depsMap.set(key, deps)
        }
        deps.add(dep)
      }
      const trigger = (target, key) => {
        const depsMap = targetMap.get(target)
        const deps = depsMap.get(key)
        deps.forEach(fn => fn())
      }
      const state = reactive({
        name: 'vue3'
      })
      const effect = fn => {
        const e = createReactiveEffect(fn)
        e()
        return e
      }
      const createReactiveEffect = fn => {
        effectStack.push(fn)
        const effect = function (...args) {
          let cur
          try {
            cur = effectStack[effectStack.length - 1]
            cur(...args)
          } finally {
            cur = null
            effectStack.pop()
          }
        }
        return effect
      }
      const app = document.querySelector('#app')
      effect(() => {
        app.innerHTML = `hello: ${state.name}`
      })
      setTimeout(() => {
        state.name = 'bobo'
      }, 1000)
    </script>
  </body>
</html>