[Vue源码解读]render函数生成vnode

495 阅读3分钟

先来看下vnode什么样

VNode构造函数

(源码目录:core/vdom/vnode.js)

	class 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	//节点数据包括class,	style,attrs等
	    this.children = children	//子节点
	    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	//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用来描述DOM节点的,包括tag, data, text, children, elm, parent等。

data属性主要保存一些节点数据,比如style,class,attrs等。

vnode对象 由render function运行生成,下面先从 render 函数的创建开始说

两种生成render函数的方式:

  1. 第一种是用户在Vue对象中直接创建 render function。

  2. 第二种是 Vue 编译模板生成 render function。详细过程请看这篇文章:template编译成render function

用户自己写 render 函数和 vue 编译生成的差不多,下面看下第一种方式:

	<div id="app"></div>
	<script type="text/javascript">
		const vm = new Vue({
			el: '#app',
			render(h) {
				return h('div', {
					attrs:{
						id: 'app'
					}
				}, [ h('h1', [' hello ' + this.name]) ])
			},
			data: {
				name: 'ludeng',
			}
		});
	</script>

如上,render函数有一个参数h,这个参数h是用来创建vnode虚拟节点的函数。h函数接收三个参数,h( tag,data| Object,children| String/Array ),分别对应 标签名数据(class, style, attrs等),子级vnode

data,和 children 都是可选的,另外 children 可以是 Array 或 String 类型,内容只有文本可以用 String 类型。

接下来,我们通过一小段代码(第二种方式)看编译生成的渲染函数:

vue模板编译示例

	<div id="app">
		<h1> hello {{ name }}</h1>
	</div>
	<script type="text/javascript">
		const vm = new Vue({
			el: '#app',
			data: {
				name: 'ludeng',
			}
		});
	</script>

在控制台,通过vm.$options.render打印出渲染函数:

	function anonymous() {
		with(this) {
			return _c('div', {
				attrs: {
					"id": "app"
				}
			}, [_c('h1', [_v(" hello " + _s(name))])])
		}
	}

函数内使用了with方法,with(this)里的属性和方法,相当于通过this来调用,这样写是为了减少代码量,这里this指向Vue实例。(比如:_c 实际调用 vm._c(), name 实际访问 vm.name )

与用户写的render function不同是有了_c, _v, _s等方法,这些都是创建vnode相关的方法。源码定义如下:

_c _v _s

(源码目录:/core/instance/render.js)

	vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)	//a, b, c 分别对应 tag, data, children, d 是辅助变量
	vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)	//第一种方式的 *h函数!

(源码目录:core/instance/render-helpers/index.js)

	Vue.prototype._s = toString	//将值转换为字符串
	Vue.prototype._v = createTextVNode	//创建 text vnode

下面根据示例运行顺序进行讲解:

( *** 在此之前说明一点,_c函数并不会先运行,这是传参的方式调用函数,_s才是第一个运行的函数)

(1) render函数运行,先访问name,然后调用_s(name)转化成String类型,与" hello "进行字符串拼接。

(2) 调用 _v ( createTextVNode ) 函数创建 text vnode

看下createTextVNode函数:

(源码目录:core/vdom/vnode.js)

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

text vnode创建很简单,只传入一个字符串。

(3) _v 运行完,调用 _c 继续创建 vnode

_c 和 h(第一种方式) 这俩函数都会调用 createElement 方法,下面看 createElement方法:

createElement

(源码目录:core/vdom/create-element.js)

	function createElement (
	  context: Component,
	  tag: any,
	  data: any,
	  children: any,
	  normalizationType: any,
	  alwaysNormalize: boolean
	): VNode | Array<VNode> {
	  if (Array.isArray(data) || isPrimitive(data)) {	
		//data是数组 或者 原始值(string number symbol boolean),此时参数不对应
	    normalizationType = children
	    children = data
	    data = undefined
	  }
	  if (isTrue(alwaysNormalize)) {	
		//用户写的render函数这里为true(_c: false, h: true)
	    normalizationType = ALWAYS_NORMALIZE
	  }
	  return _createElement(context, tag, data, children, normalizationType)
	}
	

createElement方法又调用了_createElement方法。多此一举的嵌套调用主要是为了让接口更灵活。比如:

	_c('h1', [ _v(" hello " + _s(name)) ])

_c 没有 data 参数,造成下面参数不对应现象:

	tag:'h1', 
	data: [ _v(" hello " + _s(name)) ]

if判断进行调整,调整后:

	tag:'h1', 
	data:undefined, 
	children:[ _v(" hello " + _s(name)) ]

有了调整这个步骤,使得参数设置更灵活,尤其是用户自己写render函数的时候。

_createElement函数才是真正创建 vnode 对象的函数,下面来看这个函数:

_createElement

(源码目录:core/vdom/create-element.js)

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

	  // 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
	  }
	  // 标准化 children
	  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)) {	
	      //平台内置元素: 比如 html, div, h1等标签
	      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), //web平台 直接返回tag
			data, children,
	        undefined, undefined, context
	      )
	    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
	      // 通过 vue 注册的自定义 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)) {// vnode != null
	    if (isDef(ns)) applyNS(vnode, ns)
	    if (isDef(data)) registerDeepBindings(data)
	    return vnode
	  } else {
	    return createEmptyVNode()
	  }
	}

_createElement创建VNode对象,如果是 reserved tag(比如 html, div, h1等标签)则创建普通 VNode 对象,如果是component tag(通过 Vue 注册的自定义 component), 则会创建 Component VNode 对象

其实,主要都是根据 tag, data, children 这三个数据创建。

最后,在控制台打印vm._vnode,看下虚拟DOM树

_node