Skip to content

Latest commit

 

History

History
222 lines (204 loc) · 7.21 KB

10.md

File metadata and controls

222 lines (204 loc) · 7.21 KB

入口开始,解读Vue源码(九)—— $mount 内部实现 --- render函数 --> VNode

前面我们用三小节介绍了compile渲染template,完成了 template --> AST --> render function 的过程。这篇我们主要介绍一下VNode的生成过程,在此之前,我们先来了解一下一个基础的VNode:

VNode 了解一下

constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.functionalContext = undefined
    this.functionalOptions = undefined
    this.functionalScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

包含了我们常用的一些标记节点的属性,为什么是VNode?其实网上已经有大量的资料可以了解到VNode的优势,比如大量副交互的性能优势,ssr的支持,跨平台Weex的支持...这里不再赘述。我们接下来看一下VNode的基本分类: VNode 分类

比如我们创建一个 emptyVNode:

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

比如一个 textVNode:

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

生成 VNode

有了之前章节的知识,现在我们需要编译下面这段template

<div id="app">
  <header>
    <h1>I'm a template!</h1>
  </header>
  <p v-if="message">
    {{ message }}
  </p>
  <p v-else>
    No message.
  </p>
</div>

得到这样的render function

(function() {
  with(this){
    return _c('div',{   //创建一个 div 元素
      attrs:{"id":"app"}  //div 添加属性 id
      },[
        _m(0),  //静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render function
        _v(" "), //空的文本节点
        (message) //三元表达式,判断 message 是否存在
         //如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
        ?_c('p',[_v("\n    "+_s(message)+"\n  ")])
        //如果不存在,创建 p 元素,元素里面有文本,值为 No message.
        :_c('p',[_v("\n    No message.\n  ")])
      ]
    )
  }
})

要看懂上面的 render function,只需要了解 _c,_m,_v,_s 这几个函数的定义,其中 _c 是 createElement,_m 是 renderStatic,_v 是 createTextVNode,_s 是 toString。 我们在编译的过程中调用了vm._render() 方法,其中_render函数中有这样一句:

const { render, _parentVnode } = vm.$options
...
vnode = render.call(vm._renderProxy, vm.$createElement)

也就是通过调用自身的render方法,传入$createElement方法来生成VNode节点。那么核心便在$createElement的方法上了。

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode {
  // 兼容传递参数顺序
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  // 调用_createElement创建虚拟节点
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode {

  /**
   * 如果存在data.__ob__,说明data是被Observer观察的数据
   * 不能用作虚拟节点的data
   * 需要抛出警告,并返回一个空节点
   *
   * 被监控的data不能被用作vnode渲染的数据的原因是:
   * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
   */

  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }

  // 当通过 :is 动态设置组件时
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }

  // 如果这里tag被设置成false或者不存在tag,创建一个空的节点。下面的注释应该是写错了...
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // 作用域插槽
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }

  // 根据normalizationType的值,选择不同的处理方法
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 如果是保留的标签,处理...
    if (config.isReservedTag(tag)) {

      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
      // 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
    // 当tag不是字符串的时候,我们认为tag是组件的构造类
    // 所以直接创建
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  // 经过上面的处理,如果能正常获得VNode,则返回,否则创建一个空的VNode
  if (isDef(vnode)) {
    if (ns) applyNS(vnode, ns)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

经过上面的一系列过程,最终得到了我们的VNode,看起来是这样的:

总结

render函数的执行,调用了createElement方法,其内部通过传入的相关参数,根据不同类型,一步步解析出了VNode。那么 VNode如果进行渲染成不同平台所需的内容呢?下篇我们将会继续介绍patch的工作。