关注若川视野, 回复"pdf" 领取资料,回复"1",可加群长期交流学习

刘崇桢,微医云服务团队前端工程师,左手抱娃、右手持家的非典型码农。

9 月初 Vue.js 3.0 正式发布,代号 "One Piece"。大秘宝都摆到眼巴前了,再不扒拉扒拉就说不过去了。那我们就从初始化开始。

目标:

  • 弄清楚 createApp(App).mount("#app") 到底做了什么

  • 弄清楚 Vue3.0 的初始化渲染是怎么样的过程

能收获到什么:

  • 了解 Vue3.0 的初始化过程

  • 介绍一个阅读 Vue3.0 源码的入口和方向

先跑起来

vue-next 代码克隆到本地,打开 package.jsonscripts dev 末尾加上 --sourcemap

然后  yarn devvue 目录下的  dist  打包出了一份  vue.global.js 和相应的 sourcemap 文件。这样方便我们一步一步调试代码,查看程序在 call Stack 中的每一步调用。

查看 vue 官方给出的 demo,发现 vue 的使用分为 classiccomposition,我们先用 classic 方式,实现一个最简单的 demo。

const app = {data () {return {counter: 1}}
}
Vue.createApp(app).mount("#app")

ok,页面跑起来了。我们就在这段代码打个断点,然后一步一步的调试,观察createApp(App).mount("#app")到底做了什么,了解Vue3.0的初始化过程。

在这之前,简单了解一下整体的背景,我们这次主要涉及到 runtime 运行时的代码。

runtime-dom

我们先跟着代码进入:createApp(App).mount("#app");

这个 createApp() 来自 runtime-dom,我们通过这个图可以看到他大致做的事情:return 了一个注册了 mount 方法 app。这样我们的 demo 至少能跑起来不报错。

createApp 调用了 ensureRenderer 方法,他确保你能得到一个 renderer 渲染器。renderer 是通过调用创建渲染器的 createRenderer 来生成的,这个 createRenderer 来自于 runtime-core,后面我们会看到。

而这个 rendererOptions 是什么呢?

const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps);export const nodeOps: Omit<RendererOptions<Node, Element>, "patchProp"> = {insert: (child, parent, anchor) => {parent.insertBefore(child, anchor || null);},remove,createElement,createText,// ...
};

是不是就是一些 DOM API 的高阶封装,这个在 vue 的生态中,叫平台特性。vue 源码中的平台特性就是针对 web 平台的。如果开发者想要在别的平台上运行 vue,比如 mpvue、weex,就不需要 fork 源码库改源码了,直接把 nodeOps 中的方法按着平台的特性逐一实现就可以了。这也是 createRenderer 等跨平台的代码放到 runtime-core 中的原因。

当然 runtime-dom 远远不只图中这些东西,我们先大致过一下初始化过程,以对 vue3.0 有一个大致的了解。

runtime-core

紧接着,进入 runtime-core,创建渲染器

我们注意 baseCreateRenderer 这个 fn,2000 多行的代码量,里面的东西都是渲染的核心代码,从平台特性 options 取出相关 API,实现了 patch、处理节点、处理组件、更新组件、安装组件实例等等方法,最终返回了一个对象。这里我们看到了【2】中渲染器调用的 createApp 方法,他是通过 createAppAPI 创建的。代码进入 createAppAPI

这里我们又看见了熟悉的 Vue2.x 中的 API,挂载在 app 上面。

至此,Vue.createApp(app).mount("#app"),创建 app 实例的流程,终于在【7】中 return app 告一段落,我们拿到了【2】中的 app 实例。

大致瞄一眼 app ,我们可以在 apiCreateApp.ts 中找到其实现

初次渲染 .mount("#app")

上面的介绍中,其实有两处 .mount 的实现,一处是在 runtime-dom【2】中的 mount,我们叫他 dom-mount。一处是【7】中的 mount,我们叫他 core-mount

dom-mount的实现:

