From 36b0f4fcf4d4958106ef9321b6d121df95383a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=BD=A3=E1=BD=BB=E1=BD=A4?= Date: Tue, 13 Aug 2024 14:32:14 +0800 Subject: [PATCH] --- ...235\345\220\246 (2024_8_13 14_32_09).html" | 1426 +++++++++++++++++ 1 file changed, 1426 insertions(+) create mode 100644 "javascript - \346\236\227\344\270\211\345\277\203\347\224\273\344\272\2068\345\274\240\345\233\276\357\274\214\346\234\200\351\200\232\344\277\227\346\230\223\346\207\202\347\232\204Vue3\345\223\215\345\272\224\345\274\217\346\240\270\345\277\203\345\216\237\347\220\206\350\247\243\346\236\220 - \344\270\252\344\272\272\346\226\207\347\253\240 - SegmentFault \346\200\235\345\220\246 (2024_8_13 14_32_09).html" diff --git "a/javascript - \346\236\227\344\270\211\345\277\203\347\224\273\344\272\2068\345\274\240\345\233\276\357\274\214\346\234\200\351\200\232\344\277\227\346\230\223\346\207\202\347\232\204Vue3\345\223\215\345\272\224\345\274\217\346\240\270\345\277\203\345\216\237\347\220\206\350\247\243\346\236\220 - \344\270\252\344\272\272\346\226\207\347\253\240 - SegmentFault \346\200\235\345\220\246 (2024_8_13 14_32_09).html" "b/javascript - \346\236\227\344\270\211\345\277\203\347\224\273\344\272\2068\345\274\240\345\233\276\357\274\214\346\234\200\351\200\232\344\277\227\346\230\223\346\207\202\347\232\204Vue3\345\223\215\345\272\224\345\274\217\346\240\270\345\277\203\345\216\237\347\220\206\350\247\243\346\236\220 - \344\270\252\344\272\272\346\226\207\347\253\240 - SegmentFault \346\200\235\345\220\246 (2024_8_13 14_32_09).html" new file mode 100644 index 0000000..bd2a986 --- /dev/null +++ "b/javascript - \346\236\227\344\270\211\345\277\203\347\224\273\344\272\2068\345\274\240\345\233\276\357\274\214\346\234\200\351\200\232\344\277\227\346\230\223\346\207\202\347\232\204Vue3\345\223\215\345\272\224\345\274\217\346\240\270\345\277\203\345\216\237\347\220\206\350\247\243\346\236\220 - \344\270\252\344\272\272\346\226\207\347\253\240 - SegmentFault \346\200\235\345\220\246 (2024_8_13 14_32_09).html" @@ -0,0 +1,1426 @@ + javascript - 林三心画了8张图,最通俗易懂的Vue3响应式核心原理解析 - 个人文章 - SegmentFault 思否
19
头图

前言

大家好,我是林三心,大家也知道,本菜鸟平时写基础文章比较多,我始终坚信两句话

  • 用最通俗易懂的话,讲最难的知识点
  • 基础是进阶的前提
    其实Vue3已经出来很久了,可能大部分公司都用上了,但是,Vue3究竟比Vue2好在哪里?其实很多人都不知道。今天我就先给大家讲一讲Vue3的响应式原理吧,顺便说一说Vue3的响应式到底比Vue2的响应式好在哪

好在哪?

好的,咱们先来讲讲为什么Vue3的响应式 优于 Vue2响应式。可能平时问大家:请问你知道Vue的响应式是怎么实现的吗?大家都能粗略回答出来

  • Vue2的响应式是基于Object.defineProperty实现的
  • Vue3的响应式是基于ES6的Proxy来实现的

是的,虽然上面的回答抽象了点,但是确实是回答出了Vue的两个版本的响应式的核心原理,并且Vue的两个版本响应式的好坏,也确实就是体现在Object.definePropertyProxy的差异上。

Vue2

大家都知道Vue2的响应式是基于Object.defineProperty的,那我就拿Object.defineProperty来举个例子

