Vue2源码梳理:vdom结构与createElement的实现

本文详细解释了VirtualDOM(vdom)在Vue.js中的作用,它是如何通过更小的JavaScript对象描述DOM节点,对比真实DOM的创建成本。讲解了VNode的数据结构,以及createElement方法在构建VNode树和渲染过程中的角色,强调了vdom在性能优化和组件复用中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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)
      
  • 进入 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数组
  • 以上是 createElement 创建 VNode 的过程
  • 每个 VNode 有 children,children 每个元素也是一个 VNode
  • 这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值