大家好,我是 辉夜真是太可爱啦 。这是我最近在写的【手把手教你搓Vue响应式原理】系列,本文将一步步地为你解开vue响应式原理的面纱。由于本人也是在写这篇文章的过程中不断试错,不断学习改进的,所以,本文同样很适合和我一样的初学者。和
Vue的设计理念如出一辙,那就是渐进增强。
上文链接
【手把手教你搓Vue响应式原理】(三)observe 以及 ob
前言
之前已经将对象的响应式处理都写完了,并且,新增了 observe 方法作为入口,然后通过 Observer 类在初始化的时候完成遍历当前层的所有属性的同时,添加 __ob__ 属性。
现在这篇文章,主要将对数组进行响应式处理。
数组自有的遍历
首先的一点,就是要先区分数组的遍历,和之前的 walk 遍历对象,有所区分。
我们新增一个 observeArray 专门用来遍历数组。
可能有人会疑问,为什么数组的 observeArray 循环用的是 observe ,而 walk 用的是 defineReactive 。 这是因为数组中的数据可能不是对象,所以要走 observe 。
function observeArray(list) {
for(let i=0,l=list.length;i<l;i++){
observe(list[i])
}
}
然后在 Observer 构造函数出进行类型判断
class Observer{
constructor(obj){
def(obj,'__ob__',this,false)
if (Array.isArray(obj)){
observeArray(obj)
}else{
this.walk(obj);
}
}
// ...
}
改写数组改变元素的七大方法
数组的 push , pop , shift , unshift , splice , sort , reverse 都能在改变数组的同时,不会触发object.defineProperty 的 set 监听。
所以,我们需要改写它的方法。
思路前瞻
通过 Object.create() ,以 Array.prototype 为原型创建 arrayMethods 对象对 arrayMethods 中的七大方法进行循环通过 inserted 变量将 push unshift splice 中的新增项进行保存,对他们进行 observe 响应式用 def 对他们进行改写,通过 apply 调用原本的方法,不丧失原本的功能,在这里可以添加自定义的回调,例如添加视图更新在 Observer 类的构造函数中,对所有的数组进行Object.setPrototypeOf() 强写,将当前数组对象的原型链强行指向 arrayMethods
当然,如果这个思路前瞻不太看得懂也不要紧,可以跟着下面的实际执行,我们一步步来。
实际执行
先将 Array.prototype 进行拷贝一份,因为这七大方法都存在于原型链上。然后,将数组的原型都赋值到我们创建的新对象 arrayMethods 上。
那我们先创建拷贝一个数组的原型,可以进行打印输出来看一下
// 拷贝一份数组的原型
const arrayPrototype=Array.prototype;
console.log(arrayPrototype)

