[Vue] 解析 Vue 框架底层源码(下)

2023 年 1 月 31 日 星期二(已编辑)
2

[Vue] 解析 Vue 框架底层源码(下)

温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释

链接: https://juejin.cn/column/7149818417325801503

目录

1. 生命周期

2. KeepAlive

3. provide | inject

4. Vue Diff

5. toRef | toRefs | proxyRefs

异步组件

代码示例

let asyncComponent = defineAsyncComponent({
  loader: async () => {
    await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
      }, 3000)
    })

    return import('./test.js').then(data => data.default)
  },
  timeout: 2000,
  delay: 1000,

  loadingComponent: {
    render: () => {
      return h('div', 'loading…………')
    }
  },
  errorComponent: {
    render: () => {
      return h('div', 'error…………')
    }
  }
})

createApp(asyncComponent).mount('#app')
// ./test.js
const App = {
  setup() {
    return () => h('div', 'App')
  }
}
export default App

第一:首先调用 defineAsyncComponent 函数,并传入 option. 在 函数中初始化状态后,返回一个 defineComponent 定义的组件对象。

const {
  loader,
  loadingComponent,
  errorComponent,
  delay = 200,
  timeout, // undefined = never times out
  suspensible = true,
  onError: userOnError
} = source

let retries = 0

const retry = () => {
  retries++
  pendingRequest = null
  return load()
}

const load = (): Promise<ConcreteComponent> => {}
return defineComponent({
    return () => {
      if (loaded.value && resolvedComp) {
        return createInnerComp(resolvedComp, instance)
      } else if (error.value && errorComponent) {
        return createVNode(errorComponent as ConcreteComponent, {
          error: error.value
        })
      } else if (loadingComponent && !delayed.value) {
        return createVNode(loadingComponent as ConcreteComponent)
      }
    }
  }
}) as T

第二:执行,createApp(asyncComponent).mount('#app'),执行 asyncComponent 组件对象的 setup 函数:

  • 初始化 loaded, error, delay 响应式状态。
  • 调用 load 函数,调用我们传入的 loader 异步函数。异步函数中异步 resolve 挂起微任务队列。继续执行同步任务。
  • 同步任务继续执行,判断当前 loaded 状态为 false, 然后 delay 为 true, 白屏渲染。
return () => {
  if (loaded.value && resolvedComp) {
    return createInnerComp(resolvedComp, instance)
  } else if (error.value && errorComponent) {
    return createVNode(errorComponent as ConcreteComponent, {
      error: error.value
    })
  } else if (loadingComponent && !delayed.value) {
    return createVNode(loadingComponent as ConcreteComponent)
  }
}
  • delay 一秒之后,将 delay 置为 false, delay 响应式对象触发组件更新,重新执行 render 函数,然后 delay 为 false 和 loadingComponent, loadingComponent渲染。
if (delay) {
  setTimeout(() => {
    delayed.value = false
  }, delay)
}
  • timout 到第三秒的时候,这时 loaded 仍然为 false,但是触发超时,error 响应式对象触发组件更新,触发 onError 函数,errorComponent渲染

  • 等到第三秒的时候,resolve 的微任务队列执行。拿到要渲染的组件对象赋值给 resolvedComp,然后将 loaded 置为 true, loaded 响应式对象触发组件更新, 显示 App 组件。

return () => {
  if (loaded.value && resolvedComp) {
    return createInnerComp(resolvedComp, instance)
  } else if (error.value && errorComponent) {
    return createVNode(errorComponent as ConcreteComponent, {
      error: error.value
    })
  } else if (loadingComponent && !delayed.value) {
    return createVNode(loadingComponent as ConcreteComponent)
  }
}

至此异步组件渲染完毕。

生命周期

代码示例

<div id="app"></div>
<script>
  let {
    createApp,
    reactive,
    Fragment,
    toRefs,
    h,
    getCurrentInstance,
    provide,
    inject,
    onMounted
  } = Vue

  const MyCpn = {
    setup(props, { emit, slots }) {
      const state = inject('state')
      return () => h('div', null, [state.count])
    }
  }

  const App = {
    setup() {
      const state = reactive({ count: 0 })
      provide('state', state)

      onMounted(() => {
        setTimeout(() => {
          state.count++
        }, 2000)
      })
    },

    render() {
      return h(MyCpn, null)
    }
  }

  createApp(App).mount('#app')
