Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

虚拟DOM #37

Open
janeLLLL opened this issue Nov 23, 2020 · 0 comments
Open

虚拟DOM #37

janeLLLL opened this issue Nov 23, 2020 · 0 comments

Comments

@janeLLLL
Copy link
Owner

概念

  • 虚拟DOM:使用JavaScript对象描述真实DOM
  • Vue.js中的虚拟DOM借鉴Snabbdom,并添加了Vue.js的特性:指令和组件机制
  • 作用:
    1. 避免直接操作DOM,提高开发效率
    2. 作为一个中间层可以跨平台
    3. 虚拟DOM不一定会提高性能
      • 首次渲染的时候会增加开销
      • 复杂视图情况下提升渲染性能

代码演示

    const vm = new Vue({
      el: '#app',
      render (h) {
        // h(tag, data, children)
        // return h('h1', this.msg)
        // return h('h1', { domProps: { innerHTML: this.msg } })
        // return h('h1', { attrs: { id: 'title' } }, this.msg)
        const vnode = h(
          'h1', 
          { 
            attrs: { id: 'title' } 
          },
          this.msg
        )
        console.log(vnode)
        return null
      },
      data: {
        msg: 'Hello Vue'
      }
    })

h函数

  • 等于vm.$createElement(tag, data, children, normalizeChildren),返回了一个虚拟节点VNode
    • tag:标签名称或者组件对象
    • data:描述tag,可以设置DOM的属性或者标签的属性
    • children:tag中的文本内容或者子节点

VNode

概念:VNode是JavaScript对象。VNode表示Virtual DOM,用JavaScript对象来描述真实的DOMDOM标签,属性,内容都变成对象的属性。

作用:通过rendertemplate模版描述成VNode,然后进行一系列操作之后形成真实的DOM进行挂载。

生成:在Vue源码中,VNode是通过一个构造函数生成的。

  • 核心属性
    • tag:调用h函数传入的tag
    • data:存储节点的属性,绑定的事件等
    • children (h函数传入)
    • text
    • elm:真实DOM 节点
    • key:复用当前元素

虚拟DOM创建的整体过程

createElement

  • src\core\vdom\create-element.js

  • createElement() 函数,用来创建虚拟节点 (VNode),我们的 render 函数中的参数 h,就是createElement()

    render(h) {
    // 此处的 h 就是 vm.$createElement
        return h('h1'
        , this.msg)
    }
  • h函数,render()中调用,用户传递的或者编译生成的 render 函数,这个时候传递了 createElement,通过代码演示

_update

src\core\instance\lifecycle.js

  • vnode处理过程

  • 判断是否有prevVnode

    • 首次执行:vm.patch(vm.$el, vnode, hydrating, false)
    • 数据更新:vm.patch(prevVnode, vnode)
  • 运用__patch__方法创建$el

image

__patch__

  • src\core\vdom\patch.js

  • 运用__patch__方法创建$el

createElm

  • src\core\vdom\patch.js
  • 创建了真实 DOM 元素

patchVnode

  • src\core\vdom\patch.js

image

  1. 新节点没有文本
    • 老节点和老节点都有子节点:对子节点进行diff操作,调用updateChildren
    • 新的有子节点,老的没有子节点:先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点
    • 老节点有子节点,新的没有子节点:删除老节点中的子节点
    • 老节点有文本,新节点没有文本:清空老节点的文本内容
  2. 新老节点都有文本节点:setTextContent修改文本

updateChildren

  • 更新新旧节点的子节点
  • 在patchVnode中,当新老节点都有子节点,并且子节点是sameVNode的时候会调用updateChildren
  • 会对比新老子节点,找到子节点的差异,更新到DOM树,如果节点没有变化,会重用该节点
  • 和snabbdom是一样的
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    //当新节点和旧节点都没有遍历完成
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        //oldStartVnode和newStartVnode,相同sameVnode
        //直接将该VNode节点进行patchVnode
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        //获取下一组开始节点
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        //直接将该VNode节点进行patchVnode
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        //直接将该VNode节点进行patchVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        //获取下一组结束节点
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        //oldStarVnode和newStartVnode相同
        //进行patchVnode,把oldStartVnode移动到最后
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        //移动游标,获取下一组节点
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        //以上四种清空都不满足
        //newStartVnode 依次和旧的节点比较

        //从新的节点开头获取一个,去老节点中查找相同节点
        //先找新开始节点的key和老节点相同的索引,如果没找到再通过sameVnode找
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
          //如果没找到
        if (isUndef(idxInOld)) { // New element
          //创建节点并插入到最前面
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          //获取要移动的老节点
          vnodeToMove = oldCh[idxInOld]
          //如果使用 newStartVnode 找到相同的老节点
          if (sameVnode(vnodeToMove, newStartVnode)) {
            //执行patchVnode,并且将找到的节点移动到最前面
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            //如果key相同,但是是不同的元素,创建新元素
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    //当结束时oldStartIdx > oldEndIdx,旧节点遍历完,但是新节点还没有
    if (oldStartIdx > oldEndIdx) {
      //说明新节点比老节点多,把剩下的新节点插入到老的节点后面
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      //当结束时 newStartIdx > newEndIdx,新节点遍历完,但是旧节点还没有
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

设置key

**作用:**在v-for中,为每个节点设置key可以使它跟踪每个节点的身份。进行比较时,会基于key的变化重新排列元素顺序,从而重用和重新排序现有元素,并且移除key不存在的元素,方便让Vnode在diff过程中找到对应的节点,然后复用。

**好处:**减少DOM操作,减少diff和渲染所需事件,提高性能

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant