Vue源码解读(五):render和VNode

本文深入探讨Vue 2.0中的VNode概念及其在渲染过程中的作用。讲解了如何通过_createElement方法创建VNode,组件类型的处理,包括普通组件、异步组件和函数式组件的实现细节,以及VNode在更新视图中的关键角色。文章还涵盖了Vue的render函数、虚拟DOM和diff算法,帮助理解Vue的性能优化策略。
摘要由CSDN通过智能技术生成

Vue 2.0 相比 Vue 1.0 最大的升级就是利用了虚拟DOM。 在 Vue 1.0 中视图的更新是纯响应式的。在进行响应式初始化的时候,一个响应式数据 key 会创建一个对应的 dep,这个 key 在模板中被引用几次就会创建几个 watcher。也就是一个 key 对应一个 depdep 内管理一个或者多个 watcher。由于 watcherDOM 是一对一的关系,更新时,可以明确的对某个 DOM 进行更新,更新效率还是很高的。

随着应用越来越大,组件越来越多,一个组件就可能存在大量的 watcher ,性能就成了问题。Vue 2.0 加入了虚拟DOM和 diff 后,一个组件就只需要创建一个 watcher 了,更新方式使用响应式+diff,组件之间使用响应式,组件内部使用 diff 。当数据发生变化时,通知 watcher 更新,也就是通知整个组件更新,具体更新什么元素什么位置,就可以通过 diff 算法对比新旧虚拟DOM获取差异,把差异真正的更新到视图。

虚拟DOM在 React、Vue 都有运用,一方面可以提升一些性能,另一方面也可以更好的跨平台,比如服务端渲染。

虚拟DOM,也就是VNode,本质上是使用 js 对象来模拟真实DOM中存在的节点。举个栗子:

<div id="app">
  <h1>虚拟DOM<h1>
</div>
{
   
  tag: "div",
  attrs: {
   
    id: "app"
  },
  children: [
    {
   
      tag: "h1",
      text: "虚拟DOM"
    }
  ]
}

VNode

Vue 中的虚拟DOM是用 一个 Class 去描述的。我们先来看看:

// src/core/vdom/vnode.js
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
  }
}

_render

在之前介绍过,响应式数据发生变化时,触发更新其实就是执行下面代码:

updateComponent = () => {
   
  vm._update(vm._render(), hydrating)
}

先执行 vm._render() ,获取VNode ,做为参数传递给 vm._update 去更新视图。我们先看看 vm._render() 的定义:

// src/core/instance/render.js
export function renderMixin (Vue: Class<Component>) {
   
  // install runtime convenience helpers
  // 在组件实例上挂载一些运行时需要用到的工具方法
  installRenderHelpers(Vue.prototype)
  Vue.prototype.$nextTick = function (fn: Function) {
   
    return nextTick(fn, this)
  }
  /**
   * 通过执行 render 函数生成 VNode
   * 不过里面加了大量的异常处理代码
   */
  Vue.prototype._render = function (): VNode {
   
    const vm: Component = this
    const {
    render, _parentVnode } = vm.$options
    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.
    // 设置父 vnode。这使得渲染函数可以访问占位符节点上的数据。
    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
      // 执行 render 函数,生成 vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
   
      handleError(e, vm, `render`)
      // 错误处理,开发环境渲染错误信息,生产环境返回之前的 vnode,以防止渲染错误导致组件空白
      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
    }
    // 如果 vnode 是数组且只有一项,直接放回该项
    if (Array.isArray(vnode) && vnode.length === 1) {
   
      vnode = vnode[0]
    }
    // render 函数出错时,返回一个空的 vnode
    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 中会调用 render 方法,这里有两种情况:

  • 模板编译而来的 render
with(this){
   return _c(tag, data, children, normalizationType)}
  • 用户定义的 render
render: function (createElement) {
   
  return createElement('div', {
   
     attrs: {
   
        id: 'app'
      },
  }, this.msg)
}

上面可以看出模板编译出来的 render 实际就是调用 _c 方法,用户自定义的 render 实际就是 vm.$createElement 方法。

// src/core/instance/render.js
export function initRender (vm: Component) {
   
/**
 * @param {*} a 标签名
 * @param {*} b 属性的 JSON 字符串
 * @param {*} c 子节点数组
 * @param {*} d 节点的规范化类型
 * @returns VNode or Array<VNode>
 */
  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 方法。

createElement

// src/core/vdom/create-element.js
// 生成组件或普通标签的 vnode,一个包装器函数,用于提供更灵活的接口。
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
  }
  // 执行 _createElement 方法创建组件的 VNode
  return _createElement(context
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值