Vue初始化过程(2)

586 阅读6分钟

前言


初始化模板代码

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Examples</title>
<meta name="description" content="">
<meta name="keywords" content="">
<link href="" rel="stylesheet">
</head>
<body>
  <div id="app">{{ name }}</div>
</body>
</html>
new Vue({
	el: '#app',
  	data () {
    	return {
        	name: 'rookie'
        }
    }
})

紧接着我们上一篇章的分析,我们继续对Vue初始化的过程进行探讨,点击Vue初始化过程(1)可查看上一篇章。

updateComponent


	updateComponent = () => {    // 函数定义
       vm._update(vm._render(), hydrating)
    }

可以看到updateComponent函数由vm._update跟vm._render两个核心函数所构成。我们先看下_render函数,代码如下,源码戳这里

_render


	Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options // 从$options中取出render函数

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement) // 初始化会走到这一步
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode // 返回VNode
  }
}

首先,会从$options取到render,这个render函数其实就是在Vue初始化过程中,在执行Vue原型上的$$mount方法时,生成的render函数,并挂载到options对象上,具体代码如下:

	const { render, staticRenderFns } = compileToFunctions(template, {
           outputSourceRange: process.env.NODE_ENV !== 'production',
           shouldDecodeNewlines,
           shouldDecodeNewlinesForHref,
           delimiters: options.delimiters,
           comments: options.comments
         }, this)
	options.render = render
    options.staticRenderFns = staticRenderFns

接着,会执行vnode = render.call(vm._renderProxy, vm.$createElement),render函数最终返回vnode对象,其中vm._renderProxy是在Vue执行_init方法的时候进行赋值的

	/* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm) // development环境执行
    } else {
      vm._renderProxy = vm
    }

而initPorxy方法定义如下:

	initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }

其实就是判断是否支持原生Proxy,如果支持则通过Proxy代理的方式赋值给vm._renderProxy,不然就直接用vm实例赋值。其实proxy代理对vm上的data数据进行了一些拦截和处理操作。

$createElment


render.call(vm._rnederProxy, vm.createElement),可以看到render接受一个vm.createElement),可以看到render接受一个vm.createElement的参数,源码戳这里

// bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 
  • vm._c: 从模板中编译出来的render函数会使用这个函数进行传参
  • vm.$createElment: 用户手写的render会使用这个函数进行传参 下面这行代码是否很熟悉
new Vue({
	el: '#app',
    render: h => h(app)
})

这里的h变量其实就是vm.$createElment这个函数,h(app)其实就是调用了createElment函数,可以结合Vue官方文档进行分析,实现如下:

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

首先会判断data是否是数据还是基础数据类型,如果是的话,则表明没有传入data,相应的会对children、nomalizationType进行重新赋值,相当于参数的重载,最后又执行了_createElment方法

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<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()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  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)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

可以看到,函数首先会对传入的children参数进行规范化处理(normalizeChildren和simpleNormalizeChildren),定义如下,源码:

/* @flow */

import VNode, { createTextVNode } from 'core/vdom/vnode'
import { isFalse, isTrue, isDef, isUndef, isPrimitive } from 'shared/util'

// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
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. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

function isTextNode (node): boolean {
  return isDef(node) && isDef(node.text) && isFalse(node.isComment)
}

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}
  • simpleNormalizeChildren: 只对children数组进行了一层的降维,不会递归去遍历子数组中的子数组
  • normalizeChildren
    • 首先会对children进行判断,如果children是基本类型则返回包含一个文本VNode的数组,如果children是一个数组则会调用normalizeArrayChildren
    • 初始化一个res数组,用来存放最后的VNode Array的结果,c变量为每一个子元素VNode,lastIndex是记录res最大的索引值,last就是res的最后一个元素,c和last的关系就是last是c的前一个元素,c是当前遍历的元素
    • 知道了last和c之间的关系之后,下面的分析就一目了然了。先遍历children中的每一个元素,取出当前遍历的子元素并赋值给c,并把lastIndex设置为数组的最大索引值,把数组中最后的一个元素赋值给last,接下来主要分一下几种判断条件:
    • 如果c是一个数组,并且数组长度大于1,此时递归调用normalizeChildren函数,并把结果赋值给c。接下来,如果last和c数组中的第一个元素c[0]两个都是文本节点的话,则将两个节点进行合并赋值给res[lastIndex],然后将c[0]的值删除(这是Vue一些细节上的处理),最后将c插入到res的尾部。
      • 如果c是一个基本数据类型,并且last是一个文本节点的话,则直接将last.text和c合并后的文本VNode赋值给res[lastIndex],如果last不是一个文本节点的话,则直接新创建一个文本VNode,并插入到res尾部。
      • 不满足以上两个情况的话:
        • 如果last和c都是文本节点,则将两个文本合并之后创建的文本VNode赋值给res[lastIndex]
        • 如果c是v-for循环生成的节点,并满足tag、key、nestedIndex都存在的情况,则执行c.key = __vlist${nestedIndex}_${i}__, 接着将c插入到res的尾部中。

children规范化之后,对于我们的初始化代码来讲,我们会走到下面这个逻辑

if (config.isReservedTag(tag)) { // 是否是原生保留标签
    // platform built-in elements
    if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
      warn(
        `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
        context
      )
    }
    vnode = new VNode( // 创建VNode
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
 }

首先会判断是不是一个原生的保留标签,然后就进行new VNode(),生成一个VNode实例并且返回。VNode其实就是一个对真实DOM的描述,大家可以自行点击VNode源码进行了解。

VNode


export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag // 标签名
    this.data = data // 传入的数据对象 包含 class id on 等属性,具体可参考Vue官方render函数
    this.children = children // VNode 数组
    this.text = text // 文本
    this.elm = elm // 真实DOM
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key // 标记VNode的唯一性,用于后续组件更新的diff算法
    this.componentOptions = componentOptions // 组件选项
    this.componentInstance = undefined // 组件实例
    this.parent = undefined // VNode占位符
    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
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

定义一个VNode如下

{
	tag: 'div',
    data: {
    	id: 'a',
        class: 'b'
    },
    children: [
        {
        	tag: 'span',
            text: 'lxb'
        }
    ]
}

则生成下面的DOM

<div id="a" class="b">
  <span>lxb</span>
</div>

总结


这篇文章分析完了_render函数的执行,通过createElment函数内部的一系列判断之后生成VNode,VNode是一个用来描述真实DOM的JS对象,之后我们会继续分析VNode是如何映射成真实的DOM进而渲染视图的。文中如有不足或有误之处,望指出,共勉!!

参考文章