大家好,已经有好久没写文章了,整理了上次面试的一个面试题。希望对大家有帮助。
Vue 为了监听数组变化,重写了数组原型上的方法,那么重写后怎么保证其他数组不会被污染?
面试经历
回想上次面试经历记忆犹新:
面试官 :Vue 怎么实现数据双向绑定的?
我 (张口就背) :vue 使用了 Object.defineProperty 劫持了 get set 方法,通过...,
面试官 :好,defineProperty 不能代理数组你知道吧?那你知道 Vue 是怎么实现数组的响应式?
我(心里窃喜,面试前有简单看过这里源码):Vue 重写了数组原型上的方法, 在重写的方法中触发了更新
面试官 :重写原型方法?那不是会使所有数组都受到影响? Vue 是怎么解决的?
我: .....
Vue 源码解析
1、首先我们要知道创建了一个继承原型对象,这样无论怎么重写继承对象属性,都不会影响原对象。
// 数组原型对象
var arrayProto = Array.prototype;
// 继承数组原型对象
var arrayMethods = Object.create(arrayProto);
// 被重写的数组方法名
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(function (method) {
// 缓存原始方法
var original = arrayProto[method];
// 覆盖新方法到继承对象上
// arrayMethods - 继承对象
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;
debugger
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
// push、unshift、splice 会产生新数组元素
// 这里主要是将新元素 item 也加入到观察数组中
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
2、上面的操作已经将重写的方法挂载到继承原型对象 arrayMethods 了,接下来就是需要 proxy 的数组 __proto__ 指向 arrayMethods 就可以了, 例如:
const vueArray = ['a', 'b', 'c']
vueArray.__proto__ = arrayMethods
// 这里的 push 方法事实上是用的 arrayMethods 的push, 而不是 Array.protype.push
vueArray.push('d')
3、我们看看 Vue 是何时替换 __proto__ 的
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
// 就是这个地方
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
function protoAugment (target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
在执行 new Observer(value) 时,value 如果是数组,那么 Vue 就会替换 value.proto = arrayMethods, 实现重写。
到此为止,就完成了重写数组原型方法。
步骤总结
- 创建一个继承数组原型的新对象
- 重写新对象中部分方法
- 将新的原型对象赋值给响应式数组上
回答上面的提问
“重写原型方法?那不是会使所有数组都受到影响? Vue 是怎么解决的?”
答: vue 重写的是自己继承的原型对象,而且只作用于响应式数据,不会污染原始数组原型。
引发思考
这里分享一下自己阅读源码的技巧:(大佬可略过)
带着问题看源码相比直接上手通篇阅读源码更有目标性,也能找到那种为了搞明白一个问题不断探索学习的精神。经过一些连续的提问然后去源码中找到答案,就像寻宝一样,寻宝的过程中 vue 源码也就熟悉的差不多了。
再例如之前一直不太理解 vue 中 computed 是怎么实现函数里面的变量响应式更新的,还能缓存,带着这个有趣的问题就又把 vue 源码又熟悉了一边,这里就不过多讲述具体过程,有机会后面专门出一篇文章。
在使用 vue 框架时,我们不仅需要深入掌握 vue 官网给出的 api, 也要大概知道其原理,那么,什么时候最适合去了解某个 api 的相关源码呢?其实就是抓住我们在使用 vue 过程中遇到的每一个小问题,遇到问题不仅仅是百度搜索出解决方案,并且需要带着问题去源码中找到属于我们自己的答案。