入口文件
// 1. 先实现虚拟dom, 主要就是一个对象, 来表述dom节点, jsx
// createElement, h
import { h, render } from './vdom/index';
const vnode = h(
"div",
{
key: "keyCode",
id: "container",
},
h("span", { style: { color: "red" } }, "hello"),
"dom diff"
);
// 渲染 render: 将虚拟节点转换成为真实的dom节点, 最后插入到app元素中
render(vnode, document.getElementById('app'))
// 来一个新的节点 替换老的节点
const newVnode = h('h2', {}, 'hello world');
/**
* <div id="container"><span style="color:red">hello</span>dom diff</div>
h(
'div',
{
key: 'keyCode',
id: 'container'
},
h(
'span',
{ style: { color: 'red' } },
'hello'
),
'dom diff'
)
生成的虚拟节点对象
{
type: 'div',
props: { id: 'container' },
children: [
{
type: 'span',
props: {
style: {
color: red
}
},
children: [],
text: 'hello'
},
{
type: '',
props: {},
children: [],
text: 'dom diff'
}
]
}
*/
h方法的实现 (创建虚拟节点)
import { vnode } from './vnode';
/**
* 创建元素节点
* @param {String|Object} type 标签类型
* @param {Object} props 标签对应属性
* @param {...any} children 子节点数组
*/
export default function createElement(type, props, ...children) {
let key = null;
if (props.key) {
key = props.key;
delete props.key;
}
// 将不是虚拟节点的子节点变成虚拟节点
children = children.map((child) => {
if (typeof child !== 'string') {
return child;
}
return vnode(null, null, null, null, child);
});
return vnode(type, key, props, children);
}
创建虚拟节点 vnode.js
/**
* 创建虚拟节点
* @param {String|Object} type 标签类型
* @param {any} key 节点key
* @param {Object} props 组件属性
* @param {Array} children 组件子节点
* @param {String} text 组件文本值
* @returns
*/
function vnode(type, key, props, children, text) {
return {
type,
props,
key,
children,
text,
};
}
export {
vnode
}
patch (render) 将虚拟节点转化成为真实节点
/**
* 将虚拟节点渲染到真实的dom节点上
* @param {Object} vnode 虚拟节点
* @param {HTMLElement} container 真实dom节点
*/
function render(vnode, container) {
// 1. 将虚拟节点转化成为真实的节点
const realElement = createDomElementFromVnode(vnode);
container.appendChild(realElement);
}
/**
* 根据虚拟节点生成真实的html节点
* @param {Object} vnode 虚拟节点
*/
function createDomElementFromVnode(vnode) {
const {
type,
props,
children,
key,
text
} = vnode;
// 表示是一个标签节点
if (type) { // 标签节点
// 建立虚拟节点和真实节点的关系, 后面可以用来更新真实dom
vnode.domElement = document.createElement(type);
// 根据当前虚拟节点的属性, 去更新真实的dom元素
updateProperties(vnode);
// 针对子节点的渲染
children.forEach((childVnode) => {
// 将childVnode变成真实节点, 插入到当前节点中
render(childVnode, vnode.domElement); // 递归渲染子节点
});
} else { // 文本节点
vnode.domElement = document.createTextNode(text);
}
return vnode.domElement;
}
/**
* 更新真实dom的属性
* @param {Object} newVnode 新的虚拟节点
* @param {Object} oldProps 之前的属性
*/
function updateProperties(newVnode, oldProps = {}) {
// 真实的dom节点
const domElement = newVnode.domElement;
// 当前虚拟节点的属性
const newProps = newVnode.props;
/*
div[a=1 b=2 c=3]
属性的增加和缺失情况: div[a=1 d=4]
*/
// 1. 如果老节点有属性, 新节点是没有的, 那么表示本次更新, 需要删除该属性
for (let oldPropName in oldProps) {
if (!newProps[oldPropName]) {
delete domElement[oldPropName]
}
}
// 如果新的里面有style, 老的里面也有style, 但是老的style里面有background, 但是新的里面没有
const newStyleObj = newProps.style || {};
const oldStyleObj = oldProps.style || {};
for (let propName in oldStyleObj) {
if (!newStyleObj[propName]) {
// 老节点更新成为新节点之后, 某个样式的属性没有了, 将其值为空即可
domElement.style[propName] = '';
}
}
// 2. 如果老节点里面, 新的里面有, 表示新节点的属性直接覆盖老节点的属性
for (let newPropName in newProps) {
// 多余考虑 @click EventListener
if (newPropName === 'style') {
const styleObj = newProps.style;
for (const s in styleObj) {
domElement.style[s] = styleObj[s];
// domElement.style.setProperty(s, styleObj[s]);
}
} else {
domElement[newPropName] = newProps[newPropName];
}
}
}
export {
render,
}
网友评论