在做vue开发的时候,会经常遇到下面的报错,那么这样的提示是如何实现的呢?
为什么有这样的报错?
是因为在模版(template)或者render函数中使用了没有声明过的属性或者方法。
那么vue又是如何发现用户使用了未定义的属性呢?
首先这段报错是出现在render期间,也就是触发update生成vnode期间,在源码中我们可以看到这样一段代码:
try {
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleError(e, vm, "render function");
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
vnode = vm.$options.renderError
? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
: vm._vnode;
} else {
vnode = vm._vnode;
}
}
复制代码
这段代码的作用就是根据render函数生成vnode,这段不多做解释,感兴趣的话,可以自己了解一下vue的源码。 可以看到render.call(vm._renderProxy, vm.$createElement)
,其中看到render的上下文并不是vue实例本身(vm),而是vm._renderProxy
。其实vm._renderProxy
指向是经过代理过的vm的属性访问的代理对象(使用Proxy实现)。
vm._renderProxy
的实现(源码位于 src/core/instance/proxy.js中)
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
}
}
}
复制代码
在这段代码中,可以看出vue使用了Proxy来生成一个拦截对象。并且这里有一个判断语句
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
复制代码
这里定义了两个函数分别是getHandler和hasHandler,这个两个函数分别定义拦截vm的get行为和has行为,源码如下:
const hasHandler = {
// 拦截in操作符, 例如 'name' in vm
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) || 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]
}
}
复制代码
如上hasHandler和getHandler分别拦截了has操作(例如 'a' in obj 会触发has拦截)和get操作。
那么这里为什么需要分为getHandler和hasHandler,按理说只要拦截.
操作符,就可以在模版取变量的时候,对属性值的取值过程进行处理,从而报错了,应该是不需要hasHandler呀。
其实是针对两种情况的:
- 用户书写模版,模版编译生成render函数
- 用户书直接写render函数
当options.render不存在的时候,也就是用户是通过书写模版(template)形式通过编译来生成reder函数的时候。此时render函数会生成下面这样的代码:
function anonymous(
) {
with(this){return _c('div')}
}
复制代码
那么此时用的是hasHandler也就是拦截in操作符,我们可以看到生成的render函数使用with包含者的,当去属性值的时候,会先检查正常的上下文上有没有绑定相应的属性,如果没有则查看绑定的this上下文中有没有这个属性,此时就会触发hasHandler拦截操作了。
当options.render存在的时候,则是用户自己编写render函数,例如:
render (h) {
return h('div', this.prop)
}
复制代码
此时如果在render函数通过.
操作符取值的话,则会触发getHandler操作
那么如果我使用Number,或者Infinity这样的全局变量或者方法,这些方法也没有绑定在this上下问上,会不会报错呢?
在拦截函函数里面我们可以看到:
const has = key in target
const isAllowed = allowedGlobals(key) || key.charAt(0) === '_'
if (!has && !isAllowed) {
warnNonPresent(target,key)
}
复制代码
不仅判断了当前变量是否存在与当前对象上,并且还有一个isAllowed变量表示是否可以允许。在allowedGlobals中就存储了绑定在全局上下文的变量。从而避免了对于全局变量和函数的拦截操作。
全局变量定义如下:
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
)
复制代码