您的位置:首页 > 职场人生

如何一鸣惊人在响应式原理的面试题中脱引而出?(一)

2020-07-14 06:35 441 查看

在Vue的 深入响应式原理 的讲解中说到,当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把 “接触” 过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

检测变化的注意事项

由于 JavaScript 的限制,Vue 不能检测数组变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。这是怎么做到的?

数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,我们称其为变异方法,这些方法有:push、pop、shift、unshift、splice、sort 以及 reverse 等。这个时候我们就要考虑一件事,即当用户调用这些变异方法改变数组时需要触发依赖。换句话说我们需要知道开发者何时调用了这些变异方法,只有这样我们才有可能在这些方法被调用时做出反应。

一种缓存思路

function wait(){
console.log('hello')
}

我们有这样一个需求,在不改动 wait 函数源码的情况下,在打印字符串 ‘hello’ 之后输出字符串 ’ wait’。这时候我们可以这样做:

var cache = wait;
wait = function(cache){
cache();
console.log('wait');
}

完美实现, 上述代码首先使用 cache 变量缓存原来的 wait 函数,然后重新定义 wait 函数,并在新定义的 wait 函数中调用缓存下来的 cache。这样我们就保证了在不改变 wait 函数行为的前提下对其进行了功能扩展。而 Vue 正是通过这个技巧实现了对数据变异方法的拦截,即保持数组变异方法原有功能不变的前提下对其进行特殊的魔改操作。

拦截数组变异操作思路(代理原型)

数组本身也是一个对象,所以它实例的 proto 属性指向的就是数组构造函数的原型: 如图。

现在我们的思路是通过设置 proto 属性的值为一个新的对象,且该新对象的原型是数组构造函数原来的原型对象。

上图中数组对象的 proto 属性指向了 Object.create(Array.prototype) 创建的对象(简称纯对象),同时纯对象的 proto 属性指向了真正的数组原型对象。

并且纯对象上定义了与数组变异方法同名的函数,这样当通过数组实例调用变异方法时,首先执行的是纯对象上的同名函数然后在去调用数组原型上的函数,在这个过程中就能够实现对数组变异方法的拦截并进行代码的魔改。

// 要拦截的数组变异方法
const intercept = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
var arrayProto = Array.prototype // 缓存 Array.prototype
var arrayMethods = Object.create(Array.prototype)

intercept.forEach(function(method) {
arrayMethods[method] = function() {
const result = arrayProto[method].apply(this, arguments);
console.log("我要开始魔改" + method + "函数了");
return result;
}
});

const arr = [1, 2, 3, 4];
arr.__proto__ = arrayMethods;

arr.push(5);

如上代码所示我们已经能侦测到 arr 数组中的变异操作。

Vue拦截数组变异源码

我们已经了解了拦截数组变异方法的思路,接下来我们就可以具体的看一下 Vue 源码是如何实现的。

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];

/**
* Intercept mutating methods and emit events
*/
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
});
});

可以看到在源码中对比之前写的唯一有变化的是对于 push、unshift 和 splice 方法的处理。这三个变异方法都可以为数组添加新的元素需要重点关注。

为什么要重点关注呢?原因是因为新增加的元素是非响应式的,所以我们需要获取到这些新元素,并将其变为响应式数据才行。

来看下具体实现看一下具体实现首先定义了 inserted 变量,这个变量用来保存那些被新添加进来的数组元素。当遇到 splice 操作时 splice 函数从第三个参数开始到最后一个参数都是数组的新增元素,所以直接使用 args.slice(2) 作为 inserted 的值即可。最后 inserted 变量中所保存的就是新增的数组元素,我们只需要调用 observeArray 函数对其进行观测即可:

ob.observeArray(inserted);

最后数据发生更改,通知所以依赖此数据的观察者更新视图。

ob.dep.notify();

推荐:

申请即送:

  • BAT大厂面试题、独家面试工具包,

  • 资料免费领取,包括 各类面试题以及答案整理,各大厂面试真题分享!

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: