- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
<script src="./renderer.js"></script>
<script>
// 1. 通过h函数来创建一个vnode
const vnode = h('div', {
class: "vnode-01",
id:'aaa'
}, [
h('h2', null, "当前计数:100"),
h('button', {
onClick: handleClick,
}, '+1')
]); // vdom
// 2. 通过mount函数,将vnode挂载到 dev#app上
mount(vnode, document.querySelector('#app'))
// 3. 创建新的vnode
setTimeout(()=>{
let newVnode = h('div', {class: 'vnode-02', id:'aaa'}, [
h('h2', null, "呵呵呵🍺"),
h('button', {
onClick: handleClick,
}, '+9999')
])
patch(vnode, newVnode)
},5000)
function handleClick(e){
console.log(e,"h函数事件")
}
</script>
- renderer.js
const h = (tag, props, children) => {
// vnode -> javascript对象 -> {}
return {
tag,
props,
children
}
}
const mount = (vnode, container) => {
// vnode -> element
// 1.创建出真实的原生,并在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")) {
// 判断是否以 on 开头,如果是事件需要把 on 截掉,同时转换成小写字母
// 事件监听方法里 事件名称都是小写没有 on
// 第一个参数是事件的类型 (如 "click" 或 "mousedown").
// 第二个参数是事件触发后调用的函数。
// 第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
// 如果不是on开头就是元素的属性
el.setAttribute(key, value)
}
}
}
// 3. 处理children
if (vnode.children) {
if (typeof vnode.children === "string") {
// 如果是字符串 就直接插入到元素内
el.textContent = vnode.children;
} else {
vnode.children.forEach(item => {
mount(item, el);
})
}
}
// 4. 将el挂载到container上
container.appendChild(el)
}
// 节点更新 diff算法 n1旧的节点,n2新的节点
const patch = (n1, n2) => {
// 判断更新的节点元素类型是否一样,不一样就直接删除旧的,替换新的node节点
if (n1.tag !== n2.tag) {
// 先拿到旧节点的父元素
const n1ElParent = n1.el.parentElement;
// 通过父元素删除旧的元素
n1ElParent.removeChild(n1.el);
// 在旧的父元素上重新创建新的元素
mount(n2, n1ElParent);
} else {
// 1.如果新的节点和旧节点的元素一样,就统一保留旧的节点元素
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')){
// 在新元素里移除旧元素的事件 移除事件需要加value
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(),value);
}else{
// 在新元素里移除旧元素的属性 移除属性 不需要加value
el.removeAttribute(key);
}
}
}
// 3. 处理children
const oldChildren = n1.children || [];
const newChildren = n2.children || [];
// 情况一:newChildren本身是一个string 如果新的节点是一个字符串就直接替换旧的节点
if(typeof newChildren === "string"){
// 边界判断 edge case
if(typeof oldChildren === "string"){
if(newChildren !== oldChildren){
el.textContent = newChildren;
}
}else{
el.innerHTML = newChildren;
}
}else{
// 情况二:newChildren本身是一个数组
if(typeof oldChildren === "string"){
// 如果旧的节点是一个字符串,先清空旧的字符串
el.innerHtml = "";
newChildren.forEach(item => {
mount(item, el);
})
}else{
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
// 1. 前面有相同节点的元素进行patch操作
// 对比新旧节点,拿到更短的长度
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++){
// 相等的节点会在这里处理
patch(oldChildren[i], newChildren[i]);
}
// 2. 如果旧的节点长做移除操作,新的节点长就做添加操作
// newChildren.length > oldChildren.length
if(newChildren.length > oldChildren.length){
newChildren.splice(oldChildren.length).forEach(item=>{
mount(item,el);
})
}
// 3. newChildren.length < oldChildren.length
if (newChildren.length < oldChildren.length){
oldChildren.splice(newChildren.length).forEach(item=>{
el.removeChild(item.el);
})
}
}
}
}
}