浅曦Vue源码-40-挂载阶段-$mount-(28) createElm

(juejin.cn/post/705288… "juejin.cn/post/705288…

一、前情回顾 & 背景

上一篇篇小作文开始介绍 patch 函数,重点讨论的是进行初次渲染的过程,我们把 createPatchFunction 和它返回的 patch 函数进行了简化,只留下能够表达初次渲染过程的代码,具体如下:

其中 patch 函数会调用 createElm 方法将 vnode 节点树变成真实 DOM 树并插入到 bodycreateElm 中会创建原生的 HTML 元素和自定义组件

因涉及了自定义组件的渲染是一个大篇幅的工作,本篇就展开聊一聊自定义组件的渲染过程。

注意:这个自定义组件的渲染不是独立的,他是初次渲染的一个分支流程。为什么这么说?以这个模板为例子: <div id="app"><some-com /></div>,初次渲染从 idappdiv 虚拟节点渲染,当渲染他的 children 时就会渲染 some-com 这个自定义组件,这时 createElem 就开始处理自定义组件了。所以根实例的初次渲染和组件的初次渲染不冲突。

二、createElm

方法位置:src/core/vdom/patch.js -> function createPatchFunction 内部方法

方法参数:

  1. vnode:虚拟节点实例对象,在初次渲染时就是前面调用 vm._render() 获取到的整棵虚拟 DOM 树
  2. insertedVnodeQueue,待插入的节点队列
  3. parentElm,父节点,当把 vnode 变成真实 dom 后插入到这个父节点中
  4. refElm:参照物元素,是 div#app 的兄弟节点,如果有值,要把 vnode 渲染出来的DOM插入到它的前面
  5. nested,是否嵌套
  6. owernArray,所有者数组
  7. index,索引

方法作用:创建原生 HTML 元素和自定义组件;

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  

  vnode.isRootInsert = !nested 

  // 重点来啦:
  // 这个  createComponent 负责处理 vnode 是自定义组件的情况
  // 如果是 vnode 是一个普通元素,createComponent 调用后返回 false
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    // 如果 vnode 是自定义组件,createComponent 执行后返回 true,到这里就终止了
    return
  }

  // 能走到这里说明 vnode 是个普通的元素
  // 获取 data 对象
  const data = vnode.data
  
  // 获取子节点列表
  const children = vnode.children
  
  // vnode 的标签名
  const tag = vnode.tag
  if (isDef(tag)) {
  

    // 创建新节点,并挂载到 vnode 对象上,
    // vnode.elm 是个真实的 DOM 元素
    vnode.elm = vnode.ns // ns 是命名空间,忽略他
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode) // 咱们研究这种情况
   

    if (__WEEX__) {
   
    } else {
      // 递归创建所有子节点(普通元素,组件)
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        // 调用 createHooks
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }

      // 初次渲染时将节点插入父节点,这是至关重要的一步了,
      // vnode.elm 是创建出来的真实元素,到了这里包含所有模板内容的一整棵 DOM 树,
      // parentElm 是 body 元素
      // 把 DOM 元素插入到 body,实现渲染
      insert(parentElm, vnode.elm, refElm)
    }
  } else if (isTrue(vnode.isComment)) {
    // vnode.tag 属性不存在,即不是元素或者自定义组件
    // 到这里就是注释节点,创建注释节点并插入父节点
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // 不是注释、也不是元素,就当文本处理了
    // 文本节点,创建文本节点并插入父节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}
复制代码

2.1 createComponent

方法位置:src/core/vdom/patch.js -> function createPatchFunction 的内部方法

方法参数:

  1. vnode:节点对象
  2. insertedVnodeQueue,待插入节点列表
  3. parentElm, 父元素
  4. refElm, ref 参照元素

方法作用:

  1. 如果 vnode 是一个组件,则执行 init 钩子,创建组件实例并挂载
  2. 然后为组件执行各个模块的 create 钩子
  3. 如果组件被 keep-alive 包裹,则激活组件
  4. 如果是自定义组件返回 true,如果是普通HTML元素什么也不处理返回 undefined
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  // 获取 vnode.data 对象
  let i = vnode.data
  if (isDef(i)) {
    // 判断组件实例是否已经存在 && 被 <keep-alive/> 包裹
    // 被 keep-alive 包裹的组件是激活和失活,普通组件需要新建和销毁
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive

    // 执行 vnode.data.hook.init 钩子,
    // 这个东西是生成 vnode 时,通过 installComponentHooks 
    // 如果是被 keep-alive 包裹的组件:
    // 则执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
    // 如果是组件未被 keep-alive 包裹或首次渲染,则初始化组件,并进入挂载阶段
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    
    // 当调用过 data.hook.init 钩子后,如果这个 vnode 是个子组件,
    // 此时就应该已经生成过子组件实例并完成挂载了
    // 给子组件设置 vnode.elm
    if (isDef(vnode.componentInstance)) {
      // 如果一个 vnode 是一个子组件,
      // 则调用 data.hook.init 钩子之后会创建一个组件实例,并实施挂载
      // 这个时候就可以给组件执行各个模块的 create 钩子
      initComponent(vnode, insertedVnodeQueue)

      // 将组件的 DOM 节点插入到父节点内
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        // 组件被 keep-alive 包裹的情况,激活组件
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
复制代码

2.1.1 data.hook.init

data 就是 parse 阶段解析模板中元素的行内属性、指令所得的对象,在创建自定义组件的 VNode 时会在 VNode.data 上增加 hook 属性,其中包含了四个钩子方法:init/prepatch/insert/destroy,这个过程大致过程的代码示例如下:

export function createComponent () {
  installComponentHooks(data);
}

// installComponentHooks
function installComponentHooks (data: VNodeData) {
  // data.hook 对象初始化
  const hooks = data.hook || (data.hook = {})

  // 遍历 hooksToMerge 数组,
  // hooksToMerge = Object.keys(componentVnodeHooks)
  // hooksToMerge = ['init', 'prepatch', 'insert', 'destroy']
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
   
  }
}
复制代码

从上面的代码可以看出这些钩子方法应该在 componentVnodeHooks 对象里面,其核心工作如下:

  1. 处理 vnode.componentInstance 已经存在并且组件处于 keep-alive 包裹,则直接调用 hook.prepatch 进入 patch,因为 keep-alive 的组件没有销毁,不需要再重新创建组件实例了,直接走 patch 的更新渲染;
  2. 另一种场景就是需要创建组件实例并且挂载到 vnode.componentInstance 上。创建组件实例时通过 new 组件的构造函数,而这个构造函数则是在 createComponent 时通过组件的选项对象Vue.options 合并,然后通继承 Vue 得到的子类: function VueComponent
  3. 得到实例以后,手动调用子组件的 $mount 方法,使子组件进入挂载阶段;这就有趣了,子组件 $mount 就会接着走子组件模板的编译(parse+generate)得到 子组件render 函数,然后创建子组件渲染 watcher,得到 VNode,然后调用 子组件._update() ... 你会发现这是个递归的过程,如果子组件还有子组件,就接着进入这个循环过程,直到所有的组件都被挂载到对应的父节点。
const componentVNodeHooks = {
  // 初始化
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance && // 组件实例已存在
      !vnode.componentInstance._isDestroyed && // 组件实例未被销毁
      vnode.data.keepAlive // 组件处于 keep-alive 的包裹中
    ) {
      
      // 被 keep-alive 包裹的组件,其 init 的过程走 prepatch
      const mountedNode: any = vnode
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // 普通组件的创建过程:
      // 执行过这里,就得到了 vnode.componentInstance 即组件实例,
      // 也就是通过组件构造函数创建出来的实例
      // 这个实例的构造函数是扩展 Vue 得到的子类,继承了 Vue 的能力
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // 执行组件的 $mount 方法手动挂载
      // 进入挂载阶段,接下来就是通过编译器得到 render 函数,
      // 然后创建渲染 watcher 接着走组件 mount、patch 
      // 这条路直到组件被挂载到父节点上
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  // 更新 VNode,用新的 VNode 配置更新就的 VNode
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  
  },

  // 执行组件的 mounted 生命周期钩子
  insert (vnode: MountedComponentVNode) {
   
  },

  // 销毁组件:
  destroy (vnode: MountedComponentVNode) {
    
  }
}
复制代码

2.1.2 data.hook.prepatch

prepatch 更多的是表达响应式数据发生变化后进行 diff + patch 的这一渲染过程,暂时先不展开

const componentVNodeHooks = {
  // 初始化
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},

  // 更新 VNode,用新的 VNode 配置更新旧的 VNode
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // 新的 VNode,用新的 VNode 配置更新就的 VNode 上的各种属性
    const options = vnode.componentOptions
    // 老的 VNode 组件的组件实例
    const child = vnode.componentInstance = oldVnode.componentInstance

    // 用 vnode 上的属性更新 child 上的各种属性
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },


  insert (vnode: MountedComponentVNode) {},
  destroy (vnode: MountedComponentVNode) {}
}
复制代码

