虛擬dom的實現和diff算法

現在主流框架都採用虛擬dom,爲什麼?
一個真實dom的生成代價很昂貴,不過js的運行速度很快,這樣我們通過對js的虛擬dom樹操作,通過Diff算法來對真實dom做出相應的操作,效率事倍功半。
首先看看一個虛擬dom的結構,其實就是一個js的對象,裏面包含了各種需要實現的屬性:

 const ul = {
  tagName: 'ul',
  props: {
    id: 'list'
  },
  children: [
    {
      tagName: 'li',
      props: {
        class: 'item'
      },
      children: [{
        tagName: 'button',
        children: ['這裏是個按鈕']
      }]
    },{
      tagName: 'li',
      children: ['li信息']
    }
  ]
}

Virtual DOM生成真實dom:

 class Element {
 	constructor (vdom) {
 		this.vitrual = vdom
 		const { props, children } = vdom
 		this.key = props ? props.key : void 666
 		// 這邊比較重要,DFS的標記,爲後面leftNode標記算法使用
 		let count = 0
		children.forEach((child, i) => {
			if (child instanceof Element) {
				count += child.count
			}
			count ++
		})
		this.count = count
 	}
 	render () {
 		const { tagName, props, children } = this.vitrual
 		 el = document.createElement(tagName)
 		const fregment = children ? document.createDocumentFragment() : null
 		children && children.map(elem => {
			const child = (elem instanceof Element) ? 
			elem.render() : document.createTextNode(elem)
			fregment.appendChild(child)
		}) 
		fregment && el.appendChild(fregment)
 		return el
 	}
 }
// 遍歷virtual dom, DFS遍歷
function createTree (vdom) {
	const { children } = vdom
	vdom.children = children && children.map(v => 
		(v instanceof Object) ? createTree(v) : v
	)
	return new Element(vdom)
}

diff算法:先找變化的地方,通過標記,改變真是DOM;

  1. 先找diff

參考react Virtual DOM的Diff思路:分爲4種;

  1. REPLACE 替換節點
  2. PROPS 修改屬性
  3. REORDER children的重排
  4. TEXT 文本內容的替換

首先通過DFS方式遍歷標記找到diff

import _ from './utils'
import listDiff from './diff-list'

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

function diff (oldTree, newTree) {
	const index = 0 // 從0開始標記
	const patches = {} // 存放所有的變更
	dfsWalk(oldTree, newTree, index, patches)
	return patches
}

// DFS計算diff
function dfsWalk(oldNode, newNode, index, patches) {
	const currentPatch = []
	if (!newNode) {}
	else if (_.isString(oldNode) && _.isString(newNode)) {
		if (oldNode !== newNode) {
			currentPatch.push({
				type: TEXT,
				content: newNode
			})
		}
	} else if (oldNode.tagName === newNode.tagName &&
		oldNode.key === newNode.key) {
			const propPatches = diffProps(oldNode, newNode)
			propPatches && currentPatch.push({
					type: PROPS,
					props: propPatches
				})
		// 這邊不管ignore
		diffChildren(
			oldNode.children,
			newNode.children,
			index, 
			patches,
			currentPatch
		)	
	} else {
		currentPatch.push({
			type: REPLACE,
			node: newNode
		})
	}
}

// 比較list
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  const diffs = listDiff(oldChildren, newChildren, 'key')
  newChildren = diffs.children

  if (diffs.moves.length) {
    currentPatch.push({ type: REORDER, moves: diffs.moves })
  }

  let leftNode = null
  let currentNodeIndex = index
  oldChildren.forEach((child, i) => {
	const newChild = newChildren[i]
	// 這邊要聯繫上下文,count在一開始建立virtual時
	// 這邊比較重要了
    currentNodeIndex = (leftNode && leftNode.count)
      ? currentNodeIndex + leftNode.count + 1
      : currentNodeIndex + 1
    dfsWalk(child, newChild, currentNodeIndex, patches)
    leftNode = child
  })
}

// 比較props
function diffProps (oldNode, newNode) {
	const propsPatches = {}
	const oldProps = oldNode.props
	const newProps = newNode.props
	for (const [key, val] of Object.entries(oldProps)) {
		if (val !== newProps[key]) {
			propsPatches[key] = val
		}
	}

	for (const key of Object.keys(newProps)) {
		if (oldProps.hasOwnProperty(key)) {
			propsPatches[key] = newProps[key]
		}
	}

	const { length } = Object.keys(propsPatches)
	if (!length) return null
	return propsPatches
}

export default diff

上面找出對於的diff存入patches中,在應用修改對應的diff

講講list-diff2的對比:
通過把新老的children進行對比,通過key值對比,不存在key的項存入free數組然後對應覆蓋,其他將被移除的key元素,用null代替,標記爲待移除(remove)存入moves數組,新增的key元素(待插入)加入moves數組,返回一個對象,所以在列表遍歷時候框架會提示需要傳入key,最大的原因就是爲了Virtual DOM的diff操作,這邊是一個example:
在這裏插入圖片描述
這邊第二個元素被移除用null代替,並在moves列表新增改元素的刪除操作;
type: 0 刪除 1 新增插入
然後children爲何oldChildren對應長度的列表,繼續走正常的diff操作。

找出了diff後,那就應用diff進行對真實dom進行修改:

import _ from './utils'
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

function patch(node, patches) {
    // DFS累計標記,通過對象存儲,指向同一個地址
    let walker = { index: 0 }
    dfsWalker(node, walker, patches)
}

function dfsWalker (node, walker, patches) {
    const currentPatches = patches[walker.index]
    const childNodes = node.childNodes
    childNodes.length && childNodes.forEach((v, i) => {
        walker.index++
        dfsWalker(v, walker, patches)
    })
    if (currentPatches) applyPatches(node, currentPatches)
}

function applyPatches (node, currentPatches) {
    currentPatches.forEach(currentPatch => {

        switch(currentPatch.type) {
            case REPLACE:
                setReplace(node, currentPatch)
                break
            case REORDER:
                setReorder(node, currentPatch.moves)
                break
            case PROPS:
                setProps(node, currentPatch.props)
                break
            case TEXT:
                node.textContent = currentPatch.content
        }
    })
}

function setProps (node, props) {
    for (const key of Object.keys(props)) {
        !props[key] && node.removeAttribute(key)
        props[key] && _.setProps(node, key, props[key])
    }
}

function setReplace (node, currentPatch) {
    const newNode = (typeof currentPatch.node === 'string') ?
       document.createTextNode(currentPatch.node) :
       currentPatch.node.render()
    node.parentNode.replaceChild(newNode, node)
}

function setReorder (node, moves) {
    const map = {}
    const staticNodes = Array.from(node.childNodes)
    staticNodes.forEach(child => {
        if (child.nodeType === 1) {
            const key = child.getAttribute('key')
            if (key) {
                map[key] = child
            }
        }
    })
    moves.forEach((move, i) => {
        const { index } = move
        if (move.type === 0) { // remove
            // 此處判斷元素是否因爲insert操作已經被刪除
            if (staticNodes[index] === node.childNodes[index]) {
                node.removeChild(node.childNodes[index])
            }
        } else { // insert type === 1
            const newNode = map[move.item.key] ? map[move.item.key].childNodes(true)
            : (typeof move.item === 'object')
                ? move.item.render()
                : document.createTextNode(move.item)
            staticNodes.splice(index, 0, newNode)
            node.insertBefore(newNode, node.childNodes[index] || null)
        }
    })

}

export default patch

後續對應四個不同的操作進行修改即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章