![f26ddf98fcf560efddf28ee37fc4a2ca.png](https://i-blog.csdnimg.cn/blog_migrate/1c208a738dbacf36b3886324c39a11cb.jpeg)
阅读源码通常是枯燥无味的,类似 Vue 这种框架级的,代码量更是巨大;且各个实现之间关联性很大,跟踪源码非常跳跃,看完后总是稀里糊涂。今天,从一个常见的错误说起,与使用场景相结合,带着目的去查看源码。
从一个告警说起
Vue 工程中,在 data 对象中,使用 _
或 &
开头命名变量,且将该变量应用到模板中,会收到如下警告(开发模式下):
[Vue warn]: Property myName must be accessed with $data._myName because properties starting with "$" or "" are not proxied in the Vue instance to prevent conflicts with Vue internals.
const app = new Vue({
el: '#test',
data () {
return {
_myName: 'ligang'
}
}
})
注意:上面的限定词(模板中使用!)
- 在 data 中声明变量,并不会报错(如,上述
_myName
) - 在非模板中使用,不会报错,但会返回 undefined
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官网
如何达到效果
![47844f0bed03a69a2420df1ea603cda1.png](https://i-blog.csdnimg.cn/blog_migrate/99e4bb9a806c13705bca059855ce3015.jpeg)
通过数据代理(劫持) 实现!访问或者修改对象的某个属性时,拦截这个行为并进行额外的操作或者修改返回的结果(在访问时进行依赖收集,在修改更新时对依赖进行更新),这也是 Vue 响应式系统的核心。
Object.defineProperty()
:利用存取描述符中的getter/setter
来进行数据的监听
对于数组索引的新增等,Object.defineProperty()
不具备代理的能力,Vue
在响应式系统中对数组的方法进行了重写,间接的解决了这个问题。--
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:L2108
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:
- getHandler:_withStripped 为 true 的情况,单元测试中存在,其他地方暂未发现(欢迎大家补充);
- hasHandler:
has (target, key)
重点关注 - 属性查询:
foo in proxy
- 继承属性查询:
foo in Object.create(proxy)
with
检查:with(proxy) { (foo) }
Reflect.has()
- initState:L5000
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(){returnthis._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
钩子,这也是模板渲染时之所有会触发代理拦截的原因!
hasHandler L2085
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
}
};
- 模板中允许出现的非vue实例定义的变量
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
);
2.以$/_
开头,或者是否是data
中未定义的变量做判断过滤
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
注意,这里并没有 $ 了啊,这要具体看 initData L4733
3. 错误提示
- warnReservedPrefix:开头处报的错误
- warnNonPresent:未定义
不支持 proxy 的情况
数据过滤就失效,直接跑错 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(初始化数据仍然会被拦截)。
参考地址
- https://cn.vuejs.org/v2/api/#data
- https://github.com/vuejs/vue/blob/v2.6.11/dist/vue.js
- https://ocean1509.github.io/In-depth-analysis-of-Vue/src/%E5%9F%BA%E7%A1%80%E7%9A%84%E6%95%B0%E6%8D%AE%E4%BB%A3%E7%90%86%E6%A3%80%E6%B5%8B.html