diff算法与虚拟DOM简易实现

438 阅读3分钟

一、虚拟DOM实现

虚拟dom的整个流程:

  1. 创建虚拟dom的数据结构
  2. 生成虚拟dom
  3. 挂载虚拟dom
  4. 如果父元素没有虚拟dom,就直接调用mount方法进行挂载
  5. 如果父元素有虚拟dom,并且需要更新,就调用patch方法进行更新
  6. patch方法中对新旧VNode的type,props,进行更新
  7. 其次是对新旧VNode的children进行更新
  8. 如果旧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]
}