vdom 结构
- 浏览器原生dom对象,本身就是一个非常复杂的对象,单单把 div 这个dom对象拿出来,遍历它的属性,将是一个庞大的存在
- 因为浏览器的标准就是把这个dom设计的非常复杂,所以当我们去频繁的操作dom的话,一定会有一些性能问题
- vdom(Virtual DOM), 其实就是用一个原生的js对象去描述一个dom节点,它的创建比创建一个真实的dom的代价要小很多
- 在vue.js中的 vdom 的定义在 src/core/vdom/vnode.js
/* @flow */ 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 this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = 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 } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
- 上述
VNodeData
,定义在 flow/vnode.js 中declare interface VNodeData { key?: string | number; slot?: string; ref?: string; is?: string; pre?: boolean; tag?: string; staticClass?: string; class?: any; staticStyle?: { [key: string]: any }; style?: string | Array<Object> | Object; normalizedStyle?: Object; props?: { [key: string]: any }; attrs?: { [key: string]: string }; domProps?: { [key: string]: any }; hook?: { [key: string]: Function }; on?: ?{ [key: string]: Function | Array<Function> }; nativeOn?: { [key: string]: Function | Array<Function> }; transition?: Object; show?: boolean; // marker for v-show inlineTemplate?: { render: Function; staticRenderFns: Array<Function>; }; directives?: Array<VNodeDirective>; keepAlive?: boolean; scopedSlots?: { [key: string]: Function }; model?: { value: any; callback: Function; }; };
- vdom实际上它比真实的dom对象创建的代价要小很多
- vdom 是借鉴了一个开源库 snabbdom 的实现
- 它的设计比较巧,实现的diff算法和react是不太一样的,号称是性能非常高的
- 除了vuejs,其他的vdom的实现也是基于它的
- 所以,它是 vue.js 实现的一个基础, 但在它上面又做了很多扩展
- 总结来说
- vNode 它其实就是对原生dom的一种抽象的描述
- 它的核心无非就几个关键属性,如:标签名、数据、子节点、键值等
- 那其他属性它其实都是来为来扩展 vnode 灵活性以及实现一些特殊feature
- 由于 vNode 它只是用来映射真实dom渲染的,它不需要包括这些操作dom的方法
- 所以说它是比较轻量和简单的
- vdom 除了它的数据结构的定义映射到真实的dom
- 还有create, diff 和 patch 的过程
- 在 vNode 的 create 的过程就是通过 createElement 方法
- 也就是在 render 函数中调用的vm.$createElement 返回的Vnode
createElement 的实现
- 在 render 函数提到的生成vnode方法,也就是它最终会调用这个 createElement 的方法来生成vnode
- render方法,它最终就会调用 option.render 函数
- 这个函数的执行分为两种情况
- 一种情况就是通过把模板编译出来的 render 函数,它内部实际上会调用
vm._c
- 而用户手写的render函数,最终会调用 vm.$createElement 这这个方法
- 这两个方法最终都会调用这个 createElement 这个函数
- 它的函数唯一的区别就是最后一个参数,也就是这六个参数,false 或 true
// initRender 函数中 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
- 一种情况就是通过把模板编译出来的 render 函数,它内部实际上会调用
- 进入 createElement,定义在 src/core/vdom/create-element.js 中
const SIMPLE_NORMALIZE = 1 const ALWAYS_NORMALIZE = 2 export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { // 这个是参数检测,如果符合,说明,第三个参数是 children, 后面的参数前移 // 这时候,之前的 data 变成了 children, 之前的 children 变成了 normalizationType // 对 children 做一个data的赋值,并且清理 data 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) }
- 它定义支持了六个参数
- 第一个参数 是 vm 实例
- 第二个就是 vnode的tag标签
- 第三个就是data,就是跟vNode相关的一些数据
- 第四个children是它的一些子节点 vnode, 由此构造出vNode tree, 完美映射 dom tree
- 内部对参数个数不一致,进行处理
- 最终调用
_createElement
- 也就是这个函数,主要是用于处理参数的
- 进入
_createElement
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { // 响应式对象会被添加上 __ob__ 这个属性 // 首先对 data 做校验,data是不能是响应式的,否则进行警告 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 ) // 创建一个 空的VNode, 这个 VNode 本质上就是一个 注释 VNode return createEmptyVNode() } // 获取 component is // 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 } // 对 children 做 normalize, 当手写 render 函数时,比如传递一个字符串作为children // 但是 children 实际上是一个数组,每一个数组都是 vnode // 另外在编译的时候,会有不同的情况产生 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } // 对children进行 normalize 后就能很好处理children了 let vnode, ns // 对 tag 做一些判断 // tag 可能是 string 也可能是组件 if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) // 判断是否是html保留标签 if (config.isReservedTag(tag)) { // platform built-in elements if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') { warn( `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`, context ) } // 创建一些平台内建元素实例化的vnode 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() } }
- _createElement 才是真正创建 vNode 的参数
- 进入
simpleNormalizeChildren
,定义在 src/core/vdom/helpers/normalize-children.js/* @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. // 基础类型,返回text基础类型;否则判断是数组,进行处理,否则是undefined 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) } // 存储返回的 res 数组,遍历children, children 本身又是 array, 它处理有可能的多层嵌套,递归处理 // 子节点,像 slot, v-for 可能生成 深层数据结构 function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> { const res = [] // 最终返回的结果集 let i, c, lastIndex, last // 遍历 children 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 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() } // 最终push c 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 }
- 其实 normalizeArrayChildren 比 simpleNormalizeChildren 做的更多的是
- 递归处理很多层,拍平到一维数组中
- 最后处理节点和新处理节点同样是文本节点,进行合并
- 最终 normalizeArrayChildren 要把深层数据变成一维vnode数组
- 其实 normalizeArrayChildren 比 simpleNormalizeChildren 做的更多的是
- 它定义支持了六个参数
- 以上是 createElement 创建 VNode 的过程
- 每个 VNode 有 children,children 每个元素也是一个 VNode
- 这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree