Vnode到真实DOM是如何转变的?
一、写在前面
在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
方法,接收了rootComponent
和rootProps
两个参数。
我们在应用层面执行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)}
}
如果n1
为null
,则执行挂载组件的逻辑。
如果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)}
}
如果n1
为null
, 走挂载元素节点的逻辑
否则走更新节点的逻辑。
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是如何转变的?相关推荐
- Vue 原理解析(五)之 虚拟Dom 到真实Dom的转换过程
上一篇 vue 原理解析(四): 虚拟Dom 是怎么生成的 再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系.我们先回顾之前的mountComponn ...
- 传递HTML字符串virtual,理解Virtual DOM(1) 真实DOM和虚拟DOM的映射
什么是Virtual DOM? 所谓virtual,指的是对真实DOM的一种模拟.相对于直接操作真实的DOM结构,我们构建一棵虚拟的树,将各种数据和操作直接应用在这棵虚拟的树上,然后再将对虚拟的树的修 ...
- vue核心之虚拟DOM(vdom)与真实DOM页面渲染过程
一.真实DOM和其解析流程? 浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树--创建StyleRules--创建Render树--布局Layout--绘制Painting 第一步,用HTM ...
- Patch:虚拟DOM最核心的部分--如何对比虚拟DOM树,以及如果生成真实DOM
虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实DOM. patch也可以叫做patching算法,通过它渲染真实DOM时,并不会暴力覆盖原有DOM.而是比对新旧俩个vnode之间有哪 ...
- 从VirtualDom(虚拟Dom)到真实DOM
浏览器中的Dom更新 在浏览器中渲染引擎将 node 节点添加到 另外节点中时会触发样式计算.布局.绘制.栅格化.合成等任务,这一过程称为重排. 除了重排之外,还有可能引起重绘或者合成操作,也就是&q ...
- 真实DOM和虚拟DOM
文章目录 如何高效操作DOM 什么是DOM 浏览器真实解析DOM的流程 为什么说操作DOM耗时 如何高效操作DOM 虚拟DOM 什么是虚拟DOM 为什么要有虚拟DOM 虚拟DOM的作用 Vue中的虚拟 ...
- 真实dom转换为虚拟dom的简单实现
标题真实dom转换为虚拟dom的简单实现 首先给出虚拟dom的数据结构. function vnode(tag, data) {this.tag = tag;this.data = data;this ...
- 深入Preact源码分析(二)virtualDOM如何变为真实dom
一个简单的Preact代码如下 // 一个简单的Preact demo import { h, render, Component } from 'preact';class Clock extend ...
- React 虚拟Dom 转成 真实Dom 实现原理
React 和 React-Dom 是核心模块 React:是核心库,当使用JSX语法时,必须让React 存在当前作用域下 React元素:是通过JSX语法创建的在JS中存在的HTML的标签 JSX ...
最新文章
- LeetCode简单题之卡牌分组
- nginx多层反向代理获取客户端真实ip
- J2ME下访问.net的webservice
- 如何使用小程序自定义组件功能
- MongoDB小结07 - update【$pop】
- mysql be_Amobe实现MySQL读写分离
- android camera无预览拍照 后台拍照
- Linux 基础入门 04
- 浅谈android应用之编程语言
- (信贷风控一)互联网金融业申请评分卡的介绍
- 最简洁的呼吸灯实验verilog
- Retrofit 2简单使用教程
- f_sync解决fatfs文件掉电数据丢失问题
- lscpu与cat /proc/cpuinfo获取的CPU信息释义
- 电脑不用,不用电脑,你还会写字吗?
- Java服务端NIO多线程编程库系列(一)
- String.Format 方法
- 为什么你的简历没人看?
- Why YY:腾讯负责复制一切 YY负责复制腾讯
- android+酷炫动画效果,Android酷炫动画效果之3D星体旋转效果
热门文章
- Feast on Amazon 解决方案
- I2C协议和驱动框架分析(二)
- codeforces739C - Skills 金中市队儿童节常数赛
- c语言判断是文件还是文件夹
- 零基础学画画有什么快速的方法
- 点击密码input框禁止浏览器弹出已经记录的账号密码
- Ubuntu18.04.2 Linux Receiving the error: snapd.snap-repair.service is a disabled or a static unit
- linux学习路线-韦东山:史上最全嵌入式Linux学习路线图
- 深富策略:股票市场中如何购买国债
- JavaScript split()方法