// 响应式函数
+function reactive(obj, key, value) {
+  Object.defineProperty(data, key, {
+    get() {
+      console.log(`访问了${key}属性`)
+      return value
+    },
+    set(val) {
+      console.log(`将${key}由->${value}->设置成->${val}`)
+      if (value !== val) {
+        value = val
+      }
+    }
+  })
+}
+
+
+const data = {
+  name: '林三心',
+  age: 22
+}
+Object.keys(data).forEach(key => reactive(data, key, data[key]))
+console.log(data.name)
+// 访问了name属性
+// 林三心
+data.name = 'sunshine_lin' // 将name由->林三心->设置成->sunshine_lin
+console.log(data.name)
+// 访问了name属性
+// sunshine_lin

通过上面的例子,我想大家都对Object.defineProperty有了一个了解,那问题来了?它到底有什么弊端呢?使得尤大大在Vue3中抛弃了它,咱们接着看:

// 接着上面代码
+
+data.hobby = '打篮球'
+console.log(data.hobby) // 打篮球
+data.hobby = '打游戏'
+console.log(data.hobby) // 打游戏

这下大家可以看出Object.defineProperty有什么弊端了吧?咱们可以看到,data新增了hobby属性,进行访问和设值,但是都不会触发get和set,所以弊端就是:Object.defineProperty只对初始对象里的属性有监听作用,而对新增的属性无效。这也是为什么Vue2中对象新增属性的修改需要使用Vue.$set来设值的原因。

Vue3

从上面,咱们知道了Object.defineProperty的弊端,咱们接着讲Vue3中响应式原理的核心Proxy是怎么弥补这一缺陷的,老样子,咱们还是举例子(先粗略讲,具体参数下面会细讲):

const data = {
+  name: '林三心',
+  age: 22
+}
+
+function reactive(target) {
+  const handler = {
+    get(target, key, receiver) {
+      console.log(`访问了${key}属性`)
+      return Reflect.get(target, key, receiver)
+    },
+    set(target, key, value, receiver) {
+      console.log(`将${key}由->${target[key]}->设置成->${value}`)
+      Reflect.set(target, key, value, receiver)
+    }
+  }
+
+  return new Proxy(target, handler)
+}
+
+const proxyData = reactive(data)
+
+console.log(proxyData.name)
+// 访问了name属性
+// 林三心
+proxyData.name = 'sunshine_lin'
+// 将name由->林三心->设置成->sunshine_lin
+console.log(proxyData.name)
+// 访问了name属性
+// sunshine_lin

可以看到,其实效果与上面的Object.defineProperty没什么差别,那为什么尤大大要抛弃它,选择Proxy呢?注意了,最最最关键的来了,那就是对象新增属性,来看看效果吧:

proxyData.hobby = '打篮球'
+console.log(proxyData.hobby)
+// 访问了hobby属性
+// 打篮球
+proxyData.hobby = '打游戏'
+// 将hobby由->打篮球->设置成->打游戏
+console.log(proxyData.hobby)
+// 访问了hobby属性
+// 打游戏

所以现在大家知道Vue3的响应式比Vue2好在哪了吧?

截屏2021-08-26 下午8.48.43.png

Vue3响应式原理

说完Proxy的好处,咱们正式来讲讲Vue3的响应式原理的核心部分吧。

前言

先看看下面这段代码

let name = '林三心', age = 22, money = 20
+let myself = `${name}今年${age}岁,存款${money}元`
+
+console.log(myself) // 林三心今年22岁,存款20元
+
+money = 300
+
+// 预期:林三心今年22岁,存款300元
+console.log(myself) // 实际:林三心今年22岁,存款20元

大家想一下,我想要让myself跟着money变,怎么办才行?嘿嘿,其实,只要让myself = '${name}今年${age}岁,存款${money}元'再执行一次就行,如下

let name = '林三心', age = 22, money = 20
+let myself = `${name}今年${age}岁,存款${money}元`
+
+console.log(myself) // 林三心今年22岁,存款20元
+
+money = 300
+
+myself = `${name}今年${age}岁,存款${money}元` // 再执行一次
+
+// 预期:林三心今年22岁,存款300元
+console.log(myself) // 实际:林三心今年22岁,存款300元

