render和renderProxy

介绍完$mount后,我们来看一下render以及renderProxy相关的逻辑,这一节的主要目标是:弄清楚renderProxy的作用以及render的实现原理。

renderProxy

我们在之前介绍的initMixin方法中,有下面这样一段代码:

import { initProxy } from './proxy'
export default initMixin (Vue) {
  Vue.prototype._init = function () {
    // ...
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // ...
  }
}

initProxy是定义在src/core/instance/proxy.js文件中的一个方法,其代码如下:

let initProxy
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
  }
}

代码分析:

  • 这个方法首先判断了当前环境是否支持原生Proxy,如果支持则创建一个Proxy代理,其中hasProxy是一个boolean值,它的实现逻辑如下:
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
  • 然后根据options.renderoptions.render._withStripped的值来选择使用getHandler还是hasHandler,当使用vue-loader解析.vue文件时,这个时候options.render._withStripped为真值,因此选用getHandler。当选择使用compiler版本的Vue.js时,我们的入口文件中根实例是这样定义的:
import Vue from 'vue'
import App from './App'
new Vue({
  el: '#app',
  components: { App },
  template: '<App/>'
})

这个时候,对于根实例而言其options.render._withStrippedundefined,因此使用hasHandler。在搞清楚什么时候使用getHandlerhasHandler后,我们可能会有另外的问题: getHandlerhasHandler是干什么的?怎么触发?

在回答第一个问题之前,我们先来看一下getHandlerhasHandler的定义:

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 warnReservedPrefix = (target, key) => {
  warn(
    `Property "${key}" must be accessed with "$data.${key}" because ` +
    'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
    'prevent conflicts with Vue internals. ' +
    'See: https://vuejs.org/v2/api/#data',
    target
  )
}

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

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

我们可以看到,getHandlerhasHandler所做的事情几乎差不多,都是在渲染阶段对不合法的数据做判断和处理。对于warnNonPresent而言,它提示我们在模板中使用了未定义的变量;对于warnReservedPrefix而言,它提示我们不能定义带$或者_开头的变量,因为这样容易和一些内部的属性相互混淆。

<template>
  {{msg1}}
  {{$age}}
</template>
<script>
// msg1报错
// $age报错
export default {
  data () {
    return {
      msg: 'message',
      $age: 23
    }
  }
}
</script>

紧接着,我们第二个问题:getHandlerhasHandler如何触发?这其实涉及到一点ES6 Proxy方面的知识,我们以下面这段代码为例来进行说明:

const obj = {
  a: 1,
  b: 2,
  c: 3
}
const proxy = new Proxy(obj, {
  has (target, key) {
    console.log(key)
    return key in target
  },
  get (target, key) {
    console.log(key)
    return target[key]
  }
})

// 触发getHandler,输出a
proxy.a 

// 触发hasHandler,输出 b c
with(proxy){
  const d = b + c
}

在以上代码中,我们定义了一个proxy代理,当我们访问proxy.a的时候,根据Proxy相关的知识会触发getHandler,因此会输出a。当我们使用with访问proxy的时候,在其中任何属性的访问都会触发hasHandler,因此会输出bc

在以上代码分析完毕后,我们就可以对initProxy的作用进行一个总结:在渲染阶段对不合法的数据做判断和处理

render

在之前的代码中,我们在mountComponent中遇到过下面这样一段代码:

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

在这一节,我们来分析一下_render函数的实现,它其实是在src/core/instance/render.js文件中被定义:

export function renderMixin (Vue) {
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    // ...省略代码
    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`)
    }
    // ...省略代码
    vnode.parent = _parentVnode
    return vnode
  }
}

其中通过$options解构出来的render,就是我们实例化的时候提供的render选择或者通过template编译好的render函数。在_render代码中,最重要的一步是render.call函数的调用,render函数执行后会返回VNodeVNode会在之后的处理过程中使用到。

我们在render.call方法调用的时候,除了传递我们的renderProxy代理,还传递了一个$createElement函数,其中这个函数是在initRender方法中被定义:

export function initRender (vm) {
  // ...省略代码
  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)
  // ...省略代码
}

我们发现,vm.$createElementvm._c的函数定义是差不多的,唯一的区别是在调用createElement方法的时候,传递的最后一个参数不相同。$createElement_c方法虽然方法定义差不多,但使用场景是不一样的,$createElement通常是用户手动提供的render来使用,而_c方法通常是模板编译生成的render来使用的。

根据render函数的定义,我们可以把template例子改写成使用render的形式:

<template>
  <div id="app">
    {{msg}}
  </div>
</template>
<script>
export default () {
  data () {
    return {
      msg: 'message'
    }
  }
}
</script>

render改写后:

export default {
  data () {
    return {
      msg: 'message'
    }
  },
  render: ($createElement) {
    return  $createElement('div', {
      attrs: {
        id: 'app'
      }
    }, this.message)
  }
}

在这一小节,我们分析了render的实现,在下一小节我们将深入学习createElement方法的实现原理。

最后更新时间:
贡献者: wangtunan