vue如何创建vnode_vue 中Virtual Dom被创建的方法

本文将通过解读render函数的源码,来分析vue中的vNode是如何创建的。在vue2.x的版本中,无论是直接书写render函数,还是使用template或el属性,或是使用.vue单文件的形式,最终都需要编译成render函数进行vnode的创建,最终再渲染成真实的DOM。 如果对vue源码的目录还不是很了解,推荐先阅读下 深入vue -- 源码目录和编译过程。

01  render函数

render方法定义在文件 src/core/instance/render.js 中

Vue.prototype._render = function (): VNode {

const vm: Component = this

const { render, _parentVnode } = vm.$options

// ...

// 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 {

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

}

}

// 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

}

_render定义在vue的原型上,会返回vnode,vnode通过代码render.call(vm._renderProxy, vm.$createElement)进行创建。

在创建vnode过程中,如果出现错误,就会执行catch中代码做降级处理。

_render中最核心的代码就是:

vnode = render.call(vm._renderProxy, vm.$createElement)

接下来,分析下这里的render,vm._renderProxy,vm.$createElement分别是什么。

render函数

const { render, _parentVnode } = vm.$options

render方法是从$options中提取的。render方法有两种途径得来:

在组件中开发者直接手写的render函数

通过编译template属性生成

参数 vm._renderProxy

vm._renderProxy定义在 src/core/instance/init.js 中,是call的第一个参数,指定render函数执行的上下文。

/* istanbul ignore else */

if (process.env.NODE_ENV !== 'production') {

initProxy(vm)

} else {

vm._renderProxy = vm

}

生产环境:

vm._renderProxy = vm,也就是说,在生产环境,render函数执行的上下文就是当前vue实例,即当前组件的this。

开发环境:

开发环境会执行initProxy(vm),initProxy定义在文件 src/core/instance/proxy.js 中。

let initProxy

// ...

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

}

}

hasProxy的定义如下

const hasProxy =

typeof Proxy !== 'undefined' && isNative(Proxy)

用来判断浏览器是否支持es6的Proxy。

Proxy作用是在访问一个对象时,对其进行拦截,new Proxy的第一个参数表示所要拦截的对象,第二个参数是用来定制拦截行为的对象。

开发环境,如果支持Proxy就会对vm实例进行拦截,否则和生产环境相同,直接将vm赋值给vm._renderProxy。具体的拦截行为通过handlers对象指定。

当手写render函数时,handlers = hasHandler,通过template生成的render函数,handlers = getHandler。 hasHandler代码:

const hasHandler = {

has (target, key) {

const has = key in target

const isAllowed = allowedGlobals(key) ||

(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))

if (!has && !isAllowed) {

if (key in target.$data) warnReservedPrefix(target, key)

else warnNonPresent(target, key)

}

return has || !isAllowed

}

}

getHandler代码

const getHandler = {

get (target, key) {

if (typeof key === 'string' && !(key in target)) {

if (key in target.$data) warnReservedPrefix(target, key)

else warnNonPresent(target, key)

}

return target[key]

}

}

hasHandler,getHandler分别是对vm对象的属性的读取和propKey in proxy的操作进行拦截,并对vm的参数进行校验,再调用 warnNonPresent 和 warnReservedPrefix 进行Warn警告。

可见,initProxy方法的主要作用就是在开发时,对vm实例进行拦截发现问题并抛出错误,方便开发者及时修改问题。

参数 vm.$createElement

vm.$createElement就是手写render函数时传入的createElement函数,它定义在initRender方法中,initRender在new Vue初始化时执行,参数是实例vm。

export function initRender (vm: Component) {

// ...

// 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.$createElement是为开发者手写render函数提供的方法,vm._c是为通过编译template生成的render函数使用的方法。它们都会调用createElement方法。

02  createElement方法

createElement方法定义在 src/core/vdom/create-element.js 文件中

const SIMPLE_NORMALIZE = 1

const ALWAYS_NORMALIZE = 2

// wrapper function for providing a more flexible interface

// without getting yelled at by flow

export function createElement (

context: Component,

tag: any,

data: any,

children: any,

normalizationType: any,

alwaysNormalize: boolean

): VNode | Array {

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)

}

createElement方法主要是对参数做一些处理,再调用_createElement方法创建vnode。

下面看一下vue文档中createElement能接收的参数。

// @returns {VNode}

createElement(

// {String | Object | Function}

// 一个 HTML 标签字符串,组件选项对象,或者

// 解析上述任何一种的一个 async 异步函数。必需参数。

'div',

// {Object}

// 一个包含模板相关属性的数据对象

// 你可以在 template 中使用这些特性。可选参数。

{

},

// {String | Array}

// 子虚拟节点 (VNodes),由 `createElement()` 构建而成,

// 也可以使用字符串来生成“文本虚拟节点”。可选参数。

[

'先写一些文字',

createElement('h1', '一则头条'),

createElement(MyComponent, {

props: {

someProp: 'foobar'

}

})

]

)

