高阶JavaScript笔记(五)

116 阅读18分钟

八、ES7~ES12常用函数

ES7

Array Includes

  • 在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。
  • 在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true,否则返回false。
const names = ["abc", "cba", "nba", "mba", NaN];
​
if (names.indexOf("cba") !== -1) {
  console.log("包含abc元素");
}
​
// ES7 ES2016
if (names.includes("cba", 2)) {
  console.log("包含abc元素");
}
​
if (names.indexOf(NaN) !== -1) {
  console.log("包含NaN");
}
​
if (names.includes(NaN)) {
  console.log("包含NaN___");
}

指数(乘方) exponentiation运算符

  • 在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。
  • 在ES7中,增加了 ** 运算符,可以对数字来计算乘方。
const result1 = Math.pow(3, 3);
// ES7: **
const result2 = 3 ** 3;
console.log(result1, result2);

ES8

Object values

  • 之前我们可以通过 Object.keys 获取一个对象所有的key,在ES8中提供了 Object.values 来获取所有的value值:
const obj = {
  name: "why",
  age: 18
}
​
console.log(Object.keys(obj)) // [ 'name', 'age' ]
console.log(Object.values(obj)) // [ 'why', 18 ]// 用的非常少
console.log(Object.values(["abc", "cba", "nba"]))
console.log(Object.values("abc"))

Object entries

  • 通过Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组。
const obj = {
  name: "why",
  age: 18
}
​
console.log(Object.entries(obj))  // [ [ 'name', 'why' ], [ 'age', 18 ] ]
const objEntries = Object.entries(obj)
objEntries.forEach(item => {
  console.log(item[0], item[1])
})
​
console.log(Object.entries(["abc", "cba", "nba"]))
console.log(Object.entries("abc"))
String Padding
  • 某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的
const message = "Hello World";
​
const newMessage = message.padStart(15, "*").padEnd(20, "-");
console.log(newMessage);
​
// 案例 比如需要对身份证、银行卡的前面位数进行隐藏:
const cardNumber = "321324234242342342341312";
const lastFourCard = cardNumber.slice(-4);
const finalCard = lastFourCard.padStart(cardNumber.length, "*");
console.log(finalCard);

Trailing Commas

在ES8中,我们允许在函数定义和调用时多加一个逗号:

function foo(m, n) {}
​
foo(20, 30);

Object Descriptors

Object.create(prototype, descriptors) 以指定的原型创建对象,并且可以(可选) 的 设置对象的属性 Object.defineProperty(object, propertyname, descriptor) 对指定的对象的一个属性设置丰富的值控制

ES9

Async iterators:后续迭代器讲解

Object spread operators:

你可以通过展开操作符(Spread operator)...扩展一个数组对象和字符串。展开运算符(spread)是三个点(…),可以将可迭代对象转为用逗号分隔的参数序列。如同rest参数的逆运算。

Promise finally:后续讲Promise讲解

ES10

flat flatMap

  • flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

  • flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。

    • 注意一:flatMap是先进行map操作,再做flat的操作;
    • 注意二:flatMap中的flat相当于深度为1;
// 1.flat的使用
const nums = [
  10,
  20,
  [2, 9],
  [
    [30, 40],
    [10, 45],
  ],
  78,
  [55, 88],
];
const newNums = nums.flat();
console.log(newNums); // [ 10, 20, 2, 9, [ 30, 40 ], [ 10, 45 ], 78, 55, 88 ]
​
const newNums2 = nums.flat(2);
console.log(newNums2);
/*
[
  10, 20,  2,  9, 30,
  40, 10, 45, 78, 55,
  88
] */
​
// 2.flatMap的使用
const nums2 = [10, 20, 30];
const newNums3 = nums2.flatMap((item) => {
  return item * 2;
});
const newNums4 = nums2.map((item) => {
  return item * 2;
});
​
console.log(newNums3); // [ 20, 40, 60 ]
console.log(newNums4);
​
// 3.flatMap的应用场景
const messages = ["Hello World", "hello lyh", "my name is coderwhy"];
const words = messages.flatMap((item) => {
  return item.split(" ");
});
​
console.log(words);
​
/*
[
  'Hello', 'World',
  'hello', 'lyh',
  'my',    'name',
  'is',    'coderwhy'
] */

Object fromEntries

  • 在前面,我们可以通过 Object.entries 将一个对象转换成 entries ,那么如果我们有一个entries了,如何将其转换成对象呢?
  • ES10提供了 Object.formEntries 来完成转换:
