1 Introduction

I heard someone complain before, saying that the interview allows the realization of a simple vue3.
Let's not say that this question is outrageous, just analyze it briefly, and think about it if you encounter it.
First, vue is divided into the following parts

  • response system
  • Renderer (mount, patch, domdiff)
  • componentized
  • translater

It is impossible for the compiler to write componentized code, which involves vnode and is not essential. The renderer can be simplified and replaced by innerhtml, so the principle of responsiveness is still tested.

2. Simple implementation

Let's implement a super simplified vuedemo in 40 lines of code

 <!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 uses proxy mode to proxy an object, which has the following advantages compared to vue2's defineproperty:

  • without iterating over every property
  • passive hijacking
  • Proxy provides 13 kinds of hijacking capture operations, which can be more refined hijacking capture operations

The core idea is to hijack get and set
get to collect (track), set to trigger (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 side effect function, when the data changes, the function in the effect will be automatically executed

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

What I want to do now is to make the function in effect execute immediately when obj changes.

we can

  • proxy hijacks obj
  • When get obj.text, put fn (actually the render function) somewhere
  • When set obj.text, take out the fn of this place and execute it

Therefore, when executing the effect, on the one hand, it needs to execute the fn function inside, and on the other hand, it needs to use a global variable to save it.

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

2.3 Data structure of weakmap-map-set

Where we use to save fn is actually a weakmap-map-set data structure

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

2.4 reactive

To do reactive processing of an object, you can encapsulate a reactive method

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

If the attribute of the object is still an object, we want to 深响应 , it can be called recursively in get, of course, the shallow response does not need recursion

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

So far, basically a simple responsive vue has been implemented, and there should be no problem in writing the interview like this.

3 ref

If a value is an ordinary object, we can't use proxy. Of course, we can hang this value on an attribute of the object, but people with different attribute names may define them differently, resulting in inconsistency, so vue helps We define a reactive object that can only take value

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

4.computed

Computed has two characteristics: one is lazy, does not call and does not calculate, and the other is cached, and the dependencies do not change and do not calculate

4.1 Implement lazy

Implement the lazy feature first. The function in the effect can choose whether to execute it directly, so it needs to be changed to return an executor effctfn.

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

Computed accepts a getter and returns an obj. When the value is called, it will continuously execute the executor returned by the effect method, that is, continuously call the getter method to complete the calculation.

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

4.2 Implement caching

The imperfect part of the above method is that constantly calling computed.value will continuously adjust the getter method calculation. We can actually only complete the calculation once and store the value as _value
If the geter's 依赖 remains unchanged, we will always return _value without recalculation. If the geter's 依赖 has changed, when we call computed.value again, we will calculate using A variable _dirty in the computed instance to identify whether its dependencies have changed, that is, whether it needs to be calculated

So the key question is, the dependency has changed, how to make _dirty change? The dependency has changed --> execute trigger--> execute the effectfn of dependency collection
Here you can add a scheduler to control how to execute effectfn, such as synchronous and asynchronous, additional operations, etc.

 //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 Complete code

 <!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

With the concept of effect scheduler, it is very simple to implement watcher

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

6. Summary

image.png
First simply implement the responsive principle of vue3, use effect to top effectfn,
proxy get->track->collect the top effectfn
proxy set->trigger->execute the corresponding effectfn collected
The collected data structure is the weakmap-map-set structure and then introduces the commputed, lazy principle, caching principle and effect scheduler principle. The scheduler is used to simply encapsulate a watcher.


Runningfyy
1.3k 声望661 粉丝