13
头图

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:

  1. Step 1: Collect dependencies ( effect function), store the effect function when reading the key
  2. 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.

Click to view

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:

  1. The proxy object being manipulated (read) state
  2. The field name text to be manipulated (read)
  3. Registered with the ---a3c65a5e084853060f11e80506b6f39e effect function effectFn 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

Click to view

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

Click to 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:

  1. What's wrong with effect nested execution?
  2. Will there be circular dependencies, infinite loops, etc.?
  3. ...

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


前端胖头鱼
3.7k 声望6.2k 粉丝