const obj = {
  name: "why",
  age: 18,
  height: 1.88,
};
​
const entries = Object.entries(obj);
console.log(entries);
​
const newObj = {};
for (const entry of entries) {
  newObj[entry[0]] = entry[1];
}
​
// 1.ES10中新增了Object.fromEntries方法
const newObj2 = Object.fromEntries(entries);
​
console.log(newObj2);
​
// 2.Object.fromEntries的应用场景
const queryString = "name=why&age=18&height=1.88";
const queryParams = new URLSearchParams(queryString);
for (const param of queryParams) {
  console.log(param);
}
​
const paramObj = Object.fromEntries(queryParams);
console.log(paramObj); // { name: 'why', age: '18', height: '1.88' }

trimStart trimEnd

  • 去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?
  • ES10中给我们提供了trimStart和trimEnd;
const message = "    Hello World    "
​
console.log(message.trim())
console.log(message.trimStart())
console.log(message.trimEnd())
/*
Hello World
Hello World    
    Hello World
*/

Symbol description:已经讲过了

symbol.description是JavaScript中的内置属性,用于返回指定符号对象的可选描述。\

console.log(Symbol('desc').description);
// expected output: "desc"

Optional catch binding:后面讲解try cach讲解

ES11

BigInt

  • 在早期的JavaScript中,我们不能正确的表示过大的数字:

    大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的。

  • 那么ES11中,引入了新的数据类型BigInt,用于表示大的整数:

    BitInt的表示方法是在数值的后面加上n

// ES11之前 max_safe_integer
const maxInt = Number.MAX_SAFE_INTEGER
console.log(maxInt) // 9007199254740991
console.log(maxInt + 1)
console.log(maxInt + 2)
​
// ES11之后: BigInt
const bigInt = 900719925474099100n
console.log(bigInt + 10n)
​
const num = 100
console.log(bigInt + BigInt(num))
​
const smallNum = Number(bigInt)
console.log(smallNum)

Nullish Coalescing Operator

  • Nullish Coalescing Operator增加了空值合并操作符:
// ES11: 空值合并运算 ??
​
const foo = undefined;
// const bar = foo || "default value"
const bar = foo ?? "defualt value";
​
console.log(bar); // defualt value

Optional Chaining

  • 可选链也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁:
const info = {
  name: "why",
  // friend: {
  //   girlFriend: {
  //     name: "hmm"
  //   }
  // }
};
​
// console.log(info.friend.girlFriend.name);
if (info && info.friend && info.friend.girlFriend) {
  console.log(info.friend.girlFriend.name);
}
​
// ES11提供了可选链(Optional Chainling)
console.log(info.friend?.girlFriend?.name);
​
console.log("其他的代码逻辑");

Global This

  • 在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的
  • 比如在浏览器中可以通过this、window来获取;
  • 比如在Node中我们需要通过global来获取;
  • 那么在ES11中对获取全局对象进行了统一的规范:globalThis
// 获取某一个环境下的全局对象(Global Object)// 在浏览器下
// console.log(window)
// console.log(this)// 在node下
// console.log(global)// ES11
console.log(globalThis);

for..in标准化

  • 在ES11之前,虽然很多浏览器支持for...in来遍历对象类型,但是并没有被ECMA标准化。
  • 在ES11中,对其进行了标准化,for...in是用于遍历对象的key的:
// for...in 标准化: ECMA
const obj = {
  name: "why",
  age: 18,
};
​
for (const item in obj) {
  console.log(item);
}

Dynamic Import:

后续ES Module模块化中讲解。

Promise.allSettled:

后续讲Promise的时候讲解。

import meta:

后续ES Module模块化中讲解

ES12

FinalizationRegistry

  • FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。

    • FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调。(清理回调有时被称为 finalizer );
    • 你可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;
// ES12: FinalizationRegistry类
const finalRegistry = new FinalizationRegistry((value) => {
  console.log("注册在finalRegistry的对象, 某一个被销毁", value)
})
​
let obj = { name: "why" }
let info = { age: 18 }
​
finalRegistry.register(obj, "obj")
finalRegistry.register(info, "value")
​
obj = null
info = null

WeakRefs

  • 如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用:
  • 如果我们希望是一个弱引用的话,可以使用WeakRef;
// ES12: WeakRef类
// WeakRef.prototype.deref:
// > 如果原对象没有销毁, 那么可以获取到原对象
// > 如果原对象已经销毁, 那么获取到的是undefined
const finalRegistry = new FinalizationRegistry((value) => {
  console.log("注册在finalRegistry的对象, 某一个被销毁", value);
});
​
let obj = { name: "why" };
​
// 弱引用
let info = new WeakRef(obj);
​
finalRegistry.register(obj, "obj");
​
obj = null;
​
setTimeout(() => {
  console.log(info.deref()?.name);
  console.log(info.deref() && info.deref().name);
}, 10000);

