この記事では、vue3 の応答性を詳細に分析します (脳マップ付き)。この記事の目標は、最も基本的な状況の処理を含む、基本的な vue3 の応答性を実装することです。
#この記事では、
データ バインディングのための関数を実行することです。データが変更されると、関数の実行が再度トリガーされます。 (学習ビデオ共有: vue ビデオ チュートリアル)
たとえば、オブジェクトdata があり、次の場合にこれを応答性の高いデータにしたいとします
data のデータが変更されると、
effect 関数が自動的に実行され、
nextVal 変数の値も変更されます。上の例では、
data
age の値を変更しますが、
effect 関数は実行されません。これは、
effect 関数が異なるためです。
data このオブジェクトとのつながりがないとは言えません。まったくつながりがありません。
では、これら 2 つの無関係な関数とオブジェクトを関連付けるにはどうすればよいでしょうか?
data
の値が変更されるたびに、data オブジェクト用のスペースを定義することは可能ですか? ? このスペースの関数は実行されますか?
答えは「はい」です。
Object.defineProperty()
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Parametersobj
プロパティを定義する必要があるオブジェクト。prop 定義または変更する必要があるプロパティの名前。
descriptor (記述子) 定義または変更する必要がある属性の記述子。
descriptor
さらに、データ記述子 (属性は次のとおりです:
、value
、writable
) とアクセス記述子 (属性は enumerable
、configurable
、set()
、get()
) それらの間には相互排他的な関係があります。 set()
と get()
を定義した後、記述子はアクセス操作が定義されたとみなして、value
と writable# を定義します。 ## はエラーになります。
1 |
|
上記の例では、オブジェクトの最も基本的なインターセプトが完了しました。これは、
vue2.x でオブジェクトを監視する方法でもありますが、
Object.defineProperty が原因です。 () にはいくつかの問題があります。例: 一度に 1 つの属性のみを監視でき、すべての属性を監視するにはそれを走査する必要があります
push
およびでは、
vue3
Proxy です。
この記事は主に
vue3 バージョンの応答性の高い実装に関するものであるため、
Object.defineProperty()
<h3 data-id="heading-3"><strong><span style="font-size: 18px;">2. Proxy</span></strong></h3>
<p><code>proxy
是es6
版本出现的一种对对象的操作方式,Proxy
可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy
这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
通过proxy
我们可以实现对一个对象的读取,设置等等操作进行拦截,而且直接对对象进行整体拦截,内部提供了多达13种拦截方式。
get(target, propKey, receiver) :拦截对象属性的读取,比如 proxy.foo
和 proxy['foo']
。
set(target, propKey, value, receiver) :拦截对象属性的设置,比如 proxy.foo = v
或 proxy['foo'] = v
,返回一个布尔值。
has(target, propKey) :拦截 propKey in proxy
的操作,返回一个布尔值。
deleteProperty(target, propKey) :拦截 delete proxy[propKey]
的操作,返回一个布尔值。
ownKeys(target) :拦截 Object.getOwnPropertyNames(proxy)
、 Object.getOwnPropertySymbols(proxy)
、 Object.keys(proxy)
、 for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey) :拦截 Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。
defineProperty(target, propKey, propDesc) :拦截 Object.defineProperty(proxy, propKey, propDesc)
、 Object.defineProperties(proxy, propDescs)
,返回一个布尔值。
preventExtensions(target) :拦截 Object.preventExtensions(proxy)
,返回一个布尔值。
getPrototypeOf(target) :拦截 Object.getPrototypeOf(proxy)
,返回一个对象。
isExtensible(target) :拦截 Object.isExtensible(proxy)
,返回一个布尔值。
setPrototypeOf(target, proto) :拦截 Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args) :拦截 Proxy (代理) 实例作为函数调用的操作,比如 proxy(...args)
、 proxy.call(object, ...args)
、 proxy.apply(...)
。
construct(target, args) :拦截 Proxy (代理) 实例作为构造函数调用的操作,比如 new proxy(...args)
。
如果想要详细了解proxy
,请移步:es6.ruanyifeng.com/#docs/proxy…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
有了proxy
,我们就可以根据之前的思路实现一个基本的响应式功能了,我们的思路是这样的:在对象被读取时把函数收集到一个“仓库”,在对象的值被设置时触发仓库中的函数。
由此我们可以写出一个最基本的响应式功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
我们创建了一个用于保存依赖函数的“仓库”,它是Set
类型,然后使用proxy
对对象data
进行代理,设置了set
和get
拦截函数,用于拦截读取和设置操作,当读取属性时,将依赖函数effect
存储到“仓库”中,当设置属性值时,将依赖函数从“仓库”中取出并重新执行。
还有一个小问题,怎么触发对象的读取操作呢?我们可以直接调用一次effect
函数,如果在effect
函数中存在需要收集的属性,那么执行一次effect
函数也是比较符合常理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
一开始会执行一次effect
,然后函数两秒钟后会执行代理对象设置操作,再次执行effect
函数,输出20。
此时整个响应式流程的功能是这样的:
阶段一,在属性被读取时,为对象属性收集依赖函数:
阶段二,当属性发生改变时,再次触发依赖函数
这样就实现了一个最基本的响应式的功能。
问题一
其实上面实现的功能还有很大的缺陷,首先最明显的问题是,我们把effect
函数给固定了,如果用户使用的依赖函数不叫effect
怎么办,显然我们的功能就不能正常运行了。
所以先来进行第一步的优化:抽离出一个公共方法,依赖函数由用户来传递参数。
我们使用effect
函数来接受用户传递的依赖函数:
1 2 3 4 5 6 7 8 9 10 11 |
|
但是effect
函数内部只是执行了,在get
函数中怎么能知道用户传递的依赖函数是什么呢,这两个操作并不在一个函数内啊?其实可以使用一个全局变量activeEffect
来保存当前正在处理的依赖函数。
修改后的effect
函数是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
调用effect
函数传递一个匿名函数作为依赖函数,当执行时,首先会把匿名函数赋值给全局变量activeEffect
,然后触发属性的读取操作,进而触发get
拦截,将全局变量activeEffect
进行收集。
问题二
从上面我们定义的对象可以看到,我们的对象data
中有两个属性,上面的例子中我们只给age
建立了响应式连接,那么如果我现在也想给name
建立响应式连接怎么办呢?那好说,那我们直接向“仓库”中继续添加依赖函数不就行了吗。
其实这会带来很严重的问题,由于 “仓库”并没有与被操作的目标属性之间建立联系,而上面我们的实现只是将整个“仓库”遍历了一遍,所以无论哪个属性被触发,都会将“仓库”中所有的依赖函数都取出来执行一遍,因为整个执行程序中可能有很多对象及属性都设置了响应式联系,这将会带来很大的性能浪费。所谓牵一发而动全身,这种结果显然不是我们想要的。
1 2 3 4 |
|
所以我们要重新设计一下“仓库”的数据结构,目的就是为了可以在属性这个粒度下和“仓库”建立明确的联系。
就拿我们上面进行操作的对象来说,存在着两层的结构,有两个角色,对象data
以及属性name``age
1 2 3 4 |
|
他们的关系是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
接下来我们实现一下代码,为了方便调用,将设置响应式数据的操作封装为一个函数reactive
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
我们将读取操作封装为了函数track
,触发依赖函数的动作封装为了trigger
方便调用,现在的整个“仓库”结构是这样的:
可能有人会问了,为什么设置“仓库”要使用WeakMap
呢,我使用一个普通对象来创建不行吗? -
WeakMap
结构与 Map
结构类似,也是用于生成键值对的集合。
WeakMap
与 Map
的区别有两点。
首先, WeakMap
只接受对象作为键名( null
除外),不接受其他类型的值作为键名。
1 2 3 4 5 6 7 |
|
上面代码中,如果将数值 1
和 Symbol
值作为 WeakMap 的键名,都会报错。
其次, WeakMap
的键名所指向的对象,不计入垃圾回收机制。
WeakMap
的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。
1 2 3 4 5 6 |
|
上面代码中, e1
和 e2
是两个对象,我们通过 arr
数组对这两个对象添加一些文字说明。这就形成了 arr
对 e1
和 e2
的引用。
一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放 e1
和 e2
占用的内存。
1 2 3 4 |
|
上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。
它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap
里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
如果我们上文中target
对象没有任何引用了,那么说明用户已经不需要用到它了,这时垃圾回收器会自动执行回收,而如果使用Map
来进行收集,那么即使其他地方的代码已经对target
没有任何引用,这个target
也不会被回收。
在vue3中的实现方式和我们的基本实现还有一点不同就是在vue3中是使用Reflect
来操作数据的,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
那么为什么要使用Reflect
来操作数据呢,像之前一样直接操作原对象不行吗,我们先来看一下一种特殊的情况:
1 2 3 4 5 6 |
|
在effect
依赖函数中通过代理对象p访问bar属性:
1 2 3 |
|
可以分析一下这个过程发生了什么,当effect
函数被调用时,会读取p.bar
属性,他发现p.bar
属性是一个访问器属性,因此会执行getter
函数,由于在getter
函数中通过this.foo
读取了foo
属性的值,因此我们会认为副作用函数与属性foo
之间也会建立联系,当修改p.foo
的值的时候因该也能够触发响应,使依赖函数重新执行才对,然而当修改p.foo
的时候,并没有触发依赖函数:
1 |
|
实际上问题就出在bar
属性中的访问器函数getter
上:
1 2 3 4 |
|
当通过代理对象p访问p.bar
,这回触发代理对象的get
拦截函数执行:
1 2 3 4 5 6 |
|
可以看到在get
的拦截函数中,通过target[key]
返回属性值,其中target
是原始对象obj
,而key
就是字符串'bar'
,所以target[key]
就相当于obj.bar
。因此当我们使用p.bar
访问bar
属性时,他的getter
函数内的this
其实指向原始对象obj
,这说明我们最终访问的是obj.foo
。所以在依赖函数内部通过原始对象访问他的某个属性是不会建立响应联系的:
1 2 3 4 |
|
那么怎么解决这个问题呢,这时候就需要用到 Reflect
出场了。
先来看一下Reflect
是啥:
Reflect
函数的功能就是提供了访问一个对象属性的默认行为,例如下面两个操作是等价的:
1 2 3 4 5 6 7 |
|
实际上Reflect.get
函数还能接受第三个函数,即制定接受者receiver
,可以把它理解为函数调用过程中的this
:
1 2 3 |
|
在这段代码中,指定了第三个参数receiver为一个对象{ foo: 2 }
,这是读取到的值时receiver
对象的foo
属性。
而我们上文中的问题的解决方法就是在操作对象数据的时候通过Reflect
的方法来传递第三个参数receiver
,它代表谁在读取属性:
1 2 3 4 5 6 7 8 |
|
当使用代理对象p
访问bar
属性时,那么receiver
就是p,可以把它理解为函数调用中的this
。
所以我们改造一下reactive
函数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
扩展
Proxy -> get()
get
方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy
(代理) 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
Reflect.get(target, name, receiver)
Reflect.get
方法查找并返回 target
对象的 name
属性,如果没有该属性,则返回 undefined
。
1 2 3 4 5 6 7 8 9 10 11 |
|
如果 name
属性部署了读取函数( getter ),则读取函数的 this
绑定 receiver
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
如果第一个参数不是对象, Reflect.get
方法会报错。
1 2 |
|
Reflect.set(target, name, value, receiver)
Reflect.set
方法设置 target
对象的 name
属性等于 value
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
如果 name
属性设置了赋值函数,则赋值函数的 this
绑定 receiver
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
注意,如果 Proxy
对象和 Reflect
对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了 receiver
,那么 Reflect.set
会触发 Proxy.defineProperty
拦截。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
上面代码中, Proxy.set
拦截里面使用了 Reflect.set
,而且传入了 receiver
,导致触发 Proxy.defineProperty
拦截。这是因为 Proxy.set
的 receiver
参数总是指向当前的 Proxy
实例(即上例的 obj
),而 Reflect.set
一旦传入 receiver
,就会将属性赋值到 receiver
上面(即 obj
),导致触发 defineProperty
拦截。如果 Reflect.set
没有传入 receiver
,那么就不会触发 defineProperty
拦截。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
如果第一个参数不是对象, Reflect.set
会报错。
1 2 |
|
到这里,一个非常基本的响应式的功能就完成了,整体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
在日常的工作中,effect
函数并不是单独存在的,比如在vue的渲染函数中,各个组件之间互相嵌套,那么他们在组件中所使用的effect
是必然会发生嵌套的:
1 2 3 4 5 |
|
当组件中发生嵌套时,此时的渲染函数:
1 2 3 4 5 6 7 8 |
|
但是此时我们实现的effect
并没有这个能力,执行下面这段代码,并不会出现意料之中的行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
在这段代码中,定义了代理对象obj
,里面有两个属性foo
和bar
,然后定义了收集foo
的依赖函数,在依赖函数的内部又定义了bar
的依赖函数。
在理想状态下,我们希望依赖函数与属性之间的关系如下:
1 2 3 4 5 |
|
当修改obj.foo
的值的时候,会触发effect1
函数执行,由于effect2
函数在effect
函数内部,所以effect2
函数也会执行,而当修改obj.bar
时,只会触发effect2
函数。接下来修改一下obj.foo
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
看一下执行结果:
可以看到effect2
函数竟然执行了两次?按照之前的分析,当obj.foo
被修改后,应当触发effect1
这个依赖函数,但是为什么会effect2
会被再次执行呢?
来看一下我们effect
函数的实现:
1 2 3 4 5 6 7 8 |
|
其实在这里就已经很容易看出问题了,在接受用户传递过来的值时,我们直接将activeEffect
这个全局变量进行了覆盖!所以在内部执行完后,activeEffect
这个变量就已经是effect2
函数了,而且永远不会再次变为effect1
,此时再进行收集依赖函数时,永远收集的都是effect2
函数。
那么如何解决这种问题呢,这种情况可以借鉴栈结构来进行处理,栈结构是一种后进先出的结构,在依赖函数执行时,将当前的依赖函数压入栈中,等待依赖函数执行完毕后将其从栈中弹出,始终activeEffect
指向栈顶的依赖函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
此时两个属性所对应的依赖函数便不会发生错乱了。
如果现在将effect
函数中传递的依赖函数改一下:
1 2 3 4 5 6 7 8 9 10 11 |
|
在这段代码中,我们将代理对象obj
的age
属性执行自增操作,但是执行这段代码,却发现竟然栈溢出了?这是怎么回事呢?
其实在effect
中处理依赖函数时,obj.age++
的操作其实可以看做是这样的:
1 2 3 4 |
|
这段代码的执行流程是这样的:首先读取obj.foo
的值,这会触发track
函数进行收集操作,也就是将当前的依赖函数收集到“仓库”中,接着将其加1后再赋值给obj.foo
,此时会触发trigger
操作,即把“仓库”中的依赖函数取出并执行。但是此时该依赖函数正在执行中,还没有执行完就要再次开始下一次的执行。就会导致无限的递归调用自己。
解决这个问题,其实只需要在触发函数执行时,判断当前取出的依赖函数是否等于activeEffect
,就可以避免重复执行同一个依赖函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
computed
是vue3中的计算属性,它可以根据传入的参数进行响应式的处理:
1 |
|
根据computed
的用法,我们可以知道它的几个特点:
懒执行,值变化时才会触发
缓存功能,如果值没有变化,就会返回上一次的执行结果 在实现这两个核心功能之前,我们先来改造一下之前实现的effect
函数。
怎么能使effect
函数变成懒执行呢,比如计算属性的这种功能,我们不想要他立即执行,而是希望在它需要的时候才执行。
这时候我们可以在effect
函数中传递第二个参数,一个对象,用来设置一些额外的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这时,如果传递了lazy
属性,那么该effect
将不会立即执行,需要手动进行执行:
1 2 3 4 5 6 |
|
但是如果我们想要获取手动执行后的值呢,这时只需要在effect
函数中将其返回即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
接下来开始实现computed
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
但是此时还做不到对值进行缓存和对比,增加两个变量,一个存储执行的值,另一个为一个开关,表示“是否可以重新执行依赖函数”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
但是上面的实现还有一个问题,就是好像isRun
执行一次后好像永远都不会变成true
了,我们的本意是在数据发生变动的时候需要再次触发依赖函数,也就是将isRun变为true,实现这种效果,需要我们为options
再传递一个函数,用于用户自定义的调度执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
接下来需要修改一下trigger
如果传递了scheduler
这个函数,那么只执行scheduler
这个函数而不执行依赖函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
那么在computed
中就可以实现重置执行开关isRun
的操作了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
当computed
传入的依赖函数中的值发生改变时,会触发响应式对象的trigger
函数,而计算属性创建响应式对象时传入了scheduler
,所以当数据改变时,只会执行scheduler
函数,在scheduler
函数内我们将执行开关重置为true
,再下次访问数据触发get
函数时,就会重新执行依赖函数。这也就实现了当数据发生改变时,会再次触发依赖函数的功能了。
为了避免计算属性被另外一个依赖函数调用而失去响应,我们还需要为计算属性单独进行绑定响应式的功能,形成一个effect
嵌套。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
先来看一下watch
函数的用法,它的用法也非常简单:
1 2 3 4 5 6 |
|
watch
接受两个参数,第一个参数为绑定的响应式数据,第二个参数为依赖函数,我们依然可以沿用之前的思路来进行处理,利用effect
以及scheduler
来改变触发执行时机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
watch
函数还有另外一种用法,就是除了接收对象,还可以接受一个getter
函数,例如:
1 2 3 4 5 6 |
|
这种情况下只需要将用户传入的getter
将我们自定义的bindData
替代即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
其实watch
函数还有一个很重要的功能:就是在用户传递的依赖函数中可以获取新值和旧值,但是我们目前还做不到这一点。实现这个功能我们可以配置前文中的lazy
属性来实现。
来回顾一下lazy
属性:设置了lazy
之后一开始不会执行依赖函数,手动执行时会返回执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
此外,watch
函数还有一个功能,就是可以自定义执行时机,比如immediate
属性,他会在创建时立即执行一次:
1 2 3 4 5 |
|
我们可以把scheduler
封装为一个函数,以便在不同的时机去调用他:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
watch
函数还支持其他的执行调用时机,这里只实现了immediate
。
深响应和浅响应的区别:
1 2 3 4 5 6 7 8 |
|
因为之前实现的拦截,无论对于什么类型的数据都是直接进行返回的,如果实现深响应,那么首先应该判断是否为对象类型的值,如果是对象类型的值,应当递归调用reactive
方法进行转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
将创建响应式对象的方法抽离出去,通过传递isShallow
参数来决定是否创建深响应/浅响应对象。
1 2 3 4 5 6 7 8 9 |
|
有时候我们并不需要对值进行修改,也就是需要值为只读的,这个操作也分为深只读和浅只读,首先需要在createReactive
函数中增加一个参数isReadOnly
,代表是否为只读属性。
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
如果为只读属性,那么也不需要为其建立响应联系
如果为只读属性,那么在进行深层次遍历的时候,需要调用readOnly
函数对值进行包装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
如果操作数组时,设置的索引值大于数组当前的长度,那么要更新数组的length
属性,所以当通过索引设置元素值时,可能会隐式的修改length
的属性值,因此再j进行触发响应时,也应该触发与length
属性相关联的副作用函数重新执行。
1 2 3 4 5 6 7 8 |
|
在判断操作类型时,新增对数组类型的判断,如果代理目标是数组,那么对于操作类型的判断作出处理:
如果设置的索引值小于数组的长度,就视为SET
操作,因为他不会改变数组长度,如果设置的索引值大于当前数组的长度,那么应该被视为ADD
操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
还有一点:其实修改数组的length
属性也会隐式的影响数组元素:
1 2 3 4 5 6 7 8 9 |
|
如上所示,在副作用函数内部访问了第0个元素,然后将数组的length
属性修改为0,这回隐式的影响数组元素,及所有的元素都会被删除,所以应该触发副作用函数重新执行。
然而并非所有的对length
属性值的修改都会影响数组中的已有元素,如果设置的length
属性为100,这并不会影响第0个元素,当修改属性值时,只有那些索引值大于等于新的length
属性值的元素才需要触发响应。
调用trigger
函数时传入新值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
判断新的下标值与需要操作的新的下标值进行判断,因为数组的key
为下标,所以副作用函数搜集器是以下标作为key
值的,当length
发生变动时,只需要将新值与每个下标的key
判断,大于等于新的length
值的需要重新执行副作用函数。
如上图所示,Map
为根据数组的key
,也就是id
组成的Map
结构,他们的每一个key
都对应一个Set
,用于保存这个key
下面的所有的依赖函数。
当length
属性发生变动时,应当取出所有key
值大于等于length
值的所有依赖函数进行执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
本文的实现数组这种数据结构只考虑了针对长度发生变化的情况。
由于Proxy的代理目标是非原始值,所以没有任何手段去拦截对原始值的操作:
1 2 3 |
|
解决方法是:使用一个非原始值去包裹原始值:
1 2 3 4 5 6 7 8 |
|
如何判断是用户传入的对象还是包裹对象呢?
1 2 |
|
只需要在包裹对象内部定义一个不可枚举且不可写的属性:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
在使用...解构赋值时会导致响应式丢失:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
首先创建一个响应式对象obj,然后使用展开运算符得到一个新对象newObj
,他是一个普通对象,不具有响应式的能力,所以修改obj.foo
的值不会触发副作用函数重新更新。
解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
将单个值包装为一个对象,相当于访问该属性的时候会得到该属性的getter
,在getter
中返回原始的响应式对象。
相当于解构访问newObj.foo
=== obj.foo
。
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
使用toRefs
处理整个对象,在toRefs
这个函数中循环处理了对象所包含的所有属性。
1 |
|
当设置value
属性值的时候,最终设置的是响应式数据的同名属性值。
一个基本的vue3
响应式就完成了,但是本文所实现的依然是阉割版本,有很多情况都没有进行考虑,还有好多功能没有实现,比如:拦截 Map
,Set
,数组的其他问题,对象的其他问题,其他api的实现,但是上面的实现已经足够让你理解vue3响应式原理实现的核心了,这里还有很多其他的资料需要推荐,比如阮一峰老师的es6教程,对于vue3底层原理的实现,许多知识依然是需要回顾和复习,查看原始底层的实现,再比如霍春阳老师的《vue.js的设计与实现》这本书,这本书目前我也只看完了一半,但是截止到目前我认为这本书对于学习vue3
的原理是非常深入浅出,鞭辟入里的,本文的许多例子也是借鉴了这本书。
最后当然是需要取读一读源码,不过在读源码之前能够先了解一下实现的核心原理,再去看源码是事半功倍的。希望大家都能早日学透源码,面试的时候能够对答如流,工作中遇到的问题也能从原理层面去理解和更好地解决!
目前我也在实现一个mini-vue
,截止到目前只实现了响应式部分,而且与本文的实现方式有所不同,后续还会继续实现编译和虚拟DOM部分,欢迎star!
k-vue:
https://github.com/konvyi/k-vue
如果想学习《vue.js的设计与实现》这本书这本书,那么请关注下面这个链接作为参考,里面包含了根据具体的问题的功能进行拆分实现,同样也只实现了响应式的部分!
vue3-analysis:
https://github.com/konvyi/vue3-analysis