effect

上面说了,每一次money改变就得再执行一次myself = '${name}今年${age}岁,存款${money}元',才能使myself更新,其实这么写不优雅,咱们可以封装一个effect函数

let name = '林三心', age = 22, money = 20
+let myself = ''
+const effect = () => myself = `${name}今年${age}岁,存款${money}元`
+
+effect() // 先执行一次
+console.log(myself) // 林三心今年22岁,存款20元
+money = 300
+
+effect() // 再执行一次
+
+console.log(myself) // 林三心今年22岁,存款300元

其实这样也是有坏处的,不信你可以看看下面这种情况

let name = '林三心', age = 22, money = 20
+let myself = '', ohtherMyself = ''
+const effect1 = () => myself = `${name}今年${age}岁,存款${money}元`
+const effect2 = () => ohtherMyself = `${age}岁的${name}居然有${money}元`
+
+effect1() // 先执行一次
+effect2() // 先执行一次
+console.log(myself) // 林三心今年22岁,存款20元
+console.log(ohtherMyself) // 22岁的林三心居然有20元
+money = 300
+
+effect1() // 再执行一次
+effect2() // 再执行一次
+
+console.log(myself) // 林三心今年22岁,存款300元
+console.log(ohtherMyself) // 22岁的林三心居然有300元

增加了一个ohtherMyself,就得再写一个effect,然后每次更新都执行一次,那如果增加数量变多了,那岂不是每次都要写好多好多的effect函数执行代码?

track和trigger

针对上面的问题,咱们可以这样解决:用track函数把所有依赖于money变量effect函数都收集起来,放在dep里,dep为什么用Set呢?因为Set可以自动去重。搜集起来之后,以后只要money变量一改变,就执行trigger函数通知dep里所有依赖money变量effect函数执行,实现依赖变量的更新。先来看看代码吧,然后我再通过一张图给大家展示一下,怕大家头晕哈哈。

let name = '林三心', age = 22, money = 20
+let myself = '', ohtherMyself = ''
+const effect1 = () => myself = `${name}今年${age}岁,存款${money}元`
+const effect2 = () => ohtherMyself = `${age}岁的${name}居然有${money}元`
+
+const dep = new Set()
+function track () {
+    dep.add(effect1)
+    dep.add(effect2)
+}
+function trigger() {
+    dep.forEach(effect => effect())
+}
+track() //收集依赖
+effect1() // 先执行一次
+effect2() // 先执行一次
+console.log(myself) // 林三心今年22岁,存款20元
+console.log(ohtherMyself) // 22岁的林三心居然有20元
+money = 300
+
+trigger() // 通知变量myself和otherMyself进行更新
+
+console.log(myself) // 林三心今年22岁,存款300元
+console.log(ohtherMyself) // 22岁的林三心居然有300元

截屏2021-08-26 下午10.43.12.png

对象呢?

上面都是讲基础数据类型的,那咱们来讲讲对象吧,我先举个例子,用最原始的方式去实现他的响应

const person = { name: '林三心', age: 22 }
+let nameStr1 = ''
+let nameStr2 = ''
+let ageStr1 = ''
+let ageStr2 = ''
+
+const effectNameStr1 = () => { nameStr1 = `${person.name}是个大菜鸟` }
+const effectNameStr2 = () => { nameStr2 = `${person.name}是个小天才` }
+const effectAgeStr1 = () => { ageStr1 = `${person.age}岁已经算很老了` }
+const effectAgeStr2 = () => { ageStr2 = `${person.age}岁还算很年轻啊` }
+
+effectNameStr1()
+effectNameStr2()
+effectAgeStr1()
+effectAgeStr2()
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// 林三心是个大菜鸟 林三心是个小天才 22岁已经算很老了 22岁还算很年轻啊
+
+person.name = 'sunshine_lin'
+person.age = 18
+
+effectNameStr1()
+effectNameStr2()
+effectAgeStr1()
+effectAgeStr2()
+
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// sunshine_lin是个大菜鸟 sunshine_lin是个小天才 18岁已经算很老了 18岁还算很年轻啊

