vlambda博客
学习文章列表

从使用者角度来分析一下 vue3 原理


社区或论坛上大多都是从最底层原理讲起, 比如 Proxy/Reactive 响应式原理、VNode 虚拟节点 patch过程、 nextTick 如何批量更新等等。大多从深度的层面出发,进行 vue3 的源码剖析。这里我们换一种角度从一个vue 使用者出发。

如何进行 vue3 源码调试

  1. 首先,下载vue3 源码到本地,然后发现新版本的 vue3 的项目组织形式分成了单独 package (采用lerna框架的 monorepo)。

  2. 然后,看根目录下的 scripts 脚本命令, 发现有一个 dev 命令(也就是 "dev": "node scripts/dev.js"),有前端基础的同学一看就知道这是运行调试代码库的命令,我们 npm install 安装一下依赖,再运行一下 npm run dev。发现打包生成了 vue.global.js

[scripts/dev.js 这个命令脚本起了一个 rollup 服务 , 输入是 packages\vue\src\index.ts , 输出是 packages\vue\dist\vue.global.js, 具体打包过程不是重点,这里不展开讲解]

  1. 再然后,可以发现 vue 文件夹下的 src/index.js 就是打包 vue3 库的入口,顺藤摸瓜,我们看到了 examples 示例代码,以 examples\composition\todomvc.html 为例,可以发现里面引入了 vue.global.js。到此为止,我们已经完成了调试 vue 源代码的第一步。

从示例代码发现 vue3 的不同之处

首先,观察一下示例代码的书写形式。

const { 
  createApp, reactive, 
  computed, watchEffect, 
  onMounted, onUnmounted 
} = Vue

createApp({
 setup () {
  const state = reactive({
   xx: computed(() => xxxx))
   // ...
  })

  watchEffect(() => {})

  onMounted(() => {})

  // ...

  return {
   state,
   // ...
  }
 }
}).mount('#app')

只看 js 部分就可以看出很多新的变化和疑惑,比如:

  1. vue3 怎么多了一些全新的的 api reactive watchEffect, 这是做什么的?原理是什么?
  2. vue3 怎么是通过 createApp 来创建 Vue 实例的?具体干了什么?
  3. setup 函数是什么东西?执行过程中做了什么?
  4. onMounted  生命周期钩子是怎么和对应组件实例进行绑定执行的?

带着这些问题,下面对其进行一一分析。

首先,我们先确定一下代码的底层引用关系,这样可以缩小我们的关注范围,不然 packages 下的文件夹太多,根本不知道从那里下手,从 vue/src/index.js 可以看出,主要引用了以下几个库,以及依赖关系。

 vue.js 里包含了三类包:
 1.
 commom 公共基础包
 @vue/shared 
 
 2.
 tamplate 模板编译 相关包
 @vue/compiler-dom -- @vue/compiler-core
 
 3.
 runtime 运行时 相关包
 @vue/runtime-dom -- @vue/runtime-core -- @vue/reactivity

下面是依赖关系:
    
                      +---------------------+    +----------------------+
                      |                     |    |                      |
        +------------>|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
        |             |                     |    |                      |
   +----+----+        +---------------------+    +----------------------+
   |         |
   |   vue   |
   |         |
   +----+----+        +---------------------+    +----------------------+    +-------------------+
        |             |                     |    |                      |    |                   |
        +------------>|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
        |             |                     |    |                      |    |                   |
        |             +---------------------+    +----------------------+    +-------------------+
        |
        |
        |             +---------------------+
        |             |                     |
        +------------>|      @vue/share     |
                      |                     |
                      +---------------------+

现在我们确定了 packages 下的这些包 是我们需要重点关注的对象。

正式开始调试分析

上面已经运行了 npm run dev 命令,rollup 会 watch 检查代码更新实时打包, 接下来运行 npm run serve 可以运行一个 http://localhost:5000 服务,在浏览器打开 http://localhost:5000/packages/vue/examples/composition/todomvc 即可。

createApp 做了什么?

正式开始,从第一行 createApp 开始 debugger,然后跟着代码执行可以跟踪到 packages\runtime-dom\src\index.ts 文件下有一个 export const createApp ,下面我们逐步地对其进行刨根问底。

下面是执行的核心代码:

/* 
  from '@vue/runtime-dom'
*/

export const createApp = ((...args) => {
 /*
   示例代码中 args = [{ directives: {…}, setup: ƒ }]
 */

 // 调用 ensureRenderer().createApp() 生成 app 实例
  const app = ensureRenderer().createApp(...args)

 // 从 app 解构出 mount 方法
  const { mount } = app;
 // 在 app 上挂载一个 mount 私有方法
  app.mount = (containerOrSelector: Element | string): any => {
  // 获取 container dom
    const container = normalizeContainer(containerOrSelector)
    if (!containerreturn
  
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
   // 对 app._component.template 赋值
      component.template = container.innerHTML
    }

  // 挂载之前清空 container.innerHTML
    container.innerHTML = ''
  // 创建 vnode 并 render
    const proxy = mount(container)
    // container.removeAttribute('v-cloak')
    // container.setAttribute('data-v-app', '')
    return proxy
  }
 // 返回 app 实例,实现可链式调用
  return app
}
as CreateAppFunction<Element>
// 序列化 container
function normalizeContainer(container: Element | string): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    return res
  }
  return container
}

