@kvinc/mreact
Version:
martin's simple react
180 lines (156 loc) • 6.19 kB
JavaScript
/**
* @description {比对现有的虚拟Dom和现有的真实Dom, 好处是不需要保存上一次的虚拟Dom, 并且可以随时更新真实Dom}
* @param {vnode} 修改的虚拟DOM
* @param {HTMLELement} 上一次转化后的真实DOM
* @return {HTMLELement} 修改后的真实DOM
*/
import {diffComponent} from './ComponentDiff'
const noRenderOption = [false, null, undefined]
const isSameNodeType = function (vnode, dom) {
if (typeof vnode === 'string' || typeof vnode === 'number') {
return dom.nodeType === 3;
}
if (typeof vnode.tag === 'string') {
return dom.nodeName.toLowerCase() === vnode.tag.toLowerCase();
}
return dom && dom._component && dom._component.constructor === vnode.tag // 获取相同的组件
}
const diffTextNode = function (vnode, dom) { // 比对文本节点
if (noRenderOption.includes(vnode)) {
vnode = ''
}
if (dom && dom.nodeType === 3) { // 如果原来文本节点
if (dom.textContent !== vnode) {
dom.textContent = vnode
}
} else { // 如果原来不是文本节点
const text = document.createTextNode(vnode)
if (dom && dom.parentNode) {
dom.parentNode.replaceChild(text, dom) // 替换掉原来的DOM
}
return text
}
return dom
}
const diffElementNode = function (vnode, dom) { // 比对Element
if (typeof vnode.tag === 'function') {
return diffComponent(vnode, dom)
}
let out = dom
const childNodes = [...Array.from(dom.childNodes)] // 转化类数组为数组
const tagName = dom.tagName.toLowerCase()
if (vnode.tag === tagName) {
diffAttribute(vnode.attrs, dom)
} else if (!dom || vnode.tag.toLowerCase() !== tagName) {
const tag = document.createElement(vnode.tag)
const fragement = document.createDocumentFragment()
childNodes.map(fragement.appendChild) // 将原来所有的子节点移入文档碎片
tag.appendChild(fragement)
if (dom.parentNode) {
dom.parentNode.replaceChild(tag, dom)
}
out = tag
}
if (vnode.children) {
diffChildren(out, vnode.children)
}
return out
}
const diffAttribute = function (attrs = {}, dom) { // attrs是修改后的, Dom是修改前的
const attributes = Array.from(dom.attributes)
attributes.forEach(attr => {
let key = attr.name
if (key === 'class') key = 'className'
if (key === 'for') key = 'htmlFor'
if (!attrs[key]) { // 删除没有设置的属性
dom.setAttribute(attr.name, '')
}
})
Object.keys(attrs || {}).forEach(key => { // 现在有属性, 则对比之前的DOM
const value = attrs[key]
if (key === 'className') key = 'class'
if (key === 'htmlFor') key = 'for'
const domAttr = dom.getAttribute(key)
if (!/^on\w+/.test(key) && ((domAttr && domAttr !== value) || !domAttr)) { // 如果有该属性, 但是值却不一样, 或者没有该属性, 则设置该属性
dom.setAttribute(key, value)
}
})
}
const diffChildren = function (dom, vChildren) { // 比对子节点
const domChildren = dom.childNodes
let keys = {} // 区分有key和没有key的DOM
let nokeysArray = []
domChildren.forEach(domC => { // 区分key和非key的子节点
if (typeof domC === 'object' && domC.nodeType === 1 && domC.getAttribute('key')) {
keys[domC.getAttribute('key')] = domC
} else {
nokeysArray.push(domC)
}
})
if (vChildren && vChildren.length) {
let min = 0
let childrenLen = vChildren.length
for (let i = 0; i < childrenLen; i++) {
let child = null
let vChild = vChildren[i]
const key = (vChild.attrs || {}).key
if (keys[key]) { // 拿到key所对应的的虚拟DOM
child = keys[key]
keys[key] = null
} else if (min < childrenLen) {
for (let j = min; j < nokeysArray.length; j++) { // 没有key的情况下 优先寻找相同TagName的DOM类型
let c = nokeysArray[j]
if (c && isSameNodeType(vChild, c)) { // 因为是同级比较
child = c
nokeysArray[j] = null
if (j === childrenLen - 1) childrenLen--
if (j === min) min++
break
}
}
}
child = diff(vChild, child) // 对比找出的子节点和虚拟DOM
const originDOM = domChildren[i]
if (child && child !== dom && child !== originDOM) {
if (!originDOM) { // 如果没有originDOM, child却是存在的, 说明修改后的DOM长度是大于修改前的, 转化后的DOM child不等于dom, 也不等于相同位置的子节点, child是新增的
dom.appendChild(child)
} else if (child === originDOM.nextSibling) { // 如果转化后的Child 和 之前的相同位置的DOM的下一个节点相同, 说明之前的那个节点被删除了
dom.removeChild(originDOM)
} else { // 如果没有以上两种情况, child又不等于相同位置的DOM, 那么将节点插入相同位置的前面
dom.insertBefore(child, originDOM)
}
}
const newDomChildren = Array.from(dom.childNodes)
if (domChildren.length < newDomChildren.length) {
const newFilterDOM = newDomChildren.slice(0, newDomChildren.length - domChildren.length)
dom.parentNode.innerHTML = '';
[...newFilterDOM].map(el => dom.parentNode.appendChild(el))
}
}
}
}
const diffMap = new Map([ // 策略模式
[/(boolean|string|number)/, diffTextNode],
[/object/, diffElementNode]
])
const diff = function (vnode, dom) {
const type = typeof vnode
if (Array.isArray(vnode)) {
vnode.forEach(vn => {
const type = typeof vn
for (let diffType of diffMap) {
if (diffType[0].test(type)) {
diffType[1](vnode, dom) // 使用策略模式分发不同情况下的diff算法
break
}
}
})
} else {
for (let diffType of diffMap) {
if (diffType[0].test(type)) {
return diffType[1](vnode, dom) // 使用策略模式分发不同情况下的diff算法
}
}
}
}
export default diff