事实上,Vue源码包含三大核心:
在下载后的Vue源码的package文件中,可看到这些模块:

大概流程就是通过开发者编写的template模板,经过编译系统后生成VNode,然后再通过渲染系统来生成真实DOM,最后,通过响应式系统对数据进行监听,当数据发生改变时,页面会通过diff算法对比VNode的变化,然后以最大粒度复用代码,保证性能相对来说消耗最小。
该模块主要包含三个功能:
标题已经说得很清楚了,只是一个迷你版的Vue,所以断然不会和真正的Vue源码一样写那么多Edge Case(边际判断),主要是从本篇文章中学到这个流程,然后做到看源代码时不至于一头雾水。
上一篇文章我已经介绍过,VNode本质上就是一个JavaScript对象。你可能会好奇为什么要经过VNode这一层,直接渲染不是更加方便快捷吗?首先,VNode可以对真实的元素节点进行抽象,因此很多直接操作DOM存在的限制,比如diff比对,clone操作在转换为一个JavaScript对象后,就变得更加简单了。此外,鉴于目前跨端开发的火爆,编写一套代码可以在多终端的需求越来越高涨,你肯定不想你写的代码只能在浏览器上运行吧?毕竟对大多数人来说,个人手机比PC的优先级要高太多了。
话不多说,下面开始上代码。
h函数主要的作用就是生成VNode,所以它的实现比较简单。
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
也许你没有看过我上一篇文章,所以我在这里再解释一下,tag表示标签名;props表示标签上所加的各种属性,如css样式、方法绑定;最后的children则可能是文本,也可能是另一个对象。
熟悉Vue开发的肯定知道mount函数是干嘛的,要是连这个都不知道,那也没必要来看源码了。
mount函数的实现可以分为散步:
const mount = (vnode, container) => {
// 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')) {
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 // textContent是内置属性,给元素添加文本节点
} else {
vnode.children.forEach(item => {
mount(item, el)
})
}
}
// 4.将el挂载到container上
container.appendChild(el)
}
const patch = (n1, n2) => {
// 如果 n1 和 n2 是不同类型的节点
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement // 拿到conatiner
n1ElParent.removeChild(n1.el)
mount(n2, n1ElParent)
} else {
// 1.取出element对象,并且在n2对象中保存,这里要明白,n2中是不包含el节点的,具体看上述h函数的实现
const el = n2.el = n1.el
// 2.处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1 遍历新节点的所有props属性,进行比较
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) {
// 看到这里你可能会疑惑,为什么要把这个判断提上来,其实这里如果不提出来的话,就会导致两次添加事件监听,导致事件一直不会停止,如果你不相信的话,可以把这个代码放下去,然后写一个点击+1的小demo,那么你就会理解了
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);
})
}
}
}
}
}
至此,Mini-Vue从编译后的生成的VNode到挂载节点、生成DOM并进行新旧VNode对比的算法部分就告一段了。