update和patch

来回顾一下之前提到的mountComponent方法,它有这样一段代码:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

在之前的章节中,我们介绍了_render方法及其createElementcreateComponent的逻辑,知道_render返回的是一个VNode树形结构。而_update方法的作用就是利用这个VNode树形结构,来生成真正DOM节点。

这一章节,我们来分析一下update方法的实现以及其中patch的逻辑。

$forceUpdate

在正式介绍update/patch之前,我们先来认识一个API方法:$forceUpdate,这个方法是用来强制组件重新渲染的。在开发Vue应用的时候,我们有可能遇到过虽然我们的响应式数据更改了,但组件并没有正确渲染。当遇到这种情况的时候,我们可以调用$forceUpdate来强制组件重新进行渲染。它的实现代码如下:

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

我们可以看到$forceUpdate方法的代码非常简单,它首先判断了vm._watcher是否存在,也就是判断当前组件的render watcher是否存在,如果存在则调用render watcherupdate方法。在调用update方法后,这里的过程就跟派发更新的过程相同 ,因为这里是render watcher,因此它最后会调用下面这段代码,也就是我们这个章节的核心update/patch

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

update

_update其实是一个内部私有方法,它的调用时机有两个:初始化挂载阶段派发更新阶段,其代码是在lifecycleMixin方法中被定义的,如下:

export function lifecycleMixin (Vue: Class<Component>) {
  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.
    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.
  }
}

_update代码并不是很多,其核心就是调用__patch__方法。在介绍__patch__之前,为了更好的理解后续相关的逻辑,我们先介绍几个小的知识点。

  • activeInstance:从命名可以看出来,它的意思是当前激活的实例对象。我们知道组件渲染是一个递归的过程,渲染顺序是先子后父。那么在这种递归渲染的过程中,我们必须正确保证一对引用关系:当前渲染的组件实例以及其父级组件实例。activeInstance就是当前渲染的组件实例,它是一个模块变量:
export let activeInstance: any = null

_update方法中,它使用setActiveInstance来设置当前激活的实例,使用restoreActiveInstance来恢复,setActiveInstance方法定义如下:

const restoreActiveInstance = setActiveInstance(vm)
export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

我们可以看到在setActiveInstance中,它首先定义了闭包变量保存了当前激活的实例,然后把activeInstance设置为当前的参数vm,最后返回了一个函数,这个函数的目的是用来恢复activeInstance到上一个缓存下来的激活实例,也就是调用restoreActiveInstance方法。

既然当前渲染的实例已经解决了,那么我们来看一下父级在这个过程中是如何保证的。在initLifecycle的过程中,有这样一段代码:

export function initLifecycle (vm: Component) {
  const options = vm.$options
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
  // ...省略代码
}

initLifecycle方法执行的过程中,通过while循环来保存parentchildren父子关系,对于父级来说,$children中存储了它所有的子节点,对于子级来说,可以通过vm.$parent获取到它的父级。

  • _vnode和$vnode_vnode$vnode也是一对父子关系,其中_vnode表示当前VNode节点,$vnode表示其父节点。我们来回顾一下_render方法,它有这样几段代码:
Vue.prototype._render = function () {
  // ...省略代码
  const { render, _parentVnode } = vm.$options
  vm.$vnode = _parentVnode
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  }
  vnode.parent = _parentVnode
  return vnode
}

在介绍完这两组对应关系后,我们来看一下最核心的__patch__方法的实现,这个方法是多平台公用方法,它在src/platforms/web/runtime/index.jssrc/platforms/weex/runtime/index.js文件中都有定义,我们主要看第一种,其定义代码如下:

import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

在以上代码中,它使用inBrowser判断了当前是否处于浏览器环境,如果是则赋值为path,否则就是noop空函数。这样判断是因为Vue还可以运行在node服务端。接下来,我们在path.js中看一下path方法是如何定义的:

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'

const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })

在这里我们可以看到,patch赋值的是createPatchFunction方法的调用结果,我们先不看createPatchFunction是如何定义的,我们先看来一下它传递的参数。

  • nodeOps: nodeOps是引入了web/runtime/node-ops.js文件中的内容,我们选取一部分来举例说明它到底是什么。
export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

我们可以发现,node-ops.js文件中封装的方法,实际上就是对真实DOM操作的一层封装,传递nodeOps的目的是为了在虚拟DOM转成真实DOM节点的过程中提供便利。

  • modules: modulesplatformModulesbaseModules两个数组合并的结果,其中baseModules是对模板标签上refdirectives各种操作的封装。platformModules是对模板标签上classstyleattr以及events等操作的封装。

小结

  1. update这一节,我们知道了首次渲染和派发更新重新渲染的patch是有一点差异的,其差异为首次渲染时提供的根节点是一个真实的DOM元素,在派发更新重新渲染时提供的是一个VNode,这里差异的逻辑是在下面这段代码中:
if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
}
  1. 在父子组件递归渲染的时候,首先渲染子组件,子组件渲染完毕后才会去渲染父组件,在这个递归的过程中,activeInstance始终指向当前渲染的组件实例。同时根据父子组件递归渲染的顺序,我们可以知道父子组件关于createmount两个生命周期的执行顺序:
// parent beforeCreate
// parent created
// parent beforeMount
// child beforeCreate
// child created
// child beforeMount
// child mounted
// parent mounted
  1. render函数执行会得到一个VNode的树形结构,update的作用就是把这个虚拟DOM节点树转换成真实的DOM节点树。因此结合前面介绍的所有内容,我们可以得到一个从实例初始化到最终渲染成真实DOM到视图的一个主线流程图。
流程图

patch

在上一个章节,我们遗留了一个createPatchFunction方法还没有分析,在patch这个章节,我们主要任务就是弄清楚createPatchFunction的实现原理。

因为在v2.6.11版本中,createPatchFunction方法代码非常多,因此我们采取分段来说明,建议一边看文章一边对照源码学习。

hooks钩子函数

createPatchFunction最开始,它首先处理了一些hooks钩子函数,代码如下:

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

注意:这里定义的hooks与我们组件的生命周期钩子函数很相似,但它并不是处理组件生命周期的,它们是在VNode钩子函数执行阶段或者其它时机调用的,例如:在VNode插入的时候,需要执行created相关的钩子函数,在VNode移除的时候,需要执行remove/destroy相关的钩子函数。

代码分析:

  • 首先通过解构获取到modules,它是一个数组,每个数组元素都有可能定义createupdateremove以及destroy等钩子函数,它可能是下面这样:
const modules = [
  {
    created: function () {},
    update: function () {}
  },
  {
    update: function () {},
    remove: function () {}
  },
  {
    remove: function () {},
    destroy: function () {}
  }
]
  • 解构获取到modules后,使用for循环来遍历modules,目的是要把hooks当做keyhooks的函数当做value,循环遍历完毕后,它可能是下面这样:
// 遍历前
const cbs = {}

// 遍历后
const cbs = {
  create: [ function () {}, function () {} ],
  activate: [],
  update: [ function () {}, function () {}, function () {} ],
  remove: [ function () {} ],
  destroy: [ function () {} ]
}
  • create钩子函数为例,它会在合适的时机调用,调用create的代码如下:
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)
  }
}

invokeCreateHooks方法中,它通过for来遍历cbs.create钩子函数数组,然后依次调用这里面的每一个方法。在方法的最后,它还调用了VNode的2个钩子函数,在createComponent章节中我们提到过vnode.data.hook

const componentVNodeHooks = {
  init: function () {},     // 初始化时触发
  prepatch: function () {}, // patch之前触发
  insert: function () {},   // 插入到DOM时触发
  destroy: function () {}   // 节点移除之前触发
  ...
}

返回patch函数

我们回顾一下之前的代码:

export const patch: Function = createPatchFunction({ nodeOps, modules })

我们可以发现,patch赋值的是createPatchFunction方法调用的结果,那么我们看一下createPatchFunction方法内部是如何定义这个返回函数的:

export function createPatchFunction (backend) {
  // ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) { 
      // ...
    } else {
      // ...
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

patch返回函数的最开始,它判断了vnode是否为undefined或者null,如果是并且oldVnode条件判断为真,那么它会调用invokeDestroyHook。执行invokeDestroyHook是为了触发子节点的销毁动作,那么很显然这段代码会在组件销毁的时候执行,我们可以在$destroy方法中看到下面这段代码($destroy方法我们会在组件生命周期小节中介绍):

Vue.prototype.$destroy = function () {
  // ...
  vm.__patch__(vm._vnode, null)
  // ...
}

判断完vnode后,我们发现它对oldVnode也进行了判断,因此会有一个if/else分支逻辑。那么什么时候走if分支逻辑?什么时候走else分支逻辑?

当使用isUndef方法对oldVnode逻辑判断为真时,证明此时的oldVnode没有,那么它表示组件是首次渲染,因此会走if分支逻辑。当挂载根实例或者派发更新的时候,此时的oldVnode存在,它会走else分支逻辑。由于这两块的分支逻辑相对来说比较复杂,因此我们会在后续单独划分模块说明。

在返回函数patch的最后,它调用了invokeInsertHook,这个方法的目的是为了触发VNodeinsert钩子函数,其代码如下:

 function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}

对于VNodeinsert钩子函数而言,它主要做的事情就是触发组件的mounted钩子函数。对于组件一系列生命周期,我们会在下一个章节中介绍,这里只做了解。

根实例patch

我们回顾一下_update方法,它有这样一段代码:

if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
}