logical assignment operators

// 1.||= 逻辑或赋值运算
let message = "hello world";
message = message || "default value";
message ||= "default value";
console.log(message);
​
// 2.&&= 逻辑与赋值运算
// &&
const obj = {
  name: "why",
  foo: function () {
    console.log("foo函数被调用");
  },
};
​
obj.foo && obj.foo();
​
// &&=
let info = {
  name: "why",
};
​
// 1.判断info
// 2.有值的情况下, 取出info.name
// info = info && info.name
info &&= info.name;
console.log(info);
​
// 3.??= 逻辑空赋值运算
let message2 = 0;
message2 ??= "default value";
console.log(message2);

Numeric Separator:

讲过了;

String.replaceAll:

字符串替换;

九、监听对象与响应式原理

监听对象方法

第一种:Object.defineProperty

Object.defineProperty的存储属性描述符来对属性的操作进行监听

但是这样做有什么缺点呢?

  • 首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。

    • 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
  • 其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。

  • 所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。

// 遍历object所有的key
Object.keys(obj).forEach((key) => {
  let value = obj[key];
​
  Object.defineProperty(obj, key, {
    get: function () {
      console.log(`监听到obj对象的${key}属性被访问了`);
      return value;
    },
    set: function (newValue) {
      console.log(`监听到obj对象的${key}属性被设置值`);
      value = newValue;
    },
  }); 
});

第二种:Proxy类

这个类从名字就可以看出来,是用于帮助我们创建一个代理的:

  • 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象);
  • 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;

我们可以将上面的案例用Proxy来实现一次:

  • 首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler;

    • const p = new Proxy(target, handler)
  • 其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;

const objProxy = new Proxy(obj, {
  // 获取值时的捕获器
  get: function (target, key) {
    console.log(`监听到对象的${key}属性被访问了`, target);
    return target[key];
  },
​
  // 设置值时的捕获器
  set: function (target, key, newValue) {
    console.log(`监听到对象的${key}属性被设置值`, target);
    target[key] = newValue;
  },
});

Proxy的set和get捕获器

  • 如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):

  • set和get分别对应的是函数类型;

    • set函数有四个参数:

      • target:目标对象(侦听的对象);
      • property:将被设置的属性key;
      • value:新属性值;
      • receiver:调用的代理对象;
    • get函数有三个参数:

      • target:目标对象(侦听的对象);
      • property:被获取的属性key;
      • receiver:调用的代理对象;
const objProxy = new Proxy(obj, {
  // 获取值时的捕获器
  get: function (target, key) {
    console.log(`监听到对象的${key}属性被访问了`, target);
    return target[key];
  },
​
  // 设置值时的捕获器
  set: function (target, key, newValue) {
    console.log(`监听到对象的${key}属性被设置值`, target);
    target[key] = newValue;
  },
​
  // 监听in的捕获器
  has: function (target, key) {
    console.log(`监听到对象的${key}属性in操作`, target);
    return key in target;
  },
​
  // 监听delete的捕获器
  deleteProperty: function (target, key) {
    console.log(`监听到对象的${key}属性in操作`, target);
    delete target[key];
  },
});

其他的捕获器

image-20220910003743283

Proxy的construct和apply

到捕捉器中还有construct和apply,它们是应用于函数对象的:

function foo() {}
​
const fooProxy = new Proxy(foo, {
  apply: function (target, thisArg, argArray) {
    console.log("对foo函数进行了apply调用");
    return target.apply(thisArg, argArray);
  },
  construct: function (target, argArray, newTarget) {
    console.log("对foo函数进行了new调用");
    return new target(...argArray);
  },
});
​
fooProxy.apply({}, ["abc", "cba"]);
new fooProxy("abc", "cba");

Reflect对象

  • Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射

  • 那么这个Reflect有什么用呢?

    • 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法;
    • 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf();
    • 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;
  • 如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?

    • 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面;
    • 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
    • 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
    • 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;
  • 那么Object和Reflect对象之间的API关系,可以参考MDN文档

Reflect的常见方法

image-20220910005035278

Reflect的使用

那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:

const obj = {
  name: "why",
  age: 18,
};
​
const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    console.log("get---------");
    return Reflect.get(target, key);
  },
​
  set: function (target, key, newValue, receiver) {
    console.log("set---------");
    target[key] = newValue;
    const result = Reflect.set(target, key, newValue);
    if (result) {
    } else {
    }
  },
});
​
objProxy.name = "kobe";
console.log(objProxy.name);

