网络日志

vue3简易实现——响应式原理

1.前言

之前听有人吐槽,说面试让实现一个简易vue3。咱们先不说这题离不离谱,简单分析下,如果遇到了该怎么思考。首先vue分为以下几个部分

  • 响应系统
  • 渲染器(mount,patch,domdiff)
  • 组件化
  • 编译器

编译器不可能写出来组件化代码比较多 涉及vnode 而且不是必不可少的渲染器可以用innerhtml简化代替因此还是考响应式原理。

2.简单实现

让我们40行代码实现一个超级简化的vuedemo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
let activeEffect = undefined  
const map = new WeakMap()      
const effect = (fn)=>{
    activeEffect = fn
    fn()
    activeEffect = undefined
}
const track = (t,k)=>{
    // activeEffect 入set
    if(!activeEffect) return //避免执行fn的时候又重复track(只在执行effect时搜集)
    if(!map.has(t)){
        map.set(t,new Map())
    }
    if(!map.get(t).has(k)){
        map.get(t).set(k,new Set())
    }
    const deps = map.get(t).get(k)
    deps.add(activeEffect)
}
const trigger = (t,k)=>{
    // 取对应的effect 执行
    if(map.get(t)){
        if(map.get(t).get(k)){
            let deps = map.get(t).get(k)
            deps.forEach(fn => {
                fn()
            });
        }
    }
}
const reactive = (t)=>{
    return new Proxy(t,{
        get(t,k){
            track(t,k)
            return t[k]
        },
        set(t,k,v){
            t[k] = v
            trigger(t,k)
            console.log('属性变化了')
        },
    })
}
const obj = reactive({name:'fyy'})
effect(()=>{
    document.body.innerHTML = `${obj.name}` //在effect里面执行渲染逻辑,从而利用响应式,数据更新->视图更新
    console.log('render')
})
setTimeout(()=>{
    obj.name = 'fyy123'
},1000)


    </script>
</body>
</html>

2.1 proxy

vue3采用proxy方式代理一个对象,相较vue2的defineproperty有以下几个好处,

  • 不用遍历每个属性
  • 被动劫持
  • Proxy提供了13种劫持捕捉操作,可以更加精细化劫持捕捉操作

核心思路还是劫持 get和set get进行搜集(track),set进行触发(trigger)

new Proxy(t,{
        get(t,k){
            track(t,k)
            return t[k]
        },
        set(t,k,v){
            t[k] = v
            trigger(t,k)
            console.log('属性变化了')
        },
    })

2.2 effect

effect 副作用函数,当数据变化的时候effect里面的函数会自动执行

const obj = {text: 'hello'}
const render = ()=> document.body.innerHTML = `${obj.text}`
effect(()=>{
    render()
})

现在想做的就是让obj变化的时候,effect里面的函数会立刻执行

我们可以

  • proxy劫持obj
  • get obj.text时候把fn(其实就是render函数)放到某个地方
  • set obj.text的时候 把这个地方的fn再拎出来执行

所以执行effect的时候 一方面要执行里面的fn函数,一方面要用个全局变量去保存

const effect = (fn)=>{
    activeEffect = fn
    fn()
    activeEffect = undefined
}

2.3 weakmap-map-set的数据结构

我们用于保存fn的地方实际上是一个weakmap-map-set的数据结构

weakmap       map        set
    obj       
             text属性         [fn1,fn2....]

2.4 reactive

对一个对象做响应式处理,可以封装一个reactive方法

const reactive = (t)=>{
    return new Proxy(t,{
        get:xxx,
        set: xxx
    })
}

如果对象的属性还是一个对象,我们想深响应,可以在get里面递归调用,当然浅响应则不用递归

 get(t,k){
            track(t,k)
            return reactive(t[k])
        },

至此,基本一个简易的响应式vue就实现了,面试这么写应该没啥问题。

3 ref

如果某个值是普通对象,我们是没法用proxy的我们当然可以把这个值挂着对象的某个属性上,但是这个属性名不同的人可能会定义成不同的,造成不统一所以vue帮我们定义个一个只能取value值的响应式对象

    function ref(val){
        const wrapper = {
            value: val
        }
        return reactive(wrapper)
    }

4.computed

computed有两个特性 一个是懒,不调用不计算一个是有缓存,依赖不变动不计算

4.1 实现lazy

先实现lazy特性。effect里面的函数可以选择是否直接执行,所以需要改一下,返回一个执行器effctfn。

