从new Vue到Hello World!(下)

539 阅读5分钟

前言

本文主要阐述如何生成vnode以及vnode如何渲染至页面上。

ps:vue在vnode前做了什么操作请参考上篇,再次强调本文与源码搭配更加美味~

一、createElement

通过上篇我们知道最后vnode是通过createElement方法实现的。

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

而其定义在core/instance/vdom/create-element.js

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

这里其实并不是真的创建方法,由于我们写的render函数的参数个数不是固定的,这边就进行了统一的处理当我们没有写data时,也就是说原本的children在data的位置,所以这边就判断是否出现了这个情况,如果有,则所有的参数往后顺延,且data设置为undefined,后面也是对参数的处理,看alwaysNormalize的值决定是否将normalizationType 设置为常量,而我们之前的例子中最后传的时true,所以normalizationType 被设置为2

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

通过这一些列的操作后在执行真正的_createElement方法,其就定义在下方 这边我们先理一下第一个content是 vue实例,第二个tag是 ’div’,第三个data是

{
          attrs:{
            id:'app'
          }
     }

这样的对象第四个children是this.message,最后一个normalizationType为2。 首先判断data是不是响应式的,如果是则会进行警告并最终

return createEmptyVNode()

这个createEmptyVNode方法定义在vnode.js中

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

而Vnode也定义在这里,可以看看其结构,而这个createEmptyVNode很简单就是创建了一个空的Vnode并且设置了text以及isComment 属性,最后将其返回。

回到_createElement,接着插卡data的is属性如果有的话就将其赋值,并且如果它为假则还是调用createEmptyVNode,接着判断data中的key是否为基础类型,如果不是则就警告了。 接下来

if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }

这边我们走到上面的normalizeChildren它被定义在同级的helpers/normalize-children中

export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

这边判断children是不是基础类型是的话直接调用createTextVNode方法并且以数据的形式返回,如果不是就再判断是不是数组如果是的话调用normalizeArrayChildren,如果还是不是就直接返回undefined。我们这边传入的是this.message也就是我们在data中定义的字符串,所以走createTextVNode,其定义在core/vdom/vnode.js

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

非常简单,在new Vnode的时候在第四个参数传入值,从上面的Vnode对象可以知道这个值对应的是Text属性,所以返回的是这样一个Vnode对象。

如果是数据的话意味着有很多层结构,看看normalizeArrayChildren方法,该方法其实就是一层层从最深处构建出一个深层次的vnode并最后返回。

而什么时候走下面的simpleNormalizeChildren呢,实际上在之前render中有这么一句

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

实际上这个是给编译模板后形成的render函数使用的,看到这里最后传的是false,也就是说最后调用了simpleNormalizeChildren,他和normalizeChildren定义在一起

export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

实际上是将数组拍平一次,即2层之后不拍平,然后在直接返回该数组。继续往下走。 首先判断tag是不是字符串类型,这边的tag是可以为组件,如果为组件的话调用

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

因为例子中不是组件所以我们看下去。

首先查看tag是不是html上对应的标签名,如果是的话则通过

 vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )

创建一个html标签的vnode,再判断tag是不是组件名,如果是则

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

如果都不符合以上条件则

 vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )

) 也是创建一个vnode,通过以上最终将生成的Vnode返回。

二、_update

综上,我们最后执行到

vm._update(vm._render(), hydrating)

并且_render返回的是一个Vnode,而_update定义在core/instance/lifecycle中 首先这边判断了一个prevVnode,由于我们是初次渲染,所以这边的值是空的,于是我们走到

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

而_patch_定义在core/platforms/web/runtime/index.js

Vue.prototype.__patch__ = inBrowser ? patch : noop

这边其实就是判断是否在浏览器中,如果没有则返回空函数,如果在浏览器中就返回patch,因为vue是支持服务器渲染的所以这里就是用来做这个区分。

而patch定义在同级的patch.js中,首先定义了一个modules是将platformModules与baseModules合并后为值

const modules = platformModules.concat(baseModules)

而这边的platformModules定义在web/runtime/modules/index中

import attrs from './attrs'
import klass from './class'
import events from './events'
import domProps from './dom-props'
import style from './style'
import transition from './transition'

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

可以看的出来这边是用来对dom上面的属性方法进行设置用的模块,这边先跳过往下看。 最后执行了

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

也就是说最后返回的就是createPatchFunction方法,这里的第二个参数就是上面的modules,而nodeOps定义在web/runtime/node-ops,可以看到里面全都是原生dom的操作方法。而vue对于_path_这个方法绕这么大圈子处理原因是多平台。因为我们用的是web所以传入的都是dom相关的api以及操作方法,如果是weex可能就是传入其他的处理方式,vue用这种方式可以分开两个平台的差异性不用到处写if else的判断,在源头就进行区分,让后面的代码可以更专注在功能上。

之后createPatchFunction定义在core/vdom/patch中 这边是循环将dom的处理模块设置到一个cbs的对象上,而这里有个hooks,仔细看进去的话

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

是不是和声明周期很像,对。这个其实是借鉴一个snabdom(一个虚拟dom的库),将每个对dom的操作的模块分别挂载在不同的钩子上调用。

而后面定义了非常多的辅助函数,最后其返回了一个patch的方法

 return function patch (oldVnode, vnode, hydrating, removeOnly) {
  .....
}

所以最后的最后实际上是调用了这个函数这时我们看看之前调用的函数

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

第一个是我们在挂载的真实dom,第二个参数是我们通过render配置生成的vonde。后面两个都是false。接着我们从patch上看下去直接看到这么一行

const isRealElement = isDef(oldVnode.nodeType)

由于我们是第一次所以这里的为true,进入到下面的分支,先判断了是否为服务器渲染,后面又判断最后一个参数是否为真,由于我们这边都不满足所以直接跳过最后到达

oldVnode = emptyNodeAt(oldVnode)

而emptyNodeAt定义在本文件内

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

实际上就是根据我们真实的dom去生成vnode。接下来用oldElm缓存了挂载的真实dom,并且用nodeOps.parentNode找出了挂载dom的父dom,在这里也就是body。

做了这些缓存后调用了createElm方法,其也是定义在本文件中

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

看参数第一个是我们根据配置生成的新vnode,第三个是body,其他可以先忽略。直接看到先定义了一个tag值为vnode的tag,也就是div,然后判断tag是不是正常的的html标签,这边是正常的,所以进到下面的处理,对这个tag进行判断,看有没有是不是注册的组件标签或者是html中规定的标签,如果都不满足的话就发出警告

'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.'

接下来一个人重要的语句

vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)

由于我们没有设置namespace所以运行的是nodeOps.createElement,而其定义在platforms/web/runtime/node-ops

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
}

这边就是最简单的根据tag和vnode调用原生的createElement。这边也就是创建了一个id为app的div的dom。

接下来

createChildren(vnode, children, insertedVnodeQueue)

该方法定义在本文件中这边比较复杂

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

查看该vnode下的children,并且循环调用createElm,并且将字节点的dom也传进去,然后createElm又进行createChildren ,所以这上面的一系列操作讲明白了就是一个递归,一层层深入后将复杂的vnode关系最终转化成真实的dom。在例子中我们是一个文本所以走到

nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))

该方法也是定义在之前的node-ops中非常简单的appendChild操作,并且传入了vnode.elm也就是我们刚刚生成的div的dom,所以最终的文字就被append到其下。最后

insert(parentElm, vnode.elm, refElm)

传入了三个参数:父节点的真实dom,当前vonde生成的dom,以及一个参考节点,该方法定义在文件中

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

说白了就是将当前的dom根据条件appendChild或者insertBefore到父节点的dom中,所以在例子中我们就将之前生成好的div,append到了body下,然后我们回到patch方法中在最后

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

我们用removeVnodes将父节点中的原来的dom清除。

至此,从new Vue到渲染的过程就全部完成了。

结语

这次vue的学习暂时告一段落了,虽然之前看源码是抱着一种任务的感觉去看,好像别人都看过自己没有看过会落后一样,但是实际上随着深入的学习,发现源码最重要的是作者的思路以及对于整个架构的把握,可以说正好是现在我所需要的,所以吸收到了非常多的东西,也是希望自己学习后能将之前的小开源项目改改,并最终能在这条路上越走越远吧!