</script>

第一:调用 onMounted 生命周期函数,实际上是调用了 createHook 函数,并通过枚举类型,传入生命周期的 type。

export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)

生命周期 枚举类型的 type, 就是首字母的缩写

export const enum LifecycleHooks {
  BEFORE_CREATE = 'bc',
  CREATED = 'c',
  BEFORE_MOUNT = 'bm',
  MOUNTED = 'm',
  BEFORE_UPDATE = 'bu',
  UPDATED = 'u',
  BEFORE_UNMOUNT = 'bum',
  UNMOUNTED = 'um',
  DEACTIVATED = 'da',
  ACTIVATED = 'a',
  RENDER_TRIGGERED = 'rtg',
  RENDER_TRACKED = 'rtc',
  ERROR_CAPTURED = 'ec',
  SERVER_PREFETCH = 'sp'
}

第二:在 createHook 函数中,返回生命周期钩子函数,我们组件当中调用的生命周期函数就是 createHook 返回的这个函数。

export const createHook =
  <T extends Function = () => any>(lifecycle: LifecycleHooks) =>
  (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
    // post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
    (!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
    injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)

第三:传入钩子 hook 和 当前组件实例,调用 injectHook 函数。

第四:在 injectHook 函数中,将生命周期函数,加入到组件实例对应的生命周期队列当中去。

{
 m: [() => {}, () => {}],
 u: [() => {}, () => {}]
}
export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    // cache the error handling wrapper for injected hooks so the same hook
    if (prepend) {
      hooks.unshift(wrappedHook)
    } else {
      hooks.push(wrappedHook)
    }
    return wrappedHook
  }
}

最后就是将这些生命周期的钩子函数,在组件挂载和更新的阶段进行调用了。

KeepAlive 原理

代码示例

const c1 = {
  name: "c1",
  setup() {
    return () => {
      console.log('c1 渲染')
      return h('div', null, 'c1')
    }
  }
}

const c2 = {
  name: "c2",
  setup() {
    return () => {
      console.log('c2 渲染')
      return h('div', null, 'c2')
    }
  }
}

render(h(KeepAlive, null, { default: () => h(c1) }), app)

setTimeout(() => {
  render(h(KeepAlive, null, { default: () => h(c2) }), app)
}, 2000)

setTimeout(() => {
  render(h(KeepAlive, null, { default: () => h(c1) }), app)
}, 3000)

第一:第一次 KeepAlive 组件挂载的时候,还是走正常逻辑,组件挂载可以看之前的这篇文章, [# [Vue 源码] Vue 3.2 - 组件挂载原理](https://juejin.cn/post/7210584352635125818) 这里我们直接到执行 setup 函数,及其返回的 render 函数流程。

  • 执行 setup 函数。

    • const keys: Keys = new Set()存储缓存组件的 key, 如果没有就存储 组件的 name。
    • const cache: Cache = new Map() 存储缓存 KeepAlive 组件 name 和 subTree 的映射 (也就是组件 rener函数/vue 模板的虚拟dom)

    • onMounted(cacheSubtree) 挂载的时候去收集 subTree 和 Name 映射。本案例是 c1 -> c1 的subTree.
    • onUpdated(cacheSubtree) 更新的时候去收集 subTree 和 Name 映射。
    • 初始化 activate 和 deactivate 方法,前者是 激活 KeepAlive 组件方法,后者是 KeepAlive 卸载的失活方法。
    • 从组件示例的 ctx 拿到 renderer 渲染器,上面定义了一些供 keepAlive 操作 Dom 的一些方法。这些方法是在第一次挂载 keepAlive 组件时挂载的。
// mountComponent 这些方法是在第一次挂载 keepAlive 组件时挂载的。
// inject renderer internals for keepAlive
if (isKeepAlive(initialVNode)) {
  ;(instance.ctx as KeepAliveContext).renderer = internals
}
// 定义了一些供 keepAlive 操作 Dom 的一些方法
const {
      renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement }
      }
    } = sharedContext
  • 执行 setup 函数返回的 render 函数。
    • keepAlive 组件的 children 会被编译成组件的 default 插槽。render(h(KeepAlive, null, { default: () => h(c1) }), app)
    • 渲染的时候通过 const children = slots.default(),取出默认插槽进行渲染。