const effect = (fn,options={})=>{
    let effectFn = ()=>{
        activeEffect = fn
        const res = fn()
        activeEffect = undefined
        return res
    }
    if(!options.lazy) effectFn()
    return effectFn
}

computed接受一个getter 返回一个obj,被调用value的时候会不断执行effect方法返回的执行器,也就是不断调用getter方法完成计算。

const computed = (getter)=>{
    const effectfn = effect(getter,{lazy:true}) //computed是一个lazy effect
    const obj = {
        get value (){
            return effectfn()  //当调用这个commputed的值的时候才执行getter(进行依赖搜集)
        }
    }
    return obj
}

4.2 实现缓存

上面的方法不完美的地方也就是不断调用computed.value 会不断的去调getter方法计算我们实际上可以只完成一次计算后将值储成_value如果geter的依赖不变, 我们就一直返回_value,不重新计算如果geter的依赖变了, 再调用computed.value的时候,我们就进行计算我们用computed实例中的一个变量_dirty来标识它的依赖是否变化,也即是它需不需要计算

那么关键问题来了,依赖变化了,怎么让_dirty改变呢依赖变化了-->执行trigger-->执行依赖搜集的effectfn这里可以增加一个调度器,去控制如何执行effectfn,比如同步异步,额外的操作等等

//effect的option增加scheduler选项
effect(fn,{
    lazy: true
    scheduler: ()=>{xxx}
})
//修改trigger
const trigger = (t,k)=>{
               //...其余代码省略
             //如果effectfn有scheduler就执行scheduler
            deps.forEach(effectfn => {
                effectfn.options.scheduler?effectfn.options.scheduler():effectfn()
            });
 
}

4.3 完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
let activeEffect = undefined  
const map = new WeakMap()      
const effect = (fn,options={})=>{
    let effectFn = ()=>{
        activeEffect = effectFn //这是最终trigger要要执行的函数,给它挂点东西
        effectFn.options = options
        const res = fn()
        activeEffect = undefined
        return res
    }
    if(!options.lazy) effectFn()
    return effectFn
}
const track = (t,k)=>{
    if(!activeEffect) return
    if(!map.has(t)){
        map.set(t,new Map())
    }
    if(!map.get(t).has(k)){
        map.get(t).set(k,new Set())
    }
    const deps = map.get(t).get(k)
    deps.add(activeEffect)
}
const trigger = (t,k)=>{
    console.log('trigger',t,k)
    if(map.get(t)){
        if(map.get(t).get(k)){
            let deps = map.get(t).get(k)
            deps.forEach(effectfn => {
                effectfn.options.scheduler?effectfn.options.scheduler():effectfn()
            });
        }
    }
}
const reactive = (t)=>{
    return new Proxy(t,{
        get(t,k){
            track(t,k)   
            return t[k]
        },
        set(t,k,v){
            t[k] = v
            trigger(t,k)
        },
    })
}

//-----computed(带缓存)实现--------
const computed = (getter)=>{
    //应当添加一个变量去看是否有变动
    let _value
    let _dirty = true //关键是这个_dirty怎么和trigger联系上,添加一个scheduler调度器,决定如何以及怎么执行effectfn

    const effectfn = effect(getter,{
        lazy:true,
        scheduler(){
            _dirty = true //只改dirty不计算了
        } 
    })
    const obj = {
        get value (){
            let res 
            if(_dirty){ //有缓存取缓存,没有则重新计算
                res = effectfn()
                _value = res
                _dirty = false
            }else{
                res = _value
            }   
            return res
        }
    }
    return obj
}
const obj = reactive({a:1,b:2})
const sum = computed(()=>{console.log('执行了compute里的getter');return obj.a+obj.b})
//--------

console.log('此时sum : ' + sum.value)
obj.a = 2
console.log('此时sum : ' + sum.value)
console.log('此时sum : ' + sum.value)
console.log('此时sum : ' + sum.value)
    </script>
</body>
</html>

5.watcher

有了effect调度器的概念实现watcher就很简单了

const watcher = (source,cb)=>{
    effect(source,{
        scheduler(){
        cb()
    }})
}

6.总结

先简单实现了一下vue3的响应式原理,利用effect置顶effectfn,proxy get->track->搜集置顶的effectfnproxy set->trigger->执行搜集的对应的effectfn搜集的数据结构是weakmap-map-set结构再介绍了一下commputed, lazy原理以及缓存原理以及effect调度器原理利用调度器很简单的封装了一个wathcher