/* 
 from '@vue/runtime-core'
*/
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
/* 
  createRenderer 函数接受两个通用参数: HostNode 和 HostElement,它们对应于宿主环境中的 Node和 Element类型。
  对于runtime-domHostNode 是 DOM 的 'Node' 接口,HostElement 是 DOM 的 'Element' 接口。
*/ 

function createRenderer(options) {
 // 在源码里 baseCreateRenderer 是重载函数,所以单独剥离了出来
 return baseCreateRenderer(options)
}

function baseCreateRenderer(options) {
 const {
    inserthostInsert,
    removehostRemove,
    patchProphostPatchProp,
    forcePatchProphostForcePatchProp,
    createElementhostCreateElement,
    createTexthostCreateText,
    createCommenthostCreateComment,
    setTexthostSetText,
    setElementTexthostSetElementText,
    parentNodehostParentNode,
    nextSiblinghostNextSibling,
    setScopeIdhostSetScopeId = NOOP,
    cloneNodehostCloneNode,
    insertStaticContenthostInsertStaticContent
  } = options

 // ...上千行代码,这里先不展开
 // 里面包括 render 的整个流程

 return {
  render,
  hydrate,
  createAppcreateAppAPI(render, hydrate) // 返回 createApp 函数
  }
}

const renderRootRenderFunction = (vnode, container) =>
 {
 if (vnode == null) {
  if (container._vnode) {
   unmount(container._vnode, nullnulltrue)
  }
 } else {
  // patch 具体过程这里不展开,后续单独进行讲解
  patch(container._vnode || null, vnode, container)
 }
 // 更新任务队列
 flushPostFlushCbs()
 container._vnode = vnode
}

export function flushPostFlushCbs(seen?: CountMap{
 if (pendingPostFlushCbs.length) {
  // 对 pendingPostFlushCbs 任务队列去重
  const deduped = [...new Set(pendingPostFlushCbs)]
  // 清空 pendingPostFlushCbs
    pendingPostFlushCbs.length = 0

  if (activePostFlushCbs) {
   // 更新 activePostFlushCbs 队列
      activePostFlushCbs.push(...deduped)
      return
    }
    activePostFlushCbs = deduped
  // flush 之前进行排序
  activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
  for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
   // 遍历执行 activePostFlushCbs 队列
   activePostFlushCbs[postFlushIndex]()
  }
  // 清空队列以及重置索引
  activePostFlushCbs = null
    postFlushIndex = 0
 }
}

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement
{
  return function createApp(rootComponent, rootProps = null{
    if (rootProps != null && !isObject(rootProps)) {
      rootProps = null
    }
  // 初始化 app 上下文 context
    const context = createAppContext()
    const installedPlugins = new Set()

    let isMounted = false

    // 初始化app对象 (具备通用能力)
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      version,
   get config() {},
   set config(v) {},
   use(plugin: Plugin, ...options: any[]) {},
   mixin(mixin: ComponentOptions) {},
   component(name: string, component?: Component): any {},
   directive(name: string, directive?: Directive) {},
   mount(rootContainer: HostElement, isHydrate?: boolean): any {
     if (!isMounted) {
     // 将 rootComponent 初始化成虚拟节点 vnode 
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // 在根节点存储 app 上下文
     // 在根结点实例初始化挂载时将被设置
          vnode.appContext = context
     // 执行 render 函数
          render(vnode, rootContainer)
          
          isMounted = true
          app._container = rootContainer

          return vnode.component!.proxy
        }
   },
   unmount() {},
   provide(key, value) {},
  })

  return app;
 }
}

export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      isCustomElement: NO,
      errorHandler: undefined,
      warnHandler: undefined
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

到这里,整个 createApp 流程就执行完了,在整个环节中,忽略了很多细节,不过不重要,这里我们主要是摸清楚 createApp 在主要做了什么。

  1. 首先, ensureRenderer() 执行, 初始化 render 函数和 createApp 函数,
  2. 调用 createApp() 生成 vue 实例对象 app (类似 new Vue),
  3. 把 mount 方法包装, 使其具备从 vnode 渲染到真实 dom 的能力。

另外,好奇的你可能发现,在这里所列的函数中没有发现进行依赖收集和响应式处理相关的过程?这个后续专门进行讲解(会涉及到上面 redner 内部的 patch 处理过程,详细过程相对较复杂)

