坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

我们刚确认了分析流程

坏蛋Dan:vue runtime源码分析学习——day3:确定后续分析流程

当然,也有可能在不同地方使用不同方式分析。。。


入口

createApp这个方法在@vue/runtime-dom[1]这个包里面

也是作为我们开发者项目的入口

在看代码之前先确定下测试用例

选择测试用例

直接选择第一个就好,这会没有特殊要求

注意,这里在模拟client的环境,document.createElement就是client的东西,只有jest.config 中配置的testEevironmentjsdom时才能使用,并且要搭配jsdom这个包。


createApp

  • ensureRenderer:这个方法我们放到下面去分析。这里看名字是创建一个renderer也就是渲染器。
  • injectNativeTagCheck:这个方法代码就不看了,就是给这个appconfig字段(这字段是一个对象),用Object.defineProperty给它绑定一个isNativeTag的方法,这个方法用来检测这个tag是否是符合要求的,在这里app就是一个组件,所以这里是用来检测apptag是不是原生的tag。原生tag有两种:1. svg: https://developer.mozilla.org/en-US/docs/Web/SVG/Element; 2. html: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
  • injectCompilerOptionsCheck:这玩意儿是用来检测runtime-only是否还在用compiler相关的东西,比如app.config.compilerOptions,这个是runtime-compiler才会访问的,然后给出警告。

注意injectCompilerOptionsCheckinjectNativeTagCheck都是dev only的,也就是说prod版本不会有检测。一般也不需要,因为如果你这组件的tag不符合,在dev阶段应该就已经测试出来了。至于为啥都要用Object.defineProperty来绑定,大概是防止被覆盖掉了。重写set或者writable置为false

  • normalizeContainer:这个代码也没啥好看的。如果你这个传入的selector是一个string,那么就调用document.querySelector获取对应的dom并返回,否则直接返回。

这里面做的事情挺好理解的:

  1. 调用ensureRenderer,生成渲染器并缓存
  2. 调用渲染器的createApp,生成app实例
  3. 封装appmount方法:
  • 获取挂载dom对象,也就是container
  • 清空它的所有子dom
  • 兼容2.x写法,3.x不支持在挂载对象上使用指令,3.x中它被视为template之外的部分;
  • 调用原来appmount方法挂载到挂载对象上,并创建代理对象;
  • 去掉挂载对象的v-cloak[2],因为此时渲染完毕了;
  • 返回代理对象
  • **注意:**如果app,在这里也就是我们的component如果没有render function,也没有template,那么它会直接使用原来containerinnerHTML作为它的template

ensureRenderercreateApp以及mount三个方法我们接下来再分析。


ensureRenderer

baseCreateRenderer开头三个名字一样的function,之前有提到过这是typescript提供的一种重载,可以支持不同参数。

baseCreateRenderer这个方法非常非常长,有将近2000行,所以这里就不贴出来了,到时候我们有遇到我们再具体分析。

这里我们要分析的是createApp这个方法,而它使用了createAppApirender这俩方法

我们先来看下createAppApi

这个createAppAPI的方法是runtime-core里的方法。

它返回一个createApp的方法,这样render/hydrate就会被缓存了。

  • createAppContext:看下代码,记下这些字段,后面我们会遇到

app中的属性相信大家看着都眼熟,方法也是。

  • use:这个相信大家都很熟悉,在我们使用vuex/vue-router的时候就需要先app.use才能使用,实际上这是在注册,而注册的规范是实现install这个方法,类似webpack需要插件实现apply的方法。当然,install不是必要的,如果你不想写对象的形式,直接传入一个函数也是可以的,它将作为install方法注册安装到插件集合中。具体可以看官方的文档:Plugins | Vue.js (vuejs.org),注意这里把app返回了,这也就是为什么可以链式调用的原因。
  • mixin:这个也是老熟人了,全局注册的mixin会被应用于所有的组件,当组件create的时候就会注册,而且是最先注册,要符合组件自身的mixin优先级高的理念。当然,3.x中已经不推荐了mixin,只有在支持option api的情况下才可以使用,关键key就是__FETAURE_OPTIONS_API__
  • component:也是熟人,注册全局组件就是在这。
  • directive:同上,注册全局指令。
  • unmountisMounted是一个闭包缓存的标志位,用于判断是否已经渲染到html中了。先调用render方法将当前domcontainer中注销。如果侦测到vue-devTool,调用devtoolsUnmountApp方法注销当前domdevtool里面的注册。然后移除当前domcontainer对当前dom的引用,避免内存泄漏。
  • provide[3]: 就是将provide的内容注册到context中。这个context贯穿整个app,所以多深的组件多可以inject到。
  • installAppCompatProperties:这个方法是用于兼容v2的一些方法指令的,比如filter等。这里就不看了,以后兼容性的代码我们就不看了。

