【vue设计与实现】渲染器 1-渲染器与相应系统的结合&渲染器基本概念

渲染器与相应系统的结合

渲染器就是用来执行渲染任务的,渲染器不仅能够渲染真是DOM元素,还是框架跨平台能力的关键。
这里,先暂时将渲染器限定在DOM平台,既然渲染器用来渲染真实DOM元素,那么严格来说,下面的函数就是一个合格的渲染器

function renderer(domString, container){
	container.innerHTML = domString
}

可以这样使用

renderer('<h1>Hello</h1>',document.getElementById('app'))

不仅可以渲染静态的字符串,还可以渲染动态拼接的HTML内容,如下所示:

let count=1
renderer(`<h1>${count.value}</h1>`,document.getElementById('app'))

这样最终渲染出的内容是<h1>1</h1>,注意上面的count,如果是一个响应式数据,会怎么样?利用响应系统,可以让整个渲染过程自动化:

const count = ref(1)
effect(()=>{
	renderer(`<h1>${count.value}</h1>`,document.getElementById('app'))
})

count.value++

副作用函数执行完毕后,会与响应式数据建立响应联系。修改count.value的值时,副作用函数会重新执行,完成重新渲染。所以上面的代码运行完毕后,最终渲染到页面的内容是<h1>1</h1>
这就是响应系统和渲染器之间的关系。利用响应系统,自动调用渲染器完成页面的渲染和更新。
从本章开始,将使用@vue/reactivity包提供的响应式API进行讲解。@vue/reactivity提供了IIFE模块格式,可以直接通过<script>的标签引入到页面使用:

<script src="https://unpkg.com/@vue/reactive@3.0.5/dist/reactivity.global.js"><?script>

上面例子完整的代码如下:

const {effect, ref} = VueReactivity

function renderer(domString, container){
	container.innerHTML = domString
}

const count = ref(1)
effect(()=>{
	renderer(`<h1>${count.value}</h1>`,document.getElementById('app'))
})

count.value++

可以看到,通过VueReactivity得到了effect和ref这两个api

渲染器基本概念

渲染器的作用是把虚拟DOM渲染为特定平台的真实元素。在浏览器上,渲染器会把DOM渲染为真实DOM元素
虚拟DOM通常用英文virtual DOM来表达,有时会简写成vdom。虚拟DOM和真实DOM的结构一样,都是由一个个节点组成的树型结构。虚拟节点英文是virtual Node,有时简写成vnode。虚拟DOM是树型结构,其中任何一个vnode节点都可以是一棵子树,因此vdom和vnode可以替换使用。在这里统一用vnode
渲染器把虚拟DOM节点渲染成真实DOM节点的过程叫做挂载。通常用英文mount来表示。此外渲染器通常需要一个挂载点作为参数,用来指定具体的挂载位置。一般渲染器会吧一个DOM元素作为容器元素,并把内容渲染到其中,一般用英文container来表示容器。为了更好的理解,看下面的例子:

function createRenderer(){
	function render(vnode, container){
		// ....
	}
	return render
}

关于这里为什么用了一个createRender的函数,其实渲染器和渲染的概念是不同的,渲染器是更加宽泛的概念,其包含了渲染。渲染器不仅可以用来渲染,还可以用来激活已有的DOM元素,这个过程通常发生在同构渲染的情况下,如下面代码所示:

function createRender(){
	function render(vnode, container){
		// ...
	}
	function hydrate(vnode, container){
		// ...
	}
	
	return {
		render,
		hydrate
	}
}

可见渲染器的内容非常宽泛,render函数只是其中一部分。

有了渲染器就可以用来执行渲染任务了,如下面代码:

const renderer = createRenderer()
// 首次渲染
renderer.render(vnode,document.querySelector('#app'))

上面代码中,首先用createRenderer函数创建一个渲染器,然后用渲染器renderer.render函数执行渲染。首次调用renderer.render函数时,只需要创建新的DOM元素即可,这个过程只涉及挂载

多次在同一个container上调用renderer.render函数进行渲染时,除了要挂载动作外,还要执行更新动作,例如:

const renderer = createRenderer()
// 首次渲染
renderer.render(oldVnode,document.querySelector('#app'))
// 第二次渲染
renderer.render(newVnode,document.querySelector('#app'))

由于首次渲染时已经把oldVnode渲染到container内了,所以再次调用renderer.render函数并尝试渲染newVnode的时候,就不能简单的执行挂载动作了。在这种情况下,就要渲染器就要将newVnode和上一次渲染的oldVnode进行比较,试图找到更新变更点。这个过程叫“打补丁”(或更新),英文用patch来表示。实际上,挂载动作本身也可以看成是一种特殊的打补丁,其特殊之处在于旧的vnode是不存在的。代码示例如下:

function createRenderer(){
	function render(vnode,container){
		if(vnode){
			// 新vnode存在,将其与旧vnode一起传递给patch函数,进行打补丁
			patch(container._vnode,vnode,container)
		}else{
			if(container._vnode){
				// 旧vnode存在,且新vnode不存在,说明是卸载(unmount)操作
				// 只需要将container内的DOM清空即可
				container.innerHTML = ''
			}
		}
		// 把vnode存储到container._vnode下,即后续渲染中的旧 vnode
		container._vnode = vnode
	}
	return {
		render
	}
}

上面的代码给出了render函数的基本实现

这样要注意patch的实现。实际上,patch函数时整个渲染器的核心入口,承载了最重要的渲染逻辑。patch函数至少接收三个参数

function patch(n1,n2,container){
	//...
}

参数n1表示 旧vnode
参数n2表示 新vnode
参数container表示 容器

首次渲染时,容器元素的container._vnode是不存在的,即underfined。这时patch函数会执行挂载操作,会忽略n1,直接将n2所描绘的内容渲染到容器中。这说明patch不仅可以用来打补丁,还可以用来挂载。

知识扩展
有关IIFE模块格式,详情可以参照下面的链接:
https://segmentfault.com/a/1190000040720081?sort=votes

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值