计算属性与 lazy
当我们不希望effect中的副作用函数立即执行时,我们可以在options添加一个变量来让用户控制它执行与否
//我们希望通过
effect(()=>{
console.log('哈哈哈')
},{
lazy:true
})
//我们就要在 effect 函数中修改 effectFn
function effect(fn,options = {}){
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.options = options
effectFn.deps = []
//只有当lazy为false的时候执行
if(!options.lazy){
effectFn()
}
// 否则,不执行。将当前这个副作用函数返回
return effectFn
}
但仅仅时这样的话,意义不是很大,如果我们将传递给effect的函数作为一个getter,这个getter可以返回任何值。这就要求我们在手动调用副作用函数时,就能够拿到其返回值,但我们目前的effectFn是没有返回值的(undefined)
const effectFn = ()=>{
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn() //新增
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res // 新增
}
//当lazy为true时,effect返回值就为 ()=>obj.foo + obj.bar 。与effectFn对应
const effectFn = effect(()=>obj.foo + obj.bar,{lazy:true})
// 执行effectFn ,就能拿到这个 getter 的返回值
const value = effectFn()
此时,我们的计算属性就能够大致实现了
function computed(getter){
//当lazy为true时,此时的effectFn就是getter 。将getter作为副作用函数注册
const effectFn = effect(getter,{
lazy:true
})
const obj = {
//当读取 obj 中的值的时候,在调用副作用函数
get value(){
return effectFn()
}
}
return obj
}
但此时只做到了懒计算,也就是当读取对象值时才触发。没有缓存,多次访问,副作用函数仍然会执行
function computed(getter){
//用来缓存上一次的值
let value
// 用来标识是否要重新计算值,为true意味着 '脏',需要重新计算
let dirty = true
//当lazy为true时,此时的effectFn就是getter 。将getter作为副作用函数注册
const effectFn = effect(getter,{
lazy:true,
// 当 obj中有值发生变化时,将dirty 脏值设置为true即可。
scheduler(){
dirty = true
}
})
const obj = {
//当读取 obj 中的值的时候,在调用副作用函数
get value(){
if(dirty){
//当值为脏的时候,就需要重新计算
value = effectFn()
// 计算完后,将 dirty为false
dirty = false
}else{
return value
}
}
}
return obj
}
const sum = computed(()=>obj.foo + obj.bar)
console.log(sum.value)
obj.foo++
console.log(sum.value)
目前,还有个缺陷
//当我们在另外一个effect中读取计算属性的值时
const sum = computed(()=>obj.foo + obj.bar)
effect(()=>{
//在该副作用函数中读取sum.value
console.log(sum.value)
})
// 修改obj.foo
obj.foo++
//我们希望修改obj.foo,副作用函数就重新执行。但是我们会发现修改obj.foo的值并不会触发副作用函数的执行
本质上这就是一个effect的嵌套,每个计算属性都有自己的effect,并且时懒执行的,当加载的时候再执行。对于计算属性的getter来说,它里面的只会把自己内部的effect收集为依赖集合,当把计算属性用于另外一个effect,就会发生effect嵌套,计算属性内部的effect不会被外层中的收集为依赖集合
//当读取计算属性的值的时候,我们手动调用track追踪,当依赖的响应式数据发生变化的时候,手动调用trigger函数触发响应
function computed(getter){
//用来缓存上一次的值
let value
// 用来标识是否要重新计算值,为true意味着 '脏',需要重新计算
let dirty = true
//当lazy为true时,此时的effectFn就是getter 。将getter作为副作用函数注册
const effectFn = effect(getter,{
lazy:true,
// 当 obj中有值发生变化时,
// 将dirty 脏值设置为true即可。 手动调用trigger 触发执行副作用函数
scheduler(){
dirty = true
trigger(obj,'value')
}
})
const obj = {
//当读取 obj 中的值的时候,在调用副作用函数
get value(){
if(dirty){
//当值为脏的时候,就需要重新计算
value = effectFn()
// 计算完后,将 dirty为false
dirty = false
}else{
//读取的时候,调用track追踪
track(obj,'value')
return value
}
}
}
return obj
}
watch实现
watch本质就是观测一个响应式数据,当数据发生变化时通知并执行响应的回调函数
我们可以很简单的利用,effect和scheduler来实现一个简单的watch
function watch(source,cb){
//触发读取操作,建立联系
effect(()=>souce.foo,{
scheduler(){
//当数据发生变化的时候,执行cb
cb()
}
})
}
const data = {foo:1}
const obj = new Proxy(data,{/*...*/})
watch(obj,()=>{
console.log('数据变化了')
})
obj.foo++
// 目前只能观测obj.foo的改变。所以我们还需要继续封装一个递归操作
function watch(source,cb){
effect(()=>traverse(source),{
scheduler(){
cb()
}
})
}
function traverse(value,seen = new Set){
if(typeof value != 'object' || value == null || seen.has(value)) return
seen.add(value)
for(const i in value){
traverse(value[i],seen)
}
return value
}
//上述 traverse函数,递归的对传入的对象身上的属性进行了读取操作,从而当属性发生变化时触发回调函数执行。
利用lazy属性,获取watch中的newValue与oldValue
function watch(source,cb){
//这里的source也不一定是对象,也可以是getter函数
let getter
if(typeof source == 'function'){
getter = source
}else{
getter = ()=>traverse(source)
}
let newValue,oldValue
const effectFn = effect(()=>getter(),{
lazy:true,
scheduler(){
//在scheduler中执行的是新值,因为这个函数之所以调用是因为值发生了改变。所以此处是新值
newValue = effectFn()
//将新值与旧值作为参数传递出去
cb(newValue,oldValue)
//更新旧值,不然下一次的旧值不对
oldValue = newValue
}
})
//手动调用副作用函数,拿到值为旧值
oldValue = effectFn()
}
默认我们的watch第一次不执行,如果想要第一次执行的话,需要设置immediate:true
watch(obj,()=>{
console.log('变化了')
},{
immediate:true
})
//
function watch(source,cb,options){
//这里的source也不一定是对象,也可以是getter函数
let getter
if(typeof source == 'function'){
getter = source
}else{
getter = ()=>traverse(source)
}
let newValue,oldValue
const job = ()=>{
//在scheduler中执行的是新值,因为这个函数之所以调用是因为值发生了改变。所以此处是新值
newValue = effectFn()
//将新值与旧值作为参数传递出去
cb(newValue,oldValue)
//更新旧值,不然下一次的旧值不对
oldValue = newValue
}
const effectFn = effect(()=>getter(),{
lazy:true,
scheduler:job
})
if(options.immediate){
job()
}else{
//手动调用副作用函数,拿到值为旧值
oldValue = effectFn()
}
}