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

<template><div><p>hello world</p></div>
</template>

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

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

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

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

const createApp = (...args) => {// 创建 app 对象const app = ensureRenderer().createApp(...args);const { mount } = app;// 重写 mount 方法app.mount = (containerOrSelector) => {// ...};return app;
};

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

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

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

// 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
const rendererOptions = {patchProp,...nodeOps,
};
let renderer;
// 延时创建渲染器,当用户只依赖响应式包的时候,可以通过 tree-shaking 移除核心渲染逻辑相关的代码
function ensureRenderer() {return renderer || (renderer = createRenderer(rendererOptions));
}
function createRenderer(options) {return baseCreateRenderer(options);
}
function baseCreateRenderer(options) {function render(vnode, container) {// 组件渲染的核心逻辑}return {render,createApp: createAppAPI(render),};
}
function createAppAPI(render) {// createApp createApp 方法接受的两个参数:根组件的对象和 propreturn function createApp(rootComponent, rootProps = null) {const app = {_component: rootComponent,_props: rootProps,mount(rootContainer) {// 创建根组件的 vnodeconst vnode = createVNode(rootComponent, rootProps);// 利用渲染器渲染 vnoderender(vnode, rootContainer);app._container = rootContainer;return vnode.component.proxy;},};return app;};
}

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

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

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

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

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

app.mount = (containerOrSelector) => {// 标准化容器const container = normalizeContainer(containerOrSelector)if (!container)returnconst component = app._component// 如组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容if (!isFunction(component) && !component.render && !component.template) {component.template = container.innerHTML}// 挂载前清空容器内容container.innerHTML = ''// 真正的挂载return mount(container)
}

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

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

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

1、vnode的本质是用来描述DOM的javascript对象。

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

// vnode 这样表示<button>标签
const vnode = {type: 'button',props: { 'class': 'btn',style: {width: '100px',height: '50px'}},children: 'click me'
}

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

<CustomComponent></CustomComponent>const vnode ={type: CustomCompoent,props: {msg: 'test'}
}

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

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

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

 const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)

createVNode大致实现如下:

function createVNode(type, props = null,children = null) {if (props) {// 处理 props 相关逻辑,标准化 class 和 style}// 对 vnode 类型信息编码const shapeFlag = isString(type)? 1 /* ELEMENT */: isSuspense(type)? 128 /* SUSPENSE */: isTeleport(type)? 64 /* TELEPORT */: isObject(type)? 4 /* STATEFUL_COMPONENT */: isFunction(type)? 2 /* FUNCTIONAL_COMPONENT */: 0const vnode = {type,props,shapeFlag,// 一些其他属性}// 标准化子节点,把不同数据类型的 children 转成数组或者文本类型normalizeChildren(vnode, children)return vnode
}

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

渲染vnode

  // 渲染vnodeconst render: RootRenderFunction = (vnode, container, isSVG) => {// 如果vnode为空if (vnode == null) {// 但是缓存vnode节点存在if (container._vnode) {// 销毁vnodeunmount(container._vnode, null, null, true)}} else {// 否则进行挂载或者更新patch(container._vnode || null, vnode, container, null, null, null, isSVG)}flushPostFlushCbs()// 将vnode缓存下来container._vnode = vnode}

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

  const patch: PatchFn = (n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,isSVG = false,slotScopeIds = null,optimized = false) => {// 如果存在新旧节点,并且节点类型不相同,则销毁旧节点if (n1 && !isSameVNodeType(n1, n2)) {anchor = getNextHostNode(n1)unmount(n1, parentComponent, parentSuspense, true)n1 = null}const { type, ref, shapeFlag } = n2switch (type) {case Text://处理文本节点类型processText(n1, n2, container, anchor)breakcase Comment:  //处理注释类型processCommentNode(n1, n2, container, anchor)breakcase Static:  //处理静态文本类型if (n1 == null) {mountStaticNode(n2, container, anchor, isSVG)} else if (__DEV__) {patchStaticNode(n1, n2, container, isSVG)}breakcase Fragment: //处理Fragment类型processFragment(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized)breakdefault:if (shapeFlag & ShapeFlags.ELEMENT) {processElement(  //处理DOM类型n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized)} else if (shapeFlag & ShapeFlags.COMPONENT) {processComponent( //处理组件类型n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized)} else if (shapeFlag & ShapeFlags.TELEPORT) {;(type as typeof TeleportImpl).process( //处理teleport类型n1 as TeleportVNode,n2 as TeleportVNode,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized,internals)} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {;(type as typeof SuspenseImpl).process(  //处理suspense类型n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized,internals)} else if (__DEV__) {warn('Invalid VNode type:', type, `(${typeof type})`)}}// set refif (ref != null && parentComponent) {setRef(ref, n1 && n1.ref, parentSuspense, n2)}}

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

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {if (n1 == null) {// 挂载组件mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)}else {// 更新组件updateComponent(n1, n2, parentComponent, optimized)}
}

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

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

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

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

第二件:设置组件实例

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

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

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {// 创建响应式的副作用渲染函数instance.update = effect(function componentEffect() {if (!instance.isMounted) {// 渲染组件生成子树 vnodeconst subTree = (instance.subTree = renderComponentRoot(instance))// 把子树 vnode 挂载到 container 中patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)// 保留渲染生成的子树根 DOM 节点initialVNode.el = subTree.elinstance.isMounted = true}else {// 更新组件}}, prodEffectOptions)
}

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

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {isSVG = isSVG || n2.type === 'svg'if (n1 == null) {//挂载元素节点mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)}else {//更新元素节点patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)}
}

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

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {let elconst { type, props, shapeFlag } = vnode// 创建 DOM 元素节点el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)if (props) {// 处理 props,比如 class、style、event 等属性for (const key in props) {if (!isReservedProp(key)) {hostPatchProp(el, key, null, props[key], isSVG)}}}if (shapeFlag & 8 /* TEXT_CHILDREN */) {// 处理子节点是纯文本的情况hostSetElementText(el, vnode.children)}else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {// 处理子节点是数组的情况mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)}// 把创建的 DOM 元素节点挂载到 container 上hostInsert(el, container, anchor)
}

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

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

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

function insert(child, parent, anchor) {if (anchor) {parent.insertBefore(child, anchor)}else {parent.appendChild(child)}
}

Vnode到真实DOM是如何转变的?相关推荐

  1. Vue 原理解析(五)之 虚拟Dom 到真实Dom的转换过程

    上一篇 vue 原理解析(四): 虚拟Dom 是怎么生成的 再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系.我们先回顾之前的mountComponn ...

  2. 传递HTML字符串virtual,理解Virtual DOM(1) 真实DOM和虚拟DOM的映射

    什么是Virtual DOM? 所谓virtual,指的是对真实DOM的一种模拟.相对于直接操作真实的DOM结构,我们构建一棵虚拟的树,将各种数据和操作直接应用在这棵虚拟的树上,然后再将对虚拟的树的修 ...

  3. vue核心之虚拟DOM(vdom)与真实DOM页面渲染过程

    一.真实DOM和其解析流程? 浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树--创建StyleRules--创建Render树--布局Layout--绘制Painting 第一步,用HTM ...

  4. Patch:虚拟DOM最核心的部分--如何对比虚拟DOM树,以及如果生成真实DOM

    虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实DOM. patch也可以叫做patching算法,通过它渲染真实DOM时,并不会暴力覆盖原有DOM.而是比对新旧俩个vnode之间有哪 ...

  5. 从VirtualDom(虚拟Dom)到真实DOM

    浏览器中的Dom更新 在浏览器中渲染引擎将 node 节点添加到 另外节点中时会触发样式计算.布局.绘制.栅格化.合成等任务,这一过程称为重排. 除了重排之外,还有可能引起重绘或者合成操作,也就是&q ...

  6. 真实DOM和虚拟DOM

    文章目录 如何高效操作DOM 什么是DOM 浏览器真实解析DOM的流程 为什么说操作DOM耗时 如何高效操作DOM 虚拟DOM 什么是虚拟DOM 为什么要有虚拟DOM 虚拟DOM的作用 Vue中的虚拟 ...

  7. 真实dom转换为虚拟dom的简单实现

    标题真实dom转换为虚拟dom的简单实现 首先给出虚拟dom的数据结构. function vnode(tag, data) {this.tag = tag;this.data = data;this ...

  8. 深入Preact源码分析(二)virtualDOM如何变为真实dom

    一个简单的Preact代码如下 // 一个简单的Preact demo import { h, render, Component } from 'preact';class Clock extend ...

  9. React 虚拟Dom 转成 真实Dom 实现原理

    React 和 React-Dom 是核心模块 React:是核心库,当使用JSX语法时,必须让React 存在当前作用域下 React元素:是通过JSX语法创建的在JS中存在的HTML的标签 JSX ...

最新文章

  1. LeetCode简单题之卡牌分组
  2. nginx多层反向代理获取客户端真实ip
  3. J2ME下访问.net的webservice
  4. 如何使用小程序自定义组件功能
  5. MongoDB小结07 - update【$pop】
  6. mysql be_Amobe实现MySQL读写分离
  7. android camera无预览拍照 后台拍照
  8. Linux 基础入门 04
  9. 浅谈android应用之编程语言
  10. (信贷风控一)互联网金融业申请评分卡的介绍
  11. 最简洁的呼吸灯实验verilog
  12. Retrofit 2简单使用教程
  13. f_sync解决fatfs文件掉电数据丢失问题
  14. lscpu与cat /proc/cpuinfo获取的CPU信息释义
  15. 电脑不用,不用电脑,你还会写字吗?
  16. Java服务端NIO多线程编程库系列(一)
  17. String.Format 方法
  18. 为什么你的简历没人看?
  19. Why YY:腾讯负责复制一切 YY负责复制腾讯
  20. android+酷炫动画效果,Android酷炫动画效果之3D星体旋转效果

热门文章

  1. Feast on Amazon 解决方案
  2. I2C协议和驱动框架分析(二)
  3. codeforces739C - Skills 金中市队儿童节常数赛
  4. c语言判断是文件还是文件夹
  5. 零基础学画画有什么快速的方法
  6. 点击密码input框禁止浏览器弹出已经记录的账号密码
  7. Ubuntu18.04.2 Linux Receiving the error: snapd.snap-repair.service is a disabled or a static unit
  8. linux学习路线-韦东山:史上最全嵌入式Linux学习路线图
  9. 深富策略:股票市场中如何购买国债
  10. JavaScript split()方法