vue源码分析-10-虚拟dom之render()

703 阅读4分钟

经过模版编译后,会生成一个render函数, 生成的render函数会赋值给options.render

entry-runtime-with-compiler.js中的mount方法生成render函数之后调用了runtime.js中定义的mount方法,mount方法调用了mountComponent方法,此方法中执行了如下代码

updateComponent = () => {
      // 先调用_render()方法生成vnode 然后调用_update方法,更新真实dom
      vm._update(vm._render(), hydrating)
    }

调用了vm._render()方法,_render方法是在renderMixin(Vue)方法中定义的,定义在render.js中,那么我们接下来看一下_render函数的逻辑

// 挂在_render方法
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    // 拿到render函数和_parentVnode
    // _parentVnode就是类似 <comp1 prop1="123"></comp1>  转化而来的vnode,而非comp1组件内部的vnode,组件内部的vnode是vm._vnode
    // render函数可以是从模板编译来的也可以是用户自定义的render函数
    const { render, _parentVnode } = vm.$options

    // 如果是非根组件,因为根实例没有_parentVnode
    if (_parentVnode) {
      // 处理slot相关数据
      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表示父节点的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.
      // 全局变量,当前正在渲染的Vue实例
      currentRenderingInstance = vm
      // 执行render函数,参数是 $createElement方法,this = vm
      // 如果是用户自定义render函数,那么会调用render(vm.$createElement)
      // 如果是从模板编译来的,都是调用vm._c()
      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()方法的主要逻辑在于如下这段代码

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

render就是生成的render函数,这里会执行render函数 模版编译生成的render函数类似如下,执行_c()方法

function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('comp1',{attrs:{"prop1":"hello"}})],1)}
}

_c()方法定义在vm._c()下,是在执行initRender的时候定义的,调用了createElement()方法

// 通过template编译生成的render函数会执行的_c方法
  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.
  // 用户自定义会执行的render方法
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

render.call(vm._renderProxy, vm.$createElement)传入的vm.$createElement其实是为了用户手写的render函数服务的,如下

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

接下来我们分析以下createElement方法

createElement方法中主要调用了_createElement方法 这个方法主要作用就是创建Vnode,即虚拟dom,如果是标签元素,就创建标签对应的Vnode,如果是组件那么创建组件的Vnode。

我们可以看一下Vnode的数据结构,其实本质上就是将dom节点抽象成对象

this.tag = tag // 当前节点标签名
this.data = data // 当前节点数据(VNodeData类型)
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 // 静态节点标志 keep-alive
this.isRootInsert = true // 是否作为根节点插入
this.isComment = false // 是否为注释节点
this.isCloned = false // 是否为克隆节点
this.isOnce = false // 是否为v-once节点
this.asyncFactory = asyncFactory // 异步工厂方法
this.asyncMeta = undefined // 异步Meta
this.isAsyncPlaceholder = false // 是否为异步占位

我们看到render方法中不仅有_c方法,也有很多其他方法,例如如下模版编译后的render函数

comp2: {
         template: '<h1>comp2</h1>'
       }
function anonymous(
) {
with(this){return _c('h1',[_v("comp2")])}
}

此render函数中先回执行_v方法,那么_v是什么呢, 其实还有很多生成Vnode的方法定义在Vue.prototype中,如下

export function installRenderHelpers (target: any) {
  //
  target._o = markOnce
  target._n = toNumber
  // 返回字符串
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  // 创建一个文本的vnode
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

installRenderHelpers是在renderMixin方法中调用的

// 原型挂在一些render需要用到的转换方法
  installRenderHelpers(Vue.prototype)

我们可以看到_v方法的定义,其实也是最简单的文本节点的vnode生成,直接实例化了一个Vnode对象

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

其他诸如此类的方法有的是处理数据,有的是生成Vnode,大同小异

总结

其实虚拟dom就是将render函数执行,render函数中有一系列的方法可以会生成对应的Vnode,如文本节点等,Vnode就是对模版的抽象,将模版抽象成具有树状结构的数据。虚拟的节点生成好之后,下一步就是将虚拟的dom渲染成真实的dom节点。