vue源码分析-12-渲染之createElm初次渲染

1,492 阅读5分钟

前言

通过上一节分析得知,首次渲染调用vm.patch(vm.el, vnode, hydrating, false),方法内部会调用createElm方法根据传入的vnode生成渲染一个真实的dom节点挂在至vm.el上,那么接下来我们分析一下createElm方法

createElm()

createElm方法就是用来递归创建真实的dom,我们看一下createElm具体做了如下几件事情

  • 创建dom节点(html标签/文本节点/注释节点)
  • 递归创建子元素dom节点
  • 如果是组件,递归调用createElm方法,创建子组件的dom节点
  • 调用create渲染钩子,生成标签属性,样式,事件等等
function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  )
  {
    /*非重要代码省略*/

    // 如果创建组件成功,就不再往后执行
    // 只有vnode是子组件的时候createComponent才会返回true
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    // 拿到vnode的数据
    const data = vnode.data
    // 拿到子组件vnode
    const children = vnode.children
    // 拿到标签
    const tag = vnode.tag
    // 如果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
          )
        }
      }

      // 创建真实dom节点
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // weex平台代码省略
      } else {
        // 递归创建子节点元素,如果是组件,会渲染组件的dom
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // 将生成的dom传入到父节点中
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    }
    // 否则如果tag是个注释节点
    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)
    }
  }

以上代码调用了nodeOps的方法创建dom,nodeOps其实就是定义的一系列原生dom操作的方法

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 createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

export function createComment (text: string): Comment {
  return document.createComment(text)
}

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

export function parentNode (node: Node): ?Node {
  return node.parentNode
}

export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}

export function tagName (node: Element): string {
  return node.tagName
}

export function setTextContent (node: Node, text: string) {
  node.textContent = text
}

export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

createElm方法先是进入判断是否是子组件的创建,如果不是那么继续判断,进而创建html节点/文本节点/注释节点 此方法中会在执行到

createChildren(vnode, children, insertedVnodeQueue)

时创建子节点,这个方法中会递归的调用createElm方法创建dom元素,如果是组件的话会进入createComponent方法中,如何创建子组件会在专门的部分详细讲解。

创建完dom节点并没有结束,因为dom还需要有标签属性,样式,事件绑定等等,这些都是在invokeCreateHooks(vnode, insertedVnodeQueue)方法中执行cbs.create方法实现的,那么cbs是什么呢?cbs其实就是渲染时的钩子

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

这些钩子的实现部分都是定义在传入的modules中

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

这些钩子方法被定义在vue/src/platforms/web/runtime/modules 目录下,每个文件都提供一个create方法

export default {
  create: updateAttrs,
  update: updateAttrs
}

我们以处理class为例,生成和更新class走的都是同一个方法,主要逻辑就是重新生成样式赋值给el

function updateClass (oldVnode: any, vnode: any) {
  // 拿到真实的dom
  const el = vnode.elm
  const data: VNodeData = vnode.data
  const oldData: VNodeData = oldVnode.data
  if (
    isUndef(data.staticClass) &&
    isUndef(data.class) && (
      isUndef(oldData) || (
        isUndef(oldData.staticClass) &&
        isUndef(oldData.class)
      )
    )
  ) {
    return
  }

  // 生成样式
  let cls = genClassForVnode(vnode)

  // handle transition classes
  const transitionClass = el._transitionClasses
  if (isDef(transitionClass)) {
    cls = concat(cls, stringifyClass(transitionClass))
  }

  // 重新设置样式
  // set the class
  if (cls !== el._prevClass) {
    el.setAttribute('class', cls)
    el._prevClass = cls
  }
}

export default {
  create: updateClass,
  update: updateClass
}

其他的创建,如下,感兴趣可自行分析

export default [
  attrs,
  klass,
  events,
  domProps,
  style,
  transition
]

至此如果没有定义子组件的情况下,真实的dom已经生成了

接下来我们看一下子组件是如何被渲染的。

createComponent

首先我们看一下子组件的Vnode是如何生成的,在_createElement创建vnode的方法中,定义了如下代码,如果当前render函数被判定为是一个组件的render函数,那么会调用createComponent方法生成组件的vnode

// 如果判定是组件
else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)

createComponent方法主要就是将构造函数和一系列创建子组件的钩子函数保存起来,在创建子组件的时候,从vnode中拿到构造函数并实例化,从而生成子组件的Vue实例

const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

生成的vnode结构类似如下图所示

image

言归正传我们看一下createElm方法中的createComponent方法

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      // 如果vnode.data.hook存在,vnode.data.hook.init存在
      // i = vnode.data.hook.init
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 调用组件的init方法,init回调用mount方法,i执行完以后,vnode.elm就是真实创建的dom
        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是一个子组件的话,即非根组件,返回true
      if (isDef(vnode.componentInstance)) {
        // 初始化组件
        initComponent(vnode, insertedVnodeQueue)
        // 将子组件的dom插入至父节点中
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

其主要逻辑就是调用了vnode.data.hook.init方法,那么vnode.data.hook.init方法又是在哪里定义的呢

其实是在_createElement方法中的这个方法中定义的

vnode = createComponent(Ctor, data, context, children, tag)

createComponent方法中调用了installComponentHooks方法

// 安装vnode组件的钩子
  installComponentHooks(data)

installComponentHooks方法其实就是生成一系列渲染方法,并赋值给vnode.data.hook

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

生成后类似下图

image

init方法定义如下

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

以上的child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) 是由如下方法返回

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

重点就在最后一行,其返回了一个Vue的实例,因为 Ctor就是保存在vnode中的继承自Vue的构造函数。Vue的实例被返回以后,又调用了vm.$mount方法又进行了一次 模版编译->ast->render函数->dom渲染的逻辑,子组件的dom被渲染以后,插入到父节点中,就这样递归的深度优先似的进行整个文档的首次渲染。

总结

至此,vue的初始化以及首次渲染就已经完毕,接下来在使用系统的时候,我们会与dom交互并修改数据,这时界面会因数据的变化而自动发生对应的变化,这就是所谓的数据响应式。在数据变动影响到界面的变动,我们重点关注如下几方面

  • 数据响应式原理(为什么修改数据,对应的界面就能变化呢)
  • dom更新渲染时,虚拟dom的dom diff算法