前言
响应式系统和渲染器
前面我们讨论了Vue3
响应式系统的原理,那么响应式系统跟渲染器是如何搭配使用的呢?下面先用一个见简单的例子帮我们感受一下:
// 响应式系统与渲染器的关系
import { effect, ref } from 'vue'
const count = ref(0)
const renderer = () => {
document.body.innerHTML = count.value
}
effect(() => {
renderer()
})
Promise.resolve().then(() => {
count.value = 1111
})
上面代码我们定义了一个极其简单的渲染器,在副作用函数中执行渲染器,并且读取了响应式数据count
,当count
发生改变后,renderer
重新执行,页面刷新。这便是响应式系统和渲染器搭配使用的一个非常简单的例子,在开始了解渲染器之前,我们先来了解几个概念。
概念
渲染器(renderer)
renderer
和render
是不同的,render
是动词,表示渲染,render
函数执行后会返回vnode
,renderer
则将vnode
渲染成特定平台的节点
vnode
vnode
,即虚拟节点,本质是一个Javascript
对象,用来描述真实节点,使用虚拟节点的一个好处是脱离平台,有利于跨平台的开发
挂载
渲染器将vnode
渲染成真实节点的过程
容器
渲染的节点一个放在哪里,需要指定一个节点作为容器。
正文
言归正传,一个渲染器的基本实现大概长这样,后面我们会在此基础上进行改造
// 渲染器的简易实现
const createRenderer = () => {
// 客户端渲染
const render = (vnode, container) => {
if (vnode) {
patch(container._vnode, vnode, container) // 渲染的入口
} else {
if (container._vnode) { // 旧节点存在而新节点不存在
// ... 卸载操作
}
}
container._vnode = vnode
}
// 服务端渲染
const hydrate = () => {
// ....
}
// 创建元素
const mountElement = (vnode, container) => {
const dom = document.createElement(vnode.type)
// 暂时考虑子元素节点为文本的情况
if (typeof vnode.children === 'string') {
dom.textContent = vnode.children
}
container.append(dom)
}
const patch = (n1, n2, container) => {
// 旧节点不存在,说明是创建,即挂载
if (!n1) {
mountElement(n2, container)
} else {
// 打补丁
}
}
return {
render,
hydrate
}
}
这里使用createRenderer
的原因是渲染器是更加宽泛的概念,它包含渲染。渲染器不仅可以用来渲染,还可以用来激活已有的 DOM 元素,返回的是render
和hyrate
, 这里先关注前者,在此条件上我们来分析以上代码:
render
的签名是vnode
和container
,提供要渲染的vnode
和指定容器- 判断
vnode
是否存在,存在则执行patch
方法,该方法实际上就是渲染的入口 patch
的签名是新旧节点,旧节点不存在,新节点存在,说明是创建/挂载操作,container._vnode = vnode
保存本次vnode
,作为下一次更新的旧节点;如果新旧节点同时存在,说明是更新操作。那么进行“打补丁”操作,涉及diff
算法,后面会详细讨论;如果新节点不存在,旧节点存在而新节点不存在,则执行的是卸载操作。- 根据
vnode
的类型执行不同的操作,这里只先提供了创建普通标签的方法
测试:
const vnode = {
type: 'div',
children: '这是一个文本节点'
}
const renderer = createRenderer()
renderer.render(vnode, document.querySelector('#app'))
自定义渲染函数
上面的createRenderer
创建的renderer
只对浏览器有效,因为里面包含了大量与浏览器相关的API
,如果我们希望它是跨平台的,那么需要将特殊的API
抽离出来,将它设置为可配置的,即操作的方法都是从外面传进去的,那么有
const createRenderer = (options) => {
const {
createElement,
setElementText,
insert
} = options
// 客户端渲染
const render = (vnode, container) => {
if (vnode) {
patch(container._vnode, vnode, container) // 渲染的入口
} else {
if (container._vnode) { // 旧节点存在而新节点不存在
// ... 卸载操作
}
}
container._vnode = vnode
}
// 服务端渲染
const hydrate = () => {
// ....
}
// 创建元素
const mountElement = (vnode, container) => {
const dom = createElement(vnode)
// 暂时考虑子元素节点为文本的情况
if (typeof vnode.children === 'string') {
setElementText(dom, vnode.children)
}
insert(container, dom)
}
const patch = (n1, n2, container) => {
// 旧节点不存在,说明是创建,即挂载
if (!n1) {
mountElement(n2, container)
} else {
// 打补丁
}
}
return {
render,
hydrate
}
}
这样,就可以实现跨平台了,上面的代码可以在浏览器中运行,也可以在nodejs
环境中运行,然后我们测试如下:
const vnode = {
type: 'div',
children: '这是一个文本节点'
}
const renderer = createRenderer({
createElement(vnode) {
console.log(vnode)
// return document.createElement(vnode.type)
},
setElementText(dom, text) {
console.log('-----setElement----')
// dom.textContent = text
},
insert(container, dom) {
console.log('---insert---')
// container.append(dom)
}
})
renderer.render(vnode, document.querySelector('#app'))
如此,一个自定义渲染器就实现啦。本文就先到这里啦~