2.1.3 createComponentInstanceForVnode

方法位置:src/core/vdom/create-component.js -> function createComponentInstanceForVnode

方法参数:

  1. vnode, 虚拟节点
  2. parent,父元素

方法作用:给

export function createComponentInstanceForVnode (
  vnode: any,
  parent: any // 当前处于激活状态的父实例,比如 <some-com /> 的父实例就是根实例
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true, // 标识当前实例是个组件
    _parentVnode: vnode, // 当前组件的父节点是 vnode
    parent
  }
  // 检查内联模板的渲染函数,如果是内联模板,
  // 则需要把 render 函数替换成内联模板的渲染函数
  // 内联模板,看 vue 官方文档吧
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  // vnode.componentOptions.Ctor 就是生成 vnode 的时候扩展 Vue 所得到的子类
  // 每个自定义组件都有自己的子类构造函数
  return new vnode.componentOptions.Ctor(options)
}
复制代码

2.1.4 initComponent

初始化组件,主要工作在 invokeCreateHooks 方法中,值得一提的是这个 create 并不是组件的 created 生命周期钩子,而是属性、样式、类名、指令、refattrs/stle/klass/directives/ref)的周期方法,这些方法在执行 createPatchFunction({ nodeOps, modules }) 时传入的 modules 选项中;