上面的代码,咱们也看出来了,感觉写的很无脑。。还记得前面讲的dep收集effect吗?咱们暂且把person对象里的name和age看成两个变量,他们都有各自的依赖变量

  • name:nameStr1和nameStr2
  • age:ageStr1和ageStr2
    所以name和age应该拥有自己的dep,并收集各自依赖变量所对应的effect

截屏2021-08-26 下午11.11.04.png

前面说了dep是使用Set,由于person拥有age和name两个属性,所以拥有两个dep,那用什么来储存这两个dep呢?咱们可以用ES6的另一个数据结构Map来储存

const person = { name: '林三心', age: 22 }
+let nameStr1 = ''
+let nameStr2 = ''
+let ageStr1 = ''
+let ageStr2 = ''
+
+const effectNameStr1 = () => { nameStr1 = `${person.name}是个大菜鸟` }
+const effectNameStr2 = () => { nameStr2 = `${person.name}是个小天才` }
+const effectAgeStr1 = () => { ageStr1 = `${person.age}岁已经算很老了` }
+const effectAgeStr2 = () => { ageStr2 = `${person.age}岁还算很年轻啊` }
+
+const depsMap = new Map()
+function track(key) {
+    let dep = depsMap.get(key)
+    if (!dep) {
+        depsMap.set(key, dep = new Set())
+    }
+    // 这里先暂且写死
+    if (key === 'name') {
+        dep.add(effectNameStr1)
+        dep.add(effectNameStr2)
+    } else {
+        dep.add(effectAgeStr1)
+        dep.add(effectAgeStr2)
+    }
+}
+function trigger (key) {
+    const dep = depsMap.get(key)
+    if (dep) {
+        dep.forEach(effect => effect())
+    }
+}
+
+track('name') // 收集person.name的依赖
+track('age') // 收集person.age的依赖
+
+
+
+effectNameStr1()
+effectNameStr2()
+effectAgeStr1()
+effectAgeStr2()
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// 林三心是个大菜鸟 林三心是个小天才 22岁已经算很老了 22岁还算很年轻啊
+
+person.name = 'sunshine_lin'
+person.age = 18
+
+trigger('name') // 通知person.name的依赖变量更新
+trigger('age') // 通知person.age的依赖变量更新
+
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// sunshine_lin是个大菜鸟 sunshine_lin是个小天才 18岁已经算很老了 18岁还算很年轻啊

上面咱们是只有一个person对象,那如果有多个对象呢?怎么办?我们都知道,每个对象会建立一个Map来存储此对象里属性的dep(使用Set来存储),那如果有多个对象,该用什么来存储每个对象对应的Map呢?请看下图

截屏2021-08-27 下午8.01.20.png

其实ES6还有一个新的数据结构,叫做WeakMap的,咱们就用它来存储这些对象的Map吧。所以咱们得对track函数trigger函数进行改造,先看看之前他们长啥样

const depsMap = new Map()
+function track(key) {
+    let dep = depsMap.get(key)
+    if (!dep) {
+        depsMap.set(key, dep = new Set())
+    }
+    // 这里先暂且写死
+    if (key === 'name') {
+        dep.add(effectNameStr1)
+        dep.add(effectNameStr2)
+    } else {
+        dep.add(effectAgeStr1)
+        dep.add(effectAgeStr2)
+    }
+}
+function trigger (key) {
+    const dep = depsMap.get(key)
+    if (dep) {
+        dep.forEach(effect => effect())
+    }
+}

之前的代码只做了单个对象的处理方案,但是现在如果要多个对象,那就得使用WeakMap进行改造了(接下来代码可能有点啰嗦,但都会为了照顾基础薄弱的同学)

