虚拟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

后续对应四个不同的操作进行修改即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值