文档中除了第一个参数是必选参数,其他都是可选参数。也就是说使用createElement方法的时候,可以不传第二个参数,只传第一个参数和第三个参数。刚刚说的参数处理就是对这种情况做处理。

if (Array.isArray(data) || isPrimitive(data)) {

normalizationType = children

children = data

data = undefined

}

通过判断data是否是数组或者是基础类型,如果满足这个条件,说明这个位置传的参数是children,然后对参数依次重新赋值。这种方式被称为重载。

重载:函数名相同,函数的参数列表不同(包括参数个数和参数类型),至于返回类型可同可不同。

处理好参数后调用_createElement方法创建vnode。下面是_createElement方法的核心代码。

export function _createElement (

context: Component,

tag?: string | Class | Function | Object,

data?: VNodeData,

children?: any,

normalizationType?: number

): VNode | Array {

// ...

if (normalizationType === ALWAYS_NORMALIZE) {

children = normalizeChildren(children)

} else if (normalizationType === SIMPLE_NORMALIZE) {

children = simpleNormalizeChildren(children)

}

let vnode, ns

if (typeof tag === 'string') {

let Ctor

// ...

if (config.isReservedTag(tag)) {

// platform built-in elements

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

}

}

方法开始会做判断,如果data是响应式的数据,component的is属性不是真值的时候,都会去调用createEmptyVNode方法,创建一个空的vnode。 接下来,根据normalizationType的值,调用normalizeChildren或simpleNormalizeChildren方法对参数children进行处理。这两个方法定义在 src/core/vdom/helpers/normalize-children.js 文件下。

// 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. , , 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 {

return isPrimitive(children)

? [createTextVNode(children)]

: Array.isArray(children)

? normalizeArrayChildren(children)

: undefined

}

normalizeChildren和simpleNormalizeChildren的目的都是将children数组扁平化处理,最终返回一个vnode的一维数组。

simpleNormalizeChildren是针对函数式组件做处理,所以只需要考虑children是二维数组的情况。 normalizeChildren方法会考虑children是多层嵌套的数组的情况。normalizeChildren开始会判断children的类型,如果children是基础类型,直接创建文本vnode,如果是数组,调用normalizeArrayChildren方法,并在normalizeArrayChildren方法里面进行递归调用,最终将children转成一维数组。

接下来,继续看_createElement方法,如果tag参数的类型不是String类型,是组件的话,调用createComponent创建vnode。如果tag是String类型,再去判断tag是否是html的保留标签,是否是不认识的节点,通过调用new VNode(),传入不同的参数来创建vnode实例。

无论是哪种情况,最终都是通过VNode这个class来创建vnode,下面是类VNode的源码,在文件 src/core/vdom/vnode.js 中定义

export default class VNode {

tag: string | void;

data: VNodeData | void;

children: ?Array;

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,

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 // 对应的真实DOM节点

this.ns = undefined // 命名空间

this.context = context // 当前节点上下文

this.fnContext = undefined // 函数化组件上下文

this.fnOptions = undefined // 函数化组件配置参数

this.fnScopeId = undefined // 函数化组件ScopeId

this.key = data && data.key // 子节点key属性

this.componentOptions = componentOptions // 组件配置项

this.componentInstance = undefined // 组件实例

this.parent = undefined // 父节点

this.raw = false // 是否是原生的HTML片段或只是普通文本

this.isStatic = false // 静态节点标记

this.isRootInsert = true // 是否作为根节点插入

this.isComment = false // 是否为注释节点

this.isCloned = false // 是否为克隆节点

this.isOnce = false // 是否有v-once指令

this.asyncFactory = asyncFactory // 异步工厂方法

this.asyncMeta = undefined // 异步Meta

this.isAsyncPlaceholder = false // 是否异步占位

}

// DEPRECATED: alias for componentInstance for backwards compat.

/* istanbul ignore next */

get child (): Component | void {

return this.componentInstance

}

}

VNode类定义的数据,都是用来描述VNode的。

至此,render函数创建vdom的源码就分析完了,我们简单的总结梳理一下。

_render 定义在 Vue.prototype 上,_render函数执行会调用方法render,在开发环境下,会对vm实例进行代理,校验vm实例数据正确性。render函数内,会执行render的参数createElement方法,createElement会对参数进行处理,处理参数后调用_createElement, _createElement方法内部最终会直接或间接调用new VNode(), 创建vnode实例。

03   vnode && vdom

createElement 返回的vnode并不是真正的dom元素,VNode的全称叫做“虚拟节点 (Virtual Node)”,它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,及其子节点。我们常说的“虚拟 DOM(Virtual Dom)”是对由 Vue 组件树建立起来的整个 VNode 树的称呼。

04  心得

读源码切忌只看源码,一定要结合具体的使用一起分析,这样才能更清楚的了解某段代码的意图。就像本文render函数,如果从来没有使用过render函数,直接就阅读这块源码可能会比较吃力,不妨先看看文档,写个demo,看看具体的使用,再对照使用来分析源码,这样很多比较困惑的问题就迎刃而解了。

总结

以上所述是小编给大家介绍的vue 中Virtual Dom被创建的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值