文章参考了霍春阳的《Vue.js设计与实现》,是自己在阅读过程中的一些思考和理解
文章中所用的ref函数,为介绍响应式文章中所写的响应式代码,并非官方所提供的库。推荐想了解响应式原理读者可以去看看 : )
渲染器的设计
目前程度的渲染器暂定在DOM平台。因此简单渲染函数如下:
function render(domString, container) {
container.innerHTML = domString
}
// 调用函数,将其放在id为app的元素中
render('<h1>芜湖</h1>', document.getElementById('app'))
不仅仅是简单的文本,也可以用模版字符串传变量数据,进而可以传入响应式数据。代码如下:
<body>
<div id="app"></div>
</body>
<script>
// 这里调用之前写的vue函数文件
const count = ref(1)
function render(domString, container) {
container.innerHTML = domString
}
// effect函数中调用render,传入响应式数据
effect(() => {
render(`<h1>${count.value}</h1>`, document.getElementById('app'))
})
</script>
效果如下:
渲染器的基本概念
这里用到了renderer(渲染器)和render(渲染)两个名词。vnode为虚拟节点,将虚拟节点渲染为真实DOM的过程称为挂载(mount)。而真实的DOM元素会经过渲染器将其放在container(容器)里面。代码如下:
// 构造渲染器
function createRenderer() {
// 渲染函数
function render(vnode, container) {
}
// 服务端渲染
function hydrate(vnode, container) {
}
return {
render,
hydrate
}
}
// 获取渲染器
const renderer = createRenderer()
// 在app容器里面执行渲染操作
renderer.render(vnode, document.getElementById('app'))
如果同时在同一个container上多次执行渲染操作,除了挂载操作外,还涉及到更新操作。代码如下:
function createRender() {
function render(vnode, container) {
// 如果传入了新的节点,就在patch函数中去进行更新
if(vnode) {
patch(container._vnode, vnode, container)
} else {
// 如果传入的为空,且旧的node存在,则将其置空
if(container._vnode) {
container.innerHTML = ''
}
}
// 将容器的node置为新传入的node
container._vnode = vnode
}
return {
render
}
}
自定义渲染器
自定义的渲染器是为了达成跨平台,能够通用的渲染器。定义如下vnode:
const vnode = {
type: 'h1',
children: '芜湖起飞'
}
vnode要传入渲染函数,渲染函数中会调用patch函数,这里给出patch的功能:
function patch(container._vnode, vnode, container) {
// 如果不存在旧节点,则说明是挂载
if(!container._vnode) {
// 将节点对象和要挂载的容器传入挂载函数
mountElement(vnode, container)
} else {
// 如果存在旧节点,则说明是更新
}
}
这里给出mountElement函数的简单实现:
// 挂载函数
function mountElement(vnode, container) {
// 建立传入节点的类型
const el = document.createElement(vnode.type)
// 如果children是字符串,则说明是文本类
if(typeof vnode.children === 'string') {
// 直接将其放入标签中
el.textContent = vnode.children
}
// 容器中添加节点
container.appendChild(el)
}
观察这几个函数,发现使用的都是document的操作,这样就限制了使用的平台仅限于浏览器了,这样肯定是不行的,为了兼容各平台的开发,节点的创建需要抽离出来,并作为配置项传递给createRenderer函数,这样就能在各平台使用各自的节点创建API来实现开发。代码如下:
const renderer = createRenderer({
// 创建渲染器时,传入节点创建API
createElement(el) {
return document.createElement(el)
},
// 节点构建API
insert(el, parent, anchor=null) {
parent.insertBefore(el, anchor)
},
// 内容设置API
setElementText(el, text) {
el.textContent = text
}
})
// 挂载函数
function mountElement(vnode, container) {
// 建立传入节点的类型
const el = createElement(vnode.type)
// 如果children是字符串,则说明是文本类
if(typeof vnode.children === 'string') {
// 直接将其放入标签中
setElementText(el, vnode.children)
}
// 容器中添加节点
insert(el, container)
}
可以看到,挂载函数不再依赖原始的浏览器API,而是完全使用传入的API,因此只需要配置好当前平台的节点创建API,就可以实现不同平台的开发。