const person = { name: '林三心', age: 22 }
+const animal = { type: 'dog', height: 50 }
+
+const targetMap = new WeakMap()
+function track(target, key) {
+    let depsMap = targetMap.get(target)
+    if (!depsMap) {
+        targetMap.set(target, depsMap = new Map())
+    }
+
+    let dep = depsMap.get(key)
+    if (!dep) {
+        depsMap.set(key, dep = new Set())
+    }
+    // 这里先暂且写死
+    if (target === person) {
+        if (key === 'name') {
+            dep.add(effectNameStr1)
+            dep.add(effectNameStr2)
+        } else {
+            dep.add(effectAgeStr1)
+            dep.add(effectAgeStr2)
+        }
+    } else if (target === animal) {
+        if (key === 'type') {
+            dep.add(effectTypeStr1)
+            dep.add(effectTypeStr2)
+        } else {
+            dep.add(effectHeightStr1)
+            dep.add(effectHeightStr2)
+        }
+    }
+}
+
+function trigger(target, key) {
+    let depsMap = targetMap.get(target)
+    if (depsMap) {
+        const dep = depsMap.get(key)
+        if (dep) {
+            dep.forEach(effect => effect())
+        }
+    }
+}

经过了上面的改造,咱们终于实现了多对象的依赖收集,咱们来试一试吧

const person = { name: '林三心', age: 22 }
+const animal = { type: 'dog', height: 50 }
+let nameStr1 = ''
+let nameStr2 = ''
+let ageStr1 = ''
+let ageStr2 = ''
+let typeStr1 = ''
+let typeStr2 = ''
+let heightStr1 = ''
+let heightStr2 = ''
+
+const effectNameStr1 = () => { nameStr1 = `${person.name}是个大菜鸟` }
+const effectNameStr2 = () => { nameStr2 = `${person.name}是个小天才` }
+const effectAgeStr1 = () => { ageStr1 = `${person.age}岁已经算很老了` }
+const effectAgeStr2 = () => { ageStr2 = `${person.age}岁还算很年轻啊` }
+const effectTypeStr1 = () => { typeStr1 = `${animal.type}是个大菜鸟` }
+const effectTypeStr2 = () => { typeStr2 = `${animal.type}是个小天才` }
+const effectHeightStr1 = () => { heightStr1 = `${animal.height}已经算很高了` }
+const effectHeightStr2 = () => { heightStr2 = `${animal.height}还算很矮啊` }
+
+track(person, 'name') // 收集person.name的依赖
+track(person, 'age') // 收集person.age的依赖
+track(animal, 'type') // animal.type的依赖
+track(animal, 'height') // 收集animal.height的依赖
+
+
+
+effectNameStr1()
+effectNameStr2()
+effectAgeStr1()
+effectAgeStr2()
+effectTypeStr1()
+effectTypeStr2()
+effectHeightStr1()
+effectHeightStr2()
+
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// 林三心是个大菜鸟 林三心是个小天才 22岁已经算很老了 22岁还算很年轻啊
+
+console.log(typeStr1, typeStr2, heightStr1, heightStr2)
+// dog是个大菜鸟 dog是个小天才 50已经算很高了 50还算很矮啊
+
+person.name = 'sunshine_lin'
+person.age = 18
+animal.type = '猫'
+animal.height = 20
+
+trigger(person, 'name')
+trigger(person, 'age')
+trigger(animal, 'type')
+trigger(animal, 'height')
+
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// sunshine_lin是个大菜鸟 sunshine_lin是个小天才 18岁已经算很老了 18岁还算很年轻啊
+
+console.log(typeStr1, typeStr2, heightStr1, heightStr2)
+// 猫是个大菜鸟 猫是个小天才 20已经算很高了 20还算很矮啊

Proxy

通过上面的学习,我们已经可以实现当数据更新时,他的依赖变量也跟着改变,但是还是有缺点的,大家可以发现,每次我们总是得自己手动去执行track函数进行依赖收集,并且当数据改变时,我么又得手动执行trigger函数去进行通知更新

