zoukankan      html  css  js  c++  java
  • Vue.js源码解析-Vue初始化流程之动态创建DOM

    前言

    各位道友大家好,我是LSF,在上一篇博文 中,分析了Vue初始化的整体流程,最后到了 update 动态创建 DOM 阶段。接下来这篇博文,会对这个流程进行分析,重点需要掌握 createElm 函数的执行逻辑。

    一、_update 如何判断是初始化还是更新操作?

    _update 是在Vue实例化之前,通过prototype混入的一个实例方法。主要目的是将vnode转化成真实DOM,它定义在 core/instance/lifecycle.js 文件中。

      Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this  // vm -> this
        const prevEl = vm.$el
        
        // 保存上一个vnode。
        const prevVnode = vm._vnode  
    
        // 设置 activeInstance 当前活动的vm,返回方法。
        const restoreActiveInstance = setActiveInstance(vm)  
    
        vm._vnode = vnode  // 赋值 _vnode 属性为新传入的 vnode。
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
          // initial render  初始化渲染,如果有子组件,会递归初始化
          vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
          // updates 更新
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
    
        // activeInstance 恢复到当前的vm
        restoreActiveInstance()  
    
        // update __vue__ reference
        if (prevEl) {
          prevEl.__vue__ = null
        }
        if (vm.$el) {
          vm.$el.__vue__ = vm
        }
        // if parent is an HOC, update its $el as well
        if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
          vm.$parent.$el = vm.$el
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
      }
    
    

    代码中可以看到,通过 prevVnode 是否为 null 来判断的是否是初始化 patch。由于是初始化操作,开始的时候 vm._vnode 没有被赋值成 vnode,从而 vm._vnode 为 null。所以代码的执行逻辑会走到初始化 patch。

    二、patch

    2.1 patch 定义

    web端的 Vue.prototype.__patch__ 方法,它定义的入口在 src/platforms/web/runtime/index.js 文件中。

    import { patch } from './patch'
    ...
    // install platform runtime directives & components
    extend(Vue.options.directives, platformDirectives)
    extend(Vue.options.components, platformComponents)
    
    // install platform patch function
    // 安装web端的 patch 方法。
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    ...
    

    如果是浏览器环境下,被赋值为 patch 方法,该方法定义在 src/platforms/web/runtime/patch.js中。如果是非浏览器环境,patch 被赋值成一个空函数。

    /* @flow */
    
    import * as nodeOps from 'web/runtime/node-ops'
    import { createPatchFunction } from 'core/vdom/patch'
    import baseModules from 'core/vdom/modules/index'
    import platformModules from 'web/runtime/modules/index'
    
    // the directive module should be applied last, after all
    // built-in modules have been applied.
    const modules = platformModules.concat(baseModules)
    // 调用 createPatchFunction 函数,返回 patch。
    export const patch: Function = createPatchFunction({ nodeOps, modules })
    
    

    通过代码可以看到,最终 vue 是调用了 createPatchFunction 函数,它定义在 src/core/vdom/patch.js 中。createPatchFunction 函数内部定义了如 emptyNodeAt、removeNode、createElement、createChildren 等一系列的辅助函数,通过这些辅助函数,完成了对 patch 函数的代码逻辑的封装。

    2.2 初始化的 patch

    创建Vue实例,或者组件实例的,patch 都会被执行。

    • 如果是创建vue实例执行 patch

      • isRealElement:判断是否是真实的DOM节点。

      • patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly):负责DOM的更新。

      • oldVnode = emptyNodeAt(oldVnode):对容器DOM进行vnode的转化。

      • createElm():创建新节点,初始化创建需要重点关注的函数。

    • 如果是创建组件实例执行的 patch

      • isInitialPatch:用户判断子组件否初次执行 patch,进行创建。

      • insertedVnodeQueue:新创建子组件节点,组件 vnode 会被push到这个队列中。

      • invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)。

    具体代码注释如下

    export function createPatchFunction (backend) {
    ...
    
      return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // 如果新的 vnode 为空,调用 destory 钩子,销毁oldVnode
        if (isUndef(vnode)) {
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }
    
        // 用户判断子组件否初次执行 patch,进行创建。
        let isInitialPatch = false
    
        // 新创建子组件节点,组件 vnode 会被push到这个队列中
        const insertedVnodeQueue = []
    
        if (isUndef(oldVnode)) {
          // empty mount (likely as component), create new root element
          // 空挂载(可能作为组件),创建新的根元素
          isInitialPatch = true
          // 创建组件节点的子元素
          createElm(vnode, insertedVnodeQueue)
        } else {
          // 1.作为判断是否是真实的DOM节点条件
          const isRealElement = isDef(oldVnode.nodeType)
          if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // patch existing root node
            // 更新操作
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
          } else {
            if (isRealElement) {
              ...
              // either not server-rendered, or hydration failed.
              // create an empty node and replace it
              // 2. 传入的容器DOM(如 el: "#app"),会在这里被转化成 vnode。
              oldVnode = emptyNodeAt(oldVnode)
            }
    
            // replacing existing element
            const oldElm = oldVnode.elm
            const parentElm = nodeOps.parentNode(oldElm)
    
            // create new node
            // 3. 创建新节点
            createElm(
              vnode,
              insertedVnodeQueue,
              // extremely rare edge case: do not insert if old element is in a
              // leaving transition. Only happens when combining transition +
              // keep-alive + HOCs. (#4590)
              oldElm._leaveCb ? null : parentElm,
              nodeOps.nextSibling(oldElm)
            )
    
            // update parent placeholder node element, recursively
            // 递归的更新父占位节点元素。
            if (isDef(vnode.parent)) {...
            }
    
            // destroy old node
            // 销毁旧节点
            if (isDef(parentElm)) {
              removeVnodes([oldVnode], 0, 0)
            } else if (isDef(oldVnode.tag)) {
              invokeDestroyHook(oldVnode)
            }
          }
        }
    
        // 调用 insertedVnodeQueue 队列中所有子组件的 insert 钩子。
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
        return vnode.elm
      }
    
    

    三、createElm 动态创建DOM

    createElm 函数是动态创建 DOM 的核心,作用是通过 vnode 创建真实的 DOM,并插入到它的父 DOM 节点中。它定义在 src/core/vdom/patch.js
    的 createPatchFunction 方法中。createElm 内部创建 DOM 的主要判断逻辑,可以概括为下面几种情况。

    1、如果创建组件节点

    • 如果碰到子组件标签,走创建组件节点逻辑。

    • 创建完成,插入到父亲元素中。

    2、如果创建标签元素节点

    • 如果 vnode.tag 不为空,先创建标签元素, 赋值 vnode.elm 进行占位。

    • 调用 createChildren 创建子节点,最终这些子节点会 append 到 vnode.elm 标签元素中。

    • 将 vnode.elm 标签元素插入到父亲元素中。

    3、如果创建注释节点

    • 如果 vnode.isComment 不为空,创建注释节点,赋值 vnode.elm。

    • 将注释节点插入到父亲元素中。

    4、如果创建文本节点

    • 上面三种情况都不是,则创建文本节点,赋值 vnode.elm。

    • 将文本节点插入到父亲元素中。

    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、如果碰到子组件标签,走创建组件节点逻辑,插入父亲节点。
        if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
          return
        }
    
        const data = vnode.data
        const children = vnode.children
        const tag = vnode.tag
    
        // 2、如果是标签标记,先创建标签元素进行占位。
        //    调用 createChildren 创建子节点(递归调用createElm)。
        //    将标签元素,插入父亲元素。
        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
              )
            }
          }
    
          // 通过上面的tag,创建标签元素,赋值给 vnode.elm 进行占位
          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)) {
          // 3、创建注释节点,插入到父亲元素
          vnode.elm = nodeOps.createComment(vnode.text)
          insert(parentElm, vnode.elm, refElm)
        } else {
          // 4、创建文本节点,插入到父亲元素
          vnode.elm = nodeOps.createTextNode(vnode.text)
          insert(parentElm, vnode.elm, refElm)
        }
      }
    

    下面对动态创建的几种情况分别进行说明。

    3.1 创建组件节点

    创建组件节点和 vue 的组件系统息息相关,这里先不具体展开,之后的博文中单独分析 vue 组件系统。只需要记住 vue 模板里的子组件初始化创建,是在这一步进行即可。

    function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
      ...
      // 创建组件节点
      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
      }  
      ...
    }  
    

    createComponent 这个方法也定义在 src/core/vdom/patch.js 的 createPatchFunction 的方法中,这里先简单的介绍一下这个方法的内部逻辑。

    • 通过 vnode.data 中是否包含组件相关的 hook,来判断当前 vnode 是否是子组件 vnode(组件的 vnode,会包含 init 等钩子方法)。

    • 调用 init,执行子组件的初始化流程,创建子组件实例,进行子组件挂载。

    • 将生成的子组件 DOM 赋值给 vnode.elm。

    • 通过 vnode.elm 将创建的子组件节点,插入到父亲元素中。

      function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
        let i = vnode.data
        if (isDef(i)) {
          const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    
          // 调用组件 init 钩子后,会执行子组件的**初始化流程**
          // 创建子组件实例,进行子组件挂载。
          if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */)
          }
          
          // after calling the init hook, if the vnode is a child component
          // it should've created a child instance and mounted it. the child
          // component also has set the placeholder vnode's elm.
          // in that case we can just return the element and be done.
    
          // 如果是组件实例,将创建 vnode.elm 占位符
          // 将生成的组件节点,插入到父亲元素中
          if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue)
            insert(parentElm, vnode.elm, refElm)
            if (isTrue(isReactivated)) {
              reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
            }
            return true
          }
        }
      }
    
    • 如果是创建组件节点,并且成功,createComponent 函数返回 true。createElm 函数执行到 return。

    • 如果是其他类型的节点,createComponent 函数返回 undefined,createElm 函数,会向下执行创建其他类型节点(标签元素、注释、文本)的代码逻辑。

    综上所述,createElm 函数执行,只要碰到组件标签,会递归的去初始化创建子组件,简图如下所示(绿色线路部分)。

    再调用 insert(parentElm, vnode.elm, refElm),将生成的组件节点插入到父亲元素中(遵从先子后父)。

    3.2 创建标签元素节点

    createElm 判断如果 vnode 不是组件的 vnode,它会判断是否是标签元素,从而进行创建标签元素节点的代码逻辑, 主要逻辑分析如下。

    • vnode.tag 标签属性存在,通过 tag 创建对应的标签元素,赋值给 vnode.elm 进行占位。

    • 调用 createChildren 创建子节点(遍历子vnode,递归调用 createElm 函数)。

    • 将创建的标签元素节点,插入父亲元素。

    function createElm (
        vnode,
        insertedVnodeQueue,
        parentElm,
        refElm,
        nested,
        ownerArray,
        index
      ) {
    
        ...
    
        const data = vnode.data
        const children = vnode.children
        const tag = vnode.tag
    
        // 2、如果是标签标记,先创建标签元素进行占位。
        //    调用 createChildren 创建子节点(遍历子vnode,递归调用 createElm 函数)。
        //    将标签元素,插入父亲元素。
    
        // 如果标签属性不为空
        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
              )
            }
          }
    
          // 通过上面的tag,创建标签元素,赋值给 vnode.elm 进行占位
          vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            : nodeOps.createElement(tag, vnode)
    
          setScope(vnode)
    
          /* istanbul ignore if */
          if (__WEEX__) {
            ...
          } else {
    
            // 创建子节点
            createChildren(vnode, children, insertedVnodeQueue)
            if (isDef(data)) {
              // vnode.data 不为空,调用所有create的钩子。
              invokeCreateHooks(vnode, insertedVnodeQueue)
            }
            // 将创建的标签元素节点,插入父亲元素
            insert(parentElm, vnode.elm, refElm)
          }
    
          if (process.env.NODE_ENV !== 'production' && data && data.pre) {
            creatingElmInVPre--
          }
        
        ...
    
      }
    
    

    createChildren 函数主要逻辑如下

    • 如果 vnode.children 是子 vnode 数组,遍历 vnode.children 中的每个子 vnode,递归的调用了 createElm 函数,创建对应的子节点,并插入到父亲元素中(此时的父亲元素 parentElm 为 vnode.elm)。

    • 如果 vnode.text 为空字符串。就创建一个空文本节点,插入到 vnode.elm 元素中。

      function createChildren (vnode, children, insertedVnodeQueue) {
        if (Array.isArray(children)) {
          if (process.env.NODE_ENV !== 'production') {
            checkDuplicateKeys(children)
          }
          // 遍历子vnode数组,递归调用 createElm 
          for (let i = 0; i < children.length; ++i) {
            createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
          }
        } else if (isPrimitive(vnode.text)) {
          // 创建空文本节点,appendChildren 到 vnode.elm 中
          nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
        }
      }
    

    上面已经创建完成子标签节点,invokeCreateHooks 调用执行所有子组件相关的 create 钩子。这个方法createElm、
    initComponent 中都会被调用。如果在 initComponent 中调用,说明创建的子节点中有组件节点,还会将组件 vnode 添加到 insertedVnodeQueue 队列中。

        // createElm 中 
        if (isDef(data)) {
          // vnode.data 不为空,调用所有create的钩子。
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
    
      function initComponent (vnode, insertedVnodeQueue) {
        if (isDef(vnode.data.pendingInsert)) {
          insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
          vnode.data.pendingInsert = null
        }
        vnode.elm = vnode.componentInstance.$el
        if (isPatchable(vnode)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
          setScope(vnode)
        } else {
          // empty component root.
          // skip all element-related modules except for ref (#3455)
          registerRef(vnode)
          // make sure to invoke the insert hook
          insertedVnodeQueue.push(vnode)
        }
      }
    
      function invokeCreateHooks (vnode, insertedVnodeQueue) {
       // 所有组件相关的create钩子都调用
       // initComponent调用的话,还会将各个子组件的 vnode 添加到 insertedVnodeQueue 队列中。
        for (let i = 0; i < cbs.create.length; ++i) {
          cbs.create[i](emptyNode, vnode)
        }
        i = vnode.data.hook // Reuse variable
        if (isDef(i)) {
          if (isDef(i.create)) i.create(emptyNode, vnode)
          if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
        }
      }
    
    

    综上所述,createElm 创建标签节点内部通过 createChildren 实现了对 createElm 的遍历递归调用,实现了深度优先遍历,简图如下所示(蓝色线路部分)。

    再调用 insert(parentElm, vnode.elm, refElm),将生成的元素节点插入到父亲元素中(遵从先子后父)。

    3.3 创建注释节点

    如果不是创建组件节点和元素节点,vue 就通过 vnode.isComment 属性判断,是否创建注释节点。创建完成之后,插入到父亲元素中(遵从先子后父)

      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    

    3.3 创建文本节点

    如果不是创建组件节点、元素节点、注释节点,vue 就创建文本节点,创建完成之后,插入到父亲元素中(遵从先子后父)。

      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    

    四、销毁旧节点

    通过前面章节的分析,知道了 patch 函数,主要通过 createElm 动态的创建好了 DOM,并且已经成功添加到了旧DOM的后面,所以下一步操作,就只需要将旧 DOM 进行删除即可。

      // destroy old node
      // 销毁旧的节点(如 el: "app" 这个DOM)
      // 创建完成的整个dom会append到 el: "app", 的父亲元素(如 parentElm 为 body)上
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    

    五、总结

    • vue 通过调用 patch 函数进行初始化 DOM 的创建。

    • patch 的关键是理解内部 createElm 这个函数,它会判断组件、元素、注释、文本这些类型的节点,来创建相应的DOM,完成之后添加到父元素。

    • vue 的组件系统实现,关键在于动态创建组件节点的逻辑当中。

    • 新 DOM 创建添加过程是从子到父的,而组件的实例化是从父到子的。

    在这儿,特感谢大家观看!如有不妥之处,还请大家批评指正,大家可以联系我,或在下方评论,谢谢大家!

    本文来自博客园,作者:骆三疯,转载请注明原文链接:https://www.cnblogs.com/elmluo/p/14943262.html

  • 相关阅读:
    java 的三种代理模式 (二)——子函数切面
    王者荣耀为什么不使用微服务架构,服务的极简主义,为什么交易网关使用redis做持久
    tcp_syncookies 半连接
    tcp_tw_recycle tcp_tw_reuse与timewait【yetdone】
    动态代理,没有被代理对象
    一次jstack解决update停顿
    动态代理反向
    注解的继承
    51单片机状态机键盘检测
    28335scififo中断接收与发送
  • 原文地址:https://www.cnblogs.com/elmluo/p/14943262.html
Copyright © 2011-2022 走看看