Vnode到真实DOM是如何转变的?

x33g5p2x  于2022-03-17 转载在 其他  
字(9.5k)|赞(0)|评价(0)|浏览(307)

一、写在前面
在vue开发中,组件是非常重要的概念,但是我们在编写组件的时候,是否知道其内部是如何进行运转的。本文将总结一下vue3.0中组件是如何进行渲染的?
二、内容
在我们编写组件代码时,会经常编写如下所示模板代码。

  1. <template>
  2. <div>
  3. <p>hello world</p>
  4. </div>
  5. </template>

从上述表现上看,组件的模板决定了组件生成的DOM标签,而在vuejs内部,一个组件如果想要生成真正的DOM,需要经过如下几个步骤。

如上图所示,需要经过创建vnode,渲染vnode,以及生成DOM的三个过程。接下来我们将从程序入口开始,一步一步看真实DOM是如何生成的。
1、应用程序初始化

  1. // 在 Vue.js 3.0 中,初始化一个应用的方式如下
  2. import { createApp } from "vue";
  3. import App from "./app";
  4. const app = createApp(App);
  5. app.mount("#app");

如上图所示,我们可以看到入口函数是createApp

  1. const createApp = (...args) => {
  2. // 创建 app 对象
  3. const app = ensureRenderer().createApp(...args);
  4. const { mount } = app;
  5. // 重写 mount 方法
  6. app.mount = (containerOrSelector) => {
  7. // ...
  8. };
  9. return app;
  10. };

上述就是createApp主要做的事,一个是创建app对象,另一个是重写app.mount方法。
2、创建app对象
首先我们首先执行代码:

  1. const app = ensureRenderer().createApp(...args);

其中ensureRenderer()来创建一个渲染器对象,它内部代码为:

  1. // 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
  2. const rendererOptions = {
  3. patchProp,
  4. ...nodeOps,
  5. };
  6. let renderer;
  7. // 延时创建渲染器,当用户只依赖响应式包的时候,可以通过 tree-shaking 移除核心渲染逻辑相关的代码
  8. function ensureRenderer() {
  9. return renderer || (renderer = createRenderer(rendererOptions));
  10. }
  11. function createRenderer(options) {
  12. return baseCreateRenderer(options);
  13. }
  14. function baseCreateRenderer(options) {
  15. function render(vnode, container) {
  16. // 组件渲染的核心逻辑
  17. }
  18. return {
  19. render,
  20. createApp: createAppAPI(render),
  21. };
  22. }
  23. function createAppAPI(render) {
  24. // createApp createApp 方法接受的两个参数:根组件的对象和 prop
  25. return function createApp(rootComponent, rootProps = null) {
  26. const app = {
  27. _component: rootComponent,
  28. _props: rootProps,
  29. mount(rootContainer) {
  30. // 创建根组件的 vnode
  31. const vnode = createVNode(rootComponent, rootProps);
  32. // 利用渲染器渲染 vnode
  33. render(vnode, rootContainer);
  34. app._container = rootContainer;
  35. return vnode.component.proxy;
  36. },
  37. };
  38. return app;
  39. };
  40. }

首先ensureRenderer()来延时创建渲染器。
好处:

  1. 当用户值依赖响应式包的时候,就不会创建渲染器。
  2. 可以通过tree-shaking的方式来移除核心渲染逻辑相关的代码。
  1. 我都其理解是:因为我们在调用createApp才会执行`ensureRenderer()`方法,如果我们只使用响应式的包的时候,并不使用渲染器,此时我们就可以在打包的时候,使用tree-shaking来将没有使用到的函数取消。

其次通过createRenderer创建一个渲染器,这个渲染器内部存在一个createApp方法,接收了rootComponentrootProps两个参数。
我们在应用层面执行createAp(App)方法时 ,会把App组件对象作为跟组件传递给rootComponet,这样createApp内部就会创建一个App对象。他会提供mount方法,这个方法是用来挂载组件的。
值得注意的是:app在创建对象时,vue利用闭包和函数的柯里化的技巧,很好的实现了参数保留。
3、重写app.mount方法
createApp返回的app兑现已经拥有了mount方法,那为什么还要重写?

  1. 1、为了支持跨平台。
  2. createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程。代码如下所示。
  1. mount(rootContainer) {
  2. //创建跟组件的vnode
  3. const vnode = createVNode(rootComponent, rootProps)
  4. //利用渲染器渲染vnode
  5. render(vnode, rootContainer)
  6. app._container = rootContainer
  7. return vnode.component.proxy
  8. }