Receiver的作用

  • 我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?

    • 如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this;
  • 我们来看这样的一个对象

const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    // receiver是创建出来的代理对象 改变this
    console.log("get方法被访问--------", key, receiver);
    console.log(receiver === objProxy); // ture
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, newValue, receiver) {
    console.log("set方法被访问--------", key);
    Reflect.set(target, key, newValue, receiver);
  },
});

Reflect的construct

function Student(name, age) {
  this.name = name;
  this.age = age;
}
​
function Teacher() {}
​
// const stu = new Student("why", 18)
// console.log(stu)
// console.log(stu.__proto__ === Student.prototype)
​
// 执行Student函数中的内容, 但是创建出来对象是Teacher对象
const teacher = Reflect.construct(Student, ["why", 18], Teacher);
console.log(teacher); // Teacher { name: 'why', age: 18 }
console.log(teacher.__proto__ === Teacher.prototype); // ture
    

响应式原理

  • 我们先来看一下响应式意味着什么?我们来看一段代码:

    • m有一个初始化的值,有一段代码使用了这个值;
    • 那么在m有一个新的值时,这段代码可以自动重新执行;
  • 上面的这样一种可以自动响应数据变量的代码机制,我们就称之为是响应式的。

    • 那么我们再来看一下对象的响应式:
// 对象的响应式
const obj = {
  name: "why",
  age: 18
}
​
const newName = obj.name
console.log("你好啊, 李银河")
console.log("Hello World")
console.log(obj.name) // 100行
​
obj.name = "kobe"

image-20220910010352190

  • 首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中:

    • 那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数;
  • 但是有一个问题:在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?

    • 很明显,下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应;
    • bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作;

image-20220910010506596

响应式函数的实现watchFn

// 封装一个响应式的函数
let reactiveFns = [];
function watchFn(fn) {
  reactiveFns.push(fn);
}
​
// 对象的响应式
const obj = {
  name: "why",
  age: 18,
};
​
watchFn(function () {
  const newName = obj.name;
  console.log("你好啊, 李银河");
  console.log("Hello World");
  console.log(obj.name); // 100行
});
​
//当值改变时进行遍历
obj.name = "kobe";
reactiveFns.forEach((fn) => {
  fn();
  • 但是我们怎么区分呢?

    • 这个时候我们封装一个新的函数watchFn;
    • 凡是传入到watchFn的函数,就是需要响应式的;
    • 其他默认定义的函数都是不需要响应式的;

响应式依赖的收集

  • 目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:

    • 我们在实际开发中需要监听很多对象的响应式;
    • 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
    • 我们不可能在全局维护一大堆的数组来保存这些响应函数;
  • 所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:

    • 相当于替代了原来的简单 reactiveFns 的数组;
class Depend {
  constructor() {
    this.reactiveFns = [];
  }
​
  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn);
  }
​
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
​
// 封装一个响应式的函数
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
​
// 对象的响应式
const obj = {
  name: "why", // depend对象
  age: 18, // depend对象
};
​
watchFn(function () {
  const newName = obj.name;
  console.log("你好啊, 李银河");
  console.log("Hello World");
  console.log(obj.name); // 100行
});
​
watchFn(function () {
  console.log(obj.name, "demo function -------");
});
​
obj.name = "kobe";
depend.notify();
  • 我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:

    • 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
    • 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
  • 在前面我们刚刚学习过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应

式的数据依赖:

image-20220910011205066

监听对象的变化

  • 那么我们接下来就可以通过之前学习的方式来监听对象的变量:

    • 方式一:通过 Object.defineProperty的方式(vue2采用的方式);
    • 方式二:通过new Proxy的方式(vue3采用的方式);

Proxy的方式

​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver);
    depend.notify();
  },
});
​

Object.defineProperty的方式

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function() {
        return value
      },
      set: function(newValue) {
        value = newValue
        depend.notify()
      }
    })
  })
  return obj
}

对象依赖管理的实现

我们可以写一个getDepend函数专门来管理这种依赖关系:

// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}
​
// 封装一个获取depend函数 获取每个响应式函数的depend对象
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
​
// 对象的响应式
const obj = {
  name: "why", // depend对象
  age: 18 // depend对象
}
​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // depend.notify()
    const depend = getDepend(target, key)
    depend.notify()
  }
})

正确的依赖收集

  • 我们之前收集依赖的地方是在 watchFn 中:

    • 但是这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖;
    • 你只能针对一个单独的depend对象来添加你的依赖对象;
  • 那么正确的应该是在哪里收集呢?应该在我们调用了Proxy的get捕获器时

    • 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;
