阅读源码通常是枯燥无味的,类似 Vue 这种框架级的,代码量更是巨大;且各个实现之间关联性很大,跟踪源码非常跳跃,看完后总是稀里糊涂。今天,从一个常见的错误说起,与使用场景相结合,带着目的去查看源码。
Vue 工程中,在 data 对象中,使用 _
或 &
开头命名变量,且将该变量应用到模板中,会收到如下警告(开发模式下):
[Vue warn]: Property myName must be accessed with data.myNamebecausepropertiesstartingwith"data._myName because properties starting with "data.myNamebecausepropertiesstartingwith"" or "" are not proxied in the Vue instance to prevent conflicts with Vue internals.
const app = new Vue({
el: '#test',
data () {
return {
_myName: 'ligang'
}
}
})
注意:上面的限定词(模板中使用!)
_myName
)created () {
console.log(this._myName) // undefined
console.log(this.$data._myName) // ligang
}
<div id="#test">
<span>{{_myName}}</span> <!-- 报错 -->
<span>{{$data._myName}}</span> <!-- 可以正常渲染 -->
</div>
查阅 Vue 源码之前,先思考,为什么要这样设计?以及如何才能达到上述的效果?
以 _ 或 开头的属性 不会 被 Vue 实例代理,因为它们可能和 Vue 内置的属性、API 方法冲突。你可以使用例如 vm.data._property 的方式访问这些属性。 – Vue官网
通过数据代理(劫持) 实现!访问或者修改对象的某个属性时,拦截这个行为并进行额外的操作或者修改返回的结果(在访问时进行依赖收集,在修改更新时对依赖进行更新),这也是 Vue 响应式系统的核心。
Object.defineProperty()
:利用存取描述符中的getter/setter
来进行数据的监听
对于数组索引的新增等,Object.defineProperty()
不具备代理的能力,Vue
在响应式系统中对数组的方法进行了重写,间接的解决了这个问题。-- https://github.com/vuejs/vue/blob/v2.6.11/dist/vue.js#L860
var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; methodsToPatch.forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); });
Proxy
:针对目标对象会创建一个新的实例对象,并将目标对象代理到新的实例对象上(通过操作新的实例对象就能间接的操作真正的目标对象了)
Vue 对 vm 实例设置代理,为 vue 在模板渲染前做数据筛选。
Vue.prototype._init:L4991
Vue.prototype._init = function (options) {
initProxy(vm);
initState(vm);
}
initProxy = function initProxy (vm) {
if (hasProxy) { // 是否支 持Proxy,typeof Proxy !== 'undefined' && isNative(Proxy)
var options = vm.$options;
var handlers = options.render && options.render._withStripped
? getHandler
: hasHandler;
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
Proxy
时,vm._renderProxy
会代理 vm
实例Proxy
时,直接将 vm
赋值给 vm._renderProxy
getHandler & hasHandler:
has (target, key)
重点关注foo in proxy
foo in Object.create(proxy)
with
检查:with(proxy) { (foo) }
Reflect.has()
function initState (vm) {
initData(vm);
}
initData(isReserved
):L4733
function initData(vm) {
vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
if (!isReserved(key)) {
// 数据代理,用户可直接通过vm实例返回data数据
proxy(vm, "_data", key);
}
}
function isReserved (str) {
var c = (str + '').charCodeAt(0);
// 首字符是$, _的字符串
return c === 0x24 || c === 0x5F
}
基于上述提到的 Object.defineProperty
来实现的。
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
this._myName
实际访问的是 this._data._myName
,以 $, _
开头,没有被代理,所以无法通过 this._myName
访问到。
为什么
this.$data._myName
可以访问: L4925 Object.defineProperty(Vue.prototype, '$data', dataDef dataDef.get = function () { return this._data };
触发数据代理拦截是因为模板中使用了变量{{_myName}}}
。而如果我们在模板中使用了未定义的变量,这个过程就被. proxy
拦截,并定义为不合法的变量使用
模板 ==> AST ==> render函数 ==> vnode对象(virtual dom) ==> 真实Dom
由于模板使用了变量 vm._renderProxy
被调用(接上述)。Vue.prototype._render:L3527
Vue.prototype._render = function () {
vnode = render.call(vm._renderProxy, vm.$createElement);
}
通过 render 函数,生成 with 包裹的执行语句。
console.log(app.$options.render)
//输出, 模板渲染使用with语句
ƒ anonymous() {
with(this){return _c('div',{attrs:{"id":"test"}},[_c('span',[_v(_s(_myName))])])}
}
在执行 with
语句的过程中,该作用域下变量的访问都会触发上述 has
钩子,这也是模板渲染时之所有会触发代理拦截的原因!
var hasHandler = {
has: function has (target, key) {
var has = key in target;
var 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
}
};
var 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
);
$/_
开头,或者是否是data
中未定义的变量做判断过滤 (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
注意,这里并没有 $ 了啊,这要具体看 initData L4733
数据过滤就失效,直接跑错 ReferenceError: _myName is not defined
js 语法错误。Vue 层面无法做拦截,报告详细的错误信息。
上述遗漏了关于直接使用 render 函数的情况。
render(createElement){
return createElement('div', {'class': {
info: true
}}, [
`<span>${this._myName}</span>`
])
}
由于提供了相关 render 函数,所以不会生成上述 with 语句 function () {with(){}},因此不会被 hasHandler 拦截,也不会报错!直接返回undefined(初始化数据仍然会被拦截)。