js拖拽元素到另一个元素_Vue为什么要求组件模版只有一个根元素

3b70340e03ccb97d3c4f0250a8ddcbb8.png

Vue为什么要求组件模版只有一个根元素

emmmm~~ 其实自己对vue的认知感觉一般,犹豫了很久才敢尝试解读,或者回答一下这个问题,以下均为个人见解,如有不妥,可以私信或者关注公众号发消息给我,谢谢~

面试题: Vue为什么要求组件模版只有一个根元素?

初见面

开始看到这道题的时候,脑子里第一个想法就是: 嗯?为什么?。然后就开始google这个问题。这才慢慢的发现,原来在2.X之前, Vue是允许在一个模版内存在多个平行的根节点的。emmmm~~ 怪不得会衍生出现在这个问题。
下面是一些吃瓜群众的见解:

因为"树"状数据结构,肯定要有个"根",一个遍历起始点吧,我是这么理解的。 --- 来自某github用户 Vue其实并不知道哪一个才是我们的入口,因为对于一个入口来讲,这个入口就是一个 Vue类Vue需要把这个入口里面的所有东西拿来渲染,处理,最后再重新插入到 dom中。如果同时设置了多个入口,那么 vue就不知道哪一个才是这个‘类’。 --- 来自某github用户
通过这个‘根节点’,来递归遍历整个 vue‘树’下的所有节点,并处理为 vdom,最后再渲染成真正的 HTML,插入在正确的位置。那么这个入口,就是这个树的‘根’,各个子元素,子组件,就是这个树的‘枝叶’,而自然而然地,这棵‘树’,就是指一个 vue实例了。 --- 来自某github用户
Each child component is represented in its parent virtual dom by a single vnode. In the current implementation, the diffing algorithm (responsible for comparing the current with the old virtualDOM and patching differences into the real DOM) can rely on the fact that every vnode of a child component has a single matching HTML element in the real dom, so the next vnode in the virutalDOM after the child component vnode is guaranteed to match the next HTML Element in the real DOM.
(Sidenote about your fiddle: functional components don't have that restriction because the are not represented with a vnode in the parent, since they don't have an instance and don't manage their own virtualdom).
Allowing fragments requires significant changes to that algorithm, since we now would somehow have to keep the parent informed at all times about how many root nodes the child is currently managing in the real DOM, so when the parent re-renders, it knows how many HTML-Elements it has to "skip" to reach the next HTML Element that doesn't belong to the child component, That's a very intricate/complicated piece of code at the heart of Vue, and it is critical for render performance - so it's not only important to make it work correctly but also to make it highly performant ---- 来自 LinusBorg

用我二毛钱的英语水平简单的摘抄一下重点哈: 每个子组件在其父级virtual dom中作为单个vnode,而在diffing的时候, 当前的结构可以保证每个vnode的唯一性。因为当前父级的virtual dom作为一个实例管理着自己的每一个vnode。而且当父级知道自己有多少个vnode的时候便于在diff进行重大更改重渲染的时候,始终可以知道多少根节点的HTML ELEMENT可以跳过。它是渲染性能至关重要的一环。
我们简单的概括的话,可以理解成单个根节点可以更好的提高重新渲染的性能,并且让Vue更好的管理当前的虚拟dom。那么,更新看来是一个突破口,假装我可以看懂源码,扒一扒。

解析(假装.png)

我们知道, 在Vue里面关于更新的两个生命钩子是beforeUpdateupdated。我们看一下。 =。- 其实我觉得我好像在跑题,一点一点的在跑题。

lifecycle

emmm~~ 既然是生命钩子,那么一眼就觉得感觉是它了。/src/core/instance/lifecycle.js, 搜索update的身影。

export function lifecycleMixin (Vue: Class<Component>) {
  // _(:зゝ∠)_  看到了update, 从下面的代码中我们可以看的出来初次渲染和重新渲染都会调用update的,它的作用是把vnode渲染成真实的dom
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // 这个地方我们看到不管是初次渲染还是重新渲染,我们都需要调用vm.__patch__
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    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.
  }

  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  Vue.prototype.$destroy = function () {
    // ...
  }
}
源码地址: https:// github.com/vuejs/vue/bl ob/v2.6.10/src/core/instance/lifecycle.js

vm.__patch__

继续找,在/src/platforms/web/runtime/index.js下找到挂载的节点:

import Vue from 'core/index'
import { devtools, inBrowser } from 'core/util/index'

// 顺着这个import我们找到patch
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
源码地址: https:// github.com/vuejs/vue/bl ob/v2.6.10/src/platforms/web/runtime/index.js

patch

/src/platforms/web/runtime/patch.js找到patch的定义

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)
// 这里我们可以看出来,最终patch是由createPatchFunction来实现的,传入两个参数,分别为nodeOps(一些操作dom的方法)和modules(模块钩子的实现)。继续往上找
export const patch: Function = createPatchFunction({ nodeOps, modules })
源码地址: https:// github.com/vuejs/vue/bl ob/v2.6.10/src/platforms/web/runtime/patch.js

createPatchFunction

/src/core/vdom/patch.js找到createPatchFunction的定义

import VNode, { cloneVNode } from './vnode'

export const emptyNode = new VNode('', {}, [])

// 这里的hook是作为callhook的参数
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  // ...

  let creatingElmInVPre = 0

  // createElm 方法通过虚拟节点创建真实的 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
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.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__) {
        // 我们讨论web所以无视了weex相关代码
      } 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)
    }
  }

  // 联系createElm最终以递归的方式按照先子后父的顺序将节点插入
  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)
      }
    }
  }

  // 创建子元素
  function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(children)
      }
      // 这里其实就是遍历子节点并递归调用createElm,并且每次都会传入父节点
      for (let i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
      }
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }

  ...

  /*
   * 这个地方接收四个参数
   * @params oldVnode: 旧的或者是不存在的节点或dom
   * @params vnode: 执行_render之后的vnode节点
   * @params hydrating: 是否服务端渲染
   * @params removeOnly: transition相关
   * 结合我们现在的场景,那么后面的两个参数都按照false处理
  */
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.

          ...

          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        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)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
源码地址: https:// github.com/vuejs/vue/bl ob/v2.6.10/src/core/vdom/patch.js

未完待续

接下来updateChildren是我们要讲述的重点。篇幅过长留作明天的文章,喜欢的点个在看,关注一下我咩~~

a7f0da26585a5bf9a8e571d27b7c3f5c.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值