响应系统作用和实现
响应式收集
1 | const effectFn = () => { |
首先需要收集响应式数据,比如执行副作用函数时访问了obj.age,会被proxy get拦截,这时存入响应式数据和副作用函数的映射关系
- obj
- age
- effectFn1
- effectFn2
- name
- effectFn1
- effectFn2
- age
其中,effectFn即为副作用函数,比如更新视图等。
obj放在一个WeakMap里面,key为这个proxy对象的引用地址,value为一个Map,里面存obj里面的所有被访问的属性;每个属性可能对应一个或者多个副作用函数,因此使用一个Set存入。
存储结构对应为
1 | WeakMap { |
Map 是哈希表实现,查询复杂度O(1)。Set底层也是哈希表实现,它的key和value相等,可以认为是一种特殊的Map。
哈希表是一种特殊的数据结构,在堆内存中存在数组,数组的每一个元素都指向一个链表,这个数组就叫桶数组。
一组数据使用哈希表存储时,会用key先算hashCode,一般情况下hashCode会均匀分步在每个哈希桶中,此时每个哈希桶中存储数量为1的链表结构。在数据读取时,会使用key算出hashCode,直接去对应hashCode中取出该链表,因为一般链表长度为1,因此查询复杂度为O(1);假如哈希冲突很厉害,最坏的情况是所有key全部映射到同一个哈希桶中,此时相当于数据存在一个链表中,从中找出所需成员的时间复杂度为遍历链表的复杂度,为O(n).
副作用再次执行
当数据变化时,会被proxy set拦截,此时把数据对应的副作用函数重新执行即可
代码实现
vue3使用proxy实现响应式,vue2使用defineProperty实现,这里给出大概代码实现
1 | let data = { |
这样每次访问obj.age,就会被proxy的get拦截;每次给obj.age赋值,比如obj.age = 13,就会被proxy的set拦截。在get拦截中收集依赖关系(第一次页面挂载时会执行一遍副作用函数,此时会有依赖收集的过程),在set拦截中执行副作用函数。这样当每次重新给obj.age赋值时,都会重新执行副作用函数,实现了数据的响应式更新。
effect
vue执行过程如下:
setup执行
创建reactive、ref
解析computed、watch
执行render()函数,读取里面的响应式变量,触发依赖收集
生成虚拟DOM
beforeMount钩子执行
DOM挂载完成
mounted钩子执行
在依赖收集阶段,为了方便收集副作用函数,比如匿名的副作用函数,会声明一个activeEffect变量,每次执行副作用函数如下:
1 | let activeEffect = null; |
这样的好处是在track和trigger阶段,都可以通过activeEffect访问到当前正在起作用的副作用函数
嵌套effect
为了解决嵌套的effect,需要把activeEffect变成一个栈结构,这样每次栈顶的effectFn就是当前正在活动的副作用函数,否则副作用函数收集会混乱。
调度执行
当trigger触发副作用函数重新执行时,有能力控制副作用函数执行的时机,次数,方式。是实现computed和watch的必要前置条件
在effect函数中,增多一个接受参数options,在其中传入调度器scheduler,大概代码如下:
1 | let activeEffect = null; |
调度器由外部传入,可以控制副作用函数如何执行
computed和lazy
当options中传入lazy,则副作用函数不会立即执行,并且在effect函数中,会返回effectFn。此时通过返回的effectFn,可以控制它手动执行
1 | function computed = (getter)=>{ |
上面的极简实现并没有缓存数据的功能,每次访问computed它总是会重新执行effectFn,返回对应的值。因此需要引入一个flag来标识它是否需要更新,并且更新时用调度器传入flag值
1 | function computed(getter) { |
watch
watch利用effect和scheduler来实现:在effect中会收集响应式数据依赖,在其中加入调度器,即可在响应式数据发生改变时,执行调度器中的内容,这就是watch的基本原理,监控响应式数据的变化,然后做出相应的动作。
1 | function watch(source,cb){ |
source 可以是一个函数(getter),可以是一个响应式数据,把它放入effect中即可,通过lazy可以实现新旧值的获取,通过调度器可以实现响应式数据变化以后的cb函数执行
watch还能首次执行,此时把scheduler内部的代码封装成一个job函数,然后再首次执行时执行该函数即可,因为首次执行和后续执行其实完全一样
watch还可以控制调度函数执行的时机,即是否在vue dom变化以后执行回调函数,通过flush关键值控制。比如当flush为’post’时,会把回调函数放入微任务队列末尾执行,因此此时在回调函数中能拿到vue dom变化以后的元素(在vue中,vue dom更新操作都是在微任务队列中进行的)
1 | function watch(source, cb, options = { immediate: true, flush: "post" }) { |