主要流程为:先创建vnode,再渲染vnode
参数rootContainer根据平台不同而不同。
这里的代码不应该包含任何特定平台的相关逻辑,所以我们需要在外部重写。
4、app.mount重写做了哪些事情

  1. app.mount = (containerOrSelector) => {
  2. // 标准化容器
  3. const container = normalizeContainer(containerOrSelector)
  4. if (!container)
  5. return
  6. const component = app._component
  7. // 如组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容
  8. if (!isFunction(component) && !component.render && !component.template) {
  9. component.template = container.innerHTML
  10. }
  11. // 挂载前清空容器内容
  12. container.innerHTML = ''
  13. // 真正的挂载
  14. return mount(container)
  15. }

首先是通过normalizeContainer表转化容器(这里可以传入字符串选择器或者DOM对象,但是如果是字符串渲染器,会将其转化为DOM对象,作为最终挂载的容器。)
然后做一个if判断,如果组件对象没有定义render函数或者没有定义render函数或者template模板,则取容器的innterHTML作为组件模板的内容。
在挂载前将容器中的内容清空。
最终再进行挂载。
优势:

  1. 1、跨平台的实现
  2. 2、兼容vue2.0写法
  3. 3app.mount既可以传dom,也可以传字符串选择器。

三、核心渲染流程:创建Vnode和渲染vnode
1、创建vnode

  1. 1vnode的本质是用来描述DOMjavascript对象。

如果我们想要描述一个button标签可以通过下面代码所示进行描述。

  1. // vnode 这样表示<button>标签
  2. const vnode = {
  3. type: 'button',
  4. props: {
  5. 'class': 'btn',
  6. style: {
  7. width: '100px',
  8. height: '50px'
  9. }
  10. },
  11. children: 'click me'
  12. }

type属性表示DOM的标签类型。
props属性表示DOM的附加信息,比如style, class等。
children顺序表示DOM的子节点,它也可以是一个vnode数组,只不过vnode可以用字符串表示简答的文本。
2、vnode除了可以用来描述真实的ODM外,也可以用来描述组件

  1. <CustomComponent></CustomComponent>
  2. const vnode ={
  3. type: CustomCompoent,
  4. props: {
  5. msg: 'test'
  6. }
  7. }

3、其他的,还有纯文本vnode,注释vnode
4、vue3.x中,vnode的type,做了更详尽的分类,包括suspense, teleport等,且把vnode的类型信息做了编码,一遍在后面的patch阶段,可以根据不同的类型执行相关的处理。
Vnode的优势

  1. 1、抽象
  2. 2、跨平台
  3. 3、但是和手动修改DOM对比,并不一定存在优势。

如何创建Vnode
app.mount函数的实现,内部是通过createVnode函数来创建跟组件的Vnode

  1. const vnode = createVNode(
  2. rootComponent as ConcreteComponent,
  3. rootProps)

createVNode大致实现如下:

  1. function createVNode(type, props = null,children = null) {
  2. if (props) {
  3. // 处理 props 相关逻辑,标准化 class 和 style
  4. }
  5. // 对 vnode 类型信息编码
  6. const shapeFlag = isString(type)
  7. ? 1 /* ELEMENT */
  8. : isSuspense(type)
  9. ? 128 /* SUSPENSE */
  10. : isTeleport(type)
  11. ? 64 /* TELEPORT */
  12. : isObject(type)
  13. ? 4 /* STATEFUL_COMPONENT */
  14. : isFunction(type)
  15. ? 2 /* FUNCTIONAL_COMPONENT */
  16. : 0
  17. const vnode = {
  18. type,
  19. props,
  20. shapeFlag,
  21. // 一些其他属性
  22. }
  23. // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
  24. normalizeChildren(vnode, children)
  25. return vnode
  26. }