至此第一次挂载完毕,页面上显示出了 c1.并在控制台打印出 c1 渲染。

  • 两秒过后,再次渲染 c2 的 KeepAlive 组件,先执行 c1 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
sharedContext.deactivate = (vnode: VNode) => {
     const instance = vnode.component!
     move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
}
  • 在 deactivate 中将 c1 的 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。
  • storageContainer 就是在 KeepAlive 中创建的 div, const storageContainer = createElement('div').
  • c1 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
  • 开始渲染 c2 KeepAlive 组件, setup 函数 -> render 函数, 再将 c2 和 对应的subTree 放入缓存中去
  • 执行 c2 的 render 函数,打印 c2 渲染,返回虚拟dom,patch 更新插槽之后完成渲染。

自此 c2 被渲染到了页面上。

  • 过了一秒后,再次渲染 c1 的 KeepAlive 组件,先执行 c2 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
  • 在 deactivate 中将 c2 的 dom 并没有被卸载/remoe, 而是还存在了 storageContainer 这个 dom 中。
  • storageContainer 就是在 KeepAlive 中创建的 div, const storageContainer = createElement('div').
  • c2 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
  • 开始渲染 c1 KeepAlive 组件,发现 c1 已经被缓存过了, 给 KeepAlive innerChild 孩子打上COMPONENT_KEPT_ALIVE 标记 。 if (cachedVNode) { // copy over mounted state vnode.el = cachedVNode.el vnode.component = cachedVNode.component vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE }
  • 由于 KeepALive 组件的第一个孩子 vnode 的 shapeFlag 打上了缓存标记,所以走 active 逻辑。
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
  ;(parentComponent!.ctx as KeepAliveContext).activate(
    n2,
    container,
    anchor,
    isSVG,
    optimized
  )
}
  • 在 active 中将 subTree 指向的 dom 引用,从 storageContainer 缓存容器中移动了回来。并没有触发 render 函数,这时候界面上已经 有 c1 了。
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  const instance = vnode.component!
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense)

自此 KeepAlive 缓存组件渲染完毕!

核心原理

KeepAlive 中的核心原理,就是 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。希望以下的这段代码可以给读者更多的启示和思考。

当然 KeepAlive 中的 LRU 最近最少使用算法,也值得读者去阅读和探索。

<div id="app"></div>
<div id="storage"></div>
<script>
  let divItem = document.createElement('div')
  divItem.innerHTML = 'app'

  let obj = { item: divItem }

  storage.appendChild(obj.item)

  setTimeout(() => {
    app.appendChild(obj.item)
  }, 2000)
</script>

provide | inject 原理

代码示例

<div id="app"></div>
<script>
  let {
    createApp,
    reactive,
    Fragment,
    toRefs,
    h,
    getCurrentInstance,
    provide,
    inject
  } = Vue

  const MyCpn = {
    setup(props, { emit, slots }) {
      const state = inject('state')
          
      return () => h('div', null, [state.count])
    }
  }

  const App = {
    setup() {
      const state = reactive({ count: 0 })
      provide('state', state)
          
      setTimeout(() => {
        state.count++
      }, 2000)
    },

    render() {
      return h(MyCpn, null)
    }
  }

  createApp(App).mount('#app')
</script>

第一:mountComponent 挂载组件阶段, 创建每个组件实例的时候,每个子组件实例的 provides 属性,默认继承自父组件实例的 provides 属性。

const instance: ComponentInternalInstance = {
  provides: parent ? parent.provides : Object.create(appContext.provides),
}