那么,到底有没有办法可以实现,自动收集依赖,以及自动通知更新呢?答案是有的,Proxy可以为我们解决这个难题。咱们先写一个reactive函数,大家先照敲,理解好Proxy-track-trigger这三者的关系,后面我会讲为什么这里Proxy需要搭配Reflect

function reactive(target) {
+    const handler = {
+        get(target, key, receiver) {
+            track(receiver, key) // 访问时收集依赖
+            return Reflect.get(target, key, receiver)
+        },
+        set(target, key, value, receiver) {
+            Reflect.set(target, key, value, receiver)
+            trigger(receiver, key) // 设值时自动通知更新
+        }
+    }
+
+    return new Proxy(target, handler)
+}

然后改一改之前的代码,把手动track手动trigger去掉,发现也能实现之前的效果

const person = reactive({ name: '林三心', age: 22 }) // 传入reactive
+const animal = reactive({ type: 'dog', height: 50 }) // 传入reactive
+
+effectNameStr1()
+effectNameStr2()
+effectAgeStr1()
+effectAgeStr2()
+effectTypeStr1()
+effectTypeStr2()
+effectHeightStr1()
+effectHeightStr2()
+
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// 林三心是个大菜鸟 林三心是个小天才 22岁已经算很老了 22岁还算很年轻啊
+
+console.log(typeStr1, typeStr2, heightStr1, heightStr2)
+// dog是个大菜鸟 dog是个小天才 50已经算很高了 50还算很矮啊
+
+person.name = 'sunshine_lin'
+person.age = 18
+animal.type = '猫'
+animal.height = 20
+
+console.log(nameStr1, nameStr2, ageStr1, ageStr2)
+// sunshine_lin是个大菜鸟 sunshine_lin是个小天才 18岁已经算很老了 18岁还算很年轻啊
+
+console.log(typeStr1, typeStr2, heightStr1, heightStr2)
+// 猫是个大菜鸟 猫是个小天才 20已经算很高了 20还算很矮啊

可能有的同学会有点懵逼,对上面的代码有点疑惑,也可能有点绕,我还以为通过一张图给大家讲解一下流程,图可能会被压缩,建议点开看看

截屏2021-08-27 下午9.13.55.png

解决写死问题

在上面有一处地方,咱们是写死的,大家都还记得吗,就是在track函数

function track(target, key) {
+    let depsMap = targetMap.get(target)
+    if (!depsMap) {
+        targetMap.set(target, depsMap = new Map())
+    }
+
+    let dep = depsMap.get(key)
+    if (!dep) {
+        depsMap.set(key, dep = new Set())
+    }
+    // 这里先暂且写死
+    if (target === person) {
+        if (key === 'name') {
+            dep.add(effectNameStr1)
+            dep.add(effectNameStr2)
+        } else {
+            dep.add(effectAgeStr1)
+            dep.add(effectAgeStr2)
+        }
+    } else if (target === animal) {
+        if (key === 'type') {
+            dep.add(effectTypeStr1)
+            dep.add(effectTypeStr2)
+        } else {
+            dep.add(effectHeightStr1)
+            dep.add(effectHeightStr2)
+        }
+    }
+}

实际开发中,肯定是不止两个对象的,如果每多加一个对象,就得多加一个else if判断,那是万万不行的。那我们要怎么解决这个问题呢?其实说难也不难,Vue3的作者们想出了一个非常巧妙的办法,使用一个全局变量activeEffect来巧妙解决这个问题,具体是怎么解决呢?其实很简单,就是每一个effect函数一执行,就把自身放到对应的dep里,这就可以不需要写死了。

截屏2021-08-27 下午9.31.37.png

我们怎么才能实现这个功能呢?我们需要改装一下effect函数才行,并且要修改track函数

