Vue 渲染系统主要分成3个模块
h函数,用于返回一个VNode对象mount函数,用于将VNode挂载到DOM上patch函数,用于对两个VNode进行对比,决定如何处理新的VNode
h函数实现
/**
* 创建h函数,返回vnode
* @param {*} type
* @param {*} props
* @param {*} children
* @returns
*/
function h(type, props, children) {
return {
type,
props,
children
}
}
Mount函数(挂载VNode)
/**
* 给el添加属性
* @param {*} el
* @param {*} key
* @param {*} value
*/
function addAttr(el, key, value) {
// 如果属性值是string类型,如class等属性,直接添加
if (typeof value === "string") {
el.setAttribute(key, value)
}
// 如果是style样式对象,则将属性对象转换成string,并以;号分割
if (key === "style" && Object.prototype.toString.call(value) === "[object Object]") {
el.setAttribute(key, JSON.stringify(value).replace(/^{|"|}$/g, "").replace(/,/g, ";"))
}
// 如果是事件函数,以on开头的,如onClick等,则是绑定事件
if (key.startsWith("on") && Object.prototype.toString.call(value) === "[object Function]") {
const event = key.slice(2).toLowerCase()
el.addEventListener(event, value)
}
}
/**
* 创建mount函数,添加到指定元素
* @param {*} vnode
* @param {*} container
*/
function mount(vnode, container) {
const { type, props, children } = vnode
// 处理type元素类型,创建元素,并在vnode上保留el
const el = vnode.el = document.createElement(type)
// 处理props属性,给元素添加属性
if (props) {
for (const key in props) {
addAttr(el, key, props[key])
}
}
// 处理children子节点(这里的子节点一般是字符串或者数组,这里不考虑对象的写法)
if(children) {
// 如果是string,直接添加内容即可
if (typeof children === "string") {
el.textContent = children
} else {
children.forEach(child => {
mount(child, el)
})
}
}
// 添加元素
container.appendChild(el)
}
patch函数(对比两个VNode)
/**
* 创建patch函数,进行新旧vnode对比
* @param {*} oldVnode
* @param {*} newVnode
*/
function patch(oldVnode, newVnode) {
// 新节点和旧节点一样
if (oldVnode.type === newVnode.type) {
const el = newVnode.el = oldVnode.el
// 把新节点上在旧节点上不同的属性添加进去
const newProps = newVnode.props || {}
const oldProps = oldVnode.props || {}
for(const key in newProps) {
if (newProps[key] !== oldProps[key]) {
addAttr(el, key, newProps[key])
}
}
// 把旧节点在新节点中不存在的属性移除
for(const key in oldProps) {
// 如果是事件函数,以on开头的,如onClick等,则是移除绑定事件
if (key.startsWith("on") && Object.prototype.toString.call(propValue) === "[object Function]") {
const propValue = oldProps[key]
const event = key.slice(2).toLowerCase()
el.removeEventListener(event, propValue)
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 处理children子节点
const oldChildren = oldVnode.children || []
const newChildren = newVnode.children || []
// 如果子节点是一个string
if (typeof newChildren === "string") {
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else { // 子节点是一个数组
if (typeof oldChildren === "string") {
el.innerHTML = ""
newChildren.forEach(item => {
mount(item, el)
})
} else {
// oldChildren [v1, v2, v3]
// newChildren [v1, v2, v3, v4 ,v5]
// 取出新旧children数组的最小长度进行对比
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
// 如果新children大于旧的children,则把新的多出来的那部分挂载到el
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
// 如果新children小于旧的children,则把旧的多出来的那部分el移除掉
if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
} else {
// 如果新的节点和旧的节点不一样,则直接用新的节点替换掉旧的节点
const oldVnodeParent = oldVnode.el.parentElement
oldVnodeParent.removeChild(oldVnode.el)
mount(newVnode, oldVnodeParent)
}
}
eg:将下面这段html代码用renderer来实现
<div>
<p style="font-size: 20px;font-weight: bold;margin: 0">这是一个列表</p>
<ul class="ul-class">
<li>
<span style="color: red;text-decoration: underline;cursor: pointer;" onclick="alert('vue')">vue(我绑定了点击事件)</span>
</li>
<li>react</li>
<li>javascript</li>
</ul>
</div>
1、先准备一个div用于挂载
<div id="app"></div>
2、调用h函数生成VNode
const vnode = h("div", null, [
h("p", {
style: {
"font-size": "20px",
"font-weight": "bold",
"margin": "0"
}
}, "这是一个列表"),
h("ul", {"class": "ul-class"}, [
h("li", {
style: {
"color": "red",
"text-decoration": "underline",
"cursor": "pointer"
},
onClick() {
alert('vue')
}
}, "vue(我绑定了点击事件)"),
h("li", null, "react"),
h("li", null, "javascript")
])
])
3、通过mount函数,将vnode挂载到div#app上
mount(vnode, document.querySelector("#app"))
至此,已经完成了虚拟dom的渲染
4、调用patch函数,进行新旧节点对比
创建一个新的VNode并更新
const vnode2 = h("div", { "class": "my-div" }, [
h("p", {
style: {
"font-size": "20px",
"font-weight": "bold",
}
}, "你是什么"),
h("p", null, "再来一次"),
])
setTimeout(() => {
patch(vnode, vnode2)
}, 2000);
2秒之后页面发生变化,更新成功
结尾
如果是通过template编写html代码,最终也是被编译成VNode,返回一个render函数;
如果是通过jsx编写,如下
render() {
return (
<div>你好呀</div>
)
}
jsx代码块最终也会被编译成VNode,然后返回给render函数
render函数接收一个返回值,而这个返回值其实是VNode