2

vue的寂寞:_renderProxy属性

在vue初始化函数_init()函数中,有这样一段代码:

if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
} else {
    vm._renderProxy = vm
}

这段代码的目的主要就是为Vue实例的_renderProxy属性赋值,而这个_renderProxy目测就是用在render函数中的。我们在vue/src/core/instance/render.js中发现了这样的代码:

const { render, _parentVnode } = vm.$options
……
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)

当我们创建Vue根实例时,通常会传入一个render函数:

new Vue({
    el: '#app',
    render: h => h(App),
    router
});

因此,这个vm._renderProxy实际上指定了我们传入的这个render函数在创建Vnode的时候执行的上下文this。
回到上面,那么这个initProxy函数又是怎么给_renderProxy属性赋值的呢?我们来看看具体代码:

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

所以我们的_renderProxy属性赋值情况可以总结如下:

  1. 当前环境是开发环境,并且hasProxy条件成立,则调用Proxy方法,给vue实例添加代理
  2. 如果其他情况,则vue实例的_renderProxy属性指向vue实例本身。

要不要代理寂寞:hasProxy

export function isNative (Ctor: any): boolean {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
 const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)

结合着vue/src/core/util/env.js中的isNative函数我们知道,hasProxy函数就是他的字面意思:当前环境中Proxy是否可用。
也就是说,当前环境是开发环境,并且Proxy是否可用,则调用Proxy方法,给vue实例添加代理。

getHandler和hasHandler:寂寞的两种爆发

Proxy的handler对象是一个占位符对象,它包含了用于Proxy的陷阱(Trap)函数。从上面的代码可以知道我们在代理vue实例时用了两种Trap函数:当vue实例中的options.render存在,并且options.render._withStripped为true时,我们用getHandler函数,即handler.get()代理实例,它在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo时。其他情况下用hasHandler,即handler.has()代理实例,它在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy时。

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]
    }

}

第一种策略,我们在读取vm实例的某个属性时,如果它不是string类型或者属性值在vm实例上不存在,则抛出错误提示。
当然报错也分为两类,如果该属性在$data上找到了,就会报这样一个错:

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
    )
}

原因从报错中就能知道,如果我们在严格模式下代理了以$或者_开头的属性,那就必须通过$data.${key}的方式获得,以便跟vue的内部方法区分。
其他情况,当属性真的不存在时,就会报下面这样的警告,

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
    )
}

这个警告是不是特别熟悉?在开发环境忘记在data或者method中加属性或方法,经常会看到这个警告。

第二种策略,我们在查看vm实例是否拥有某个属性时,比如调用for in循环遍历vm实例属性时,会触发hasHandler方法

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

当读取vm对象属性时,如果属性名在vm实例上不存在,且不在特殊属性名称映射表中,或没有以_符号开头。则抛出上面那个不存在的警告。

总结:用测试用例化解寂寞

上面说了很多源码的东西,比较抽象,没有场景落地,确实不好理解,这里我们就用vue的测试用例vue/test/unit/features/instance/render-proxy.spec.js来说明一下上面的两种策略:

it('should warn missing property in render fns with `with`', () => {
new Vue({
    template: `<div>{{ a }}</div>`
}).$mount()
expect(`Property or method "a" is not defined`).toHaveBeenWarned()
})

这种情况,我们没有传入render函数,因此它触发了hasHandler。而在其中它发现a在vm实例上不存在,且不在特殊属性名称映射表中,也没有以_符号开头,因此他抛出一个不存在的警告。

it('should warn missing property in render fns without `with`', () => {
const render = function (h) {
    return h('div', [this.a])
}
render._withStripped = true
new Vue({
    render
}).$mount()
expect(`Property or method "a" is not defined`).toHaveBeenWarned()
})

这种情况,我们传入render函数,并且render._withStripped为true因此它触发了getHandler。而我们在使用this.a时,触发了get,它发现a在vm实例上不存在,且不在$data中,因此他抛出一个不存在的警告。

it('should warn properties starting with $ when not found (with stripped)', () => {
const render = function (h) {
    return h('p', this.$a)
}
render._withStripped = true
new Vue({
    data: { $a: 'foo' },
    render
}).$mount()
expect(`Property "$a" must be accessed with "$data.$a"`).toHaveBeenWarned()
})

这种情况,我们传入render函数,并且render._withStripped为true因此它触发了getHandler。而我们在使用this.$a时,触发了get,它发现a在vm实例上不存在(vm没有代理),但在$data中存在,因此他抛出一个前缀的警告。


FrankChencc
214 声望9 粉丝