class Depend {
  constructor() {
    this.reactiveFns = []
  }
​
  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)
  }
​
  notify() {
    console.log(this.reactiveFns)
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}
​
// 封装一个响应式的函数和全局activeReactiveFn
let activeReactiveFn = null
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}
​
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }
​
  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
​
// 对象的响应式
const obj = {
  name: "why", // depend对象
  age: 18 // depend对象
}
​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    // 根据target.key获取对应的depend
    const depend = getDepend(target, key)
    // 给depend对象中添加响应函数activeReactiveFn
    depend.addDepend(activeReactiveFn)
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // depend.notify()
    const depend = getDepend(target, key)
    depend.notify()
  }
})

对Depend重构

  • 但是这里有两个问题:

    • 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
    • 问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Depend的行为;
  • 所以我们需要对Depend类进行重构:

    • 解决问题一的方法:不使用数组,而是使用Set;
    • 解决问题二的方法:添加一个新的方法,用于收集依赖;
class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
​
  // 不需要手动往depend里增加了响应式函数  直接增加activeReactiveFn
  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn);
    }
  }
​
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}

封装响应式对象

我们目前的响应式是针对于obj一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象:

function reactive(obj) {
  return new Proxy(obj, {
    get: function (target, key, receiver) {
      // 根据target.key获取对应的depend
      const depend = getDepend(target, key);
      // 给depend对象中添加响应函数
      // depend.addDepend(activeReactiveFn)
      depend.depend();
​
      return Reflect.get(target, key, receiver);
    },
    set: function (target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver);
      // depend.notify()
      const depend = getDepend(target, key);
      depend.notify();
    },
  });
}

响应式原理总结

  • 我们前面所实现的响应式的代码,其实就是Vue3中的响应式原理:

    • Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖的;
    • Vue2中通过我们前面学习过的Object.defineProerty的方式来实现对象属性的监听;
  • 我们可以将reactive函数进行如下的重构:

    • 在传入对象时,我们可以遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改;
    • 在setter和getter方法中的逻辑和前面的Proxy是一致的;
Vue3响应式原理
// 保存当前需要收集的响应式函数
let activeReactiveFn = null;
​
class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  
  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn);
    }
  }
  
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn();
    });
  }
}
​
// 封装一个响应式的函数
function watchFn(fn) {
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}
​
// 封装一个获取depend函数
const targetMap = new WeakMap();
function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target);
  if (!map) {
    map = new Map();
    targetMap.set(target, map);
  }
​
  // 根据key获取depend对象
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}
​
function reactive(obj) {
  return new Proxy(obj, {
    get: function (target, key, receiver) {
      // 根据target.key获取对应的depend
      const depend = getDepend(target, key);
      // 给depend对象中添加响应函数
      // depend.addDepend(activeReactiveFn)
      depend.depend();
​
      return Reflect.get(target, key, receiver);
    },
    set: function (target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver);
      // depend.notify()
      const depend = getDepend(target, key);
      depend.notify();
    },
  });
}
​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
  name: "why", // depend对象
  age: 18, // depend对象
});
​
const infoProxy = reactive({
  address: "广州市",
  height: 1.88,
});
​
watchFn(() => {
  console.log(infoProxy.address);
});
​
infoProxy.address = "北京市";
​
const foo = reactive({
  name: "foo",
});
​
watchFn(() => {
  console.log(foo.name);
});
​
foo.name = "bar";
Vue2响应式原理
// 保存当前需要收集的响应式函数
let activeReactiveFn = null/**
 * Depend优化:
 *  1> depend方法
 *  2> 使用Set来保存依赖函数, 而不是数组[]
 */class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }
​
  // addDepend(reactiveFn) {
  //   this.reactiveFns.add(reactiveFn)
  // }
​
  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }
​
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}
​
// 封装一个响应式的函数
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}
​
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }
​
  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
​
function reactive(obj) {
  // {name: "why", age: 18}
  // ES6之前, 使用Object.defineProperty   
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function() {
        const depend = getDepend(obj, key)
        depend.depend()
        return value
      },
      set: function(newValue) {
        value = newValue
        const depend = getDepend(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}
​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
  name: "why", // depend对象
  age: 18 // depend对象
})
​
const infoProxy = reactive({
  address: "广州市",
  height: 1.88
})
​
watchFn(() => {
  console.log(infoProxy.address)
})
​
infoProxy.address = "北京市"const foo = reactive({
  name: "foo"
})
​
watchFn(() => {
  console.log(foo.name)
})
​
foo.name = "bar"
foo.name = "hhh"