let activeEffect = null
+function effect(fn) {
+    activeEffect = fn
+    activeEffect()
+    activeEffect = null // 执行后立马变成null
+}
+function track(target, key) {
+    // 如果此时activeEffect为null则不执行下面
+    // 这里判断是为了避免例如console.log(person.name)而触发track
+    if (!activeEffect) return
+    let depsMap = targetMap.get(target)
+    if (!depsMap) {
+        targetMap.set(target, depsMap = new Map())
+    }
+
+    let dep = depsMap.get(key)
+    if (!dep) {
+        depsMap.set(key, dep = new Set())
+    }
+    dep.add(activeEffect) // 把此时的activeEffect添加进去
+}
+
+// 每个effect函数改成这么执行
+effect(effectNameStr1)
+effect(effectNameStr2)
+effect(effectAgeStr1)
+effect(effectAgeStr2)
+effect(effectTypeStr1)
+effect(effectTypeStr2)
+effect(effectHeightStr1)
+effect(effectHeightStr2)

截屏2021-08-27 下午9.49.44.png

实现ref

咱们在Vue3中是这么使用ref

let num = ref(5)
+
+console.log(num.value) // 5

然后num就会成为一个响应式的数据,而且使用num时需要这么写num.value才能使用

实现ref其实很简单,咱们上面已经实现了reactive,只需要这么做就可以实现ref

function ref (initValue) {
+    return reactive({
+        value: initValue
+    })
+}

咱们可以来试试效果如何

let num = ref(5)
+
+effect(() => sum = num.value * 100)
+
+console.log(sum) // 500
+
+num.value = 10
+
+console.log(sum) // 1000

实现computed

咱们顺便简单实现一下computed吧,其实也很简单

function computed(fn) {
+    const result = ref()
+    effect(() => result.value = fn()) // 执行computed传入函数
+    return result
+}

咱们来看看结果

let num1 = ref(5)
+let num2 = ref(8)
+let sum1 = computed(() => num1.value * num2.value)
+let sum2 = computed(() => sum1.value * 10)
+
+console.log(sum1.value) // 40
+console.log(sum2.value) // 400
+
+num1.value = 10
+
+console.log(sum1.value) // 80
+console.log(sum2.value) // 800
+
+num2.value = 16
+
+console.log(sum1.value) // 160
+console.log(sum2.value) // 1600

自此咱们就实现了本文章所有功能

最终代码

const targetMap = new WeakMap()
+function track(target, key) {
+    // 如果此时activeEffect为null则不执行下面
+    // 这里判断是为了避免例如console.log(person.name)而触发track
+    if (!activeEffect) return
+    let depsMap = targetMap.get(target)
+    if (!depsMap) {
+        targetMap.set(target, depsMap = new Map())
+    }
+
+    let dep = depsMap.get(key)
+    if (!dep) {
+        depsMap.set(key, dep = new Set())
+    }
+    dep.add(activeEffect) // 把此时的activeEffect添加进去
+}
+function trigger(target, key) {
+    let depsMap = targetMap.get(target)
+    if (depsMap) {
+        const dep = depsMap.get(key)
+        if (dep) {
+            dep.forEach(effect => effect())
+        }
+    }
+}
+function reactive(target) {
+    const handler = {
+        get(target, key, receiver) {
+            track(receiver, key) // 访问时收集依赖
+            return Reflect.get(target, key, receiver)
+        },
+        set(target, key, value, receiver) {
+            Reflect.set(target, key, value, receiver)
+            trigger(receiver, key) // 设值时自动通知更新
+        }
+    }
+
+    return new Proxy(target, handler)
+}
+let activeEffect = null
+function effect(fn) {
+    activeEffect = fn
+    activeEffect()
+    activeEffect = null
+}
+function ref(initValue) {
+    return reactive({
+        value: initValue
+    })
+}
+function computed(fn) {
+    const result = ref()
+    effect(() => result.value = fn())
+    return result
+}

Proxy和Reflect

Proxy

const person = { name: '林三心', age: 22 }
+
+const proxyPerson = new Proxy(person, {
+    get(target, key, receiver) {
+        console.log(target) // 原来的person
+        console.log(key) // 属性名
+        console.log(receiver) // 代理后的proxyPerson
+    },
+    set(target, key, value, receiver) {
+        console.log(target) // 原来的person
+        console.log(key) // 属性名
+        console.log(value) // 设置的值
+        console.log(receiver) // 代理后的proxyPerson
+    }
+})
+
+proxyPerson.name // 访问属性触发get方法
+
+proxyPerson.name = 'sunshine_lin' // 设置属性值触发set方法