第二:调用 Provide 函数:

  • 先获取到当前组件实例的 provides, 再获取到当前父亲组件实例的 provides。
  • 第一次两个 provides 相同,所以会通过 Object.create() 再 parentProvides 的基础上创建子 provides (子对象的原型链指向父对象)
  • 如果在子组件中多次 provide, 就会走到 else 逻辑,在子 provide 上进行赋值操作。
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // TS doesn't allow symbol as index type
    provides[key as string] = value
  }
}

每个子组件都是将所有父组件的 provides 通过 Object.create 关联起来,再生成自己的 provides。

第二:调用 inject 函数

  • instance.parent.provides 获取到 父组件的 Provides
  • return provides[key as string] 从 provides 中找到 key 属性对应的属性值,并返回。
export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // fallback to `currentRenderingInstance` so that this can be called in
  // a functional component
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // #2400
    // to support `app.use` plugins,
    // fallback to appContext's `provides` if the instance is at root
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    if (provides && (key as string | symbol) in provides) {
      // TS doesn't allow symbol as index type
      return provides[key as string]
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue.call(instance.proxy)
        : defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  } else if (__DEV__) {
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}

子组件消费的是父组件的 provides ,不包括本组件的 provides。

Vue Diff

概述

Vue Diff ,React Diff 算法都是同级节点的递归比较, 核心就是复用。

为什么是同级比较,笔者认为用户在操作 Dom 节点的时候,很少有将儿子节点变为父亲的情况,多数都是 向后追加,向前插入,正序,反序,这是非常常见的。如果不同级比较,去比较两棵树,是非常耗费性能的。

旧节点:a b c d e q f g

新节点:a b e c d h f g

假设 key 都是自身的字母,同一字母的 type 不变。

React Diff 流程

React Difff 算法

对于以上两个新旧节点虚拟列表(React 是新的虚拟 dom 和 旧的 fiber 列表进行比较生产新的 fiber 列表),React 的做法是:

  • 遍历新节点,与旧节点进行对比
  • a,b 发现 type 和 key 相同,复用老的 fiber 去创建新的 fiber 节点
  • 发现 e 和 c 不相同,break 跳出循环
  • 将 c 之后包括 c 的旧节点加入 map中,键:key 值:旧的 fiber 节点
  • 继续遍历新节点,遍历到 e 从 map 当中找,发现有,设置 lastIndex 为 4,4为 e 在旧节点当中的索引
  • c 从 map 中找有,index < lastIndex, 2 < 4, 新的 fiber 节点标记移动
  • d 从 map 中找有,index < lastIndex, 3 < 3,新的 fiber 节点标记移动
  • h 从 map 中找,没有,lastIndex = 5,标记新增
  • f 从 map 中找,有 index > lastIndex 6 > 5, lastIndex = 6,复用呆在原地
  • g 从 map 中找,有 index > lastIndex 7 > 6, lastIndex = 7,复用呆在原地
  • 删除真实dom节点中要移动和删除的节点,展示变为 a b e f g
  • 开始移动
  • c 节点在 新结点中索引为 3, mountIndex 为 3,发现 3 在 a b e f g 有元素f。
  • insertBefore 将 c 插入到 f 前,变为 a b e c f g
  • d 节点在 新结点中索引为 4, mountIndex 为 3,发现 3 在 a b e c f g 有元素f。
  • insertBefore 将 c 插入到 f 前,变为 a b e c d h f g
  • 完成。

Vue Diff 流程

对于以上两个新旧节点虚拟列表(Vue 是新的虚拟 dom 和 旧的虚拟 dom 列表进行比较生产新的 fiber 列表),Vue 的做法是:

  • 遍历新节点,与旧节点进行对比
  • async start,从前遍历,a, b 与旧节点相同,不动
  • 发现一个不相同的节点,break
  • async end, 再向后遍历,f, g 与旧节点相同,不动
  • 发现一个不相同的节点,break
  • unkoown sequence, 两边尽可能复用,然后从中间开始遍历
  • 用新的元素做成一个映射表,遍历老链表去找新的
  • 老 c 从 新的映射表去找,发现有,复用 patch 操作
  • 老 d 从 新的映射表去找,发现有,复用 patch 操作
  • 老 e 从 新的映射表去找,发现有,复用 patch 操作
  • 老 q 从 新的映射表去找,发现没有,卸载 unMount 操作
  • 寻找最长递增序列,后缀添加 -> 贪心,替换得到最长递增子序列的个数 -> 倒叙追溯拿到不会移动的节点列表,计算出不移动的节点的索引。在这里是 3,4 也就是 c d节点
  • 继续从中间遍历的最后一个节点 h 开始遍历,索引不是 3,4 以下一个 f 为achor插入。
  • 继续遍历到 d,索引是4,则跳过不动。
  • 继续遍历到 c,索引是3,则跳过不动。
  • 继续遍历到 e ,索引不是 3,4 以下一个 d 为achor插入。
  • 完成。