const { mount } = app; // 先暂存'core-mount'
app.mount = (containerOrSelector: Element | string): any => {const container = normalizeContainer(containerOrSelector); // #app dom 节点if (!container) return;const component = app._component;if (!isFunction(component) && !component.render && !component.template) {component.template = container.innerHTML; // 平台特性的逻辑}// clear content before mountingcontainer.innerHTML = "";const proxy = mount(container); // 执行'core-mount'container.removeAttribute("v-cloak");return proxy;
};

dom-mount 并不是重写 core-mount,而是提取了平台特性的逻辑。比如上面如果 component 不是 function,又没有 rendertemplate,就读取 dom 节点内部的 html 作为渲染模板。

然后再执行 core-mountmount(container)

代码很简单,就两步:

  • 创建根组件的 vnode

  • 渲染这个 vnode

创建根组件的vnode

创建 vnode,是一个初始化 vnode 的过程,这个阶段中,下面的这些属性被初始化为具体的值(还有很多属性没有罗列,都是初始值)。

vnode 描述不同的事物时,他的属性值也各不相同,这些在 vnode 初始化阶段确定的属性在渲染组件时,能带来非常重要的效率提升。

  • type,标识 VNode 的种类

  1. html 标签的描述,type 属性就是一个字符串,即标签的名字

  2. 组件的描述,type 属性就是引用组件类(或函数)本身

  3. 文本节点的描述,type 属性就是 null

  • patchFlag,标识组件变化的地方

  • shapeFlagVNode 的标识,标明 VNode 属于哪一类,demo 中的shapeFlag4STATEFUL_COMPONENT,有状态的组件。

packages/shared/src/shapeFlags.ts中,定义了这些通过将十进制数字 1 左移不同的位数得来的枚举值。

export const enum ShapeFlags {ELEMENT = 1, // 1 - html/svg 标签FUNCTIONAL_COMPONENT = 1 << 1, // 2 - 函数式组件STATEFUL_COMPONENT = 1 << 2, // 4 - 有状态组件TEXT_CHILDREN = 1 << 3, // 8ARRAY_CHILDREN = 1 << 4, // 16SLOTS_CHILDREN = 1 << 5, // 32TELEPORT = 1 << 6, // 64SUSPENSE = 1 << 7, // 128COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 256 - 需要被 keepAlive 的有状态组件COMPONENT_KEPT_ALIVE = 1 << 9, // 512 - 已经被 keepAlive 的有状态组件COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 组件
}

为什么为 VNode 标识这些枚举值呢?在 Vue2.xpatch 过程中,代码通过 createElm 区分 VNode 是 html 还是组件或者 text 文本。

所以 Vue2.xpatch 是一个试错过程,在这个阶段是有很大的性能损耗的。Vue3.0 把对 VNode 的判断放到了创建的时候,这样在 patch 的时候就能避免消耗性能的判断。

最终,我们看一下 vnode 的结构

export interface VNode<HostNode = RendererNode,HostElement = RendererElement,ExtraProps = { [key: string]: any }
> {/*** @internal*/__v_isVNode: true // 一个始终为 true 的值,有了它,我们就可以判断一个对象是否是 VNode 对象/*** @internal 内部属性*/[ReactiveFlags.SKIP]: truetype: VNodeTypesprops: (VNodeProps & ExtraProps) | nullkey: string | number | nullref: VNodeNormalizedRef | nullscopeId: string | null // SFC onlychildren: VNodeNormalizedChildrencomponent: ComponentInternalInstance | nulldirs: DirectiveBinding[] | nulltransition: TransitionHooks<HostElement> | null// DOM 相关el: HostNode | nullanchor: HostNode | null // fragment anchortarget: HostElement | null // teleport targettargetAnchor: HostNode | null // teleport target anchorstaticCount: number // number of elements contained in a static vnode// suspense 支持 suspense 的属性suspense: SuspenseBoundary | nullssContent: VNode | nullssFallback: VNode | null// optimization only 优化模式中使用的属性shapeFlag: numberpatchFlag: numberdynamicProps: string[] | nulldynamicChildren: VNode[] | null// application root node onlyappContext: AppContext | null
}

渲染这个vnode

