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
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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。