上述做了的事情是:1、对Props做标准化处理2、对vnode的类型信息编码创建vnode对象,标准化子节点children

渲染vnode

  1. // 渲染vnode
  2. const render: RootRenderFunction = (vnode, container, isSVG) => {
  3. // 如果vnode为空
  4. if (vnode == null) {
  5. // 但是缓存vnode节点存在
  6. if (container._vnode) {
  7. // 销毁vnode
  8. unmount(container._vnode, null, null, true)
  9. }
  10. } else {
  11. // 否则进行挂载或者更新
  12. patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  13. }
  14. flushPostFlushCbs()
  15. // 将vnode缓存下来
  16. container._vnode = vnode
  17. }

如果vnode为空,则执行销毁组件的逻辑,否则执行创建或者更新组件的逻辑。
patch函数实现

  1. const patch: PatchFn = (
  2. n1,
  3. n2,
  4. container,
  5. anchor = null,
  6. parentComponent = null,
  7. parentSuspense = null,
  8. isSVG = false,
  9. slotScopeIds = null,
  10. optimized = false
  11. ) => {
  12. // 如果存在新旧节点,并且节点类型不相同,则销毁旧节点
  13. if (n1 && !isSameVNodeType(n1, n2)) {
  14. anchor = getNextHostNode(n1)
  15. unmount(n1, parentComponent, parentSuspense, true)
  16. n1 = null
  17. }
  18. const { type, ref, shapeFlag } = n2
  19. switch (type) {
  20. case Text://处理文本节点类型
  21. processText(n1, n2, container, anchor)
  22. break
  23. case Comment: //处理注释类型
  24. processCommentNode(n1, n2, container, anchor)
  25. break
  26. case Static: //处理静态文本类型
  27. if (n1 == null) {
  28. mountStaticNode(n2, container, anchor, isSVG)
  29. } else if (__DEV__) {
  30. patchStaticNode(n1, n2, container, isSVG)
  31. }
  32. break
  33. case Fragment: //处理Fragment类型
  34. processFragment(
  35. n1,
  36. n2,
  37. container,
  38. anchor,
  39. parentComponent,
  40. parentSuspense,
  41. isSVG,
  42. slotScopeIds,
  43. optimized
  44. )
  45. break
  46. default:
  47. if (shapeFlag & ShapeFlags.ELEMENT) {
  48. processElement( //处理DOM类型
  49. n1,
  50. n2,
  51. container,
  52. anchor,
  53. parentComponent,
  54. parentSuspense,
  55. isSVG,
  56. slotScopeIds,
  57. optimized
  58. )
  59. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  60. processComponent( //处理组件类型
  61. n1,
  62. n2,
  63. container,
  64. anchor,
  65. parentComponent,
  66. parentSuspense,
  67. isSVG,
  68. slotScopeIds,
  69. optimized
  70. )
  71. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  72. ;(type as typeof TeleportImpl).process( //处理teleport类型
  73. n1 as TeleportVNode,
  74. n2 as TeleportVNode,
  75. container,
  76. anchor,
  77. parentComponent,
  78. parentSuspense,
  79. isSVG,
  80. slotScopeIds,
  81. optimized,
  82. internals
  83. )
  84. } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
  85. ;(type as typeof SuspenseImpl).process( //处理suspense类型
  86. n1,
  87. n2,
  88. container,
  89. anchor,
  90. parentComponent,
  91. parentSuspense,
  92. isSVG,
  93. slotScopeIds,
  94. optimized,
  95. internals
  96. )
  97. } else if (__DEV__) {
  98. warn('Invalid VNode type:', type, `(${typeof type})`)
  99. }
  100. }
  101. // set ref
  102. if (ref != null && parentComponent) {
  103. setRef(ref, n1 && n1.ref, parentSuspense, n2)
  104. }
  105. }