setup 是个什么东东?

接着在调试源码界面搜索 setup() 函数并打上断点,

跟着一步一步往下看。当然了, 你也可以在对应的源码库中搜索 setup() ,可以发现 setup 在@vue/runtime-core/component.ts 内的setupStatefulComponent 函数中被调用,可以看出其是组件内封装的一个函数, 接着向函数调用的方向找,可以发现有一个 setupComponent  函数, 而这个函数在恰好上面 creatApp 内的 render 函数内的 patch 过程中的 mountComponent 时被调用了 。(可能有点绕,但是多看多读几次就好)

下面是执行的核心代码:

// 
export function setupComponent(
  instance: ComponentInternalInstance
{

  const { props, children, shapeFlag } = instance.vnode
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  // 
  initProps(instance, props, isStateful, isSSR)
  // 
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}
// 安装有状态组件
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
{
  const Component = instance.type as ComponentOptions

  // 0. create render proxy property access cache
  instance.accessCache = Object.create(null)
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

  // 2. call setup()
  const { setup } = Component
  if (setup) {
    // setup.length > 1 意味着传 setup(props,ctx)
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)
    // currentInstance 维护全局中 当前处理的组件实例变量
    currentInstance = instance
    // 停止依赖收集 shouldTrack = false
    pauseTracking()
    // try catch 包裹的 setup() 返回 setup() 结果
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [setupContext]
    )
    // 恢复依赖收集 shouldTrack = lastshouldTrack
    resetTracking()
    // 重置当前处理的组件实例为 null
    currentInstance = null

    if (isPromise(setupResult)) {
      // ...
    } else {
      // 执行 handleSetupResult 操作
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // 执行 compile
    finishComponentSetup(instance, isSSR)
  }
}
// 创建 SetupContext 对象
function createSetupContext(instance: ComponentInternalInstance):SetupContext {
  return {
    attrs: instance.attrs,
    slots: instance.slots, 
    emit: instance.emit 
  }
}
// 处理 setup() 执行结果对象 // 
export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
{
  if (isFunction(setupResult)) {
    // setup returned an inline render function
    instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    // 转换成 proxy 对象,  初始化到 instance.setupState 
    instance.setupState = proxyRefs(setupResult)
  } 
  // 组件 setup() 函数处理完的后续过程
  finishComponentSetup(instance, isSSR)
}

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
{
  // ...
  if (!instance.render) {
    // could be set from setup()
    if (compile && Component.template && !Component.render) {
      // 对组件 template 执行 compile (模板编译解析 parse-transform-generate) 过程 生成 h/render 函数, 
      //  function render(_ctx, _cache_xxx${optimizeSources}) { with (_ctx) { //... } }
      // 然后 初始化到 Component.render
      
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters
      })
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // for runtime-compiled render functions using `with` blocks, the render
    // proxy used needs a different `has` handler which is more performant and
    // also only allows a whitelist of globals to fallthrough.
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }
}

到这里, setup 相关执行流程已经结束, 流程如下:

  1. 首先 setupComponent 函数在 creatApp 内 rendder 函数中 mountComponent 时被调用, 并且对组件进行参数处理, 然后再对有状态 STATEFUL_COMPONENT 进行代理化处理, 然后解构出 setup ,
  2. 然后, 对 setup 函数进行 error 包装执行 (期间需要维护全局的当前处理的组件实例, 管理依赖收集的状态), 并对其返回结果进行 proxy 代理.
  3. 最后, setup 函数的处理结束之后, 对组件 template 进行 compile 处理生成最终的 render 函数.

可以发现 setup 扮演了承上启下并维护当前组件实例的关键角色.

结语

篇幅关系, 这里主要 对 createApp setup  这两个函数的整体执行流程做了解读与分析, 其中设计到的整个流程大致也是 vue3 内部的执行原理.

如下图.

初始化 vue 实例对象
createApp() 
--> 构造 render 函数 // 内部涉及 patch  
  |
  |--> 执行 setup() (在 patch 的 mountComponent 过程中, 里面还有 setupRenderEffect 依赖处理逻辑)
  |   |
  |   |--> data 数据 proxy // 数据对象进行 proxy 代理
  |   |
  |   |--> template compile  // 对模板进行编译 生成 render function
  |   |
  |   |--> ...
  |
  |--> ...
  |
--> 构造 mount 函数  // 使其挂载到真实 dom
--> ...

这样看来,  貌似和整个 vue2 内部的响应式流程也有些吻合了.

从使用者角度来分析一下 vue3 原理


 资料

[1]vue-next     https://github.com/vuejs/vue-next/releases/tag/v3.0.9


 你的三连是作者继续创作的动力 😉, 期待下期



前端小菜鸟001
定期分享优质内容,浅入深出
20篇原创内容
Official Account

点个关注   点个在看吧