preface
Hello, I'm Lin Sanxin. As you know, this rookie usually writes more basic articles. I always believe in two sentences
- Speak the most difficult knowledge in the most easy to understand words
- Foundation is the premise of advanced
In fact, Vue3 has been out for a long time and may be used by most companies. However, what is better about Vue3 than Vue2? In fact, many people don't know. Today, I'll first tell you the principle of Vue3's response. By the way, Vue3's response is better than Vue2's response.
What's good?
OK, let's talk about why Vue3's response is better than Vue2's response. May I ask you: do you know how Vue's response is implemented? Everyone can give a rough answer
- Vue2's response is based on object Implementation of defineproperty
- Vue3's response is implemented based on ES6's Proxy
Yes, although the above answer is a little abstract, it does answer the core principles of Vue's two versions of responsiveness, and the quality of Vue's two versions of responsiveness is indeed reflected in object The difference between defineproperty and Proxy.
Vue2
As we all know, Vue2's response is based on object Defineproperty, then I'll take object Define property as an example
// Response function function reactive(obj, key, value) { Object.defineProperty(data, key, { get() { console.log(`Visited ${key}attribute`) return value }, set(val) { console.log(`take ${key}from->${value}->Set as->${val}`) if (value !== val) { value = val } } }) } const data = { name: 'Lin Sanxin', age: 22 } Object.keys(data).forEach(key => reactive(data, key, data[key])) console.log(data.name) // Accessed the name attribute // Lin Sanxin data.name = 'sunshine_lin' // Set name from - > Lin Sanxin - > to - > sunshine_ lin console.log(data.name) // Accessed the name attribute // sunshine_lin
Through the above example, I think everyone is right about object With an understanding of defineproperty, what's the problem? What are its disadvantages? You da abandoned it in Vue3. Let's continue:
// Then the code above data.hobby = 'Play basketball' console.log(data.hobby) // Play basketball data.hobby = 'Play games' console.log(data.hobby) // Play games
Now you can see that object What's wrong with defineproperty? We can see that data adds the hobby attribute to access and set values, but neither get nor set will be triggered, so the disadvantage is: object Defineproperty only listens to the properties in the initial object, and is invalid for new properties. This is why Vue. Com is used to modify the new attributes of objects in Vue2$ Set to set the reason for the value.
Vue3
From above, we know object For the disadvantages of defineproperty, let's talk about how the core Proxy of the response principle in Vue3 makes up for this defect. As usual, let's give an example (first roughly, the specific parameters will be described in detail below):
const data = { name: 'Lin Sanxin', age: 22 } function reactive(target) { const handler = { get(target, key, receiver) { console.log(`Visited ${key}attribute`) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log(`take ${key}from->${target[key]}->Set as->${value}`) Reflect.set(target, key, value, receiver) } } return new Proxy(target, handler) } const proxyData = reactive(data) console.log(proxyData.name) // Accessed the name attribute // Lin Sanxin proxyData.name = 'sunshine_lin' // Set name from - > Lin Sanxin - > to - > sunshine_ lin console.log(proxyData.name) // Accessed the name attribute // sunshine_lin
You can see that the effect is actually the same as that of the above object Defineproperty is no different, so why should we abandon it and choose Proxy? Note that the most important thing is to add attributes to the object. Let's see the effect:
proxyData.hobby = 'Play basketball' console.log(proxyData.hobby) // Accessed the hobby property // Play basketball proxyData.hobby = 'Play games' // Set the hobby from - > playing basketball - > to - > playing games console.log(proxyData.hobby) // Accessed the hobby property // Play games
So now you know what's better about Vue3's response than Vue2?
Vue3 responsive principle
After finishing the benefits of Proxy, let's formally talk about the core of Vue3's responsive principle.
preface
Take a look at the following code first
let name = 'Lin Sanxin', age = 22, money = 20 let myself = `${name}this year ${age}Years old, deposit ${money}element` console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 20 yuan money = 300 // Expectation: Lin Sanxin is 22 years old and has a deposit of 300 yuan console.log(myself) // Actual: Lin Sanxin is 22 years old and has a deposit of 20 yuan
Let's think about it. I want myself to change with money. What can I do? Hey, hey, actually, just let myself = '${name} be ${age} years old this year and deposit ${money} Yuan' again, as shown below
let name = 'Lin Sanxin', age = 22, money = 20 let myself = `${name}this year ${age}Years old, deposit ${money}element` console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 20 yuan money = 300 myself = `${name}this year ${age}Years old, deposit ${money}element` // Do it again // Expectation: Lin Sanxin is 22 years old and has a deposit of 300 yuan console.log(myself) // Actual: Lin Sanxin is 22 years old and has a deposit of 300 yuan
effect
As mentioned above, every time money changes, we have to execute myself = '${name} this year ${age} years old and deposit ${money} Yuan' to update myself. In fact, it's not elegant to write this. We can encapsulate an effect function
let name = 'Lin Sanxin', age = 22, money = 20 let myself = '' const effect = () => myself = `${name}this year ${age}Years old, deposit ${money}element` effect() // Execute once first console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 20 yuan money = 300 effect() // Do it again console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 300 yuan
In fact, this is also harmful. If you don't believe it, you can take a look at the following situation
let name = 'Lin Sanxin', age = 22, money = 20 let myself = '', ohtherMyself = '' const effect1 = () => myself = `${name}this year ${age}Years old, deposit ${money}element` const effect2 = () => ohtherMyself = `${age}Year old ${name}Actually ${money}element` effect1() // Execute once first effect2() // Execute once first console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 20 yuan console.log(ohtherMyself) // Lin Sanxin, 22, has 20 yuan money = 300 effect1() // Do it again effect2() // Do it again console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 300 yuan console.log(ohtherMyself) // Lin Sanxin, 22, has 300 yuan
If you add an ohtherMyself, you have to write another effect, and then execute it every time. If you increase the number, don't you have to write a lot of effect function execution code every time?
track and trigger
To solve the above problem, we can use the track function to collect all the effect functions that depend on the money variable and put them in dep. why use Set in dep? Because Set can automatically de duplicate. After collection, as soon as the money variable changes, the trigger function will be executed to notify all effect functions in dep that depend on the money variable to update the dependent variable. Let's take a look at the code first, and then I'll show you through a picture. I'm afraid you'll be dizzy, ha ha.
let name = 'Lin Sanxin', age = 22, money = 20 let myself = '', ohtherMyself = '' const effect1 = () => myself = `${name}this year ${age}Years old, deposit ${money}element` const effect2 = () => ohtherMyself = `${age}Year old ${name}Actually ${money}element` const dep = new Set() function track () { dep.add(effect1) dep.add(effect2) } function trigger() { dep.forEach(effect => effect()) } track() //Collection dependency effect1() // Execute once first effect2() // Execute once first console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 20 yuan console.log(ohtherMyself) // Lin Sanxin, 22, has 20 yuan money = 300 trigger() // Notify the variables myself and otherMyself to update console.log(myself) // Lin Sanxin is 22 years old and has a deposit of 300 yuan console.log(ohtherMyself) // Lin Sanxin, 22, has 300 yuan
What about the object?
The above is about basic data types. Let's talk about objects. Let me give an example to implement their response in the most primitive way
const person = { name: 'Lin Sanxin', age: 22 } let nameStr1 = '' let nameStr2 = '' let ageStr1 = '' let ageStr2 = '' const effectNameStr1 = () => { nameStr1 = `${person.name}He's a big rookie` } const effectNameStr2 = () => { nameStr2 = `${person.name}He's a little genius` } const effectAgeStr1 = () => { ageStr1 = `${person.age}I'm already very old` } const effectAgeStr2 = () => { ageStr2 = `${person.age}I'm still young` } effectNameStr1() effectNameStr2() effectAgeStr1() effectAgeStr2() console.log(nameStr1, nameStr2, ageStr1, ageStr2) // Lin Sanxin is a rookie. Lin Sanxin is a little genius. At 22, he is very old. At 22, he is still very young person.name = 'sunshine_lin' person.age = 18 effectNameStr1() effectNameStr2() effectAgeStr1() effectAgeStr2() console.log(nameStr1, nameStr2, ageStr1, ageStr2) // sunshine_lin is a big rookie, sunshine_lin is a little genius. He is very old at 18. He is still very young at 18
We can also see the above code. I feel that it is very brainless.. Remember the dep collection effect mentioned earlier? Let's treat the name and age in the person object as two variables, each of which has its own dependent variables
- name: nameStr1 and nameStr2
- age: ageStr1 and ageStr2
Therefore, name and age should have their own dep and collect the effect s corresponding to their dependent variables
As mentioned earlier, dep uses Set. Because person has age and name attributes, it has two DEPs. What is used to store these two DEPs? We can use another data structure Map of ES6 to store
const person = { name: 'Lin Sanxin', age: 22 } let nameStr1 = '' let nameStr2 = '' let ageStr1 = '' let ageStr2 = '' const effectNameStr1 = () => { nameStr1 = `${person.name}He's a big rookie` } const effectNameStr2 = () => { nameStr2 = `${person.name}He's a little genius` } const effectAgeStr1 = () => { ageStr1 = `${person.age}I'm already very old` } const effectAgeStr2 = () => { ageStr2 = `${person.age}I'm still young` } const depsMap = new Map() function track(key) { let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set()) } // Let's write it down for now if (key === 'name') { dep.add(effectNameStr1) dep.add(effectNameStr2) } else { dep.add(effectAgeStr1) dep.add(effectAgeStr2) } } function trigger (key) { const dep = depsMap.get(key) if (dep) { dep.forEach(effect => effect()) } } track('name') // Collect person Name dependency track('age') // Collect person Age dependency effectNameStr1() effectNameStr2() effectAgeStr1() effectAgeStr2() console.log(nameStr1, nameStr2, ageStr1, ageStr2) // Lin Sanxin is a rookie. Lin Sanxin is a little genius. At 22, he is very old. At 22, he is still very young person.name = 'sunshine_lin' person.age = 18 trigger('name') // Notify person Name dependent variable update trigger('age') // Notify person Dependent variable update for age console.log(nameStr1, nameStr2, ageStr1, ageStr2) // sunshine_lin is a big rookie, sunshine_lin is a little genius. He is very old at 18. He is still very young at 18
We have only one person object above. What if there are multiple objects? What should I do? As we all know, each object will create a Map to store the dep of the attributes in the object (using Set). If there are multiple objects, what should be used to store the Map corresponding to each object? Please see the figure below
In fact, ES6 also has a new data structure called WeakMap. Let's use it to store the maps of these objects. So we have to transform the track function and trigger function to see what they look like before
const depsMap = new Map() function track(key) { let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set()) } // Let's write it down for now if (key === 'name') { dep.add(effectNameStr1) dep.add(effectNameStr2) } else { dep.add(effectAgeStr1) dep.add(effectAgeStr2) } } function trigger (key) { const dep = depsMap.get(key) if (dep) { dep.forEach(effect => effect()) } }
In the previous code, only a single object was processed, but now if you want multiple objects, you have to use WeakMap for transformation (the next code may be a little wordy, but it will take care of students with weak foundation)
const person = { name: 'Lin Sanxin', age: 22 } const animal = { type: 'dog', height: 50 } const targetMap = new WeakMap() function track(target, key) { let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, depsMap = new Map()) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set()) } // Let's write it down for now if (target === person) { if (key === 'name') { dep.add(effectNameStr1) dep.add(effectNameStr2) } else { dep.add(effectAgeStr1) dep.add(effectAgeStr2) } } else if (target === animal) { if (key === 'type') { dep.add(effectTypeStr1) dep.add(effectTypeStr2) } else { dep.add(effectHeightStr1) dep.add(effectHeightStr2) } } } function trigger(target, key) { let depsMap = targetMap.get(target) if (depsMap) { const dep = depsMap.get(key) if (dep) { dep.forEach(effect => effect()) } } }
After the above transformation, we finally realized multi-object dependency collection. Let's have a try
const person = { name: 'Lin Sanxin', age: 22 } const animal = { type: 'dog', height: 50 } let nameStr1 = '' let nameStr2 = '' let ageStr1 = '' let ageStr2 = '' let typeStr1 = '' let typeStr2 = '' let heightStr1 = '' let heightStr2 = '' const effectNameStr1 = () => { nameStr1 = `${person.name}He's a big rookie` } const effectNameStr2 = () => { nameStr2 = `${person.name}He's a little genius` } const effectAgeStr1 = () => { ageStr1 = `${person.age}I'm already very old` } const effectAgeStr2 = () => { ageStr2 = `${person.age}I'm still young` } const effectTypeStr1 = () => { typeStr1 = `${animal.type}He's a big rookie` } const effectTypeStr2 = () => { typeStr2 = `${animal.type}He's a little genius` } const effectHeightStr1 = () => { heightStr1 = `${animal.height}It's already very high` } const effectHeightStr2 = () => { heightStr2 = `${animal.height}Pretty short` } track(person, 'name') // Collect person Name dependency track(person, 'age') // Collect person Age dependency track(animal, 'type') // animal.type dependency track(animal, 'height') // Collect animal Dependence of height effectNameStr1() effectNameStr2() effectAgeStr1() effectAgeStr2() effectTypeStr1() effectTypeStr2() effectHeightStr1() effectHeightStr2() console.log(nameStr1, nameStr2, ageStr1, ageStr2) // Lin Sanxin is a rookie. Lin Sanxin is a little genius. At 22, he is very old. At 22, he is still very young console.log(typeStr1, typeStr2, heightStr1, heightStr2) // dog is a big rookie. dog is a little genius. 50 is already very tall. 50 is still very short person.name = 'sunshine_lin' person.age = 18 animal.type = 'cat' animal.height = 20 trigger(person, 'name') trigger(person, 'age') trigger(animal, 'type') trigger(animal, 'height') console.log(nameStr1, nameStr2, ageStr1, ageStr2) // sunshine_lin is a big rookie, sunshine_lin is a little genius. He is very old at 18. He is still very young at 18 console.log(typeStr1, typeStr2, heightStr1, heightStr2) // The cat is a rookie. The cat is a little genius. 20 is already very tall. 20 is still very short
Proxy
Through the above learning, we can realize that when the data is updated, its dependent variables also change, but there are still disadvantages. You can find that every time we have to manually execute the track function to collect dependencies, and when the data changes, we have to manually execute the trigger function to notify the update
So, is there any way to automatically collect dependencies and automatically notify updates? The answer is yes. Proxy can solve this problem for us. Let's write a reactive function first. Let's knock it first and understand the relationship between proxy track trigger. Later, I will talk about why proxy needs to be combined with Reflect
function reactive(target) { const handler = { get(target, key, receiver) { track(receiver, key) // Collect dependencies on access return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) trigger(receiver, key) // Automatically notify updates when set } } return new Proxy(target, handler) }
Then change the previous code, remove the manual track and manual trigger, and find that the previous effect can also be achieved
const person = reactive({ name: 'Lin Sanxin', age: 22 }) // Incoming reactive const animal = reactive({ type: 'dog', height: 50 }) // Incoming reactive effectNameStr1() effectNameStr2() effectAgeStr1() effectAgeStr2() effectTypeStr1() effectTypeStr2() effectHeightStr1() effectHeightStr2() console.log(nameStr1, nameStr2, ageStr1, ageStr2) // Lin Sanxin is a rookie. Lin Sanxin is a little genius. At 22, he is very old. At 22, he is still very young console.log(typeStr1, typeStr2, heightStr1, heightStr2) // dog is a big rookie. dog is a little genius. 50 is already very tall. 50 is still very short person.name = 'sunshine_lin' person.age = 18 animal.type = 'cat' animal.height = 20 console.log(nameStr1, nameStr2, ageStr1, ageStr2) // sunshine_lin is a big rookie, sunshine_lin is a little genius. He is very old at 18. He is still very young at 18 console.log(typeStr1, typeStr2, heightStr1, heightStr2) // The cat is a rookie. The cat is a little genius. 20 is already very tall. 20 is still very short
Some students may be a little confused about the above code, or they may be a little confused. I thought it would be compressed by explaining the flow chart to you through a chart. I suggest you click to have a look
Solve the problem of dead writing
There's a place above where we're dead. Do you remember, it's in the track function
function track(target, key) { let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, depsMap = new Map()) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set()) } // Let's write it down for now if (target === person) { if (key === 'name') { dep.add(effectNameStr1) dep.add(effectNameStr2) } else { dep.add(effectAgeStr1) dep.add(effectAgeStr2) } } else if (target === animal) { if (key === 'type') { dep.add(effectTypeStr1) dep.add(effectTypeStr2) } else { dep.add(effectHeightStr1) dep.add(effectHeightStr2) } } }
In actual development, there must be more than two objects. If you add one more object, you have to add an else if judgment. That's absolutely impossible. So how do we solve this problem? In fact, it's not difficult. The authors of Vue3 have come up with a very clever way to use a global variable activeEffect to skillfully solve this problem. How to solve it? In fact, it is very simple. As soon as each effect function is executed, it will put itself into the corresponding dep, so there is no need to write it dead.
How can we achieve this function? We need to modify the effect function and the track function
let activeEffect = null function effect(fn) { activeEffect = fn activeEffect() activeEffect = null // It becomes null immediately after execution } function track(target, key) { // If activeEffect is null at this time, the following steps will not be executed // The judgment here is to avoid, for example, console Log (person. Name) and trigger track if (!activeEffect) return let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, depsMap = new Map()) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set()) } dep.add(activeEffect) // Add the active effect at this time } // Each effect function is executed like this effect(effectNameStr1) effect(effectNameStr2) effect(effectAgeStr1) effect(effectAgeStr2) effect(effectTypeStr1) effect(effectTypeStr2) effect(effectHeightStr1) effect(effectHeightStr2)
Implement ref
This is how we use ref in Vue3
let num = ref(5) console.log(num.value) // 5
Then num will become a responsive data, and you need to write num.value in this way to use num
Implementing ref is actually very simple. We have implemented reactive above. We only need to do so to implement Ref
function ref (initValue) { return reactive({ value: initValue }) }
We can try how it works
let num = ref(5) effect(() => sum = num.value * 100) console.log(sum) // 500 num.value = 10 console.log(sum) // 1000
Implement computed
Let's simply implement computed by the way. It's actually very simple
function computed(fn) { const result = ref() effect(() => result.value = fn()) // Execute computed incoming function return result }
Let's see the results
let num1 = ref(5) let num2 = ref(8) let sum1 = computed(() => num1.value * num2.value) let sum2 = computed(() => sum1.value * 10) console.log(sum1.value) // 40 console.log(sum2.value) // 400 num1.value = 10 console.log(sum1.value) // 80 console.log(sum2.value) // 800 num2.value = 16 console.log(sum1.value) // 160 console.log(sum2.value) // 1600
Since then, we have realized all the functions of this article
Final code
const targetMap = new WeakMap() function track(target, key) { // If activeEffect is null at this time, the following steps will not be executed // The judgment here is to avoid, for example, console Log (person. Name) and trigger track if (!activeEffect) return let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, depsMap = new Map()) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, dep = new Set()) } dep.add(activeEffect) // Add the active effect at this time } function trigger(target, key) { let depsMap = targetMap.get(target) if (depsMap) { const dep = depsMap.get(key) if (dep) { dep.forEach(effect => effect()) } } } function reactive(target) { const handler = { get(target, key, receiver) { track(receiver, key) // Collect dependencies on access return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) trigger(receiver, key) // Automatically notify updates when set } } return new Proxy(target, handler) } let activeEffect = null function effect(fn) { activeEffect = fn activeEffect() activeEffect = null } function ref(initValue) { return reactive({ value: initValue }) } function computed(fn) { const result = ref() effect(() => result.value = fn()) return result }
Proxy and Reflect
Proxy
const person = { name: 'Lin Sanxin', age: 22 } const proxyPerson = new Proxy(person, { get(target, key, receiver) { console.log(target) // Original person console.log(key) // Attribute name console.log(receiver) // proxyPerson after proxy }, set(target, key, value, receiver) { console.log(target) // Original person console.log(key) // Attribute name console.log(value) // Set value console.log(receiver) // proxyPerson after proxy } }) proxyPerson.name // Access property trigger get method proxyPerson.name = 'sunshine_lin' // Setting the property value triggers the set method
Reflect
Here are two methods of Reflect
- get(target, key, receiver): my personal understanding is to access the key attribute of target, but this points to the receiver, so the actual accessed value is the value of the receiver's key, but this is not a direct access to the receiver[key] attribute. We should distinguish it
- set(target, key, value, receiver): my personal understanding is to set the key attribute of target to value, but this points to the receiver, so in fact, the value of the receiver's key is set to value, but this is not a direct receiver[key] = value. We should distinguish it
As we emphasized above, you can't directly receive [key] or receive [key] = value, but through reflect Get and reflect Set, go around the corner to access properties or set properties. Why? Let's take a counter example
const person = { name: 'Lin Sanxin', age: 22 } const proxyPerson = new Proxy(person, { get(target, key, receiver) { return Reflect.get(receiver, key) // Equivalent to receiver[key] }, set(target, key, value, receiver) { Reflect.set(receiver, key, value) // Equivalent to receiver[key] = value } }) console.log(proxyPerson.name) proxyPerson.name = 'sunshine_lin' // An error will be reported directly, and the stack memory overflows. Maximum call stack size exceeded
Why? Look at the solution below
Now you know why you can't directly receive [key] or receive [key] = value, because such a direct operation will lead to an infinite loop and eventually an error. So the right thing to do is
const person = { name: 'Lin Sanxin', age: 22 } const proxyPerson = new Proxy(person, { get(target, key, receiver) { return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) } }) console.log(proxyPerson.name) // Lin Sanxin proxyPerson.name = 'sunshine_lin' console.log(proxyPerson.name) // sunshine_lin
Some students must ask. It's OK to write so below. Why not suggest it? I'll put it down and say it together
const proxyPerson = new Proxy(person, { get(target, key, receiver) { return Reflect.get(target, key) }, set(target, key, value, receiver) { Reflect.get(target, key, value) } })
Why use it together
In fact, Proxy can be used without reflection. We can write this, and we can still achieve the desired effect
const person = { name: 'Lin Sanxin', age: 22 } const proxyPerson = new Proxy(person, { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { target[key] = value } }) console.log(proxyPerson.name) // Lin Sanxin proxyPerson.name = 'sunshine_lin' console.log(proxyPerson.name) // sunshine_lin
Then why do you recommend using Proxy and Reflect together? Because Proxy and Reflect methods are one-to-one correspondence, using Reflect in Proxy will improve semantics
- Get of Proxy corresponds to reflect get
- Set of Proxy corresponds to reflect set
- There are many other methods I will not list one by one, but they all correspond one by one
Another reason is to try to put this on the receiver instead of the target
Why try to put this on the proxy object receiver instead of the original object target? Because the original object target may also be the proxy object of another agent, if this is always placed on the target, the probability of bug s will be greatly improved. Therefore, why not recommend it in the previous code? You should know?
const proxyPerson = new Proxy(person, { get(target, key, receiver) { return Reflect.get(target, key) }, set(target, key, value, receiver) { Reflect.set(target, key, value) } })
epilogue
I'm Lin Sanxin, an enthusiastic front-end rookie programmer. If you are self-motivated, like the front end and want to learn from the front end, we can make friends and fish together. Ha ha, fish school, add me, please note [Si no]