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}})//批改triggerconst 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 = 2console.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