根据vue源码手动实现虚拟dom

网上实现虚拟dom的文章也很多了,本项目代码结构、函数等完全按照vue@2.5.17源码思路实现,主要也是为了总结一下自己的学习。github地址在这里

在这里插入图片描述

从图中可以看到,这个dom树改变了许多地方,但是只新建了一个div元素,这说明其余的元素只是做了移动和文本内容的修改,这比重新渲染整棵dom树要节省很多资源。

不多解释了,下面直接看代码吧。因为vue是通过模版解析之后生成的虚拟dom,我主要为了学习虚拟dom,没有做模版解析,所以手动建立了两棵虚拟dom树(这不重要),然后通过patch函数对比,改变真实的dom树结构。

<body>
    <script src="./vdom/vnode.js"></script>
    <script src="./vdom/patch.js"></script>
    <script>
    var ul = new VNode('ul',{class: 'ul'},[
        new VNode('p', {class: 'li'},[],'virtual dom'),
        new VNode('li',{class: 'li'},[],'mvvm'),
        new VNode('li', {class: 'li'},[],'virtual dom'),
        new VNode('input',{type: 'text'}),
        new VNode('li', {class: 'li'},[],'virtual dom'),
        new VNode('li',{},[],'mvvm'),
        new VNode('li',{class: 'li'},[],'buppt')
        ])
    var ul2 = new VNode('ul',{class: 'ul'},[
        new VNode('li', {class: 'li'},[],'buppt'),
        new VNode('li',{class: 'li'},[],'mvvm'),
        new VNode('p',{},[],'h1 dom'),
        new VNode('li',{class: 'li'},[],'h1 dom'),
        new VNode('div',{},[],'h1 dom'),
        new VNode('input',{type:'text'},[]),
        ])

    document.body.appendChild(ul.render())
    setTimeout(()=>{
        console.log('vnode change')
        patch(ul,ul2)
    },2000)  
    </script>
</body>

VNode类的代码如下,主要记录一个虚拟元素节点的标签名称、属性、子节点、文本内容、对应的真实dom中的element元素。render函数就是将这个虚拟的元素节点渲染成一个真实的dom节点的函数。

class VNode{
    constructor(tagName,props={},children=[],text=''){
        this.tagName=tagName;
        this.props=props ;
        this.children=children;
        this.text=text
        this.key = props && props.key
        var count = 0;
        children.forEach(child => {
            if(child instanceof VNode){
                count+=child.count;
            }
            count++;
        });
        this.count = count;
    }
    render(){
        let element = document.createElement(this.tagName);
        for(let key in this.props){
            element.setAttribute(key,this.props[key])
        }
        for(let child of this.children){
            if(child instanceof VNode){
                element.appendChild(child.render())
            }
        }
        if(this.text){
            element.appendChild(document.createTextNode(this.text))
        }
        this.elm = element;
        console.log(element)
        return element;
    }
}

这些比较简单,主要是下面对比两棵虚拟dom树的diff算法。

function patch (oldVnode, vnode) {
  if(isUndef(vnode)){
      return
  }
  if (oldVnode === vnode) {
      return 
  }
  if(sameVnode(oldVnode, vnode)){
        patchVnode(oldVnode, vnode)
  }else{
      const parentElm = oldVnode.elm.parentNode;
      createElm(vnode,parentElm,oldVnode.elm)
      removeVnodes(parentElm,[oldVnode],0,0)
  }
}
function sameVnode (a, b) {
  return (
      a.key === b.key && 
      a.tagName=== b.tagName &&
      sameInputType(a, b)
  )
}

function sameInputType (a, b) {
  if (a.tag !== 'input') return true
  return a.props.type == b.props.type
}

可以看到,如果两棵树相同,即没有发生变化,直接返回。

因为虚拟dom只是判断两棵树的同一层的树结构有没有变化,所以这里判断两个根节点是否为sameVnode,如果是,就执行更关键的patchVnode函数,如果不是,直接新建这棵新树。

function patchVnode(oldVnode, vnode){
  var ch = vnode.children
  var oldCh = oldVnode.children
  if(isUndef(vnode.text)){
    if(isDef(ch) && isDef(oldCh)){
        updateChildren(oldVnode.elm,oldCh,ch)
    }else if(isDef(ch)){
        if (isDef(oldVnode.text)) setTextContent(oldVnode.elm, '')
        addVnodes(oldVnode, ch, 0, ch.length - 1)
    }else if(isDef(oldCh)){
        removeVnodes(oldVnode.elm, oldCh, 0, oldCh.length - 1)
    }
  }else{
      setTextContent(oldVnode.elm,vnode.text);
  }
}

已知patchVnode函数是两个根节点相同的树了,需要的是判断他们两个的子节点。

根据代码中的几个判断可以得知,如果元素是文本节点,直接替换其中的文本即可。
如果新树和旧树都有子节点,则执行更为关键的updateChildren函数,如果新树有子节点,老树没有,直接添加子节点,如果新树没有子节点,老树有,直接删除子节点。

function updateChildren(parentElm, oldCh, newCh,){
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] 
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        patchVnode(oldStartVnode, newEndVnode)
        insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { 
        patchVnode(oldEndVnode, newStartVnode)
        insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) {
          createElm(newStartVnode, parentElm, oldStartVnode.elm)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode)
            oldCh[idxInOld] = undefined
            insertBefore(parentElm,vnodeToMove.elm, oldStartVnode.elm)
          } else {
            createElm(newStartVnode, parentElm, oldStartVnode.elm)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, newCh, newStartIdx, newEndIdx)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

updateChildren函数用到了四个指针,就是判断比较多。oldStartIdx 、oldEndIdx 分别指向老树的头和尾,newStartIdx 、newEndIdx 分别指向新树的头和尾。

如果新树的头等于老树的头,两个startId都++,如果新树的尾等于老树的尾,两个endId都–。

如果新树的头等于老树的尾,则把老树的尾移动到老树的头前,然后newStartIdx ++,oldEndIdx --。

如果新树的尾等于老树的头,则把老树的头移动到老树的尾后面,然后oldStartIdx ++,newEndIdx --。

如果上面四个判断都不成立,如果新树的头有key的话,就直接找有key的老树节点,没有key则将新树的头与现在老树头和尾直接的元素一一比较。如果有相同的,就把老树的这个节点移动到老树的头前,newStartIdx ++;如果没有相同的,就新建这个节点,插到老树的头前,newStartIdx ++。

操作真实dom的代码如下

function setTextContent(elm, content){
  elm.textContent = content;
}
function addVnodes (parentElm, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    createElm(vnodes[startIdx], parentElm, null)
  }
}
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
  for (let i=startIdx; i <= endIdx; i++) {
    var ch = vnodes[i]
    if(ch){
      parentElm.removeChild(vnodes[i].elm)
    }
  }
}

function createElm (vnode, parentElm, afterElm) {
  let element = vnode.render()
  vnode.elm = element;
  if(isDef(afterElm)){
    insertBefore(parentElm,element,afterElm)
  }else{
    parentElm.appendChild(element)
  }
  return element;
}
function insertBefore(parentElm,element,afterElm){
  parentElm.insertBefore(element,afterElm)
}

完整代码在这里,欢迎star~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值