vue渲染原理简单实现
实现功能:
1.渲染系统:
· 功能一:h函数,用于创建并返回一个VNode(虚拟对象);
· 功能二:mount函数,用于将VNode挂载到节点上;
· 功能三: patch函数,用于对比两个VNode,决定该如何处理新的VNode;
1.新建一个index.html的页面
其中有一个id为app
的div元素,之后我们写的所有DOM都会挂载到此元素下;然后引用了一个名为renderer.js
的js文件,用来实现虚拟DOM转化和真实挂载。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
//h函数和mount函数存储在renderer.js中
// 1.通过h函数来创建一个vnode
const vnode = h('div', { class: 'why' }, [
h('h2', null, "当前计数:100"),
h('button', {onClick:()=>{console.log("+1")}}, "+1"),
]);
// 通过mount函数,将vnode挂载到div#app上
mount(vnode,document.querySelector("#app"));
</script>
</body>
</html>
2.编写renderer.js文件
- 其中
h()
函数用于产生虚拟DOM,他将传入的标记
、属性
和子元素
集成在一个对象之中,然后返回,产生如图的一个javaScript对象
。
2.mount()
函数的作用是将vnode
(虚拟节点)转化成真实DOM然后挂载到对应的容器。
//转化成虚拟DOM
const h = (tag, props, children) => {
return {
tag,
props,
children,
};
};
//递归挂载
const mount = (vnode, container) => {
// vnode ->element
//1.创建出真实DOM,并在vnode上保留el
const el = (vnode.el = document.createElement(vnode.tag));
//2.处理props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
//对事件监听的判断
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), value);
}
el.setAttribute(key, value);
}
}
//3. 处理children
if (vnode.children) {
//如果children的内容直接是字符串
if(typeof vnode.children === 'string'){
el.textContent = vnode.children;
}else{
//如果children的内容是数组,即此元素还拥有子元素,递归调用mount
vnode.children.forEach(item => {
mount(item,el);
});
}
}
//4.将el挂载到container中
container.appendChild(el);
};
通过调用这两个函数,我们即实现了将虚拟DOM转化成真实DOM,并显示到浏览器界面中的功能.
3.模拟页面元素更新时的流程
当页面中的元素更新的时候,通过调用patch()
函数来对比前一次和后一次的VNode,添加不同的边界判断
来区分各种情况,代码如下:
const patch = (n1, n2) => {
//判断两个元素的类型
if (n1.tag !== n2.tag) {
//如果节点的类型不相同则没有比较的必要,那么可以直接移除n1,挂载n2
const n1ElParent = n1.el.parentElement;
n1ElParent.removeChild(n1.el);
mount(n2, n1ElParent);
} else {
// 1. 取出element对象,并且在n2中进行保存
const el = (n2.el = n1.el);
//2.处理props
const oldProps = n1.props || {};
const newProps = n2.props || {};
//2.1获取所有newProps添加到el
for (const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (newValue !== oldValue) {
if (key.startsWith("on")) {
//对事件监听的判断
el.addEventListener(key.slice(2).toLowerCase(), newValue);
} else {
el.setAttribute(key, newValue);
}
}
}
//2.2删除旧的props
for (const key in oldProps) {
//如果旧属性名称没有在新属性名称中出现,则删除旧的属性
if (!(key in newProps)) {
if (key.startsWith("on")) {
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(), value);
} else {
el.removeAttribute(key);
}
}
}
//3.处理children
const oldChildren = n1.children || [];
const newChildren = n2.children || [];
if (typeof newChildren === "string") {
//边界判断
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
el.textContent = newChildren;
}
} else {
el.innerHTML = newChildren;
}
} else {
//newChildren是一个数组
if (typeof oldChildren === "string") {
//oldChildren是个字符串
el.innerHTML = "";
newChildren.forEach((item) => {
mount(item, el);
});
} else {
// oldChildren和newChildren都不是string
//以最短方式再进行比对替换操作 (长度相等的情况下)
const commonLength = Math.min(oldChildren.length, newChildren.length);
//因为递归调用,倒置oldChildren
oldChildren.reverse();
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
//newChildren.length > oldChildren.length
//把newCildren中比oldChildren多出来的部分直接挂载到el上
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach((item) => {
mount(item, el);
});
}
//newChildren.length < oldChildren.length
//新节点数组长度小于旧节点数组长度,需要移除旧节点数组中的中的部分节点
if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach((item) => {
el.removeChild(item.el);
});
}
}
}
}
};
index.html
中做出对应更改,来模拟DOM的更新:
setTimeout(() => {
// 3.创建新的VNode
const vnode1 = h('div', { class: 'coderWhy', id: "aaaa" }, [
h('h3', { class: 'myTitle' }, "这是我的标题"),
h('button', { onClick: console.log("新的打印函数") }, "打印"),
h('button', { onClick: console.log("新的打印函数") }, "打印2"),
h('button', { onClick: console.log("新的打印函数") }, "打印3"),
]);
patch(vnode, vnode1);
}, 2000);