在 Vue 的官方文档中有介绍指令的周期函数,这写周期函数就是这里说的 hook

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)) {
    // 初次渲染走这里,调用 modules 中的各个模块的 create 钩子方法;
    // 注意这个不是组件的 created 生命周期,而是 attr、klass(类名)、style、directives 属性的
    // 维护工作,这里有个例子,是关于指令 directives 的钩子函数:
    // https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0
    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)
  }
}
复制代码

2.2 nodeOps.createElement

举个例子,nodeOps 中封装了浏览器的 DOM APIcreateElement 就是创建元素的方法,创建的是真实的 DOM 元素;

// 创建标签名为 tagName 的元素节点
export function createElement (tagName: string, vnode: VNode): Element {
  // 创建元素节点
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // 如果 select 元素,则为他设置 multiple 属性

  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}
复制代码

2.3 createChildren

方法位置:src/core/vdom/patch.js -> function createPatchFunction 的内部方法

方法参数:

  1. vnode,虚拟节点列表
  2. childrenvnode.children 列表,即当前虚拟节点的子节点列表;
  3. insertedVnodeQueue,待插入的节点列表

方法作用:根据 vnode.children 递归调用 createElm 方法创建元素,并插入到父元素(vnode.elm)中;

// 创建所有子节点,并将子节点插入到父节点,形成一颗 DOM 树
function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    // children 是数组,标识一组节点
    if (process.env.NODE_ENV !== 'production') {
      // 检测这一组节点的 key 是否重复
      checkDuplicateKeys(children)
    }

    // 遍历子节点列表,递归调用 createElm 创建这些节点,
    // 然后插入父节点,形成一棵 DOM 树
    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)))
  }
}
复制代码

