深入浅出Vue.js(二) 虚拟DOM & diff算法

随着时代的发展,页面上的功能越来越多,需要实现的需求越来越复杂,程序中需要维护的状态也越来越多,DOM操作也越来越频繁。当状态变得越来越多,DOM操作越来越频繁时,如果像之前那样使用jQuery来开发页面,那么代码中会有相当多的代码是在操作DOM,程序中的状态也很难管理,代码中的逻辑也很混乱。这其实是命令式操作DOM的问题,在业务越来越复杂的情况下,它会有不好维护的问题。

说明:事实上,任何应用都有状态,并不是只有使用了现代比较流行的框架之后才有状态。只不过现代框架揭露了一个事实,那就是我们的关注点应该聚焦在状态的维护上,而DOM操作其实是可以省略掉的,所以才会给我们营造一种错觉,好像只有使用了框架之后的应用才有状态。使用jQuery开发的应用也有状态,应用中所使用的变量都是状态。

在vue.js中,当状态发生变化时,,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,根本不需要比对。(vue.js 1.0实现方式)

这样做其实有一定的代价。因为细粒度太细,每一个绑定都会有一个对应的watcher来观察状态的变化,这样就会有一些内存开销以及依赖追踪的开销。当状态被越多的节点使用时,开销就越大。对于一个大型项目来说,这样的开销是非常大的。因此,vue.js 2.0开始选择了一个中等细粒度的解决方案,那就是引入虚拟DOM。组件级别是一个watcher实例,就是说即便一个组件内有多个节点使用了某个状态,但其实也只有一个watcher在观察这个状态的变化。当这个状态发生变化时,只能通知到组件,然后组件内部通过虚拟DOM去进行比对与渲染。这是一个比较折中的方案。

vue.js通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面。

之所以需要先试用状态生成虚拟节点,是因为如果直接用状态生成真实DOM,会有一定程度的性能浪费。而先创建虚拟节点在渲染视图,就可以将虚拟节点缓存,然后使用新创建的虚拟节点和上一次渲染时缓存的虚拟节点进行对比,然后根据对比结果只更新需要更新的真实DOM节点,从而避免不必要的DOM操作,节省一定的性能开销。

VirtualDom

vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
渲染视图的过程实现创建vnode,然后再使用vnode去生成真实的DOM元素,最后插入到页面渲染视图。

vue.js目前对状态的侦测策略采用了中等粒度。当状态发生变化时,只通知到组件级别,然后组件内部使用虚拟DOM来渲染视图。也就是说,只要组件使用的众多状态中有一个发生了变化,那么整个组件就要重新渲染。

VNode的类型

  • 注释节点
  • 文本节点
  • 元素节点
  • 组件节点
  • 函数式节点
  • 克隆节点

克隆节点:克隆节点是将现有节点的属性复制到新的节点中,让新创建的节点和被克隆节点的属性保持一致,从而实现克隆效果。它的作用是优化静态节点和插槽节点(slot node)。

以静态节点为例,当组件内的某个状态发生变化后,当前组件会通过虚拟DOM重新渲染视图,静态节点因为它的内容不会改变,所以除了首次渲染需要执行渲染函数获取vnode之外,后续更新不需要执行渲染函数重新生成vnode。因此,这时就会使用创建克隆节点的方法将vnode克隆一份,使用克隆节点进行渲染。这样就不需要重新执行渲染函数生成新的静态节点的vnode,从而提升一定程度的性能。

function cloneNode(vnode,deep){
    const cloned = new vnode(
        vnode.tag,
        vnode.data,
        vnode.children,
        vnode.text,
        vnode.elm,
        vnode.context,
        vnode.componentInstance,
        vnode.asyncFactory,
    )
    cloned.ns = vnode.ns
    cloned.isStatic = vnode.isStatic
    cloned.key = vnode.key
    cloned.isComment = vnode.isComment
    cloned.isCloned = true
    if(deep && vnode.children){
        cloned.children = cloneNode(vnode.children)
    }
    return cloned
}

可以看出,克隆现有节点时,只需要将现有节点的属性全部复制到新节点中即可。

克隆节点和被克隆节点之间的唯一区别是isCloned属性,克隆节点的isCloned为true,被克隆的原始节点的isCloned为false。

 

Vue中VNode实例对象中包含的所有属性和方法