Reflect

在这列举Reflect的两个方法

  • get(target, key, receiver):个人理解就是,访问targetkey属性,但是this是指向receiver,所以实际是访问的值是receiver的key的值,但是这可不是直接访问receiver[key]属性,大家要区分一下
  • set(target, key, value, receiver):个人理解就是,设置targetkey属性为value,但是this是指向receiver,所以实际是是设置receiver的key的值为value,但这可不是直接receiver[key] = value,大家要区分一下

上面咱们强调了,不能直接receiver[key]或者receiver[key] = value,而是要通过Reflect.get和Reflect.set,绕个弯去访问属性或者设置属性,这是为啥呢?下面咱们举个反例

const person = { name: '林三心', age: 22 }
+
+const proxyPerson = new Proxy(person, {
+    get(target, key, receiver) {
+        return Reflect.get(receiver, key) // 相当于 receiver[key]
+    },
+    set(target, key, value, receiver) {
+        Reflect.set(receiver, key, value) // 相当于 receiver[key] = value
+    }
+})
+
+console.log(proxyPerson.name)
+
+proxyPerson.name = 'sunshine_lin' 
+// 会直接报错,栈内存溢出 Maximum call stack size exceeded

为什么会这样呢?看看下图解答

截屏2021-08-27 下午10.55.49.png

现在知道为什么不能直接receiver[key]或者receiver[key] = value了吧,因为直接这么操作会导致无限循环,最终报错。所以正确做法是

const person = { name: '林三心', age: 22 }
+
+const proxyPerson = new Proxy(person, {
+    get(target, key, receiver) {
+        return Reflect.get(target, key, receiver)
+    },
+    set(target, key, value, receiver) {
+        Reflect.set(target, key, value, receiver)
+    }
+})
+
+console.log(proxyPerson.name) // 林三心
+
+proxyPerson.name = 'sunshine_lin'
+
+console.log(proxyPerson.name) // sunshine_lin

肯定有的同学就要问了,下面这么写也可以,为什么也不建议呢?我放到下面一起说

const proxyPerson = new Proxy(person, {
+    get(target, key, receiver) {
+        return Reflect.get(target, key)
+    },
+    set(target, key, value, receiver) {
+        Reflect.get(target, key, value)
+    }
+})

为什么要一起用

其实Proxy不搭配Reflect也是可以的。咱们可以这么写,也照样能实现想要的效果

const person = { name: '林三心', age: 22 }
+
+const proxyPerson = new Proxy(person, {
+    get(target, key, receiver) {
+        return target[key]
+    },
+    set(target, key, value, receiver) {
+        target[key] = value
+    }
+})
+
+console.log(proxyPerson.name) // 林三心
+
+proxyPerson.name = 'sunshine_lin'
+
+console.log(proxyPerson.name) // sunshine_lin

那为什么建议Proxy和Reflect一起使用呢?因为Proxy和Reflect的方法都是一一对应的,在Proxy里使用Reflect会提高语义化

  • Proxy的get对应Reflect.get
  • Proxy的set对应Reflect.set
  • 还有很多其他方法我就不一一列举,都是一一对应的

还有一个原因就是,尽量把this放在receiver上,而不放在target

为什么要尽量把this放在代理对象receiver上,而不建议放原对象target上呢?因为原对象target有可能本来也是是另一个代理的代理对象,所以如果this一直放target上的话,出bug的概率会大大提高,所以之前的代码为什么不建议,大家应该知道了吧?

const proxyPerson = new Proxy(person, {
+    get(target, key, receiver) {
+        return Reflect.get(target, key)
+    },
+    set(target, key, value, receiver) {
+        Reflect.set(target, key, value)
+    }
+})

结语

我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜欢前端,想学习前端,那咱们可以交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】

image.png

+
+

Sunshine_Lin
2.1k 声望7.1k 粉丝

\ No newline at end of file