前言:这篇文章只从架构层面分析,主要是思想方面。
以前写过一些具体带代码得分析,现在这一篇我们只从架构层面分析,不涉及细小的细节,这一篇主要是响应式架构的分析。
涉及到的一些解析,我们先用文字描述逻辑,然后再转换成代码实现
这个地址里有vue2.6的源代码,里面是好早之前写的,有部分错误的地方,主题思想还是看本篇把 segmentfault.com/a/119000004…
形成响应式的基本条件
在这里我们思考一下,形成响应式需要几个条件(这里我们不考虑具体的细节,只是大体的逻辑)。
- 首先我们需要劫持数据属性的访问和设置,也就是get和set。
- 其次我们需要在某些函数中访问劫持数据的属性,来触发get,然后在get里我们需要直接或者间接的把这些函数存起来,所以在实现劫持的时候对象每一个属性节点应该都有一个数组,比如叫属性节点Dep,然后把这些函数放到属性节点Dep中。
- 然后在对应的劫持数据属性改变时会触发set,set触发时需要找到属性节点Dep,然后把它里面的函数执行一遍。
- 某些函数我们其实可以认为是watch和计算属性回调函数和组件render函数。
这会基本响应式已经形成,还有个小问题就是属性节点Dep会有一些额外冗余存储的函数,例如obj是响应式劫持对象,在某些函数中三元表达式 obj.ok ? obj.text : 'not'中ok为true时ok和text属性节点Dep都会收集这个函数,在ok变更时false时,这会只需要ok这个节点收集就够了,那现在情况就是text中还有,有冗余函数,它需要清掉,那上面的条件需要再做补充,如下:
- 上面所说的某些函数的每个函数都会有一个函数Dep,这个函数被放到了哪个属性节点Dep中,对应的就会把这个属性节点Dep放到函数Dep中,形成一个双向关联。
- 然后在这个函数执行之后,重新收集依赖之后,这个函数Dep里存放的一定得是准确的属性节点Dep,没有任何冗余的依赖。
- 所以在函数执行之前一定还有这样一段逻辑,循环函数Dep中所有的属性节点Dep,然后把当前函数从属性节点Dep中清空,然后函数Dep清空为空数组(因为现在已经没有属性dep存储它了)。
- 然后当前函数执行又会触发get重新准确的把当前函数添加到对应的属性节点Dep上。
这里说的步骤3、4处理冗余依赖这一步 Vue.js设计与实现霍春阳 是这样做的,vue2源代码处理是另一种方式,我们下面说。
好,那接下来我们来看看vue2和vue3是如何做的?
vue2响应式的设计
那在这里我们把其他的源代码设计都抛开,我们关注核心重点形成响应式的这块,其重点就是源代码里的4步,这四步按顺序执行。
- initData()用来生成observe,也就是对data进行劫持get和set,在劫持时创建了const dep = new Dep()用来收集watcher观察者。
- initComputed()初始化计算属性,每个计算属性都会创建一个计算属性的watcher,wathcer的evaluate属性回调函数最终会调用计算属性对应的函数,并且标记缓存,然后再对计算属性进行劫持,重点是计算属性的劫持get函数也就是源码内的computedGetter函数,下面详细解释。
- initWatch()初始化用户侦听器,watch属性,每watch的属性都会创建一个用户侦听器的watcher,wathcer的run属性回调函数最终会调用用户侦听器传入的对应的函数,然后根据immediate是否初始化执行用户侦听器所传入的函数。
- 最后初始化的watcher是渲染watcher,渲染watcher对应的函数是 vm._update(vm._render(), hydrating) 也就是组件的render函数,用来更新组件,watcher的run属性回调函数也会调用它。
在这里initWatch用户侦听器在初始化的时候,例如watch: { 'person.name': function... } ,默认会解析'person.name',并且触发,也就是说如果watch的key它是data的属性那就会触发data的属性get劫持函数,然后像我们最上面说的一样被对应属性节点Dep收集,如果watcher的key是计算属性那就会触发计算属性的get,也就是computedGetter,这个函数我们放到下面说,因为在render函数中也会触发计算属性的get,我们继续说用户侦听器,也就是说用户侦听器观察者watcher一初始化,也就意味着被 属性节点Dep 收集走了。
接下来我们继续走流程
在watcher这个class中有一个get函数实现:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
这个this.getter就是我们对应的用户侦听器回调、计算属性回调、组件的render函数,而在最后初始化渲染watcher时,会默认调用一下上面这个get,也就是说会默认执行组件的render函数生成vnode并且挂载到界面上。
而在执行render函数的时候,例如模板里我们会用到计算属性也就是说会触发计算属性的get也就是computedGetter函数,这个函数实现是这样的:
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
那函数里这个watcher就是我们对应的 计算属性节点的watcher实例,dirty表示缓存是否失效,默认是true,所以默认会执行evaluate,而evaluate内部会执行上面的贴的get代码,并且标记dirty为false标记缓存生效。、
执行get代码里面有pushTarget和popTarget需要注意,先讲this.getter然后再说这俩函数把,在这里this.getter也就是计算属性的回调函数,我们一般在这个回调中 会去拿vue的data里的某个属性,例如函数体中this.message.split('').reverse().join('') ,继而去触发data的属性的get劫持函数,然后这个 属性节点Dep数组会收集当前的watcher观察者,当前的观察者是计算属性的watcher,也就意味着当前计算属性watcher被属性节点所收集。
然后这个get执行完毕,中间的pushTarget和popTarget函数我们来看一下:
// Dep.target 用来存放目前正在使用的watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 入栈并将当前 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,
// 再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
// 出栈操作
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
那这里维持了一个watcher的栈,从上面看渲染watcher触发的get和计算属性触发的get实际是嵌套的,包括我们上面说的用户侦听器watcher的get和计算属性触发的get也都是嵌套的,所以这个watcher的栈和这个嵌套关系也是对应的,至于它做什么用我们来看一下。
上面说到计算属性被data的属性节点dep收集走了,如果在这里结束的话,那意味着没法渲染,因为对应的渲染watcher没有被属性节点dep收集走,而我们最上面说了函数Dep会收集属性节点dep,在这里我们可以认为watcher的dep收集了属性节点dep,那我们就可以拿到计算属性所有相关的属性节点dep,也就是知道它被哪些data的属性节点收集了,然后再把渲染watcher放到data相关的属性节点dep中,这样对应data的属性节点dep中就收集了,计算属性watcher和渲染watcher,这也是计算属性的get computedGetter做的事情。
收集watcher这个事情再上面这个例子中发生了两次,在get收集时我们应该收集对应的watcher,所以需要有一个函数调用和watcher栈的对应关系,而对应关系的维护就是通过pushTarget和popTarge做的。
到这里get的触发 观察者的收集基本就完毕了,然后值得更改就是触发了data属性得set函数,然后从dep中取出watcher,然后执行update,计算属性标记dirty为true缓存失效,然后其他的用户侦听器watcher和渲染watcher执行queueWatcher,也就是nextTick函数,nextTick就是异步更新了,异步更新我们后续专门分一篇来讲。
那异步更新中更新 用户侦听器watcher和渲染watcher时实际是执行的watcher的run函数,run函数内部也会执行上面所说的get函数,这会关注一个重点cleanupDeps函数,这个函数作用就是用来修正watcher的dep依赖项,并且修正属性节点dep上watcher观察者依赖,这一块和我们最开始自己思考实现是不一样的,我们来看下它的实现:
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
这里的this是指当前的watcher实例,它这里有一个deps一个newDeps,也就是说一个是旧的一个是新的,新的是刚执行完收集的,那新的一定是准确的,所以这里主要就是看看新的deps里是否包含旧的dep项,如果不包含证明新一轮函数里没有用到这个旧的属性节点项,所以我们需要把当前的watcher观察者从 这个属性节点dep中去掉,防止不必要的更新,然后再把newDeps变成当前的deps,然后newDeps重置为默认值一个空的set数据结构,其实中心思想和我们上面自己思考的类似。
到这里vue2响应式就结束了,下面我们来看vue3.
vue3响应式的设计
vue3的响应式解析主要是借助了 Vue设计与实现(霍春阳) 这本书,也比较推荐这本书,这里面作者对框架的设计思路有描述,这本书的作者也是vue的开发维护人员之一。
vue3响应式由defineProperty改为了proxy代理,但其实形成响应式的基本条件及思想是没有变化的,只是数据存储结构,及具体实现上有了变化。
我借用书里的一张图来表示,我们最上面说的观察者最终的存储是什么样子的,最终是存到哪里了。
从上面这个图可以看出构建数据结构的方式,我们分别使用了 WeakMap、Map 和 Set
- WeakMap 由 data --> Map 构成;
- Map 由 key --> Set 构成。
data其实就是我们proxy代理之前的原始数据,用WeakMap存储是因为,WeakMap对key是弱引用,一旦data没了其他引用,也就是说没其他地方用了,就会被释放掉,被垃圾回收器回收。
vue2用defineProperty递归循环属性,而在每个属性劫持的函数里会有dep,由此可见2的dep是存在函数闭包里的,2的dep存储节点类似一个树的结构,因为对象结构有点类似树,而3我们看到了data对应的是个map,前面说了每个属性节点都有一个Dep,所以map里的key是就是属性节点,而值是一个Set结构,这个值也就是我们的属性dep,从这一点我们能看到各个属性节点的dep存储实际被拍平了。
而图里的effectFn副作用函数,它是什么,我们做一个类比的话,它就好比我们vue2里的 watch用户侦听器和计算属性的回调函数、组件的render函数。
那接下来我们来模拟实现一个简易版的vue3的响应式,按我们最开始的思考,那我们要有几步:
- 需要对一个对象用proxy进行劫持,触发get时给在对应的map里key是属性,然后存到对应的Set数据结构Dep里,触发set劫持时给从Set数据结构里把所有回调拿出来执行。
- 需要定义一个effect函数来执行图里传入的effectFn函数,来触发我们劫持的get函数。
- 然后我们的effectFn副作用函数上应该有一个函数Dep,存储属性节点Dep也就是上图中的那个Set数据结构依赖集合,条件就是我们上面说的那样,Set依赖集合一旦存储了effectFn副作用函数,就需要把这个Set依赖集合反存储到函数Dep上,形成一个双向关联。
- 然后在执行副作用effectFn函数收集依赖之前,我们循环函数Dep中所有的属性节点Dep Set数据结构,然后把当前函数从属性节点Dep Set数据结构中清空,然后函数Dep清空为空数组(副作用effectFn函数执行完之后触发get劫持会重新收集准确的新的依赖)。
- 然后我们需要实现一个类似上面vue2里的targetStack的函数栈,用来属性节点Set收集依赖函数时保证,函数关系不会错乱,这个函数栈所面对的处理场景下面会讲。
- 最后是处理一些错误边界限制。
上面这几步做完了就相当于基础的响应式函数完成了,然后computed和watch函数都是在它的基础上做实现。
那下面我们来开始做具体实现:
const data = {
ok: true,
text: 'hello word',
num: 1
}
//桶
const bucket = new WeakMap()
// 当前副作用函数
let activeEffect
//副作用函数栈 存储的和函数执行栈对应 用来处理组件嵌套
const targetStack = []
//收集依赖的函数
function track(target, key) {
//不存在副作用函数 不是通过effectFn访问的 直接退出收集函数
if (!activeEffect) return
// 从WeakMap桶里取出 对应的弱引用的map
let depsMap = bucket.get(target)
if (!depsMap) {
// 首次不存在时 初始化 并且做添加操作
bucket.set(target, (depsMap = new Map()))
}
let effectsDep = depsMap.get(key)
if (!effectsDep) {
// 首次访问不存在时 初始化 并添加
depsMap.set(key, (effectsDep = new Set()))
}
// 收集副作用函数到dep
effectsDep.add(activeEffect)
// 函数dep反收集 属性节点dep
activeEffect.dep.push(effectsDep)
}
//触发更新的函数
function trigger(target, key) {
const depsMap = bucket.get(target)
if (depsMap) {
const effectsDep = depsMap.get(key)
//有可能设置未被收集的属性所以加一个判断空
const effectsToRun = new Set()
effectsDep &&
effectsDep.forEach((fn) => {
if (fn !== activeEffect) {
// 防止无限循环 例如副作用函数里 出现了 obj++ 相当与obj+=1
// 读取和设置操作是在同一个副作用函数内进行的。
// 此时无论是 track 时收集的副作用函数,还是 trigger 时要触发执行的副作用函数,都是activeEffect
// 所以找出不是这个的执行
effectsToRun.add(fn)
}
})
effectsToRun.forEach((effectFn) => effectFn()) //循环依赖函数执行
}
}
const obj = new Proxy(data, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, receiver) {
Reflect.set(target, key, receiver)
trigger(target, key)
return true
}
})
// 清空函数dep里的属性节点dep 依赖等
function cleanup(fn) {
// 循环相关的属性节点dep
fn.dep.forEach((dep) => {
dep.delete(fn) //属性节点dep删除对应的副作用
})
// 清空函数dep方便重新收集
fn.dep = []
}
function effect(fn) {
//副作用函数 我们自己再包装一层
function effectFn() {
cleanup(effectFn)
//做个副作用函数栈处理 保证函数堆栈和它是对应的 get收集时保证不会收集错 解决组件嵌套
targetStack.push(effectFn)
activeEffect = effectFn
fn()
targetStack.pop()
activeEffect = targetStack[targetStack.length - 1]
}
// 函数dep
effectFn.dep = []
effectFn()
}
effect(() => {
console.log(obj.ok ? obj.text : 'not')
})
obj.ok = false
obj.text = 'sdasdas'
这里按上面写的实现了一个基础的响应式,上面targetStack栈相关的是为了处理组件嵌套,收集的activeEffect不正确,如下
effect(() => {
console.log(obj.text)
effect(() => {
console.log(obj.ok)
})
})
然后我们在这个基础上实现一下computed和watcher:
分析computed函数的需求并实现
- 计算属性是我们用的时候,才触发那个传入的副作用函数,也就是说它是懒加载的,不是默认一上来就执行的,而我们用的时候触发这个副作用函数,证明我们需要拿到它(副作用函数)。
- 我们需要获取到计算属性传入的那个函数的返回值。
- 计算属性需要有缓存,computed函数内部需要有个属性标记缓存是否生效。
- 计算属性传入的副作用函数里,所使用的响应式属性变更时, 我们的副作用函数不执行(理由参考1),需要把缓存属性标记为失效。
然后我们依次来看需要对我们最开始的响应式做哪些更改:
- effect函数支持传入第二个参数,传一个配置options对象过去,例如里面有个lazy属性,在有lazy属性时,默认不一上来就执行。
- effect函数需要把副作用函数effectFn返回出去,让外面决定什么时候调用。
- 副作用函数把传入的那个函数额返回值return出来。
- computed函数内部需要有个dirty属性来标记缓存是不是有效。
- 而我们实现上面computed需求4,需要改下trigger函数,其实需求4的本质就是需要把副作用函数的执行时机交出来,在外面确定是否执行要怎么执行,而不是在trigger里直接执行,那我们在options对象再多传一个函数进去,scheduler属性,它是一个函数,我们把options挂到effectF上,然后在trigger中effectsDep里的副作用函数依次执行时,判断副作用函数上是否有scheduler,有的话调用它并且把副作用函数传出去,scheduler(effectFn),这样就把控制权交出去了。
代码实现:
//触发更新的函数
function trigger(target, key) {
const depsMap = bucket.get(target)
if (depsMap) {
const effectsDep = depsMap.get(key)
//有可能设置未被收集的属性所以加一个判断空
const effectsToRun = new Set()
effectsDep &&
effectsDep.forEach((fn) => {
if (fn !== activeEffect) {
// 防止无限循环 例如副作用函数里 出现了 obj++ 相当与obj+=1
// 读取和设置操作是在同一个副作用函数内进行的。
// 此时无论是 track 时收集的副作用函数,还是 trigger 时要触发执行的副作用函数,都是activeEffect
// 所以找出不是这个的执行
effectsToRun.add(fn)
}
})
effectsToRun.forEach((effectFn) => {
if (effectFn.options.scheduler) { // 如果由外部调度器的话 把调度权交出去
effectFn.options.scheduler()
} else {
effectFn()
}
}) //循环依赖函数执行
}
}
function effect(fn, options = {}) {
//副作用函数 我们自己再包装一层
function effectFn() {
cleanup(effectFn)
//做个副作用函数栈处理 保证函数堆栈和它是对应的 get收集时保证不会收集错 解决组件嵌套
targetStack.push(effectFn)
activeEffect = effectFn
// 那下返回值 计算属性时需要用
const res = fn()
targetStack.pop()
activeEffect = targetStack[targetStack.length - 1]
return res
}
// 函数dep
effectFn.dep = []
// 配置存起来 方便trigger函数用
effectFn.options = options
//没有lazy时再执行
if (!options.lazy) {
effectFn()
}
// 返回副作用函数 lazy为true时 由外界决定什么时候执行
return effectFn
}
function computed(getter) {
// 默认计算属性的值时脏的
let dirty = true
// 形成一个响应式 懒加载的
const effectFn = effect(getter, {
lazy: true,
scheduler(fn) { // 传入调度器 让这个响应式把调度时机交出来
dirty = true //标记缓存失效
// 虽然这里把副作用函数fn传了出来 但是我们的副作用函数 是在实际使用的时候调用的 下面 所以这里是不需要调用的
// 只需要把缓存设置为失效 让下面能调用就可以
}
})
// 缓存值
let value
const obj = {
get value() {
if (dirty) { //缓存脏了 需要更新
value = effectFn() // 在实际用的时候再 执行副作用 重新获取值
dirty = false //标记缓存生效
}
// dirty为false 时 证明缓存是有效的 不需要更新
return value
}
}
return obj
}
const test = computed(() => obj.text)
实现计算属性我们改了这三个函数的实现,现在还有一点小问题,就是如果计算属性被其他副作用函数引用了,例如:
const test = computed(() => obj.text)
effect(() => {
console.log(test.value)
})
我们看上面的computed函数实现,返回的obj的get函数,实际是并没有调用track来收集 当前副作用函数作为依赖的(上例test没有收集effect的参数函数),所以在计算属性的scheduler被调用时(上面例子text变更时),也就没法调用它更新,要解决也很简单,我们在obj触发get时,手动用track函数来收集一下当前的副作用函数作为依赖(上例手动收集effect的参数函数,默认调用effect时,会activeEffect = effectFn,然后track会收集activeEffect),然后scheduler中手动用trigger触发,所以代码如下:
function computed(getter) {
// 默认计算属性的值时脏的
let dirty = true
// 形成一个响应式 懒加载的
const effectFn = effect(getter, {
lazy: true,
scheduler(fn) { // 传入调度器 让这个响应式把调度时机交出来
dirty = true //标记缓存失效
// 虽然这里把副作用函数fn传了出来 但是我们的副作用函数 是在实际使用的时候调用的 下面 所以这里是不需要调用的
// 只需要把缓存设置为失效 让下面能调用就可以
trigger(obj, 'value')
}
})
// 缓存值
let value
const obj = {
get value() {
if (dirty) { //缓存脏了 需要更新
value = effectFn() // 在实际用的时候再 执行副作用 重新获取值
dirty = false //标记缓存生效
}
track(obj, 'value')
// dirty为false 时 证明缓存是有效的 不需要更新
return value
}
}
return obj
}
这样计算属性就实现了。
分析watch函数的需求并实现
- watch函数有三个参数,第一个参数source可以是一个函数,也可以是一个响应式proxy对象,所以我们实现它时需要区分一下,如果要是函数那就直接给effect函数传递,如果是对象那我们需要定义一个函数,在这个函数里递归去获取所有属性触发这个对象的get让它收集依赖。
- 第二个参数cb是一个函数,cb函数是我们指定的source对应的响应式变更后,需要执行参数二cb,所以我们watch里的effect也需要有个scheduler把调用时机交出来,然后scheduler中调用cb。
- cb中接受新值和旧值,所以内部需要定义俩变量来存储,然后effect的lazy也是true,因为cb调用需要获取副作用函数的值。
- 第四个参数是个options配置,immediate来决定cb是否默认执行。
- 不管immediate是true还是false,初始都需要执行一下副作用函数,来让属性节点把副作用函数收集走
代码实现:
function traverse(source, seen = new Set()) {
//因为是递归循环对象 所以不是对象 或者不存在 或者已经递归过的 情况都返回
if (typeof source !== 'object' || source === null || seen.has(source)) return
for (const key in source) {
seen.add(source[key])
traverse(source[key], seen)
}
// 最后返回一下 因为watch副作用函数需要 调用副作用函数获取新旧值 source
return source
}
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
//函数的话直接用
getter = source
} else {
//对象的话 递归循环 触发get
getter = () => traverse(source)
}
// watch回调的新旧值
let oldValue, newValue
function runCb() {
//执行回调
newValue = effectFn()
cb(oldValue, newValue)
oldValue = newValue
}
// 创建延迟的effect
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
//拿出控制权 在监听的属性变化后 执行cb
runCb()
}
})
// 默认是否执行
if (options.immediate) {
runCb()
} else {
// 默认需要执行一下effectFn 让属性节点把effectFn副作用函数收集走 上面runCb中会调用effectFn
oldValue = effectFn()
}
}
watch(() => obj.ok, (oldValue, newValue) => {
console.log(oldValue, newValue)
})
我们的watch按上面的分析做出来是这样的。
到这里我们实现的是最简易版本的响应式,代理里只有get和set,例如删除属性劫持的deleteProperty和其他结构例如map和set、数组等等 后续的其他属性劫持就不在这里展开说了,我们这里过的是基础的响应式逻辑。
上面我们proxy代理是固定的一个对象,现在我们这里再实现一下ref和reactive:
function reactive(data) {
return new Proxy(data, {
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver) // 拿到当前值 因为代理只能代理当前这一层
//判断取出来的是不是对象 如果是的话用reactive 再包一层 把他里面的也劫持上 返回
//为了了处理obj.xx.xx这种情况
if (res && typeof res === 'object') {
return reactive(res)
}
return res
},
set(target, key, receiver) {
const res = Reflect.set(target, key, receiver)
trigger(target, key)
return res
}
})
}
function ref(val) {
const wrapper = {
value: val
}
Object.defineProperty(wrapper, '_v_isef', { //加个标记区分是ref还是reactive 不然分不出来了
value: true
})
return reactive(wrapper)
}
const datas = ref({ ll: '111' })
watch(() => datas.value.ll, (oldValue, newValue) => {
console.log(oldValue, newValue)
})
然后我们我们结合最简单的renderer使用就是这样的,我把完整代码贴一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
//桶
const bucket = new WeakMap()
// 当前副作用函数
let activeEffect
//副作用函数栈 存储的和函数执行栈对应 用来处理组件嵌套
const targetStack = []
//收集依赖的函数
function track(target, key) {
//不存在副作用函数 不是通过effectFn访问的 直接退出收集函数
if (!activeEffect) return
// 从WeakMap桶里取出 对应的弱引用的map
let depsMap = bucket.get(target)
if (!depsMap) {
// 首次不存在时 初始化 并且做添加操作
bucket.set(target, (depsMap = new Map()))
}
let effectsDep = depsMap.get(key)
if (!effectsDep) {
// 首次访问不存在时 初始化 并添加
depsMap.set(key, (effectsDep = new Set()))
}
// 收集副作用函数到dep
effectsDep.add(activeEffect)
// 函数dep反收集 属性节点dep
activeEffect.dep.push(effectsDep)
}
//触发更新的函数
function trigger(target, key) {
const depsMap = bucket.get(target)
if (depsMap) {
const effectsDep = depsMap.get(key)
//有可能设置未被收集的属性所以加一个判断空
const effectsToRun = new Set()
effectsDep &&
effectsDep.forEach((fn) => {
if (fn !== activeEffect) {
// 防止无限循环 例如副作用函数里 出现了 obj++ 相当与obj+=1
// 读取和设置操作是在同一个副作用函数内进行的。
// 此时无论是 track 时收集的副作用函数,还是 trigger 时要触发执行的副作用函数,都是activeEffect
// 所以找出不是这个的执行
effectsToRun.add(fn)
}
})
effectsToRun.forEach((effectFn) => {
if (effectFn.options.scheduler) { // 如果由外部调度器的话 把调度权交出去
effectFn.options.scheduler()
} else {
effectFn()
}
}) //循环依赖函数执行
}
}
function reactive(data) {
return new Proxy(data, {
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver) // 拿到当前值 因为代理只能代理当前这一层
//判断取出来的是不是对象 如果是的话用reactive 再包一层 把他里面的也劫持上 返回
//为了了处理obj.xx.xx这种情况
if (res && typeof res === 'object') {
return reactive(res)
}
return res
},
set(target, key, receiver) {
const res = Reflect.set(target, key, receiver)
trigger(target, key)
return res
}
})
}
function ref(val) {
const wrapper = {
value: val
}
Object.defineProperty(wrapper, '_v_isef', { //加个标记区分是ref还是reactive 不然分不出来了
value: true
})
return reactive(wrapper)
}
// 清空函数dep里的属性节点dep 依赖等
function cleanup(fn) {
// 循环相关的属性节点dep
fn.dep.forEach((dep) => {
dep.delete(fn) //属性节点dep删除对应的副作用
})
// 清空函数dep方便重新收集
fn.dep = []
}
function effect(fn, options = {}) {
//副作用函数 我们自己再包装一层
function effectFn() {
cleanup(effectFn)
//做个副作用函数栈处理 保证函数堆栈和它是对应的 get收集时保证不会收集错 解决组件嵌套
targetStack.push(effectFn)
activeEffect = effectFn
// 那下返回值 计算属性时需要用
const res = fn()
targetStack.pop()
activeEffect = targetStack[targetStack.length - 1]
return res
}
// 函数dep
effectFn.dep = []
// 配置存起来 方便trigger函数用
effectFn.options = options
//没有lazy时再执行
if (!options.lazy) {
effectFn()
}
// 返回副作用函数 lazy为true时 由外界决定什么时候执行
return effectFn
}
// effect(() => {
// console.log(obj.ok ? obj.text : 'not')
// })
function computed(getter) {
// 默认计算属性的值时脏的
let dirty = true
// 形成一个响应式 懒加载的
const effectFn = effect(getter, {
lazy: true,
scheduler(fn) { // 传入调度器 让这个响应式把调度时机交出来
dirty = true //标记缓存失效
// 虽然这里把副作用函数fn传了出来 但是我们的副作用函数 是在实际使用的时候调用的 下面 所以这里是不需要调用的
// 只需要把缓存设置为失效 让下面能调用就可以
trigger(obj, 'value')
}
})
// 缓存值
let value
const obj = {
get value() {
if (dirty) { //缓存脏了 需要更新
value = effectFn() // 在实际用的时候再 执行副作用 重新获取值
dirty = false //标记缓存生效
}
track(obj, 'value')
// dirty为false 时 证明缓存是有效的 不需要更新
return value
}
}
return obj
}
function traverse(source, seen = new Set()) {
//因为是递归循环对象 所以不是对象 或者不存在 或者已经递归过的 情况都返回
if (typeof source !== 'object' || source === null || seen.has(source)) return
for (const key in source) {
seen.add(source[key])
traverse(source[key], seen)
}
// 最后返回一下 因为watch副作用函数需要 调用副作用函数获取新旧值 source
return source
}
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
//函数的话直接用
getter = source
} else {
//对象的话 递归循环 触发get
getter = () => traverse(source)
}
// watch回调的新旧值
let oldValue, newValue
function runCb() {
//执行回调
newValue = effectFn()
cb(oldValue, newValue)
oldValue = newValue
}
// 创建延迟的effect
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
//拿出控制权 在监听的属性变化后 执行cb
runCb()
}
})
// 默认是否执行
if (options.immediate) {
runCb()
} else {
// 默认需要执行一下effectFn 让属性节点把effectFn副作用函数收集走 上面runCb中会调用effectFn
oldValue = effectFn()
}
}
function renderer(domString, container) {
container.innerHTML = domString
}
const data = ref('你好啊')
effect(() => {
renderer(`<h1>Hello ${data.value}</h1>`, document.getElementById('app'))
})
</script>
</body>
</html>
到这里vue2和3的响应式对比就完结了,其实主题设计思想还是我们最开始的思考,变化的是语法糖,如果看过的同学有不同的看法,可以留言讨论。