渲染器与相应系统的结合
渲染器就是用来执行渲染任务的,渲染器不仅能够渲染真是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