为了对Vue有进一步的了解,实现一个简洁版的Mini-Vue框架,主要包括三个模块:
- 渲染系统模块
- 可响应式系统模块
- 应用程序入口模块
本文章主要是书写渲染系统模块。
该模块主要包括三个功能:
h
函数,用于返回一个VNode对象mount
函数,用于将Vnode挂载到DOM上patch
函数,用于对两个VNode进行对比,决定如何处理新的VNode
1. 创建index.html文件,在文件下通过h函数来创建一个vnode
const vnode = h('div',{class:"why",id:"aaa"},[
h("h2",null,"当前计数:100"),
h("button", {onClick: function() {}}, "+1")
])
2. 创建render.js,在html文件中导入,书写h函数
<script src="./renderer.js"></script>
- 注意到h函数具有三个参数,分别是元素标签名、元素属性、孩子节点数,并且返回的是一个对象
const h (tag,props,children) =>{
// vnode -> javascript对象 -> {}
return {
tag,
props,
children
}
}
3. 通过mount
函数, 将vnode
挂载到div#app
上
- 在index.html文件中
mount(vnode,document.querySelector("#app")
- 书写mount函数
- 注意到函数存在两个参数,分别是需要挂载的vnode的节点,和挂载的元素
- 书写
mount函数
流程图
const mount = (vnode,container)=>{
// vnode -> element
// 1.创建出真实的原生, 并且在vnode上保留el
let el = vnode.el = document.createElement(vnode.tag)
// 2.处理props
if(vnode.props){
for(const key in props){
const value = vnode.props[key] //key :class value : why
//注意props中可能存在事件
if(key.startWith("on")){
el.addEventListener(key.slice(2).toLowerCase(),value)
}else{
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);
}
- 此时可以发现,页面渲染成功
4. 创建新的vnode,通过patch函数进行对比更新
- 在index.html文件中创建新的vnode
const vnode1 = h('div', {class: "reason", id: "aaa"}, [
h("h2", null, "呵呵呵"),
h("button", {onClick: function() {}}, "-1")
]);
patch(vnode, vnode1);
- patch函数的书写要考虑三种情况
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
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.startsWith("on")) { // 对事件监听的判断
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// 3.处理children
const oldChildren = n1.children || [];
const newChidlren = n2.children || [];
if (typeof newChidlren === "string") { // 情况一: newChildren本身是一个string
// 边界情况 (edge case)
if (typeof oldChildren === "string") {
if (newChidlren !== oldChildren) {
el.textContent = newChidlren
}
} else {
el.innerHTML = newChidlren;
}
} else { // 情况二: newChildren本身是一个数组
if (typeof oldChildren === "string") {
el.innerHTML = "";
newChidlren.forEach(item => {
mount(item, el);
})
} else {
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
// 1.前面有相同节点的原生进行patch操作
const commonLength = Math.min(oldChildren.length, newChidlren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChidlren[i]);
}
// 2.newChildren.length > oldChildren.length
if (newChidlren.length > oldChildren.length) {
newChidlren.slice(oldChildren.length).forEach(item => {
mount(item, el);
})
}
// 3.newChildren.length < oldChildren.length
if (newChidlren.length < oldChildren.length) {
oldChildren.slice(newChidlren.length).forEach(item => {
el.removeChild(item.el);
})
}
}
}
}
}