2.4 invokeCreateHooks

调用各个 modulecreate 方法,比如attrsstyledirectives,然后执行组件的 mounted 生命周期方法;这些 modulecreatePatchFunction({ nodeOps, modules }) 时传入的 modules 选项;

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)

    // 调用组件的 data.hook.insert 钩子,执行组件的 mounted 生命周期方法
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}
复制代码

i.insertVNode.data.hook.insert 钩子,是在创建 VNode 时,通过 installComponentHooksdata.hook

2.4.1 data.hook.insert

const componentVNodeHooks = {
 
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},

  // 更新 VNode,用新的 VNode 配置更新就的 VNode
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},

  // 执行组件的 mounted 生命周期钩子
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode

    // 如果组件未挂载,则调用组件的 mounted 生命周期钩子
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }

    // 处理 keep-alive 组件的异常情况....
  },

  // 销毁组件:
  destroy (vnode: MountedComponentVNode) {}
}
复制代码

2.5 insert

方法位置:src/core/vdom/patch.js -> function createPatchFunction 内部方法

方法参数:

  1. parent,父节点
  2. elm,待插入到父节点队列的元素
  3. ref,参照物节点,是 elm 弟弟节点,确保插入后节点的顺序

方法作用:将 elm 插入到 parent 子节点队列;执行这一步骤后,VNode 就变成真实 DOM 了。

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        // 插入到 ref 参照物节点的前面,所以 ref 是 elm 的弟弟节点
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      // 追加到 parent 的子节点末尾
      nodeOps.appendChild(parent, elm)
    }
  }
}
复制代码

三、总结

3.1 本文总结

本文详细讨论了 createElm 这个方法的逻辑,这个方法根据 VNode 创建真实的元素,其中包含两种场景:

  1. 如果 VNode 是自定义组件,则调用 createComponent 方法处理,它内部会调用 data.hook.init 相当于 Vue.prototype._init 方法,完成子组件的实例化,然后调用子组件的 $mount 开始进行子组件的编译以及挂载过程,完成子组件的渲染;在进行子组件的渲染过程中,就会触发 patch 函数关于子组件的初渲染逻辑;

  2. 如果是普通元素,则通过 nodeOps.createElement 创建原生 HTML 元素,并处理其子节点的过程,而处理子节点则又是一个递归调用 createElm 方法的过程;

  3. createElm 方法的结尾处会把得到的 vnode.elm 也就是根据 vnode 得到的真实 DOM 插入到 parentElm,这里的 parentElm 就是 body 元素,如下图;

image.png

3.2 挂载阶段性总结

后面伴随着移除占位符节点等一系列工作,执行栈逐步推出,直至 Vue.prototype.$mount 执行栈推出,至此 Vue 的初次渲染结束。

以下为从根实例 new Vue 一直到 VNode 插入到 body 的调用栈梳理,执行栈推出,从底出栈,至此 $mount 的初次挂载阶段同步结束;

// 调用栈梳理,最上层为栈顶
new Vue()
  -> Vue.prototype._init()
    -> Vue.prototype.$mount()
      -> compileToFunctions(template...) 编译模板获取 render 函数
      -> mount()
        -> mountComponent()
          -> new Watcher(updateComponent) 
            -> updateComponent
              -> Vue.prototype._render()
              -> Vue.prototype._update()
                -> Vue.prototype.__patch__()
                  -> createPatchFunction 返回值 patch()
                    -> createElm()
                       -> insert(parentElm, vnode.elm)
复制代码

Supongo que te gusta

Origin juejin.im/post/7076349760336035853
Recomendado
Clasificación