Maybe you and I have never met, but it is very likely that we will meet each other late, I am a front-end fat head fish
foreword
It is said that this year is the worst working year, with large factories laying off staff, small factories following suit, and very few people who have sent hundreds of resumes to reply, 金三银四
afraid it will become 铜三铁四
, deserted and desolate Miserable.
But today's protagonist, classmate Xiaoshuai , gave the interviewer a head-to-head drink in a headwind environment, and showed him how good he was. What kind of interview did he go through?
The examples and code in the text can be viewed here
1.# topic debut
Interviewer: I see that you are proficient in your resume Vue3
, and have studied its source code? The guy is crazy! So how about a live show?
After all, the interviewer gave a question on the spot...
<div id="app"></div>
<script>
const $app = document.querySelector('#app')
let state = {
text: 'hello fatfish'
}
function effect() {
$app.innerText = state.text
}
effect()
setTimeout(() => {
// 1秒后希望app的内容变成hello Vue3
state.text = 'hello Vue3'
}, 1000)
</script>
Xiaoshuai chuckle 😋: This simple, as long as the interception state
object, for text
be 取值
when collecting effect
functional dependency, Then text
when setting the value, just execute a wave of the collected effect
function.
Interviewer: Say hello, I can do it too, don't force it, write it quickly...
2 Version 1: It works, but it doesn't work, pawns
2.1# Source code implementation
Xiaoshuai quickly wrote the first version, with only two steps at the core:
- Step 1: Collect dependencies (
effect
function), store the effect function when reading the key - Step 2: When setting the value, it will depend on (
effect
function) to execute
const $app = document.querySelector('#app')
const bucket = new Set()
const state = new Proxy({ text: 'hello fatfish' }, {
get (target, key) {
const value = target[ key ]
// 第一步:收集依赖,在读取key时,将effect函数存储起来
bucket.add(effect)
console.log(`get ${key}: ${value}`)
return value
},
set (target, key, newValue) {
console.log(`set ${key}: ${newValue}`)
target[ key ] = newValue
// 第二步:设置值时,将依赖执行
bucket.forEach((fn) => fn())
}
})
function effect() {
console.log('执行了effect')
$app.innerText = state.text
}
effect()
setTimeout(() => {
state.text = 'hello Vue3'
}, 1000)
Effect preview
Click on the preview , da da da, it looks very simple, it's done in an instant!
2.2# Interviewer comments
Interviewer: The function is implemented, but I am not very satisfied. The function name you collect here is a hard-coded function name effect
, as long as you change the title a little, it will not work.
<div id="container">
<div id="app1"></div>
<div id="app2"></div>
</div>
const $app1 = document.querySelector('#app1')
const $app2 = document.querySelector('#app2')
const state = {
text: 'hello fatfish',
text2: 'hello fatfish2'
}
// 改变app1的值
function effect1() {
console.log('执行了effect')
$app1.innerText = state.text
}
// 改变app2的值
function effect2() {
console.log('执行了effect2')
$app2.innerText = state.text2
}
// 1秒钟之后两个div的值要分别改变
setTimeout(() => {
state.text = 'hello Vue3'
state.text2 = 'hello Vue3-2'
}, 1000)
3# Version 2: Support multi-attribute responsive modification and active registration
3.1# Source code implementation
Xiaoshuai thought: "I'm careless, I should register the effect
dependent function into the bucket actively through some mechanism, so that no matter whether you are an anonymous function or a named function, it will be treated equally"
Cleverly, he immediately thought of the answer.
const bucket = new Set()
let activeEffect
// 变化点:
// 通过effect函数来主动收集依赖
const effect = function (fn) {
// 每执行一次,将当前fn赋值给activeEffect,这样在fn中触发读取操作时,就可以被收集进bucket中了
activeEffect = fn
// 主动执行一次很重要,必不可少
fn()
}
const state = new Proxy({ text: 'hello fatfish', text2: 'hello fatfish2' }, {
get (target, key) {
const value = target[ key ]
// 变化点:由版本1的effect变成了activeEffect,从而不再依赖具体的函数名字
bucket.add(activeEffect)
console.log(`get ${key}: ${value}`)
return value
},
set (target, key, newValue) {
console.log(`set ${key}: ${newValue}`)
target[ key ] = newValue
bucket.forEach((fn) => fn())
}
})
effect(function effect1 () {
console.log('执行了effect1')
$app1.innerText = state.text
})
effect(function effect2() {
console.log('执行了effect2')
$app2.innerText = state.text2
})
setTimeout(() => {
state.text = 'hello Vue3'
state.text2 = 'hello Vue3-2'
}, 1000)
Effect preview <br>You can see that at this time, both app1 and app2 have become corresponding values after 1 second, and the goal has been achieved.
3.2# Interviewer comments
Interviewer: The young man is very good, his ideas are flexible, and he can change quickly! But have you ever thought about a question?
Add a non-existent attribute to state
73445fe059fbe13f669eec7c6e5fd402---, your bucket
will execute the collected dependencies once, isn't it a bit wasteful?
Can it be done effect
depends on what value of state, and the callback will be executed when its value changes?
4# Version 3: Reinvent the wheel and design the "bucket" data structure again
4.1# Redesigning the data structure
Xiaoshuai: I'm a little lost. I wrote on my resume 精通Vue
, and I have studied it in depth Vue
The source code is really a huge pit!
The interview had to continue, and after thinking hard, I finally understood the problem with the second version:
No explicit connection is made between the effect
function and the target field being manipulated :
const state = new Proxy({ text: 'hello fatfish' }, {
get (target, key) {
const value = target[ key ]
// 无论`state`上啥属性被读取了,都会执行`get`然后被收集进`bucket`
bucket.add(effect)
return value
},
set (target, key, newValue) {
target[ key ] = newValue
// 无论`state`上啥值被修改了,都会触发`set`,进而收集的依赖被执行。
bucket.forEach((fn) => fn())
}
})
1. New mapping relationship
How to design the value stored in bucket
? Let's take a look at the key code first
effect(function effectFn () {
$app.innerText = state.text
})
There are several roles in this code:
- The proxy object being manipulated (read)
state
- The field name text to be manipulated (read)
- Registered with the ---a3c65a5e084853060f11e80506b6f39e
effect
functioneffectFn
function
Then the relationship between them can be expressed by a tree
state
|__key
|__effectFn
2. Scenario 1: There are two effectFn reading the property value of the same object
effect(function effectFn1 () {
// 读取text
state.text
})
effect(function effectFn2 () {
// 读取text
state.text
})
Then according to the above tree structure, it is now expressed as follows:
text
attributes should be associated with effectFn1
, effectFn2
state
|__text
|__effectFn1
|__effectFn2
3. Scenario 2: Multiple different properties of the same object are read in effectFn
effect(function effectFn1 () {
// 读取text1和text2
state.text
state.text2
})
text
and text2
attributes should be associated with effectFn1
state
|__text
|__effectFn1
|__text2
|__effectFn1
4. Scenario 3: Different properties of different objects are read in different effectFn
effect(function effectFn1 () {
// 读取text1
state1.text1
})
effect(function effectFn2 () {
// 读取text2
state2.text2
})
The corresponding relationship is expressed as follows:
state1
|__text1
|__effectFn1
state2
|__text2
|__effectFn2
Seeing this, I believe you will understand that when we change the value of state2.text2
, only effectFn2
function will be re-executed, while effectFn1
Won't. Of course, when adding an attribute that did not exist before, effectFn1和effectFn2
will not be executed.
5. Draw a data structure diagram to understand the storage relationship:
4.2# Source code implementation
6: New version source code implementation
const $app = document.querySelector('#app')
// 重新定义bucket数据类型为WeakMap
const bucket = new WeakMap()
let activeEffect
const effect = function (fn) {
activeEffect = fn
fn()
}
const state = new Proxy({ name: 'fatfish', age: 100 }, {
get (target, key) {
const value = target[ key ]
// activeEffect无值意味着没有执行effect函数,无法收集依赖,直接return掉
if (!activeEffect) {
return
}
// 每个target在bucket中都是一个Map类型: key => effects
let depsMap = bucket.get(target)
// 第一次拦截,depsMap不存在,先创建联系
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let deps = depsMap.get(key)
if (!deps) {
// deps本质是个Set结构,即一个key可以存在多个effect函数,被多个effect所依赖
depsMap.set(key, (deps = new Set()))
}
// 将激活的effectFn存进桶中
deps.add(activeEffect)
console.log(`get ${key}: ${value}`)
return value
},
set (target, key, newValue) {
console.log(`set ${key}: ${newValue}`)
// 设置属性值
target[ key ] = newValue
// 读取depsMap 其结构是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
// 真正读取依赖当前属性值key的effects
const effects = depsMap.get(key)
// 挨个执行即可
effects && effects.forEach((fn) => fn())
}
})
effect(() => {
console.log('执行了effect')
$app.innerText = `hello ${ state.name }, are you ${state.age} years old?`
})
setTimeout(() => {
state.name = 'Vue3'
state.age = 18
}, 1000)
Effect preview
You can see that we have added a new attribute to state
text
but effect
will not be executed, modified juejin
name
property juejin
was executed after that, and the view layer was updated.
4.3# Interviewer comments
Niubi, Niubi, almost made me stunned, my brother admires!
But can you go further? You can only perform responsive processing on state
an object. Can you encapsulate it again, like Vue3
which is used in the same way as reactive
?
5# Version 4: reactive abstraction, a bit like Vue3
5.1# Source code implementation
Xiaoshuai thought: You must not want me to pass the interview and deliberately make things difficult for me, but you are the interviewer and you are the biggest. Just do it.
We have implemented the basic responsive functionality earlier, but for generalization, we can further encapsulate it.
const bucket = new WeakMap()
// 重新定义bucket数据类型为WeakMap
let activeEffect
const effect = function (fn) {
activeEffect = fn
fn()
}
// track表示追踪的意思
function track (target, key) {
// activeEffect无值意味着没有执行effect函数,无法收集依赖,直接return掉
if (!activeEffect) {
return
}
// 每个target在bucket中都是一个Map类型: key => effects
let depsMap = bucket.get(target)
// 第一次拦截,depsMap不存在,先创建联系
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let deps = depsMap.get(key)
if (!deps) {
// deps本质是个Set结构,即一个key可以存在多个effect函数,被多个effect所依赖
depsMap.set(key, (deps = new Set()))
}
// 将激活的effectFn存进桶中
deps.add(activeEffect)
}
// trigger执行依赖
function trigger (target, key) {
// 读取depsMap 其结构是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
// 真正读取依赖当前属性值key的effects
const effects = depsMap.get(key)
// 挨个执行即可
effects && effects.forEach((fn) => fn())
}
// 统一对外暴露响应式函数
function reactive (state) {
return new Proxy(state, {
get (target, key) {
const value = target[ key ]
track(target, key)
console.log(`get ${key}: ${value}`)
return value
},
set (target, key, newValue) {
console.log(`set ${key}: ${newValue}`)
// 设置属性值
target[ key ] = newValue
trigger(target, key)
}
})
}
With the above package, we really feel like Vue3
!
const $app = document.querySelector('#app')
const nameObj = reactive({
name: 'fatfish'
})
const ageObj = reactive({
age: 100
})
effect(() => {
console.log('执行了effect')
$app.innerText = `hello ${ nameObj.name }, are you ${ageObj.age} years old?`
})
setTimeout(() => {
nameObj.name = 'Vue3'
}, 1000)
setTimeout(() => {
ageObj.age = 18
}, 2000)
Effect preview
It can be seen that we defined two responsive data through reactive
, and modified the value of nameObj
after 1 second, the view was also updated immediately, and it was modified after 2 seconds ageObj
The value of ageObj
, the view is also updated immediately. It's generic enough now! Perfect
5.2# Interviewer comments
Interviewer: You are very good, but...
Xiaoshuai: But your sister! I will have an interview, do you want me to build a Vue3?
Interviewer: Good good, hahaha! This round of interviews passed, and the second interviewer will continue to let you implement a more comprehensive responsive system , so be prepared!
Xiaoshuai: Ten thousand grass and mud horses flew through my heart...
6.# Next episode preview
Although Xiaoshuai has been recognized by an interviewer, the current implementation of the responsive system is still not perfect, and there are still many problems such as:
- What's wrong with effect nested execution?
- Will there be circular dependencies, infinite loops, etc.?
- ...
Please see how Xiaoshuai solves these problems on the second side, so stay tuned.
At last
I hope to share practical, basic and advanced knowledge points with you all the time, get off work early and fish happily together.
I look forward to your following me in the Nuggets : Front-end Fat Head Fish , you can also find me in the public account: Front-end Fat Head Fish .
refer to
Vue.js design and implementation
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。