前端面试-vue(系列二)计算属性computed的实现

88 阅读4分钟

本文只针对vue3的设计与实现进行展开。后续称vue

computed的实现

调度执行

在讲解计算属性computed之前,我们需要先了解一下调度执行的概念,其实很简单,就是有能力决定副作用函数的执行时机,次数,方式。

比如下面代码:

const data = {obj: 1}

const proxy = new Proxy(data,{...}) // 响应式拦截,当proxy.obj改变的时候,触发effect函数

effect(()=> {
 console.log(proxy.obj)
})

proxy.obj++

console.log(‘end’)

// 打印结果

// 1
// 2
// end

如果我们希望打印的顺序调整为1、end、2,这个时候,就需要进行调度,这个时候就需要针对effect函数增加新的选项参数options,如下:

effect(()=> {
  console.log('log')
}, {
  scheduler(fn) {
      ...
  }
})

options是一个对象,其中允许指定的scheduler调度函数,同时我们需要在effect函数内部,把options选项挂载到对应的副作用函数

effect(fn, options={}) {
    activeEffect = fn
    activeEffect.options = options
    fn()
}

有了调度函数,我们就可以把控制权交给用户。


....
set(target,key,value) {
    target[key] = value
    let despMap = bucket.get(target)
    if(!despMap) return
    let effects = despMap.get(key)
    effects && effects.forEach(fn => {
        if(fn.options.scheduler) {
           fn.options.scheduler(fn)
        } else {
            fn()
        }

    })
    return true
}

有了上面的实现后,我们就可以实现最初的需求了

const data = {obj: 1}

const proxy = new Proxy(data,{...})

effect(()=> {
 console.log(proxy.obj)
}, {
    scheduler(fn) => {
        setTimeOut(fn)
    }
})

proxy.obj++

console.log(‘end’)

// 打印结果

// 1
// end
// 2

以上我们通过调度器,实现了对副作用函数的顺序的控制,此外,我们可以进一步考虑如何实现对执行次数的控制。

const data = {obj: 1}
const proxy = new Proxy(data,{...})
effect(()=> {
    console.log(proxy.obj)
})

proxy.obj++
proxy.obj++

// 打印结果
// 1
// 2
// 3

如果我们希望打印的结果是两次,只有1、3呢,去掉中间2的打印,改如何实现?只关注结果。


const data = {obj: 1}
const proxy = new Proxy(data,{...})
effect(()=> {
    console.log(proxy.obj)
})

// 定义一个任务队列, set数据结构有去重能力。
const jobQueue = new Set();

// 使用Promise.resolve()创建一个promise实例,用他把任务加到微任务里面
const p = Promise.resolve()

// 一个标志,代表是否正在刷新队列
let isFlushing = false


function flushJob() {
    // 如果队列正在刷新,则什么都不做。
    if(isFlushing){
        return
    }
    isFlushing = true
    p.then(() =>{
        jobQueue.forEach(job => {
            job()
        });
    }).finally(() => {
        isFlushing = false
    })
}

effect(() => {
    console.log(proxy.obj);
}, {
    scheduler(fn) {
        jobQueue.add(fn)
        flushJob()
    }
})

proxy.obj++
proxy.obj++

当proxy.obj++执行两次的时候,就会执行两次调度函数,也就是jobQueue.add(fn)会执行两次,但是由于set的去重能力,jobQueue只含有一项,然后调用flushJob函数,flushJob函数也会连续执行两次,但是因为有isFlushing的存在,第二次执行的时候,isFlushing为true。所以就直接返回了。

有因为微任务的执行时间,是在proxy.obj是3以后了,所以就满足我们最初的期望了。

Lazy

我们目前的给副作用函数传递的函数,都会立即执行

effect(
   // 这个函数会立即执行
    ()=> {
        console.log('123')
    }
)

如果我们希望是在需要他的时候,才执行,比如计算属性,这个时候,我们可以在options增加lazy属性达到目的。

effect(
    // 这个函数会立即执行
    ()=> {
        console.log('123')
    },
    {
        lazy: true
    }
)

通过lazy属性的传递,当为ture的时候,则不立即执行副作用函数,参考下面代码:

function effect(fn, options={}){
    const effectFn = fn
    effectFn.options = options
    if(!effectFn.options.lazy) {
        effectFn() // lazy:false的时候,立即执行函数
    }
    return effectFn // 将需要执行的函数返回
}

const effectFn = effect(() => {
    console.log('123')
}, {
    lazy: true
})

// 手动的执行副作用函数
effectFn()

此时我们能够实现手动执行副作用函数了,但是为了实现computed的效果,我们需要把传递effect的函数看作一个getter。那么这个getter就可以返回任何值。

const effectFn = effect(
// getter返回obj.bar+obj.foo
() => {
   return obj.bar+obj.foo
}, {
    lazy: true
})

此时对effect函数再次做修改

function effect(fn, options={}){
    const effectFn = () => {
        const res = fn() // 将res作为effectFn函数的返回值
        return res
    }
    effectFn.options = options
    if(!effectFn.options.lazy) {
        effectFn() // lazy:false的时候,立即执行函数
    }
    return effectFn // 将需要执行的函数返回
}

const effectFn = effect(() => {
    return obj.bar+obj.foo
}, {
    lazy: true
})

const value = effectFn() // 就可以拿到副作用函数执行结果的目标值了。

computed

有了上面的基础,我们就可以去实现计算属性了,如下所示:

function computed(getter) {
    const effectFn = effect(getter, {lazy: true})
    
    const obj = {
        get value() {
            return effectFn()
        }
    }
    return obj
}

我们定义一个computed函数,他接受一个getter函数作为参数,用它创建一个lazyeffect, computed函数返回一个对象,当访问对象的时候,就会执行effectFn,并把执行结果返回。

const data = {foo: 1, bar: 2}

const proxy = new Proxy(data, {...})

const sum = computed(()=> proxy.foo + proxy.bar)

console.log(sum.value) // 3

但此时我们去访问sum.value的时候,函数都会重新执行,但是结果都是3,所以可以做一下缓存优化

function computed(getter) {
    let value
    let dirty = true  // 通过dirty来控制返回结果是否需要重新计算。
    const effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            dirty = true // 调度器函数,当依赖的属性改变的时候,就会执行调度器scheduler
        }
    })
    
    const obj = {
        get value() {
            if(dirty) {
                value = effectFn()
                dirty = false
             }
            return value
        }
    }
    return obj
}

以上就是一个较为完善computed了。