手撕Vue源码之——render函数与Virtual DOM

365 阅读3分钟

render

Vue的_render方法是实例的一个私有方法,他用来吧实例渲染成一个虚拟node。他的定义在src/core/instance/render.js文件中

  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.
    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
      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
      }
    } finally {
      currentRenderingInstance = null
    }
    // 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方法的调用,我们在平时的开发工作中手写render方法的场景比较少,而写的比较多的是template模板,在之前的mounted方法的实现中,会把template编译成render方法,但这个编译过程是非常复杂的。

在Vue的官方文档中介绍了render函数的第一个参数是createElement,那么结合之前的例子:

<div id='app'>
    {{message}}
</div>

相当于我们编写如下render函数:

render:function (createElement) {
    return createElement('div', {
        attrs: {
            id: "app"
        }
    }, this.message)
}

在回到_render函数中的render方法的调用:

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

可以看到,render函数中的createElement方法就是vm.$createElement方法:

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // 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)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

实际上,vm.createElement方法定义是在执行initRender方法的时候,可以看到除了vm.createElement方法定义是在执行initRender方法的时候,可以看到除了vm.createElement方法,还有一个vm._c方法,他是被模板编译成的render函数使用,而vm.$createElement是用户手写render方法使用的,这两个方法支持的参数相同,并且内部都调用了createElement方法。

总结

vm._render最终是通过执行createElement方法返回的是vnode,他是一个虚拟Node。Vue2.0相比Vue1.0最大的升级就是李永利VirtualDOM,因此在分析createElement的实现前,我们先了解一下VirtualDOM的概念

Virtual DOM

Virtual DOM这个概念相信大部分都不会陌生,它产生的前提是浏览器中的DOM非常昂贵,为了更直观的感受,我们可以简单的把一个简单的div的属性都打印出来,如图所示:

Snipaste_2021-06-03_17-38-33.png

可以看到,真正的DOM元素是非常庞大的,因为浏览器的标准就是吧DOM设计得非常复杂。当我们频繁的去做DOM的更新,会产生一定的性能问题。

而VirtualDOM就是用一个原生的JS对象去描述一个DOM节点,所以他比创建一个DOM的代价要小很多。在Vue.js中,VirtualDOM是用VNode这么一个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
  }
}

可以看到Vue.js中的VirtualDOM的定义还是略微复杂一些的,因为他这里包含了很多Vue.js的特性。这里千万不要被这些茫茫多的属性给吓到,实际上Vue.js中VirtualDOM是借鉴了一个开源库snabbdom的实现,然后加入了一些Vue.js特色的东西,我建议大家如果想深入了解Vue.js的VirtualDOM钱不妨先阅读一下这个库的源码,因为他更加简单和纯粹。

总结

其实VNode是对真实DOM的一种抽象描述,他的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其实属性都是用来扩展VNode的陵侯兴以及实现一些特殊的feature的。由于VNode只是用来映射真实DOM的渲染,不需要包含操作DOM的方法,因此他是非常轻量和简单的。

Virtual DOM除了他的数据结构的定义,映射到真实的DOM实际上要经历VNode的create、diff、patch等过程。那么在Vue.js中,VNode的create是通过之前提到的createElement方法创建的。