Vue3源码解析之 render component(四)

310 阅读4分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 15 篇,关注专栏

前言

上篇我们分析了 组件生命周期 钩子函数是如何运行的,那 组件生命周期 钩子函数中是如何访问 响应式数据,以及组件生命周期中 响应式数据 改变是如何引起 视图 的改变呢?本篇我们就来一探究竟。

案例一

首先引入 h 、 render 函数,声明一个 component 包含 datarender 函数以及 createdmounted 两个生命周期钩子函数的组件对象,钩子函数中打印出 data 中的 msg 数据,通过 h 函数生成 组件 vnode 对象,最后通过 render 函数渲染该对象。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render } = Vue

      const component = {
        data() {
          return {
            msg: 'hello component'
          }
        },
        render() {
          return h('div', this.msg)
        },
        created() {
          console.log('created', this.msg)
        },
        mounted() {
          console.log('mounted', this.msg)
        }
      }

      const vnode = h(component)

      render(vnode, document.querySelector('#app'))
    </script>
  </body>
</html>

render component reactivity

我们重新再来看下 applyOptions 方法:

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)
  
  // 省略

  if (created) {
    callHook(created, instance, LifecycleHooks.CREATED)
  }

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  registerLifecycleHook(onBeforeMount, beforeMount)
  registerLifecycleHook(onMounted, mounted)
  registerLifecycleHook(onBeforeUpdate, beforeUpdate)
  registerLifecycleHook(onUpdated, updated)
  registerLifecycleHook(onActivated, activated)
  registerLifecycleHook(onDeactivated, deactivated)
  registerLifecycleHook(onErrorCaptured, errorCaptured)
  registerLifecycleHook(onRenderTracked, renderTracked)
  registerLifecycleHook(onRenderTriggered, renderTriggered)
  registerLifecycleHook(onBeforeUnmount, beforeUnmount)
  registerLifecycleHook(onUnmounted, unmounted)
  registerLifecycleHook(onServerPrefetch, serverPrefetch)

  // 省略
}

根据判断 if(created) 执行 callHook 方法:

function callHook(
  hook: Function,
  instance: ComponentInternalInstance,
  type: LifecycleHooks
) {
  callWithAsyncErrorHandling(
    isArray(hook)
      ? hook.map(h => h.bind(instance.proxy!))
      : hook.bind(instance.proxy!),
    instance,
    type
  )
}

我们知道 callWithAsyncErrorHandling 实际执行的是 hook 方法即传入的 created 方法,这里通过 bind 改变了 this 指向,它指向 组件实例proxy 对象上,该对象存在 msg 属性:

render-created.png

所以打印 this.msg 也就能够访问到 msg 数据,所以此时输出:

render-console-created.png

之后注册 mounted 钩子函数:

registerLifecycleHook(onMounted, mounted)

function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

我们可以看到这里通过 bind 改变了 this 指向,即 mounted 函数 this 指向了 publicThis

render-mounted-this.png

之后执行 mounted 钩子函数,this.msg 就能访问到 publicThis 中的 msg 数据,此时打印输出:

render-mounted-console.png

案例二

首先引入 h 、 render 函数,声明一个 component 包含 datarender 函数以及 created 生命周期钩子函数的组件对象,钩子函数中设置两秒后来修改 msg 值,通过 h 函数生成 组件 vnode 对象,最后通过 render 函数渲染该对象。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render } = Vue

      const component = {
        data() {
          return {
            msg: 'hello component'
          }
        },
        render() {
          return h('div', this.msg)
        },
        created() {
          setTimeout(() => {
            this.msg = '你好,世界'
          }, 2000)
        }
      }

      const vnode = h(component)

      render(vnode, document.querySelector('#app'))
    </script>
  </body>
</html>

component reactivity update

render 函数在挂载组件时,通过 setupComponent 调用 applyOptions 方法会对 data 返回的对象通过 reactive 包装为响应式数据。之后执行 setupRenderEffect 方法,创建一个 ReactiveEffect 实例 effect,这是响应式的关键,对 componentUpdateFn 方法进行依赖收集,两秒后重新修改 msg 值,触发 setter 行为,再次执行 componentUpdateFn 方法:

const componentUpdateFn = () => {
      if (!instance.isMounted) {
        // 省略
        
        instance.isMounted = true

        // 省略
      } else {
        // 省略
        const nextTree = renderComponentRoot(instance)
        
        // 省略
        
        const prevTree = instance.subTree
        instance.subTree = nextTree

        // 省略
        
        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
        )
        if (__DEV__) {
          endMeasure(instance, `patch`)
        }
        next.el = nextTree.el
        if (originNext === null) {
          // self-triggered update. In case of HOC, update parent component
          // vnode el. HOC is indicated by parent instance's subTree pointing
          // to child component's vnode
          updateHOCHostEl(instance, nextTree.el)
        }
        // updated hook
        if (u) {
          queuePostRenderEffect(u, parentSuspense)
        }
        // onVnodeUpdated
        if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
          queuePostRenderEffect(
            () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
            parentSuspense
          )
        }
        
        // 省略
      }
    }

第一次组件挂载完,isMounted 会设置为 true,所以直接走 else 逻辑。通过 renderComponentRoot 函数返回 vnode 对象赋值给 nextTree,由于 msg 数据已更改,所以此时 vnode 对象为:

render-next-tree.png

之后通过 patch 方法更新渲染,此时页面呈现:

render-comp-patch-update.png

总结

  1. 组件生命周期 钩子函数都是通过 bind 来改变了 this 指向,从而来获取 响应式 数据。
  2. 组件响应式数据 更改会通过 ReactiveEffectcomponentUpdateFn 方法的依赖收集,当修改数据时,会再次触发 componentUpdateFn 方法,之后通过 patch 方法进行挂载更新。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
  6. Vue3源码解析之 runtime
  7. Vue3源码解析之 h
  8. Vue3源码解析之 render(一)
  9. Vue3源码解析之 render(二)
  10. Vue3源码解析之 render(三)
  11. Vue3源码解析之 render(四)
  12. Vue3源码解析之 render component(一)
  13. Vue3源码解析之 render component(二)
  14. Vue3源码解析之 render component(三)
  15. Vue3源码解析之 render component(四)
  16. Vue3源码解析之 render component(五)
  17. Vue3源码解析之 diff(一)
  18. Vue3源码解析之 diff(二)
  19. Vue3源码解析之 compiler(一)
  20. Vue3源码解析之 compiler(二)
  21. Vue3源码解析之 compiler(三)
  22. Vue3源码解析之 createApp