虚拟DOM
优势如下:
- 性能更高, 方便对节点进行操作, 减少因重排/重绘产生的内存浪费
- 各平台兼容, 可跨平台, 只要将 VDOM -> 对应平台的节点 就行了
在 Vue 中可以导入 h 函数创建, 当然结合 render() 使用才有意义
import { h, createApp } from 'vue'const App = {render() {return h('div', {class: 'myCom'}, 'Good')}/*上面相当于 template: `<div class="myCom"> Good </div>`*/}createApp(App).mount('#app')
根据尤大佬的说法, h() 只是返回一个对象而已, 相当于 AST 中单节点的对象
function h(tag: string, attrs?: string|object, children?: string|tuple) {return {tag,attrs,children}}
真正重要的其实是 mount()[将虚拟DOM变成真实DOM] 和 patch() [比较新旧虚拟DOM]
function mount(vdom, parentDom) {if(typeof vdom === 'string') {const text = document.createTextNode(vdom)parentDom.appendChild(text)return true}if(!(vdom instanceof VDOM)) return new Error('vdom is not VDOM or String')if(!(parentDom instanceof HTMLElement)) return new Error('parentDom is not HTMLElement')const { tag, attrs, children } = vdomlet dom = vdom.el = document.createElement(tag) // tagfor(const key in attrs) {const value = attrs[key]if(key.startsWith("on")) {dom.addEventListener(key.silce(2).toLowerCase(), value)} else {dom.setAttribute(key, value)}} // attrsif(children) {if(typeof children === 'string') { dom.textContent = children }else {children.forEach(child => {mount(child, dom)})}} // childrenparentDom.appendChild(dom)}
patch() 在我看来是最蛋疼的, 因为涉及对象与对象/数组与数组的最优解, 也就是 diff(差) 算法
// 必须以新旧替换最小消耗为目标function patch(oldVdom, newVdom) {// 1. 比较在字符串if(oldVdom.tag === newVdom.tag) {const el = newVdom.el = oldVdom.el // 感觉没什么必要赋值给newVdom.el// 2. 比较 attrs 对象const oldAttrs = oldVdom.attrs ?? {}const newAttrs = newVdom.attrs ?? {}// 添加所有新属性for(const key in newAttrs) {const oldValue = oldAttrs[key]const newValue = newAttrs[key]if(newValue !== oldValue) {if(key.startsWith("on")) {el.addEventListener(key.silce(2).toLowerCase(), newValue)} else {el.setAttribute(key, newValue)}}}// remove 多余属性for(const key in oldAttrs) {if(!(key in newAttrs)) {if(key.startsWith("on")) {el.removeEventListener(key.silce(2).toLowerCase(), oldAttrs[key])} else {el.removeAttribute(key)}}}// 3. 比较 children 对象 ---> Array|String VS Array|Stringconst oldChild = oldVdom.children ?? []const newChild = newVdom.children ?? []if(typeof newChild === 'string') {if(typeof oldChild === 'string') {if(newChild !== oldChild) el.textContent = newChild} else {el.children.forEach( child => {el.removeChild(child.el)})el.textContent = newChild}} else {if(typeof oldChild === 'string') {el.innerHTML = ''newChild.forEach(child => {mount(child, el)})} else {const commonLength = Math.min(oldChild.length, newChild.length)for(const key in commonLength) {patch(oldChild[key], newChild[key]) // 并非最优解}if(newChild.lnegth > commonLength) {newChild.slice(commonLength).forEach(child => {mount(child, el)})} else {oldChild.slice(commonLength).forEach(child => {el.removeChild(child.el)})}}}} else {const parent = oldVdom.el.parentNode // parentElement [ie专用], parentNode [w3c标准]parent.removeChild(oldVdom.el)mount(newVdom, parent)}}