ok,书接上回,我们拿到 根组件的 VNode,接下来执行到 render 函数。

render 的核心逻辑就是 patch 函数。

patch 函数

patch 有两种含义: 1)整个虚拟 dom 映射到真实 dom 的过程;2)patch 函数。我们这里讲的是函数。

patch 就是 render 渲染组件的关键逻辑,【5】中 baseCreateRenderer 2000 行左右的代码,主要是为了 patch 服务的。

// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {anchor = getNextHostNode(n1)unmount(n1, parentComponent, parentSuspense, true)n1 = null
}
// 对于前后节点类型不同的,vue 是直接卸载之前的然后重新渲染新的,不会考虑可能的子节点复用。
...const { type, ref, shapeFlag } = n2
switch (type) { // 根据节点类型 type 分发到不同的 processcase Text:processText(n1, n2, container, anchor)breakcase Comment:processCommentNode(n1, n2, container, anchor)breakcase Static:...case Fragment: ...default: // 根据不同的节点标识 shapeFlag 分发到不同的 processif (shapeFlag & ShapeFlags.ELEMENT) { processElement(...) } else if (shapeFlag & ShapeFlags.COMPONENT) {processComponent(...)...

patch 根据节点 VNode(4.1 创建的根组件的 vnode) 的 typeshapeFlags 执行不同的 process

  1. type:Text 文本

  2. type:Comment 注释

  3. type:Static 静态标签

  4. type:Fragment 片段:VNode 的类型是 Fragment,就只需要把该 VNode 的子节点渲染到页面。有了他,就没有只能有一个根节点的限制,也可以做到组件平级递归

  5. shapeFlags:ShapeFlags.ELEMENT 原生节点,html/svg 标签

  6. shapeFlags:ShapeFlags.COMPONENT 组件节点

  7. shapeFlags:ShapeFlags.TELEPORT 传送节点,将组件渲染的内容传送到制定的 dom 节点中

  8. shapeFlags:ShapeFlags.SUSPENSE 挂起节点(异步渲染)

Vue3 新增组件 - Fragment、Teleport、Suspense,可见此链接 (https://www.yuque.com/hugsun/vue3/component)

我们的 demo 中的根组件 VNodeshapeFlag4(0100)ShapeFlags.COMPONENT(0110),按位与后结果为非零,代码会进入 processCompoent

processXXX

processXXX 是对挂载(mount)和更新(update)补丁的统一操作入口。

processXXX 会根据节点是否是初次渲染,进行不同的操作。

  • 如果没有老的 VNode,就挂载组件(mount)。首次挂载,递归创建真实节点。

  • 如果有老的 VNode,就更新组件(update)。更新补丁的的渲染系统的介绍放到下下篇来介绍。

挂载

创建组件内部实例

内部实例也会暴露一些实例属性给其他更高级的库或工具使用。组件实例属性很多很重要也能帮助理解,可以在 packages/runtime-core/src/component.ts 查看实例的接口声明 ComponentInternalInstance。很壮观啊,啪的一下 100 多行属性的定义,主要包括基本属性、响应式 state 相关、suspense 相关、生命周期钩子等等

安装组件实例
  1. 初始化 props 和 slots

  2. 安装有状态的组件,这里会初始化组件的响应式

【15】setupStatefulComponent,调用了 setup(props, setupContext)

如果没有 setup 时会调用 applyOptions,应用 vue2.xoptions API,最终对 data() 的响应式处理也是使用 vue3.0reactive

上面讲过,安装组件实例触发响应式初始化就发生在这里,具体怎么触发的,这块又是一个千层套路,放到下一篇中。

【16】主要是根据 template 拿到组件的 render 渲染函数和应用 vue2.xoptions API

我们看一下 template 模板编译后生成的 render 函数。

我们大致看下生成的 render 函数,有几点需要注意

  1. 这里的 render 函数执行后的返回是组件的 VNode

  2. _createVNode 函数,用于创建 VNode

  3. _createVNode函数的入参,typepatchFlagsdynamicProps

function _createVNode(type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, // type,标识 VNode 的种类props: (Data & VNodeProps) | null = null,children: unknown = null,patchFlag: number = 0, // 标记节点动态变化的地方dynamicProps: string[] | null = null, // 动态 propsisBlockNode = false
): VNode { ... }

createVNode 在创建根节点的时候就出现过,用于创建虚拟 DOM。这个是内部使用的 API,面向用户的 API 还是h函数。

export function h(type: any, propsOrChildren?: any, children?: any): VNode { ... }

h 的实现也是调用 createVNode,但是没有 patchFlagdynamicPropsisBlockNode 这三个参数。也就是 h 是没有 optimization 的,应该是因为这三个参数,让用户自己算容易出错。

看来这个 patchFlags 有点意思,标识组件变化的地方,用于 patch 的 diff 算法优化

export const enum PatchFlags {TEXT = 1, // 动态文字内容CLASS = 1 << 1, // [2]动态 class 绑定STYLE = 1 << 2, // [4]动态样式PROPS = 1 << 3, // [8]动态 props,不是 class 和 style 的动态 propsFULL_PROPS = 1 << 4, // [16]有动态的 key,也就是说 props 对象的 key 不是确定的。key 变化时,进行一次 full diffHYDRATE_EVENTS = 1 << 5, // [32]STABLE_FRAGMENT = 1 << 6, // [64]children 顺序确定的 fragmentKEYED_FRAGMENT = 1 << 7, // [128]children 中有带有 key 的节点的 fragmentUNKEYED_FRAGMENT = 1 << 8, // [256]没有 key 的 children 的 fragmentNEED_PATCH = 1 << 9, // [512]DYNAMIC_SLOTS = 1 << 10, // [1024]动态的插槽// SPECIAL FLAGS -------------------------------------------------------------// 以下是特殊的 flag,负值HOISTED = -1, // 表示他是静态节点,他的内容永远不会改变BAIL = -2, // 用来表示一个节点的 diff 应该结束
}

之所以使用位运算,是因为

  • | 来进行复合,TEXT | PROPS得到0000 1001,即十进制 9。标识他既有动态文字内容,也有动态 props。

  • & 进行 check,patchFlag & TEXT0000 1001 & 0000 0001,得到0000 0001,只要结果大于 0,就说明属性命中。

  • 方便扩展、计算更快...

patchFlag 被赋值到 VNode 的属性中,他在后面更新节点时会被用到。为了配合代码的正常流转,先放一放,代码继续 F10。如果你去调试代码,会发现这真的是千层套路啊,一直 shift + F11 跳出代码到怀疑人生,才终于回到 mountComponent...

总结一下 setupComponent 安装组件实例,主要做了什么事情:initProps、initSlots、响应式初始化、得到模板的 render 函数等等。

回顾前文,跳出到【13】,setup 安装组件实例后,下一步是 setupRenderEffect 激活渲染函数的副作用

激活渲染函数的副作用 setupRenderEffect

实现基于【21】,effect 副作用,意味着响应式数据变化后引起的变更。effect 源自 reactive,传入一个 fn 得到一个 reactiveEffect

effect 的入参 componentEffect 是一个命名函数,会立即执行。componentEffect 执行过程中,触发响应式数据的 getter 拦截,会在全局数据响应关系仓库记录当前componentEffect。在响应式对象发生改变时,派发更新,执行componentEffect

回到componentEffect

function componentEffect() {if (!instance.isMounted) {let vnodeHook: VNodeHook | null | undefinedconst { el, props } = initialVNodeconst { bm, m, parent } = instance// beforeMount hook 生命周期钩子函数if (bm) {invokeArrayFns(bm)}...// subTree 根节点的 subTree,通过 renderComponentRoot 根据 render 生成的 vnode//大家回忆一下 render 是什么?是不是根组件的 template 编译后得到的好多_createVNode 的渲染器函数?const subTree = (instance.subTree = renderComponentRoot(instance))...// 更新patch(null, subTree, container, ...)...if (m) { // parent 的 mounted 执行之前,先执行 subTree 的 patchqueuePostRenderEffect(m, parentSuspense)}...instance.isMounted = true // 标志实例已挂载} else { ... }
}

执行前面编译后得到的渲染函数 render,生成subTree: vnode

最后执行 patch,上文中渲染根节点的 vnode 时执行过 patch,这里就进入了一个大循环,根据组件的 childrentypeshapeFlagbaseCreateRenderer 会继续进行各种 processXXX 处理,直至基于 平台特性DOM 操作 挂载到各自的父节点中。

这个顺序是深度遍历的过程,子节点的 patch 完成之后再进行父节点的 mounted

patch 循环 && subTree 一览

// subTree 的 模板 template
<div id="app"><h1>composition-api</h1><p @click="add" :attr-key="counter">{{counter}}</p><p :class="{'counter': counter % 2}">{{doubleCounter}}</p>
</div>// patchFlag: 64
// STABLE_FRAGMENT = 1 << 6, // 64 表示:children 顺序确定的 fragment
// shapeFlag: 16
// ARRAY_CHILDREN = 1 << 4, // 16
  1. 观察上面这个模板,Vue2.x 中的模板只能有一个根元素,Vue3.0 的这个 demo 中有三个根元素,这得益于新增的 fragment 组件。

  2. vnode 标识出来 patchFlag:64,表示 children 顺序确定的 fragment

  3. vnode 标识出来 shapeFlag:16,表示当前节点是一个孩子数组。

  4. vnode 标识出来 dynamicChildren,标识动态变化的孩子节点。显然是两个 p 标签,可以想象这个数组的元素也是当前呈现的 vnode,只不过具体属性值不同罢了

等等,还有 4 吗,我不知道...

当然还有,processxxx 中一般都会判断是挂载还是更新,更新的时候就会用到 patchFlag,比如 patchElement... 下次一定

等等,还有 5 吗,我不知道...

当然还有,第五层我就已经裂开了啊...

あ:あげない      あ:不给你哦~ ????????????
い:いらない,    い:不要了啦~ ????????????
う:うごけない    う:动不了了~ ????????????
え:えらべない    え:不会选嘛~ ????????????
お:おせない      お:按不到耶~ [裂开][裂开][裂开]

刚看源码不久,只能靠 F11 、参考其他文档,凭自己的理解写出这样的文章,肯定有很多理解不对的地方,希望得到批判指正。

附录

  • Vue3初始化.drawio (https://www.yuque.com/office/yuque/0/2020/drawio/441847/1605880555730-4e18923f-c087-4082-af06-ec51986ba658.drawio?from=https%3A%2F%2Fwww.yuque.com%2Fdocs%2Fshare%2F64bd5cdc-3086-4154-a447-04032d161830%3F%23)

推荐阅读

我在阿里招前端,我该怎么帮你?(现在还可以加模拟面试群)
如何拿下阿里巴巴 P6 的前端 Offer
如何准备阿里P6/P7前端面试--项目经历准备篇
大厂面试官常问的亮点,该如何做出?
如何从初级到专家(P4-P7)打破成长瓶颈和有效突破
若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?
若川知乎高赞:有哪些必看的 JS库?

末尾

你好,我是若川,江湖人称菜如若川,历时一年只写了一个学习源码整体架构系列~(点击蓝字了解我)

  1. 关注若川视野,回复"pdf" 领取优质前端书籍pdf,回复"1",可加群长期交流学习

  2. 我的博客地址:https://lxchuan12.gitee.io 欢迎收藏

  3. 觉得文章不错,可以点个在看呀^_^另外欢迎留言交流~

小提醒:若川视野公众号面试、源码等文章合集在菜单栏中间【源码精选】按钮,欢迎点击阅读,也可以星标我的公众号,便于查找

千层套路 - Vue 3.0 初始化源码探秘相关推荐

  1. 初始化触发点击事件_【Vue原理】Event - 源码版 之 自定义事件

    专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本[2.5.17] Vue 的自定义事件很简单,就是使用 观察者模 ...

  2. sql server 2014 判断一个列某个字段是否相同_Select * from user的千层套路——一个sql是如何执行的...

    Select * from user的千层套路 作为一个程序员,可以说是无时无刻不与sql语句进行打交道,可是你真的了解MySQl的基本框架吗?以及你所写的每一条SQL是如何运行的吗?就比如下面这条平 ...

  3. 合约跟单千层套路,散户还有活路吗?

    行业竞争趋于白热化,交易所的护城河从产品技术逐渐拓展到精细服务,真正满足用户需求才能脱颖而出.跟单产品的出现,切中了目前合约领域的需求. 文 | 秦晓峰  运营 | 盖遥  编辑 | Mandy王梦蝶 ...

  4. android4.0.3源码之硬件gps简单移植

    [转]我和菜鸟一起学android4.0.3源码之硬件gps简单移植 2013-7-5阅读94 评论0 关于android定位方式 android 定位一般有四种方法,这四种方式分别是GPS定位.WI ...

  5. Ubuntu16.04编译android6.0.1源码记录

    目录 目录 一.安装环境 二.下载源码 1.下载repo 2.初始化repo 3.同步源代码 关于驱动 三.编译源码 四.导入源码到AS 五.刷入真机 六.修改源码 总结: 3.同步源代码 关于驱动 ...

  6. 【Vue原理】Diff - 源码版 之 Diff 流程

    写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 [2.5.17] 如果你觉得排版难 ...

  7. Android 8.0系统源码分析--Camera processCaptureResult结果回传源码分析

    相机,从上到下概览一下,真是太大了,上面的APP->Framework->CameraServer->CameraHAL,HAL进程中Pipeline.接各种算法的Node.再往下的 ...

  8. Quartz的Scheduler初始化源码分析

    2019独角兽企业重金招聘Python工程师标准>>> Quartz的使用:http://donald-draper.iteye.com/blog/2321886  Quartz的S ...

  9. element 往node里面增加属性值_【Vue原理】Compile - 源码版 之 Parse 属性解析

    写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 [2.5.17] 如果你觉得排版难 ...

最新文章

  1. 连续变量的转换:ECDF、Box-Cox、Yeo-Johnson
  2. Java虚拟机中的栈和堆
  3. 如何查找订单提示VPRS VE217 数量/值确定时出错
  4. open a BP will trigger text load - COM_TEXT_MAINTAIN - READ_TEXT
  5. css3 filter url,CSS3 filter(滤镜) 属性
  6. SQL 2005 Reporting Services:物理分页和逻辑分页 SSRS 2008 report export to PDF - Cannot get size to work...
  7. C++ 0x/11学习笔记
  8. Towards Characterizing the Behavior of LiDARs in Snowy Conditions
  9. Linux操作系统之虚拟化
  10. 宝可梦 图片识别python_初探利用Python进行图文识别(OCR)
  11. 十字路口通行优先权,十字路口通行规则图解
  12. 关于什么是大数据智能决策!摘自《大数据智能决策》自动化学报
  13. 查询数据库dblink
  14. 人脸识别,验证,登录开发 (三)
  15. html投影电脑,投影仪怎么连接笔记本电脑
  16. Linux学习13—网站服务
  17. navicat的连接
  18. 常见html的标题含义(1)
  19. Flume 以twitter为source,kafka为channel,hdfs为sink,再用spark streaming 读kafka topic
  20. Dynamics CRM 导入解决方案时如何做到不覆盖目标系统的站点地图

热门文章

  1. Windows下 jupyter notebook 运行multiprocessing 报错的问题与解决方法
  2. python生成日历_使用Python实现简易月历生成(2)
  3. Android OOM的解决方式
  4. vue项目实现列表页-详情页返回不刷新,再点其他菜单项返回刷新的需求
  5. 1.Rabbitmq学习记录《本质介绍,协议AMQP分析》
  6. iis express8 自动关闭
  7. baidu的服务器数据里面装的都是垃圾!
  8. Linux编程MQTT实现主题发布订阅
  9. kk 服务器信息,手机kk服务器设置
  10. Java开发中遇到具有挑战的事_Java并发编程的挑战:遇到的问题及如何解决