注意这里用的是createVNode,而不是createVNodeCall。这里创建的是一个VNode节点,而我们之前分析的compiler-core用的是createVNodeCall,创建的是一个函数,函数调用后才会返回一个VNode,辅助函数就是createVNode

然后这里使我们第一次在runtime阶段遇到reload,也就是热更新相关的功能,这个和我们正vue-loader中发现的热更新代码是有关联的。

可以看到vue-loader中的__VUE_HMR_RUNTIME__是目标对象,它的reload方法和我们这里的reload应该是同一个,不过它没有接受参数,为什么呢?因为这个是根组件,它的reload自然不需要确认是哪个组件。

  • render方法我们先不分析,等下开个一级标题,内容较多。
  • createVNode:这个方法我们这里也不分析,放下面,就是基于VNodeCall节点创建Vnode
  • cloneVNode:这个方法暂时就不看代码了,后面我们有空再说,简单的说就是将这个节点的所有数据都clone一遍,注意是deep的,当然,没有用JSON.stringify这种操作,这种操作也是很危险的,因为有些数据在parse回来会有问题。 这里采用的是递归处理。
  • hydrate:这个暂时不清楚是什么东西,mark下。
  • devtoolsInitApp:就是将当前的app组件注册到devTool里面,这样能被vue-devTool捕获,用于调试等。

这个mount方法是app挂载的核心入口,这里核心就两件事:

  1. vnodecall也就是render function转换成vnode
  2. vnode通过patch打补丁到html上,方法是render

这就是从js变成html的过程。

那么这个app就创建好了。

等到createApp.mount的时候,vnodecall就变成vnode然后再变成真实dom

这里我们暂时不看vnode的生成也就是createVnode方法里面做了什么,看一下一个vnode结构即可。

记住这个vnode的结构,我们开发的时候会经常遇到。


render

注意这里的unmount,如果已经有_vnode了,这个时候就意味着已经是patch到浏览器上的真实dom了。如果此时当前vnode变成了null,那么它就应该在真实dom中被移除了。

如果没有直接patch

同样patch方法我这里也不准备说,因为这个方法是核心之一,负责diff,然后渲染。

  • flushPreFlushCbs:代码就不看了,这里我们暂时还不知道是在做什么,后面如果直到了再贴代码分析。简单的说就是处理队列中有pre == true属性的回调。
  • flushPostFlushCbs:这里同上一样不贴代码,简单的说就是将一些pending状态的回调放到activePostFlushCbs里面并且根据id进行排序。如果此时还有active的回调,那就return,但是如果没有此时并没有,那就挨个处理。这里可以猜测是和$nextTick有关的,每一个tick都有回调需要处理,如果没处理完就放到一起下一个tick再处理。

最后再把vnode重新赋值给container._vnode,这个container是真实dom

因为patch的原因,此时vnode已经被转换为真实dom并渲染到浏览器上去了。

这里注意一点,container._vnode这一步缓存的旧的vnode,每次patch都会被更新。

patch就是比对的这个container._vnode和传入的最新的vnode

以前看别人的教程,都没有提到这一步,导致我一直很疑惑旧的vnode从哪里来的。


总结

今天分析的有些水,但createApp大体流程应该是可以知道了的。

  1. 获取真实dom,也就是我们需要挂载的app的根节点,一般都是#app
  2. 组装appContext,这里面包含了一些api,比如use、mount、umount等。
  3. createApp返回appContext实例。
  4. use方法中调用传入的插件的install方法,然后将插件注册到全局的installPlugins中。
  5. mount方法中接收vnodecall也就是sfc被处理成的render function,在这里进行被转换成vnode。然后调用render方法。
  6. render方法中调用patch方法对新的vnode进行diff操作,这个操作涉及到真实dom,此时已经从vnode变成真实dom了,也就是通过document.createElement方法生成的dom,并且被append/innerHTML到浏览器上的container里面。

参考

  1. ^@vue/runtime-dom https://github.com/vuejs/core/tree/main/packages/runtime-dom
  2. ^vue_v-cloak https://vuejs.org/api/built-in-directives.html#v-cloak
  3. ^vue-provide-inject https://vuejs.org/guide/components/provide-inject.html#provide-inject

发布于 2023-02-17 15:32・IP 属地广东