可以看到除了我们的七大方法,别的数组方法也全在上面了。
现在,我们以它为原型,创建一个新的对象 arrayMethods,这时候,就需要用到我们的 Object.create 。
Object.create() 方法主要用来创建一个新对象,使用现有的对象来提供新创建的对象的proto。
// 以 Array.prototype 为原型创建 arrayMethods 对象
const arrayMethods=Object.create(arrayPrototype);
console.log(arrayMethods);
输出之后可以看到已经成功复制了 Array.prototype 原型。
我们将七个方法名写在一个数组中,
const methodsNeedChange=[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
用一个 for 循环将 methodsNeedChange 进行遍历,将 arrayMethods 中所有涉及改变数组的方法进行重写:
for(let i=0;i<methodsNeedChange.length;i++){
// 备份原来的方法
const original=arrayMethods[methodsNeedChange[i]];
// 定义新的方法
def(arrayMethods,methodsNeedChange[i],function () {
将原本的方法执行
original.apply(this,arguments)
// 写监听到之后更新视图
},false)
}
-
为什么要用function() ?
因为比方说 a.b.c.push(),其中的 this 指向肯定要指向 c,毕竟是在c中添加,如果使用箭头函数,那就会以 a 为 this 指向。(关于this指向的问题不清楚的可以查看 一文搞懂JS系列(十)之彻底搞懂this指向)
-
为什么都使用
for循环因为 for循环是循环里面性能最高的。
-
original.apply(this,arguments)到底什么意义直接使用
original()是不行的,因为没有指定 this 的情况下,最终会指向window,导致程序会报错,所以,正确的方法是使用applycall或者bind,这三个都可以。
那么,现在,这个 arrayMethods 就是成功改写之后的数组方法,我们需要将所有对 Observer 生成的实例的数组方法,都指向我们的 arrayMethods ,而不是指向 Array.prototyppe,这时候就需要用到 Object.setPrototypeOf()
该方法主要用于设置一个指定的对象的原型 ( 即, 内部Prototype属性)到另一个对象或 null 上。
在 Observer 类中的数组判断中,加入 setPrototypeOf
class Observer{
constructor(obj){
def(obj,'__ob__',this,false)
if (Array.isArray(obj)){
observeArray(obj)
Object.setPrototypeOf(obj,arrayMethods);
}else{
this.walk(obj);
}
}
// ...
}
为了方便测试,我们在 arrayMethods 改写中加入 console.log('dddd');
for(let i=0;i<methodsNeedChange.length;i++){
// 备份原来的方法
const original=arrayMethods[methodsNeedChange[i]];
// 定义新的方法
def(arrayMethods,methodsNeedChange[i],function () {
console.log('dddd');
original.apply(this,arguments)
// 写监听到之后更新视图
},false)
}
接下来,我们来测试一下
let a= [1,2,3,4,5]
observe(a);
a.push('abc')
会成功发现,当我们执行 push 的同时,控制台输出了 dddd 。并且,成功将 abc 塞入了 a 数组。
插入的新项也要 observe
现在,我们已经能对数组的七大方法成功进行拦截操作了,但是,因为 push , unshift , splice 比较特殊,是可以对数组插入新的值,对于新的值,我们是不是也要进行 observe 响应式处理。
所以,在定义新的方法的时候,我们要对它执行的方法进行特殊处理。
for(let i=0;i<methodsNeedChange.length;i++){
// 备份原来的方法
const original=arrayMethods[methodsNeedChange[i]];
// 定义新的方法
def(arrayMethods,methodsNeedChange[i],function () {
// 用来保存新插入的值
let inserted=[];
// 由于 arguments 对象是类数组,所以先通过扩展运算符转为数组之后,再进行操作。
let args=[...arguments];
// 先判断 是否是 push unshift splice ,如果是的话,先取出插入的新值,后面进行 observeArray
switch (methodsNeedChange[i]) {
case 'push':
case 'unshift':
inserted=args;
break;
case ' ':
// splice(起始下标,删除个数,新添加的元素)
inserted=args.slice(2);
}
// 先判断 inserted 里面有东西,才执行 observeArray
inserted.length && observeArray(inserted);
// 将备份的方法进行执行,毕竟不能丢失数组方法原本的功能执行
original.apply(this,arguments)
// 写监听到之后更新视图
},false)
}
所以最终的代码如下:
// 拷贝一份数组的原型
const arrayPrototype=Array.prototype;
// 以 Array.prototype 为原型创建 arrayMethods 对象
const arrayMethods=Object.create(arrayPrototype);
const methodsNeedChange=[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
for(let i=0;i<methodsNeedChange.length;i++){
// 备份原来的方法
const original=arrayMethods[methodsNeedChange[i]];
// 定义新的方法
def(arrayMethods,methodsNeedChange[i],function () {
// 用来保存新插入的值
let inserted=[];
// 由于 arguments 对象是类数组,所以先通过扩展运算符转为数组之后,再进行操作。
let args=[...arguments];
// 先判断 是否是 push unshift splice ,如果是的话,先取出插入的新值,后面进行 observeArray
switch (methodsNeedChange[i]) {
case 'push':
case 'unshift':
inserted=args;
break;
case ' ':
// splice(起始下标,删除个数,新添加的元素)
inserted=args.slice(2);
}
// 先判断 inserted 里面有东西,才执行 observeArray
inserted.length && observeArray(inserted);
// 将备份的方法进行执行,毕竟不能丢失数组方法原本的功能执行
original.apply(this,arguments)
// 写监听到之后更新视图
},false)
}
function defineReactive(obj,key,val) {
console.log(key);
// 判断当前入参个数,两个的话直接返回当前层的对象
if(arguments.length===2){
val=obj[key];
observe(val)
}
Object.defineProperty(obj,key,{
// 可枚举,默认为 false
enumerable:true,
// 属性的描述符能够被改变,或者是删除,默认为 false
configurable:true,
get(){
return val;
},
set(newValue){
val=newValue;
observe(val)
}
})
}
function def(obj,key,value,enumerable) {
Object.defineProperty(obj,key,{
value,
//这个属性仅仅保存 Observer 实例,所以不需要遍历
enumerable
})
}
// 遍历对象当前层的所有属性,并且绑定 defineReactive
class Observer{
constructor(obj){
def(obj,'__ob__',this,false)
if (Array.isArray(obj)){
observeArray(obj)
Object.setPrototypeOf(obj,arrayMethods);
}else{
this.walk(obj);
}
}
walk(obj){
let keys=Object.keys(obj);
for(let i =0;i<keys.length;i++){
defineReactive(obj,keys[i])
}
}
}
function observe(value) {
if(typeof value !== 'object') return;
let ob;
// eslint-disable-next-line no-prototype-builtins
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer){
ob=value.__ob__;
}else{
ob = new Observer(value);
}
return ob;
}
function observeArray(list) {
for(let i=0,l=list.length;i<l;i++){
observe(list[i])
}
}
经过测试,三种方法都完全没问题
let a=[1,2,3,4,5]
observe(a)
a.push(6,7,8,9)
console.log(a); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
let a=[1,2,3,4,5]
observe(a)
a.sfhit(6,7,8,9)
console.log(a); // [6, 7, 8, 9, 1, 2, 3, 4, 5]
let a=[1,2,3,4,5]
observe(a)
a.splice(2,2,6,7,8,9)
console.log(a); // [1, 2, 6, 7, 8, 9, 5]
而且执行其他的方法也完全没问题
let a=[1,2,3,4,5]
observe(a)
let b=a.map(x=>x*2)
console.log(b); // [2, 4, 6, 8, 10]
思路回顾
通过 Object.create() ,以 Array.prototype 为原型创建 arrayMethods 对象对 arrayMethods 中的七大方法进行循环通过 inserted 变量将 push unshift splice 中的新增项进行保存,对他们进行 observe 响应式用 def 对他们进行改写,通过 apply 调用原本的方法,不丧失原本的功能,在这里可以添加自定义的回调,例如添加视图更新在 Observer 类的构造函数中 , 对所有的数组进行Object.setPrototypeOf() 强写,将当前数组对象的原型链强行指向 arrayMethods
文末思考
其实,事情已经到了这里,只要在 setter 中调用一下渲染函数来重新渲染页面,不就能完成在数据变化时更新页面了吗?确实可以,但是这样做的代价就是:任何一个数据的变化,都会导致这个页面的重新渲染,代价未免太大了吧。我们想做的效果是:数据变化时,只更新与这个数据有关的DOM结构,所以,在下一节中,我们会来讲讲 Dep 和 Watcher。
下文引荐
【手把手教你搓Vue响应式原理】(五) Watcher 与 Dep