前言
在这篇文章中梳理了关于虚拟dom的一些知识点深入剖析虚拟DOM原理,这篇文章主要理解一下vue中渲染器,,这也算是一个大的模块了,跟响应式系统属于一个级别。像挂载、更新、diff算法等这些操作都是在这个阶段进行的,下面将会对这些知识点一一进行展开。
渲染器的概念
渲染器可以用renderer来表示,但是要跟render进行区分,它们两个虽然都是函数但是都有各自的含义,前者用来表示渲染器,后者表示渲染,可以理解为一个名词一个是动词。渲染器包括render函数,是因为渲染器不仅能够把虚拟DOM渲染为真实DOM元素,而且还具有跨平台的能力,也就是说不止在浏览器中渲染。
function createRenderer(){
function render(vnode,container){
}
// ...服务端渲染时
function hydrate(vnode,container){
}
return {
render,
hydrate
}
}
根据上面的代码可以很清晰的看到render函数只是其中的一部分,当需要使用render函数进行渲染时,只需要调用createRenderer
函数得到一个render函数即可。
const renderer = createRenderer();
renderer.render(vnode,document.querySelector("#app"));
渲染器的应用
根据上述例子我们了解到通过使用createRenderer
函数创建出一个渲染器,然后使用渲染器中的render
函数进行渲染,当首次调用render
函数时会执行挂载操作,也就是创建新的DOM元素,但是当第二次渲染时就不应该执行挂载操作了,而是应该执行更新操作。
const renderer = createRenderer();
// 第一次渲染
renderer.render(vnode,document.querySelector("#app"));
// 第二次渲染
renderer.render(nnode,document.querySelector("#app"));
在这种情况下,第二次渲染就应该执行更新操作,使用新的vnode和旧的vnode进行比较找出变更点变进行变更。这个过程就叫做“打补丁”,挂载本身其实也可以看做是一种打补丁,,它的特殊之处就在于旧的vnode是不存在的,所以根据这一逻辑就很容易写出下面代码:
function createRender(){
function render(vnode,container){
if(vnode){ // 挂载或更新阶段
patch(container._vnode,vnode,container)
}else{
// 旧的vnode存在,新的vnode不存在 卸载阶段
if(container._vnode){
container.innerHTML = "";
}
}
container._vnode = vnode;
}
return {
render
}
}
const renderer = createRenderer()
// 首次渲染 挂载阶段
renderer(vnode1,document.getElementById("#app"))
// 第二次渲染 更新阶段
renderer(vnode2,document.getElementById("#app"))
// 卸载阶段
renderer(null,document.getElementById("#app"))
在render
函数中首先判断了是否有vnode来区分挂载和卸载操作,若没有vnode则是需要进行卸载,若有vnode则需要统一走patch
方法并把新的vnode和旧的vnode传递过去进行挂载和更新阶段的区分(这里再次理解依稀挂载也是一种打补丁)。
function patch(oldVnode,newVnode){
if(!oldVnode){
moutElement(oldVnode,container);
}else{
// 打补丁...
}
}
function moutElement(vnode,container){
const el = document.createElement(vnode.type);
if(typeof vnode.children=="string"){
el.textContent = vnode.children;
}else if(Array.isArray(vnode.children)){
vnode.children.forEach(child=>{
pathch(null,child,el)
})
}
container.appendChild(el);
}
在patch
方法中通过判断是否有oldVnode来区分挂载和更新阶段,在首次渲染时因为没有oldVnode所以会直接走moutElement
挂载方法,在挂载的方法中通过操作原生dom来进行页面的显示,如果子节点是一个数组则循环遍历并挂载数组中的虚拟节点,打补丁我们放到后面细聊。
Case
const vnode = {
type: 'p',
props: {
onClick: () => alert("You take me to hell !")
},
children: [
{
type: "p",
props: {},
children: "From heaven I fell"
},
{
type: "div",
props: {},
children: "Hell"
},
]
}
function createRenderer() {
function render(vnode, container) {
const el = document.createElement(vnode.type);
if (vnode.props) {
for (let key in vnode.props) {
if (/^on/.test(key)) {
el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key]) // onClick ==> click
}
}
if (typeof vnode.children === "string") {
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
render(child, el);
})
}
container.appendChild(el);
}
}
return {
render,
}
}
const renderer = createRenderer();
renderer.render(vnode, document.getElementById("app"));
以上就是渲染器的实现思路了,是不是也没有想象中的那么难,Case中只考虑了挂载阶段的代码所以没有像上面那样写的很详细。
- 首先根据虚拟节点的type创建容器元素;
- 遍历虚拟节点的props匹配以on开头的属性,将其转换为原生事件即可;
- 判断传递的虚拟节点的子节点的类型,如果是字符串则创建一个文本节点并添加到容器元素中,如果是数组则遍历进行递归,把当前的父元素传递到render函数中表示把创建的元素添加到当前的父元素容器中;
总结
这篇文章主要渲染器的相关概念,代码主要区分了一下挂载、更新和销毁阶段,现在做的还仅仅是挂载节点,归根结底都是用一些熟悉的DOM的API来完成渲染操作,渲染器的精髓在于更新节点的阶段,这个就留到后面再说了。