从 vue 源码看问题 —— vue 中如何进行 patch ?(二)

简介: 从 vue 源码看问题 —— vue 中如何进行 patch ?

invokeDestroyHook()

/*
    销毁节点:
      执行组件的 destroy 钩子,即执行 $destroy 方法 
      执行组件各个模块(style、class、directive 等)的 destroy 方法
      如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
   */
  function invokeDestroyHook(vnode) {
    let i, j
    const data = vnode.data
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    }
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j])
      }
    }
  }
复制代码

sameVnode()

function sameVnode(a, b) {
  return (
    // key 必须相同,注意 undefined === undefined 也为 true
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        // 标签相同
        a.tag === b.tag &&
        // 都属于注释节点
        a.isComment === b.isComment &&
        // 都有 data 属性
        isDef(a.data) === isDef(b.data) &&
        // input 标签类型要一致
        sameInputType(a, b)
      ) || (
        // 异步占位符节点
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
复制代码

emptyNodeAt()

// 为元素(elm) 创建一个空的 vnode
  function emptyNodeAt(elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
  }
复制代码

createElm()

// 基于 vnode 创建整棵 DOM 树,并插入到父节点上
  function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    vnode.isRootInsert = !nested // for transition enter check
    /*
       1、如果 vnode 是一个组件,则执行 init 钩子,创建组件实例并挂载,
          然后为组件执行各个模块的 create 钩子
          如果组件被 keep-alive 包裹,则激活组件
       2、如果是一个普通元素,则什么也不做
   */
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    // 获取 data 对象
    const data = vnode.data
    // 获取所有的孩子节点
    const children = vnode.children
    const tag = vnode.tag
    // tag 有值
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        // 未知标签
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      // 创建新节点
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)
      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        // 递归创建所有子节点(普通元素、组件)
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // 将节点插入父节点
        insert(parentElm, vnode.elm, refElm)
      }
      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {
      // 注释节点,创建注释节点并插入父节点
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      // 文本节点,创建文本节点并插入父节点
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
复制代码

createComponent()

/**
   * 如果 vnode 是一个组件,则执行 init 钩子,创建组件实例,并挂载
   * 然后为组件执行各个模块的 create 方法
   * 
   * @param {*} vnode 组件新的 vnode
   * @param {*} insertedVnodeQueue 数组
   * @param {*} parentElm oldVnode 的父节点
   * @param {*} refElm oldVnode 的下一个兄弟节点
   * @returns 如果 vnode 是一个组件并且组件创建成功,则返回 true,否则返回 undefined
   */
  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    // 获取 vnode.data 对象
    let i = vnode.data
    if (isDef(i)) {
      // 判断 组件实例是否已经存在 && 被 keep-alive 包裹
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      /*
        执行 vnode.data.init 钩子函数
        1. 如果是被 keep-alive 包裹的组件:
             则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
        2. 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
      */
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
      /*
        调用 init hook 之后,如果 vnode 是子组件,它应该创建一个子实例并挂载它
        若子组件还设置 vnode.elm 占位符,在这种情况下,只需返回元素就可以了
      */
      if (isDef(vnode.componentInstance)) {
        /*
         如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例并进行挂载
         这时就可以给组件执行各个模块的 create 钩子
        */
        initComponent(vnode, insertedVnodeQueue)
        // 将组件的 DOM 节点插入到父节点内
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          // 组件被 keep-alive 包裹的情况,激活组件
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }
复制代码

insert()

// 向父节点插入节点 
  function insert(parent, elm, ref) {
    if (isDef(parent)) {
      // 存在下一个兄弟节点
      if (isDef(ref)) {
        // 判断当前传入的父节点和下一个兄弟节点的父节点是否相同
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        // 不存在下一个兄弟节点
        nodeOps.appendChild(parent, elm)
      }
    }
  }
复制代码

removeVnodes()

// 移除指定索引范围(startIdx —— endIdx)内的节点 
  function removeVnodes(vnodes, startIdx, endIdx) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (isDef(ch)) {
        if (isDef(ch.tag)) {
          removeAndInvokeRemoveHook(ch)
          invokeDestroyHook(ch)
        } else { // Text node
          removeNode(ch.elm)
        }
      }
    }
  }
复制代码

patchVnode()

/*
    更新节点:
      1. 全量的属性更新
      2. 如果新老节点都有孩子,则递归执行 diff
      3. 如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
      4. 如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
      5. 更新文本节点
  */ 
  function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 若老节点和新节点相同,直接返回
    if (oldVnode === vnode) {
      return
    }
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    const elm = vnode.elm = oldVnode.elm
    // 异步占位符节点
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // 跳过静态节点的更新
    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      /*
        新旧节点都是静态的而且两个节点的 key 一样,并且
        新节点被 clone 或者 新节点有 v-once 指令
        则重用这部分节点
      */   
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
    // 执行组件的 prepatch 钩子
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    // 老节点的孩子
    const oldCh = oldVnode.children
    // 新节点的孩子
    const ch = vnode.children
    // 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
    if (isDef(data) && isPatchable(vnode)) {
      // 执行新节点所有的属性更新
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      // 新节点不是文本节点
      if (isDef(oldCh) && isDef(ch)) {
        // 如果新老节点都有孩子,则递归执行 diff 过程
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 新的孩子存在,老的孩子不存在,则创建这些新孩子节点
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 老的孩子存在,新的孩子不存在,则移除这些老孩子节点
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 老节点是文本节点,则将文本内容置空
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 新节点是文本节点,则更新文本节点
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }


目录
相关文章
|
5天前
|
JavaScript 前端开发 安全
Vue响应式设计
【5月更文挑战第30天】Vue响应式设计
25 1
|
2天前
|
JavaScript API
vue组合式和选项式
vue组合式和选项式
3 2
|
4天前
|
JavaScript 程序员 网络架构
vue路由从入门到进阶 --- 声明式导航详细教程
vue路由从入门到进阶 --- 声明式导航详细教程
vue路由从入门到进阶 --- 声明式导航详细教程
|
4天前
|
资源调度 JavaScript UED
vue路由的基础知识总结,vueRouter插件的安装与使用
vue路由的基础知识总结,vueRouter插件的安装与使用
|
4天前
|
JavaScript
|
5天前
|
编解码 JavaScript API
Vue在移动端的表现如何?
【5月更文挑战第30天】Vue在移动端的表现如何?
12 2
|
5天前
|
JavaScript 前端开发 API
Vue与其他框架的对比优势
【5月更文挑战第30天】Vue与其他框架的对比优势
11 1
|
5天前
|
JavaScript
Vue常用知识点总结
Vue常用知识点总结
13 0
|
6天前
|
JavaScript Java 测试技术
基于vue和微信小程序的校园自助打印系统+springboot+vue.js附带文章和源代码设计说明文档ppt
基于vue和微信小程序的校园自助打印系统+springboot+vue.js附带文章和源代码设计说明文档ppt
26 7
|
6天前
|
JSON JavaScript 前端开发