一、虚拟DOM实现
虚拟dom的整个流程:
- 创建虚拟dom的数据结构
- 生成虚拟dom
- 挂载虚拟dom
- 如果父元素没有虚拟dom,就直接调用mount方法进行挂载
- 如果父元素有虚拟dom,并且需要更新,就调用patch方法进行更新
- patch方法中对新旧VNode的type,props,进行更新
- 其次是对新旧VNode的children进行更新
- 如果旧VNode的children和新VNode的children都存在,就需要使用diff算法进行nodeTree的更新
1. 定义虚拟DOM的数据结构
// h.js
// VNode节点
const createVNode = (type, props, key, $$) => {
return {
type, // div / CompoentA / ''(文本)
props, // 属性以及children
key,
$$ // 内部函数
}}
// text节点
const createText = (text) => {
return {
type: '',
props: {
nodeValue: text + ''
},
$$: {
flag: NODE_FLAG.TEXT
}
}
}
// 使用位预算判断节点类型
export const NODE_FLAG = {
EL: 1, // 元素 element
TEXT: 1 << 1}
2. 定义生成虚拟DOM的对象方法
// h.js
// h('div', { className: 'padding20'}, 'hello world!')
export const h = (type, props, ...kids) => {
props = props || {}
let key = props.key || void 0
kids = normalize(props.children || kids)
if (kids.length) props.children = kids.length === 1 ? kids[0] : kids
const $$ = {}
$$.el = null
$$.flag = type === '' ? NODE_FLAG.TEXT : NODE_FLAG.EL
return createVNode(type, props, key, $$)}
例子:
import { h } from './h.js'
const vnode = h( 'ul', {
style: {
width: '100px',
height: '100px',
backgroundColor: 'green'
}
},
[
h('li', { key: 'li-a' }, 'this is li a'),
h('li', { key: 'li-b' }, 'this is li b'),
h('li', { key: 'li-c' }, 'this is li c'),
h('li', { key: 'li-d' }, 'this is li d'),
])
console.log(vnode)
3. 渲染
//render.js
import { mount } from './mount.js'
import { patch } from './patch.js'
export const render = (vnode, parent) => {
let prev = parent._vnode
// 父节点没有虚拟dom,直接挂载
if (!prev) {
// mount 方法在第4点
mount(vnode, parent)
parent._vnode = vnode
} else {
if (vnode) {
// 新旧两个 vnodeTree 都存在,进行patch
// patch方法在第6点
patch(prev, vnode, parent)
parent._vnode = vnode
} else {
// 不存在新的 vnodeTree,直接remove
parent.removeChild(prev.$$.el)
}
}
}
4. 挂载操作 mount
// mount.js
import { NODE_FLAG } from './h.js'
import { patchProps } from './patch.js'
export const mount = (vnode, parent, refNode) => {
if (!parent) throw new Error('请传入父元素')
const $$ = vnode.$$
// 为文本节点
if ($$.flag & NODE_FLAG.TEXT) {
const el = document.createTextNode(vnode.props.nodeValue)
vnode.el = el
// 渲染文本节点
parent.appendChild(el)
} else if ($$.flag & NODE_FLAG.EL) { // 元素节点 先不考虑 type 是一个组件的情况
const { type, props } = vnode
const el = document.createElement(type)
vnode.el = el
const { children, ...rest } = props
if (Object.keys(rest).length) {
for (let key of Object.keys(rest)) {
// 遍历除了children之外的属性,
// 去patch props里面的属性, 例如class,style等
// patchProps方法在第5点
patchProps(key, null, rest[key], el)
}
}
if (children) {
const __children = Array.isArray(children) ? children : [children]
for (let child of __children) {
// 如果存在chldren, 挂载到它们的VNode对应的元素上
mount(child, el)
}
}
// 渲染的位置
refNode ? parent.insertBefore(el, refNode) : parent.appendChild(el) }}
5.patchProps 方法
// patch.js
export const patchProps = (key, prev, next, el) => {
// style
if (key === 'style') {
// { style: { margin: '0px', padding: '10px' }}
if (next) {
for (let k in next) {
el.style[k] = next[k]
}
}
// { style: { padding: '0px', color: 'red' } }
if (prev) {
for (let k in prev) {
if (!next.hasOwnProperty(k)) {
el.style[k] = ''
}
}
}
}
// class
else if (key === 'className') {
if (!el.classList.contains(next)) {
el.classList.add(next)
}
}
// events
else if (key[0] === 'o' && key[1] === 'n') {
prev && el.removeEventListener(key.slice(2).toLowerCase(), prev)
next && el.addEventListener(key.slice(2).toLowerCase(), next)
}
else if (/\[A-Z]|^(?:value|checked|selected|muted)$/.test(key)) {
el[key] = next
}
else {
el.setAttribute && el.setAttribute(key, next)
}
}
6. patch 方法
// patch.js
export const patch = (prev, next, parent) => {
// type: 'div' -> type: 'p'
// type不一样,remove老的,挂载新的
if (prev.type !== next.type) {
parent.removeChild(prev.el)
mount(next, parent)
return
}
// type 一样,diff props(先不考虑 children)
const { props: { children: prevChildren, ...prevProps } } = prev
const { props: { children: nextChildren, ...nextProps } } = next
// patchProps
const el = (next.el = prev.el)
// patch 需要更新节点的props
for (let key of Object.keys(nextProps)) {
let prev = prevProps[key],
let next = nextProps[key]
patchProps(key, prev, next, el)
}
// patch 旧节点的props, 旧节点的props的key新节点的没有,直接remove旧节点的这些props
for (let key of Object.keys(prevProps)) {
if (!nextProps.hasOwnProperty(key)) patchProps(key, prevProps[key], null, el)
}
// patch children
patchChildren(prevChildren, nextChildren, el)}
7.patchChildren 方法
// patch.js
import { odiff } from './optimization-diff.js'
const patchChildren = (prev, next, parent) => {
// diff 比较耗性能,可以前置做一些处理,提升效率 // 如果没有旧节点
if (!prev) {
if (!next) {
// 同时没有新旧节点的存在,几乎不肯发生
}
else {
next = Array.isArray(next) ? next : [next]
for (const c of next) {
// 直接挂载旧节点
mount(c, parent)
}
}
}
// 只有一个 children
else if (prev && !Array.isArray(prev)) {
if (!next) parent.removeChild(prev.el)
else if (next && !Array.isArray(next)) {
patch(prev, next, parent)
}
else {
parent.removeChild(prev.el)
for (const c of next) {
mount(c, parent)
}
}
} else odiff(prev, next, parent)}
二、diff算法
1. 直接比较的diff算法
过程模拟
1. 旧children [ b, a ], 要更新为:新children[ c, d, a ]
2. prevMap { b:0, a: 1 }; 将旧children的keys和child的下标映射
3. 对c进行操作 ,结果为:[ c, b, a]
4. 对d进行操作,结果为:[ c, d, b, a]
5. 对a进行操作 ,结果为:[c, d, a, b]
6. 对b进行操作 ,结果为:[ c, d, a ]
import {
mount
}
from './mount.js'import {
patch
}
from './patch.js'
export const diff = (prev, next, parent) = >{
let prevMap = {}
let nextMap = {}
// old tree children
for (let i = 0; i < prev.length; i++) {
let {
key = i + ''
} = prev[i] prevMap[key] = i
}
console.log(prevMap, 'sss') let lastIndex = 0
for (let n = 0; n < next.length; n++) {
let {
key = n + ''
} = next[n] let j = prevMap[key] let nextChild = next[n] nextMap[key] = n
if (j == null) {
let refNode = n === 0 ? prev[0].el: next[n - 1].el.nextSibling mount(nextChild, parent, refNode)
} else {
patch(prev[j], nextChild, parent) debugger
if (j < lastIndex) {
// 这里是新创建元素,没有移动
let refNode = next[n - 1].el.nextSibling;
parent.insertBefore(nextChild.el, refNode)
} else {
lastIndex = j
}
}
}
for (let i = 0; i < prev.length; i++) {
let {
key = '' + i
} = prev[i]
if (!nextMap.hasOwnProperty(key)) parent.removeChild(prev[i].el)
}
}
2.最长上升子序列diff算法
最长上升子序列算法: 就是在一个序列中,求长度最长且顺序是升序的子序列
例如:求0 8 4 12 2 10 6 4 1 9 5 13的最长上升子序列
1. 从左到右找: 0 8 12 13
2.从左到右每隔一个数字找: 0 4 12 13
当然还有其他的解法
如果 把 [a, b, f, m, c] 更新为 [c, a, d, b],最快的是把 d 移动到第一个,a b c 不要动 (最长上升子序列算法)
import {
mount
}
from "./mount.js"import {
patch
}
from "./patch.js"
export const odiff = (prevChildren, nextChildren, parent) = >{
// 前指针
let j = 0
// 后指针
let prevEnd = prevChildren.length - 1 let nextEnd = nextChildren.length - 1
let prevNode = prevChildren[j] let nextNode = nextChildren[j] outer: {
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent) j++
if (j > prevEnd || j > nextEnd) break outer prevNode = prevChildren[j] nextNode = nextChildren[j]
}
prevNode = prevChildren[prevEnd] nextNode = nextChildren[nextEnd]
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent) prevEnd--nextEnd--
if (j > prevEnd || j > nextEnd) break outer prevNode = prevChildren[prevEnd] nextNode = nextChildren[nextEnd]
}
}
if (j > prevEnd && j <= nextEnd) {
let nextPos = nextEnd + 1 let refNode = nextPos >= nextChildren.length ? null: nextChildren[nextPos].el
while (j <= nextEnd) {
mount(nextChildren[j++], parent, refNode)
}
return
} else if (j > nextEnd) {
while (j <= prevEnd) {
parent.removeChild(prevChildren[j++].el)
}
return
}
let nextStart = j,
prevStart = j,
nextLeft = nextEnd - j + 1,
nextIndexMap = {},
source = new Array(nextLeft).fill( - 1),
patched = 0,
lastIndex = 0,
move = false
for (let i = nextStart; i <= nextEnd; i++) {
let key = nextChildren[i].key || i nextIndexMap[key] = i
}
for (let i = prevStart; i <= prevEnd; i++) {
let prevChild = prevChildren[i],
prevKey = prevChild.key || i,
nextIndex = nextIndexMap[prevKey]
if (patched >= nextLeft || nextIndex === undefined) {
parent.removeChild(prevChild.el) continue
}
patched++let nextChild = nextChildren[nextIndex] patch(prevChild, nextChild, parent)
source[nextIndex - nextStart] = i
if (nextIndex < lastIndex) {
move = true
} else {
lastIndex = nextIndex
}
}
if (move) {
const seq = lis(source);
let j = seq.length - 1;
for (let i = nextLeft - 1; i >= 0; i--) {
let pos = nextStart + i,
nextPos = pos + 1,
nextChild = nextChildren[pos],
refNode = nextPos >= nextLeft ? null: nextChildren[nextPos].el
if (source[i] === -1) {
mount(nextChild, parent, refNode)
} else if (i !== seq[j]) {
parent.insertBefore(nextChild.el, refNode)
} else {
j--
}
}
} else {
// no move
for (let i = nextLeft - 1; i >= 0; i--) {
if (source[i] === -1) {
let pos = nextStart + i,
nextPos = pos + 1,
nextChild = nextChildren[pos],
refNode = nextPos >= nextLeft ? null: nextChildren[nextPos].el
mount(nextChild, parent, refNode)
}
}
}
}
function lis(arr) {
let len = arr.length,
result = [],
dp = new Array(len).fill(1);
for (let i = 0; i < len; i++) {
result.push([i])
}
for (let i = len - 1; i >= 0; i--) {
let cur = arr[i],
nextIndex = undefined
if (cur === -1) continue
for (let j = i + 1; j < len; j++) {
let next = arr[j]
if (cur < next) {
let max = dp[j] + 1
if (max > dp[i]) {
nextIndex = j dp[i] = max
}
}
}
if (nextIndex !== undefined) result[i] = [...result[i], ...result[nextIndex]]
}
let index = dp.reduce((prev, cur, i, arr) = >cur > arr[prev] ? i: prev, dp.length - 1) return result[index]
}