1
Vue版本:2.5.17-beta.0

在《Vue源码笔记 — 数据驱动--Vue实例挂载》文章中的结尾记录过:
updateComponent 函数主要做了两件事,第一个就是通过 vm._render() 生成 vnode 对象,第二个就是通过 vm.update(vm._render(), hydrating) 挂载到最终dom上。

这次记录 vm._render() 的大体实现过程。首先 _render 的原型方法在 src/core/instance/render.js 文件中,源码如下:

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    // reset _rendered flag on slots for duplicate slot check
    if (process.env.NODE_ENV !== 'production') {
      for (const key in vm.$slots) {
        // $flow-disable-line
        vm.$slots[key]._rendered = false
      }
    }

    if (_parentVnode) {
      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }

    // 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 {
      // vm._renderProxy 在生产环境下为 vm; 开发环境下的设置 在 _init中
      // render.call是 vm.$createElement返回值 为一个vnode对象
      // 生成渲染vnode
      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') {
        if (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
        }
      } else {
        vnode = vm._vnode
      }
    }
    // return empty vnode in case the render function errored out
    // 如果有多个根节点(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指向 占位符vnode
    vnode.parent = _parentVnode
    return vnode
}

上述源码中,大体逻辑是先获取 render_parentVnode_parentVnode 其实是当前子节点的父级vnode数据,在普通节点渲染中不会有,只有组件渲染时会有,以后记录组件相关文章会提到。),然后调用 render 函数并传入 vm._renderProxyvm.$createElement,最后生成当前vnode数据并返回。

其中 vm._renderProxy 是在调用 _init 时就被初始化了,_init 部分源码如下:

if (options && options._isComponent) {
  // optimize internal component instantiation
  // since dynamic options merging is pretty slow, and none of the
  // internal component options needs special treatment.
  // 子组件实例合并较快 因为不需要调用mergeOptions
  // vm 为Sub构造器实例
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor), // 返回 Vue.options
    options || {},
    vm
  )
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件中心
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 对props、methods、data做proxy处理; 对data做响应式处理; 建立user Watcher和computed Watcher
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

上述源码中可看到,在合并配置完成后就定义了 vm._renderProxy,并且在生产环境下设置 vm._renderProxy = vm 其实就是当前实例对象,但是在开发环境下调用 initProxy(vm) 设置 vm._renderProxyinitProxy(vm) 方法在 src/core/instance/proxy.js 文件中,源码如下:

let initProxy

if (process.env.NODE_ENV !== 'production') {
  const allowedGlobals = makeMap(
    'Infinity,undefined,NaN,isFinite,isNaN,' +
    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
    'require' // for Webpack/Browserify
  )

  const warnNonPresent = (target, key) => {
    warn(
      `Property or method "${key}" is not defined on the instance but ` +
      'referenced during render. Make sure that this property is reactive, ' +
      'either in the data option, or for class-based components, by ' +
      'initializing the property. ' +
      'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
      target
    )
  }

  const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)

  if (hasProxy) {
    const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
    config.keyCodes = new Proxy(config.keyCodes, {
      set (target, key, value) {
        if (isBuiltInModifier(key)) {
          warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
          return false
        } else {
          target[key] = value
          return true
        }
      }
    })
  }

  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_')
      if (!has && !isAllowed) {
        warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

  const getHandler = {
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        warnNonPresent(target, key)
      }
      return target[key]
    }
  }

  initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
}

在上述源码中最下面的代码里,设置了 initProxy 为一个函数,并当浏览器支持 Proxy 时调用 new Proxy(vm, handlers),在当前的逻辑中 handlershasHandler 可以看到,其实当使用 this.xxx 获取内容时,会先劫持判断是否符 hasHandlerhasisAllowed 的条件,不符合就报 warnNonPresent 中的警告内容:

warn(
  `Property or method "${key}" is not defined on the instance but ` +
  'referenced during render. Make sure that this property is reactive, ' +
  'either in the data option, or for class-based components, by ' +
  'initializing the property. ' +
  'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
  target
)

vm._renderProxy 已分析完毕,而 vm.$createElement 就是生成vnode数据的重要方法了,它是在 _init 时调用 initRender(vm) 时创建的,initRendersrc/core/instance/render.js 文件中,源码如下:

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
  // 获取占位符vnode
  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
  // 编译生成render函数 提供的方法
  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)

  // $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.$createElementvm._c 都是调用 createElement 方法,只是最后一个参数不同,因为一个是通过编译生成 render 函数时调用的方法(vm._c),一个是我们手写 render 函数时调用的方法(vm.$createElement),而 createElement 方法以后会去记录。

至此,render 已记录完毕。


Dragon
40 声望4 粉丝