简易虚拟DOM

虚拟DOM算法四部分
1.虚拟DOM创建
2.渲染虚拟DOM(render)
3.虚拟DOM diff(求新旧虚拟DOM差异)
4.虚拟DOM patch(将差异以补丁形式打到旧DOM上)
虚拟DOM创建
//虚拟DOM类
    class Element {
      constructor(type, props, children) {
        this.type = type
        this.props = props
        this.children = children
      }
    }
    // 创建虚拟DOM,返回虚拟节点(object)
    function createElement(type, props, children) {
      return new Element(type, props, children)
    }
渲染虚拟DOM(render)
//render将虚拟DOM转化为真实DOM
    function render(domObj) {
      //根据type类型创建对应的元素
      let el = document.createElement(domObj.type)

      //再去遍历props属性对象,然后给创建的元素el设置属性
      for (let key in domObj.props) {
        //设置属性的方法
        setAttr(el, key, domObj.props[key])
      }

      //遍历子节点,如果是虚拟DOM,就继续递归渲染
      //如果不是就代表是文本节点,直接创建
      domObj.children.forEach(child => {
        child = (child instanceof Element) ? render(child) : document.createTextNode(child)
        //添加金对应的元素内
        el.appendChild(child)
      })

      return el
    }

    //设置属性
    function setAttr(node, key, value) {
      switch (key) {
        case 'value':
          //node是一个input或者textarea就直接设置其value即可
          if (node.tagName.toLowerCase() === 'input' ||
            node.tagName.toLowerCase() === 'textarea') {
            node.value = value
          } else {
            node.setAttribute(key, value)
          }
          break
        case 'style':
          //直接赋值行内样式
          node.style.cssText = value
          break
        default:
          node.setAttribute(key, value)
          break
      }
    }
虚拟DOM diff(求新旧虚拟DOM差异)
//DOM-diff
    //
    function diff(oldTree,newTree){
      //声明变量patches用来存放补丁的对象
      let patches = {}
      //第一次比较应该是树的第零个索引
      let index = 0
      //递归树 比较后的结果放到补丁里
      walk(oldTree,newTree,index,patches)

      return patches;
    }
    function walk(oldNode,newNode,index,patches){
      //每一个元素都有一个补丁
      let current = []

      if(!newNode){
        //规则一
        current.push({type:'REMOVE',index})
      }else if(isString(oldNode) && isString(newNode)){
        //判断文本是否一致
        if(oldNode !== newNode){
          current.push({type:'TEXT',text:newNode})
        }
      }else if(oldNode.type === newNode.type){
        //比较属性是够有更改
        let attr = diffAttr(oldNode.props,newNode.props)
        if(Object.keys(attr).length > 0){
          current.push({type:'ATTR',attr})
        }
        //如果有子节点,遍历子节点
        diffChildren(oldNode.children,newNode.children,patches)
      } else {
        //说明节点被替换掉了
        current.push({type:'REPLACE',newNode})
      }
      //当前元素确实有补丁存在
      if(current.length){
        //将补丁和元素对应起来,放到大补丁包中
        patches[index] = current
      }
    }
    function isString(obj){
      return typeof obj === 'string'
    }
    function diffAttr(oldAttrs,newAttrs){
      let patch = {}
      //判断老的属性中和新的属性的关系
      for(let key in oldAttrs){
        if(oldAttrs[key] !== newAttrs[key]){
          patch[key] = newAttrs[key];//有可能还是undefined
        }
      }

      for(let key in newAttrs){
        //老节点没有新节点的属性
        if(!oldAttrs.hasOwnProperty(key)){
          patch[key] = newAttrs[key]
        }
      }
      return patch
    }
    //所有都基于一个序号来实现
    let num = 0

    function diffChildren(oldChildren,newChildren,patches){
      //比较老的第一个和新的第一个
      oldChildren.forEach((child,index)=>{
        walk(child,newChildren[index],++num,patches)
      })
    }
虚拟DOM patch(将差异以补丁形式打到旧DOM上)
//patch补丁更新
    //打补丁需要传入两个参数,一个是要打补丁的元素,另一个是要打的补丁
    let allPatches;
    let index = 0 //默认哪个需要打补丁

    function patch(node,patches){
      allPatches = patches

      //给某个元素打补丁
      walk2(node)
    }
    function walk2(node){
      let current = allPatches[index++]
      let childNodes = node.childNodes

      //先序深度,继续遍历递给子节点
      childNodes.forEach(child => walk2(child))

      if(current){
        doPatch(node,current)//打上补丁
      }
    }
    function doPatch(node,patches){
      //遍历所有打过的补丁
      patches.forEach(patch =>{
        let type = patch.type
        if(type === 'ATTR'){
          for(let key in patch.attr){
            let value = patch.attr[key]
            if(value){
              setAttr(node,key,value)
            }else{
              node.removeAttribute(key)
            }
          }
        } else if(type === 'TEXT'){
          node.textContent = patch.text
        } else if(type === 'REPLACE'){
          let newNode = patch.newNode
          newNode = (newNode instanceof Element) ? render(newNode) :document.createTextNode(newNode)
          node.parentNode.replaceChild(newNode,node)
        } else if(type === 'REMOVE'){
          node.parentNode.removeChild(node)
        }
      })
    }
锐评该简易虚拟DOM

问题挺多的
问题定位:diff阶段的规则1
remove之后会忽略该节点的子节点,从而导致先序遍历跳过了他的子节点导致index异常

if (!newNode) {
        //规则一
        current.push({
          type: 'REMOVE',
          index
        })
}

修改后的规则:
如果当前的旧节点不是文本节点,说明还是有子节点的,应该继续diffChildren,但是newNode明显已经不存在,这里传入空数组做空操作,目的是为了让被remove的子节点也应该被遍历,从而保证index一致性

//规则一
        current.push({
          type: 'REMOVE',
          index
        })
        //如果有子节点,遍历子节点
        if (!isString(oldNode)) {
          diffChildren(oldNode.children, [], patches)
        }

问题二:patch阶段patch异常
问题定位:
在patch阶段,我们虽然使用的是先序遍历,但是打补丁的顺序并不是从最后一个节点打的,而是树遍历到尽头就开始从底部往上打这条分叉上的补丁,从而导致了forEach阶段,nodeList直接被改变了,在数组的forEach中直接改变数组是非常不好的,这会导致无法访问完整这个数组,这里我们先用先序遍历把所有需要打的补丁放进needPatchList({node:ElementNode,current:PatchesItem})
然后打补丁,这样就不会影响到之前的节点
问题代码

function walk(node) {
    let current = allPatches[index++];
    let childNodes = node.childNodes;

    // 先序深度,继续遍历递归子节点
    childNodes.forEach(child => walk(child));

    if (current) {
        doPatch(node, current); // 打上补丁
    }
}

更改后的代码

let needPatchList = []

    function patch(node, patches) {
      allPatches = patches

      //给某个元素打补丁
      walk2(node)
      console.log(needPatchList)
      for (let {
          node,
          current
        } of needPatchList) {
        doPatch(node, current)
      }
    }
function walk2(node) {
      console.log('补丁', index, node)
      let current = allPatches[index++]
      let childNodes = node.childNodes
      if (current) {
        needPatchList.push({
          node,
          current
        })
        //doPatch(node,current)//打上补丁
      }
      //先序深度,继续遍历递给子节点
      childNodes.forEach(child => walk2(child))


      // if(current){
      //   needPatchList.push({node,current})
      //   //doPatch(node,current)//打上补丁
      // }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容