Skip to content

Latest commit

 

History

History
251 lines (208 loc) · 8.59 KB

patch——创建dom.md

File metadata and controls

251 lines (208 loc) · 8.59 KB

vdom概述中,我们提到patch方法,可以根据VNode创建dom元素、对新旧VNode对象进行diff操作并更新dom、销毁dom

这里,我们来看第一种情况是如何处理的。

vm.$el = vm.__patch__(
	vm.$el, vnode, hydrating, false /* removeOnly */,
	vm.$options._parentElm,
	vm.$options._refElm
)

当整个vm实例第一次初始化的时候,上面vm.__patch__传入的参数中,vm.$el是挂载的根元素,vnode是根元素对应的虚拟dom元素,hydratingfalsevm.$options._parentElmvm.$options._refElm都是undefined

我们用一个最简单的示例来分析:

<div id="app">
	<p>初始化{{value}}</p>
</div>
<script type="text/javascript">
	new Vue({
		data: {
			value: 'text'
		}
	}).$mount('#app');
</script>

打开src/core/vdom/patch.js文件,我们对照着最下方的patch方法的代码,一一来看:

  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      ...
    } else {
      // oldValue不是VNode,而是真实的dom元素
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
          ...
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        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)
        )

        if (isDef(vnode.parent)) {
          // component root element replaced.
          // update parent placeholder node element, recursively
          let ancestor = vnode.parent
          while (ancestor) {
            ancestor.elm = vnode.elm
            ancestor = ancestor.parent
          }
          if (isPatchable(vnode)) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, vnode.parent)
            }
          }
        }

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

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

如果vnode未定义,若oldVnode有值则销毁vnode,否则返回。

如果oldVnode未定义,isInitialPatch置为true,然后调用createElm

以上都不是我们这次要讨论的东西,我们这里oldVnode是真实的dom元素,所以会走到上面代码else代码块里面的else中,同时isRealElement返回true。所以会先执行oldVnode = emptyNodeAt(oldVnode)

  function emptyNodeAt (elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
  }

该方法会创建一个div元素对应的最简单的vnode实例。parentElmdiv的父级元素,这里也就是body

接着调用了一个很重要的方法createElm,第三个参数注释中说是极其罕见的情况下才会传入null,我们这里是parentElm

  let inPre = 0
  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    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) {
          inPre++
        }
        if (
          !inPre &&
          !vnode.ns &&
          !(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) &&
          config.isUnknownElement(tag)
        ) {
          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)

      if (__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) {
        inPre--
      }
    } 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)
    }
  }

如果当前vnode是一个组件,createComponent方法会初始化该组件,并最终返回true,否则返回undefined。关于自定义组件的初始化等过程,我们之后单独详细说。

如果tag定义了,则先对tag校验。在vdom——VNode中,我们提到创建VNode对象有四种情况。如果是第三种,则会抛出错误。

然后vnode.elm指向真实创建的dom元素。

setScope函数的的作用是为了在使用scoped CSS时,给元素添加相应的属性。

之后,调用createChildren方法:

function createChildren (vnode, children, insertedVnodeQueue) {
	if (Array.isArray(children)) {
	  for (let i = 0; i < children.length; ++i) {
	    createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
	  }
	} else if (isPrimitive(vnode.text)) {
	  nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text))
	}
}

该方法中先判断了children是不是数组,如果是则循环递归调用createElm方法创建每一个子元素。否则,若vnode.text是字符串或数字,也就是说当前节点是文本节点,则添加到vnode.elm上,但是vnode对象上一般elmtext不会同时有。

再回到createElm,如果data值不为空,则调用invokeCreateHooks方法,从函数名中我们可以猜到,这里是调用创建时的一些钩子函数的。

function invokeCreateHooks (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)
  }
}

首先会调用cbscreate数组中添加的方法,这些方法是在createPatchFunction函数一开始定义的:

	export const emptyNode = new VNode('', {}, [])
	const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

	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]])
      }
    }
  }

modules我们在vdom概述中也提到过,它里面保存了一系列对data中传入的属性的处理。主要包括directivesrefattrsclassdomPropsonstyleshow

如果vnode.data.hook存在,如果有create方法,则直接调用,如果有insert方法,则把vnode添加到insertedVnodeQueue数组中。

createElm中最后会调用insert来把当前元素插入到父级元素中。

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      nodeOps.insertBefore(parent, elm, ref)
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

在我们这里parentbodyelm是当前的div元素,refdiv的下一个兄弟元素。所以会调用nodeOps.insertBefore(parent, elm, ref)elm插入到合适的位置。

因为在createChildren方法中,我们递归调用createElm方法,所以会先把子元素都拼装好,最后才把div插入到body上。