这个函数有两个功能:
一个是根据vnode挂载DOM
一个是根据旧节点更新DOM。
patch函数入参。
第一个参数n1表示旧的vnode,当n1为null的时候,表示是一次挂载的过程。
第二个参数n2表示新的vnode节点,后续会根据这个vnode进行相关的处理。
第三个参数container表示DOM容器,也就是vnode渲染生成DOM后,会挂载到container下面。
对组件进行处理
processComponent函数的实现——用来处理组件

  1. const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. if (n1 == null) {
  3. // 挂载组件
  4. mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  5. }
  6. else {
  7. // 更新组件
  8. updateComponent(n1, n2, parentComponent, optimized)
  9. }
  10. }

如果n1null,则执行挂载组件的逻辑。
如果n1不为null,则执行更新组件的逻辑。
mountCompoent挂载组件的实现

  1. const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. // 创建组件实例
  3. const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
  4. // 设置组件实例
  5. setupComponent(instance)
  6. // 设置并运行带副作用的渲染函数
  7. setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
  8. }

mountComponent主要做了三件事。
第一件:创建组件实例

  1. Vue.js 3.0虽然不像Vue.js 2.x那样通过类的方式去实例化组件,但内部也通过对象的方式去创建了当前渲染的组件实例

第二件:设置组件实例

  1. instance保留了很多组件相关的数据,维护了组件的上下文,包括对props、插槽,以及其他实例的属性的初始化处理

第三件:设置并运行带副作用的渲染函数(setupRenderEffect)

  1. const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
  2. // 创建响应式的副作用渲染函数
  3. instance.update = effect(function componentEffect() {
  4. if (!instance.isMounted) {
  5. // 渲染组件生成子树 vnode
  6. const subTree = (instance.subTree = renderComponentRoot(instance))
  7. // 把子树 vnode 挂载到 container 中
  8. patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
  9. // 保留渲染生成的子树根 DOM 节点
  10. initialVNode.el = subTree.el
  11. instance.isMounted = true
  12. }
  13. else {
  14. // 更新组件
  15. }
  16. }, prodEffectOptions)
  17. }

该函数利用响应式库的effect函数创建一个副作用渲染函数componentEffect,我们可以把它理解为组件的数据发生改变后,effect包裹的内部componentEffect函数会重新执行一遍,从而达到重新渲染组件的目的。
对DOM元素进行处理

  1. const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. isSVG = isSVG || n2.type === 'svg'
  3. if (n1 == null) {
  4. //挂载元素节点
  5. mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  6. }
  7. else {
  8. //更新元素节点
  9. patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  10. }
  11. }

如果n1null, 走挂载元素节点的逻辑
否则走更新节点的逻辑。
mountElement函数

  1. const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  2. let el
  3. const { type, props, shapeFlag } = vnode
  4. // 创建 DOM 元素节点
  5. el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
  6. if (props) {
  7. // 处理 props,比如 class、style、event 等属性
  8. for (const key in props) {
  9. if (!isReservedProp(key)) {
  10. hostPatchProp(el, key, null, props[key], isSVG)
  11. }
  12. }
  13. }
  14. if (shapeFlag & 8 /* TEXT_CHILDREN */) {
  15. // 处理子节点是纯文本的情况
  16. hostSetElementText(el, vnode.children)
  17. }
  18. else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
  19. // 处理子节点是数组的情况
  20. mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
  21. }
  22. // 把创建的 DOM 元素节点挂载到 container 上
  23. hostInsert(el, container, anchor)
  24. }

主要做了四件事:
1、创建DOM元素节点
通过hostCreateElement方法创建,这是一个平台相关的方法,在web端的实现。

  1. // 调用了底层的 DOM API document.createElement 创建元素
  2. function createElement(tag, isSVG, is) {
  3. isSVG ? document.createElementNS(svgNS, tag)
  4. : document.createElement(tag, is ? { is } : undefined)
  5. }

2、处理props
给这个DOM节点添加相关的class,style,event等属性,并做相关的处理。
3、处理children
子节点是纯文本,则执行hostSetElementText方法,它在 Web环境下通过设置DOM元素的textContent属性设置文本。
4、 挂载DOM元素到container上

  1. function insert(child, parent, anchor) {
  2. if (anchor) {
  3. parent.insertBefore(child, anchor)
  4. }
  5. else {
  6. parent.appendChild(child)
  7. }
  8. }

相关文章