乐趣区

关于vue3: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-> 收集置顶的 effectfn
proxy set->trigger-> 执行收集的对应的 effectfn
收集的数据结构是 weakmap-map-set 构造
再介绍了一下 commputed,lazy 原理以及缓存原理以及 effect 调度器原理
利用调度器很简略的封装了一个 wathcher

退出移动版