class VNode {
  constructor(tag,data,children,text,elm,context,componentOptions,asyncFactory){
    this.tag = tag //节点名称 eg:p、ul、div
    this.data = data //节点上的数据 eg:attrs、class、style
    this.children = children //当前节点的子节点列表
    this.text = text //文本
    this.elm = elm //vnode对应的真实的dom节点
    this.ns = undefined //
    this.context = context //当前组件的实例
    this.functionalContext = undefined //
    this.functionalOptions = undefined //
    this.functionalScopeId = undefined //
    this.key = data && data.key //vnode标记 在diff过程中提高diff效率
    this.componentOptions = componentOptions //组件节点的选项参数 eg:propsData、tag、children
    this.componentInstance = undefined //组件的实例
    this.parent = undefined //节点父级
    this.raw = false //
    this.isStatic = false //是否为静态
    this.isRootInsert = false //是否为根级插入
    this.isComment = false // 是否为注释
    this.isCloned = false // 是否为克隆
    this.isOnce = false //是否为只渲染一次
    this.asyncFactory = asyncFactory //
    this.asyncMeta = undefined //
    this.isAsyncPlaceholder = false //
  }
  getChild(){
    return this.componentInstance
  }
}

VirtualDom 简单实现demo

class Element {
  constructor(type,props,children){
    this.type = type
    this.props = props
    this.children = children
  }
}
/**
* 创建vnode 得到vnode实例
*/
function createElement(type,props,children){
  return new Element(type,props,children)
}


/**
* 生成真实DOM
*/
function render(domObj){
  let el = document.createElement(domObj.type)
  for(let key in domObj.props){
    setAttr(el,key,domObj.props[key])
  }
  domObj.children.forEach((child)=>{
    child = (child instanceof Element) ? render(child) : document.createTextNode(child)
    el.appendChild(child)
  })
  return el
}


/**
* setAttr方法实现
*/
function setAttr(node,key,value){
  switch(key){
    case '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
*/
function renderDom(el,target){
  target.appendChild(el)
}


// 执行以下看看效果
// 像不像render函数的写法 哈哈哈
let vNode = createElement('ul',{class:'list'},[
  createElement('li',{class:'item'},['乔丹']),
  createElement('li',{class:'item'},['科比']),
  createElement('li',{class:'item'},['韦德'])
]);
let el = render(vNode)
let root = document.getElementById('zjy')
renderDom(el,root)

patch

patch也可以叫做patching算法,通过它渲染真实DOM时,并不是暴力覆盖原有DOM,而是对比新旧两个VNode之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新。之所以这么做,主要是因为DOM操作的执行速度远不如JavaScript运算速度快。

diff算法 简单实现

//判断是否为string
function isString(str){
  return typeof str === 'string'
}


//不同attr
function diffAttr(oldAttr,newAttr){
  let patch = {}
  for(let key in oldAttr){
    if(oldAttr[key] !== newAttr[key]){
      patch[key] = newAttr[key]
    }
  }
  for(let key in newAttr){
    if(!oldAttr.hasOwnProperty(key)){
      patch[key] = newAttr[key]
    }
  }
  return patch
}


//不同children
let num = 0
function diffChildren(oldChildren,newChildren,patches){
  oldChildren.forEach((child,index)=>{
    walk(child,newChildren[index],++num,patches)
  })
}


function diff(oldTree,newTree){
  let patches = {} //存放补丁的对象
  let index = 0
  walk(oldTree,newTree,index,patches)
  return patches
}


/**
* walk方法中diff的情况相当于只是列举了几种情况,我在控制台执行了下,的确有的情况没有体现出来,例如删除 新增子节点,
* 所以下面的patch补丁方法中进行补丁的类别完全是按照diff中创在的不同type类型执行的不同操作
*/
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({tyep:'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
  }
}


/**
* patch补丁(patch补丁简单实现的原由是因为diff方法简单实现造成的,有兴趣的可以去了解下vue中实现的diff算法 超长~~)
*/
let allPatches
let index = 0
function patch(node,patches){
  allPatches = patches
  walk(node)
}
function walk(node){
  let current = allPatches[index++]
  let childNodes = node.childNodes
  childNodes.forEach((child)=>{
    walk(child)
  })
  if(current){
    doPatch(node,current)
  }
}
function doPatch(node,patches){
  patches.forEach((patch)=>{
    switch(patch.type){
      case 'ATTR':
        for(let key in patch.attr){
          let value = patch.attr[key]
          if(value){
            setAttr(node,key,value)
          }else{
            node.removeAttribute(key)
          }
        }
        break;
      case 'TEXT':
        node.textContent = patch.text
        break;
      case 'REPLACE':
        let newNode = patch.newNode
        newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode)
        node.parentNode.replaceChild(newNode,node)
        break;
      case 'REMOVE':
        node.parentNode.removeChild(node)
        break;
      default:
        break;
    }
  })
}

   

posted @ 2020-06-10 22:12  671_MrSix  阅读(288)  评论(0编辑  收藏  举报