toRef | toRefs | proxyRefs

代码示例

<script src="./dist/reactivity.global.js"></script>
<body>
  <div id="app"></div>
  <script>
    let { ref, effect, reactive, toRef } = VueReactivity
    let obj = reactive({ flag: false })
    let state = toRef(obj, "flag")
      
    effect(() => {
      app.innerHTML = `ref is ${state.value} `
    })

    setTimeout(() => {
      state.value = true
    }, 2000)
  </script>

挂载阶段

第一:调用 toRef 函数,调用 new ObjectRefImpl 创建 objectRef 对象。

export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]> {
  const val = object[key]
  return isRef(val)
    ? val
    : (new ObjectRefImpl(object, key, defaultValue) as any)
}

第二:调用 ObjectRefImpl 的 constructor 方法。初始化 object, key 属性。返回 objectRef 对象。

constructor(
  private readonly _object: T,
  private readonly _key: K,
  private readonly _defaultValue?: T[K]
) {}

第三:再 effect 函数 执行 effct.fn 函数,也就是 () => { state.value = true }, 访问 state.value, 触发 objectRef 对象 value 属性的 get 操作。收集依赖到 obj 响应式对象的 flag 属性的依赖列表中去,返回属性值。

const targetMap = {
    {flag :false}: {flag: [ReactiveEffect]}
}
get value() {
  const val = this._object[this._key]
  return val === undefined ? (this._defaultValue as T[K]) : val
}

第四:初次挂载完成,渲染完毕

更新阶段

执行第一句代码: setTimeout(() => { state.value = true }, 2000)

触发 state objectRef 对象 value 属性的 setter 方法,实际上触发的是 reactive 响应式对象 obj 的 setter 操作。

set value(newVal) {
  this._object[this._key] = newVal
}

再 setter 操作中,通过 Reflect.set 方法修改响应式对象的值,再通过 trigger 操作。触发 flag 属性的依赖列表。再次执行 ReactiveEffect 对象的 fn 属性,拿到最新值,进行渲染。

更新完毕

toRefs 原理

<script>
  let { ref, effect, toRefs, reactive } = VueReactivity
  let state = reactive({ num: 0, name: 'cyan' })
  const { num, name } = toRefs(state)

  effect(() => {
    app.innerHTML = `ref is ${num.value} ${name.value}`
  })

  setInterval(() => {
    num.value++
    name.value = 'mike'
  }, 2000)
</script>

就是将对象的每一个属性,都调用 toRef 函数,转成 objectRef 对象。这样就可以进行解构,拿到每个 objectRef 对象,且仍然具有响应式。

export function toRefs<T extends object>(object: T): ToRefs<T> {
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

proxyRefs 原理

let { ref, effect, toRefs, reactive, proxyRefs } = VueReactivity
let state = reactive({ num: 0, name: 'cyan' })
const p = proxyRefs(toRefs(state))

effect(() => {
  app.innerHTML = `ref is ${p.num} ${p.name}`
})

setInterval(() => {
  p.num++
  p.name = 'mike'
}, 2000)

proxyRefs 原理就是将 ref 对象做了一层 proxy 代理,访问 p.num 实际上是访问 p.num.value, 触发的仍然是 objectRef 对象 的 getter。 赋值p.num = "",实际上是 p.num.value = "", 触发的仍然是 objectRef 对象的 settter。


export function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      return Reflect.set(target, key, value, receiver)
    }
  }
}

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...