vnode


前言

在vue.js中,组件是一个非常重要的概念,整个应用的页面都是通过组件渲染实现的。但你知道当我们编写这些组件时,它内部的工作原理吗?从编写组件开始,到最后的真正的DOM又是怎样的一个转变过程呢?这篇文章会描述Vue3.x中的组件是如何渲染的。


一、组件

组件是一个抽象的概念,它是一棵DOM树的抽象。例如:定义了一个hello-world组件,并不会渲染hello-world,取决于hello-world模板内部的实现。
一个组件渲染成真正的DOM需要以下几个步骤:
在这里插入图片描述
vnode:这里先暂时理解成描述DOM元素的Javascript对象

二、应用程序初始化

  • 一个组件可以通过“模板加对象描述”的方式创建,那么组件创建之后是如何调用并初始化的?
  • 因为整个组件树是由根组件开始渲染的,为了找到根组件的渲染入口,需要从应用程序的初始化过程开始分析
    在这里插入图片描述
    在这里插入图片描述
    createApp内部实现
    在这里插入图片描述
    使用ensureRenderer().createApp()来创建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方法接收两个参数:根组件的对象和prop
		return function createApp(rootComponent, rootProps=null) {
			const app={
				_component: rootComponent,
				_props: rootProps,
				mount(rootContainer){
					// 创建根组件的Vnode
					const vnode=createVNode(rootComponent, rootProps)
					// 利用渲染器vnode
					render(vnode, rootContainer)
					app._containeer=rootContainer
					return vnode.component.proxy
				}
			}
			return app
		}
	}
}

  • 在整个app对象创建过程中,Vue.js利用闭包和函数柯里化的技巧,很好地实现了参数保留
  • 比如,在执行app.mount的时候,不需要传入渲染器render,因为在执行createAppAPI的时候render参数已经被保留下来了

为什么要重写app.mount方法?
因为Vue.js不仅仅是为Web平台服务的,它的目标是支持跨平台渲染。createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:

app.mount重写
在这里插入图片描述

app.mount=(containerOrSelector)=>{
	// 标准化容器
	const container=normalizeContainer(containerOrSelector)
	if(!container)
		return
	const component=app._component
	// 如组件对象没有定义render函数和template模板,则取容器的innerHTML作为模板内容
	if(!isFunction(component)&&!component.render&&!component.template) {
		component.template=container.innerHTML
	}
	// 挂载前清空容器内容
	container.innerHTML=''
	// 真正的挂载
	return mount(container)
}
  • 重写的目的:
    既能让用户在使用API时可以更加灵活,也兼容了Vue.js2.x的写法。比如app.mount的第一个参数就同时支持选择器字符串和DOM对象两种类型。

核心渲染流程:创建vnode和渲染vnode

vnode本质上是描述DOM的javascript对象,它在Vue.js中可以描述不同类型的节点
在这里插入图片描述
那么vnode有什么优势?为什么一定要设计vnode这样的数据结构呢?

  • 抽象:引入vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升;
  • 跨平台:因为patch vnode的过程不同平台可以有自己的实现,基于vnode再做服务器端渲染、weex平台、小程序平台渲染。

使用vnode不代表不操作DOM,性能也不一定比直接操作DOM好。
首先这种基于vnode实现的MVVM框架,在每次render to render的过程中,渲染组件会有一定Javascript耗时,特别是大组件。
当我们去更新组件的时候,用户会感觉到明显的卡顿。虽然diff算法在减少DOM操作方面足够优秀,但最终免不了操作DOM,所以说性能并不是vnode的优势。

通过createVNode函数创建根组件的vnode:

const vnode=createVNode(rootComponent, 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*/
	 										:0
	const vnode={
		type,
		props,
		shapeFlag,
		// 一些其他属性
	}
	// 标准化子节点,把不同数据类型的children转成数组或者文本类型
	normalizeChildren(vnode,children)
	return vnode
}

渲染创建好的vnode:

render(vnode, rootContainer)
const render = (vnode,container) => {
	if(vnode==null){
		// 销毁组件
		if(container._vnode){
			unmount(container._vnode, null, null,true)
		}
	}else{
		// 创建或者更新组件
		patch(container._vnode||null,vnode,container)
	}
	// 缓存vnode节点,表示已经缓存
	container._vnode=vnode
}

渲染vnode的代码中,patch函数的实现:

const patch=(n1,n2,container,anchor=null,parentComponent=null,parentSuspense=null,isSVG=false,optimized=false){
	// 如果存在新旧节点,且新旧节点类型不同,则销毁旧节点
	if(n1&&!isSameVNodeType(n1,n2)){
		anchor=getNextHostNode(n1)
		unmount(n1,parentComponent,parentSuspense,true)
		n1=null
	}
	const {type,shapeFlag}=n2
	switch(type){
		case Text:
			// 处理文本节点
			break
		case Comment:
			// 处理注释节点
			break
		case Static:
			// 处理静态节点
			break
		case Fragment:
			// 处理Fragment元素
			break
		default:
			if(shapeFlag&1/*ELEMENT*/){
				//处理普通DOM元素
				processElement(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG, optimized)
			}
			else if(shapeFlag&6/*COMPONENT*/){
				// 处理COMPONENT
				processComponent(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG, optimized)
			}
			else if(shapeFlag&64/*TELEPORT*/){
				// 处理TELEPORT
				...
			}
			else if(shapeFlag&128/*SUSPENSE*/){
				// 处理SUSPENSE
				...
			}
	}
}

参数n1:表示旧的vnode,当n1为null的时候,表示是一次挂载的过程
参数n2:新的vnode,后续会根据这个vnode类型执行不同的处理逻辑
参数container:表示DOM容器,在vnode渲染生成DOM后,会挂载到container下面。

vnode主要关注组件的渲染和普通元素的渲染
组件的渲染函数processComponent的实现:

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

挂载组件的mountComponent函数的实现:

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

渲染函数setupRenderEffect的实现:

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

初始渲染做了两件事:渲染组件生成subTree、把subTree挂载到container中。

processElement函数实现:

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,container,anchor,parentComponent,parentSuspense,isSVG, optimized)
	}
}

挂载元素的mountElement函数的实现:

const mountElement=(vnode,container,anchor,parentComponent,parentSuspense,isSVG, optimized)=>{
	let el
	const {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/**/){
		// 处理子节点是文本
		hostSetElementText(el,vnode,children)
	}
	else if(shapeFlag&16/**/){
		//处理子节点是数组的情况
		mountChildren(vnode.children,el,null,parentComponent,parentSuspense,isSVG&&type!=='foreignObject, optimized||!!vnode.dynamicChildren)
	}
	// 把创建的DOM元素节点挂载到container上
	hostInsert(el,container,anchor)
}

在web环境下的定义:

function createElement(tag,isSVG,is){
	isSVG?document.createElementNs(svgNS,tag):document.createElement(tag,is?{is}:undefined)
}

如果是其他平台,比如week,hostCreateElement方法不在是操作DOM,而是平台相关的API,这些API是在创建渲染器阶段传入的
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值