在首次渲染的时候,对于根实例而言,它传递的是一个真实的DOM节点,也就是说在patch返回函数中,第一个参数oldVnode不仅为真值,并且它还是一个真实的DOM节点。因此在patch返回函数中,它走下面这些代码:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  // ...
  if (isUndef(oldVnode)) {
    // ...
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // ...
    } else {
      if (isRealElement) {
        // ...
        // 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)
      )
      // ...

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

因为oldVnode参数为一个真实的DOM节点,所以isRealElement变量为true,它会调用emptyNodeAt。这个方法的作用是把一个真实DOM转换成一个VNode实例,其代码如下:

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

紧接着调用createElm方法,这个方法的主要作用就是把VNode实例转换成真实的DOM节点,我们会在后面的章节中单独介绍createElm,这里只做了解。

在代码最后,它通过判断parentElm是否为真,来调用不同的方法,我们以Vue-Cli脚手架生成的App.vue组件为例。对于根实例而言,它的挂载节点为<div id='app'></div>,因此此时的parentElbody节点,条件判断为真,因此调用removeVnodes方法,我们看一下这个方法的实现代码:

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)
      }
    }
  }
}
function removeAndInvokeRemoveHook (vnode, rm) {
  if (isDef(rm) || isDef(vnode.data)) {
    let i
    const listeners = cbs.remove.length + 1
    if (isDef(rm)) {
      // we have a recursively passed down rm callback
      // increase the listeners count
      rm.listeners += listeners
    } else {
      // directly removing
      rm = createRmCb(vnode.elm, listeners)
    }
    // recursively invoke hooks on child component root node
    if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
      removeAndInvokeRemoveHook(i, rm)
    }
    for (i = 0; i < cbs.remove.length; ++i) {
      cbs.remove[i](vnode, rm)
    }
    if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
      i(vnode, rm)
    } else {
      rm()
    }
  } else {
    removeNode(vnode.elm)
  }
}

我们可以看到,在removeVnodes方法中,它会删除旧的id等于app的节点,然后创建一个新的idapp的节点。看到这里,我们就能明白Vue官网上面这样一段话了: 所有的挂载元素会被 Vue 生成的 DOM 替换。因此不推荐挂载 root 实例到 html 或者 body 上

组件patch

patch返回函数中,对于组件的首次渲染和派发更新渲染处理逻辑是不相同的,其中的差别体现在如下代码:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  // ...
  if (isUndef(oldVnode)) {
    // 组件首次渲染
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 组件派发更新渲染
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // ...
    }
  }
}

在组件update/patch章节,我们不会分析patchVnode方法,而是把它放在后面编译章节中。在这里,我们只需要看组件首次渲染即可。对于组件的首次渲染而言,依然还是调用createElm方法,不过这里要注意,它只传递了2个参数。

createElm

在上面两个小节,我们都提到过createElm,也知道它的主要作用是把VNode实例转换成真实的DOM节点,在这个小节我们来详细介绍createElm方法。

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  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__) {
      // ...
    } 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方法的精简代码,它有几个主要的步骤:创建组件节点创建普通节点创建注释节点以及创建文本节点

  • 创建组件节点:在createElm方法的最开始,它通过调用createComponent方法尝试创建一个组件节点,如果vnode是一个组件vnode则返回true,并且提前return终止createElm方法,否则返回false。我们来看一下createComponent方法的实现代码:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    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.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

对于组件vnode而言,判断其data条件是满足的,条件满足以后它处理了i,将其赋值为i.init。其实这里的init就是组件vnodeinit钩子函数,其代码如下:

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  }
  // ...
}

i方法执行时,它会在代码中通过createComponentInstanceForVnode来创建Vue实例,然后调用实例的$mount方法来挂载子组件。因为在这里调用了子组件的$mount方法,所以子组件会从头开始递归走一遍update/patch的过程,当子组件处理完毕后,会把子组件对应的真实DOM节点树插入到父级中该组件占位符的位置。这样经过一层层递归,反复执行update/patch的方式就可以构建成一个完整的组件树形结构。

  • 创建普通节点:如果VNode实例的tag属性为真,则首先校验一遍tag是否为正确的标签,如果不是则会提示非法标签。如果是则先调用createChildren方法处理其子节点,子节点处理完毕后再调用insert直接插入到父级中。
function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    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)))
  }
}

我们可以看到在createChildren方法中,它首先判断了VNodechildren子节点是否为数组,如果不是则直接创建一个文本节点插入到父级中,如果是则遍历子节点数组,然后递归调用createElm。根据以上分析,我们可以知道创建节点的过程是一个深度优先遍历的过程,子节点首先会被创建然后插入到其父级下面,最后才是父节点。因为子节点会优先创建并被插入,因此子节点会首先调用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)
    }
  }
}

其中insertBeforeappendChild是对真实DOM的一层封装。

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}
  • 创建注释节点和创建文本节点:创建注释节点和创建文本节点非常简单,它们分别调用了createCommentcreateTextNode,其实这两个方法是对原始DOM操作的一层封装而已:
export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}
export function createComment (text: string): Comment {
  return document.createComment(text)
}
最后更新时间:
贡献者: wangtunan