In Vue, you can use the $watch instance method to observe a field. When the value of the field changes, the specified callback function (i.e., observer) will be executed, which actually functions the same as the watch option. as follows:
vm.$watch('box', () => { ('box changed') }) = 'newValue' // 'box changed'
Taking the above example, I want to implement a method with similar functions myWatch.
How do you know that the properties I observed have been modified?
-- method
This method can set a getter-setter function pair for the specified properties of the specified object. This pair of getter-setters can capture the read and modify the properties. Examples are as follows:
const data = { box: 1 } (data, 'box', { set () { ('Modified box') }, get () { ('Readed box') } }) () // 'Readed box' // undefined = 2 // 'Modified box'() // 'Readed box' // undefined
In this way, the modification and reading operations of box attributes are intercepted.
However, the modification operation with res is undefined and = 2 is also invalid.
The get and set functions are not functionally
Therefore, the modification is as follows:
const data = { box: 1 } let value = (data, 'box', { set (newVal) { if (newVal === value) return value = newVal ('Modified box') }, get () { ('Readed box') return value } }) () // 'Readed box' // 1 = 2 // 'Modified box'() // 'Readed box' // 2
With these, the myWatch method can be implemented as follows:
const data = { box: 1 } function myWatch(key, fn) { let value = data[key] (data, key, { set (newVal) { if (newVal === value) return value = newVal fn() }, get () { return value } }) } myWatch('box', () => { ('box changed') }) = 2 // 'box changed'
But there is a problem that we cannot add multiple dependencies (observers) to the same attribute:
myWatch('box', () => { ('I am an observer') }) myWatch('box', () => { ('I am another observer') }) = 2 // 'I am another observer'
The latter dependencies (observer) will overwrite the former.
How can I add multiple dependencies (observers)?
——Define an array as a dependency collector:
const data = { box: 1 } const dep = [] function myWatch(key, fn) { (fn) let value = data[key] (data, key, { set (newVal) { if (newVal === value) return value = newVal ((f) => { f() }) }, get () { return value } }) } myWatch('box', () => { ('I am an observer') }) myWatch('box', () => { ('I am another observer') }) = 2 // 'I am the observer' // 'I am another observer'
After modification, both dependencies (observers) are executed.
If the data object in the above example needs to add two new attributes foo bar that can respond to data changes:
const data = { box: 1, foo: 1, bar: 1 }
Just execute the following code:
myWatch('foo', () => { ('I am the observer of foo') }) myWatch('bar', () => { ('I am the observer of bar') })
But the problem is that the dependencies (observers) of different attributes are collected into the same dep. Modifying any attribute will trigger all dependencies (observers):
= 2 // 'I am the observer' // 'I am another observer' // 'I am the observer of foo' // 'I am the observer of bar'
I think it can be solved like this:
const data = { box: 1, foo: 1, bar: 1 } const dep = {} function myWatch(key, fn) { if (!dep[key]) { dep[key] = [fn] } else { dep[key].push(fn) } let value = data[key] (data, key, { set (newVal) { if (newVal === value) return value = newVal dep[key].forEach((f) => { f() }) }, get () { return value } }) } myWatch('box', () => { ('I am the observer of the box') }) myWatch('box', () => { ('I am another observer of the box') }) myWatch('foo', () => { ('I am the observer of foo') }) myWatch('bar', () => { ('I am the observer of bar') }) = 2 // 'I am the observer of box' // 'I am another observer of box' = 2 // 'I am the observer of foo' = 2 // 'I am the observer of bar'
But it's actually better:
const data = { box: 1, foo: 1, bar: 1 } let target = null for (let key in data) { const dep = [] let value = data[key] (data, key, { set (newVal) { if (newVal === value) return value = newVal (f => { f() }) }, get () { (target) return value } }) } function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { ('I am the observer of the box') }) myWatch('box', () => { ('I am another observer of the box') }) myWatch('foo', () => { ('I am the observer of foo') }) myWatch('bar', () => { ('I am the observer of bar') }) = 2 // 'I am the observer of box' // 'I am another observer of box' = 2 // 'I am the observer of foo' = 2 // 'I am the observer of bar'
Declare the target global variable as a transit station for dependencies (observers). When the myWatch function is executed, target caches the dependencies, and then calls data[key] to trigger the corresponding get function to collect dependencies. When the set function is triggered, all the dependencies (observers) in dep will be executed. The get set function here forms a closure that refers to the above dep constant, so that each property of the data object has a corresponding dependency collector.
Moreover, this implementation method does not require explicitly converting the properties in data into accessor properties one by one through the myWatch function.
But running the following code will reveal that there are still problems:
() = 2 // 'I am the observer of box' // 'I am another observer of box' // 'I am the observer of bar'
After the four myWatches are executed, the value cached by the target becomes the dependency (observer) passed when the myWatch method is called. Therefore, when () is used to read the value of the box attribute, the last cached dependency will be stored in the dependency collector corresponding to the box attribute. Therefore, when the value of the box is modified, 'I am the observer of bar' will be printed.
I think I can solve this problem by setting the global variable target to an empty function after each time I collect the dependencies:
const data = { box: 1, foo: 1, bar: 1 } let target = null for (let key in data) { const dep = [] let value = data[key] (data, key, { set (newVal) { if (newVal === value) return value = newVal (f => { f() }) }, get () { (target) target = () => {} return value } }) } function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { ('I am the observer of the box') }) myWatch('box', () => { ('I am another observer of the box') }) myWatch('foo', () => { ('I am the observer of foo') }) myWatch('bar', () => { ('I am the observer of bar') })
It is correct after testing.
However, during the development process, it is often encountered where nested objects need to be observed:
const data = { box: { gift: 'book' } }
At this time, the above implementation failed to observe the modification of gift, which showed shortcomings.
How to conduct in-depth observations?
--recursion
You can convert all levels of attributes into responsive attributes by recursive:
const data = { box: { gift: 'book' } } let target = null function walk(data) { for (let key in data) { const dep = [] let value = data[key] if ((value) === '[object Object]') { walk(value) } (data, key, { set (newVal) { if (newVal === value) return value = newVal (f => { f() }) }, get () { (target) target = () => {} return value } }) } } walk(data) function myWatch(key, fn) { target = fn data[key] } myWatch('box', () => { ('I am the observer of the box') }) myWatch('', () => { ('I am the observer of gift') }) = {gift: 'basketball'} // 'I am the observer of box' = 'guitar'
At this time, although gift is already an accessor attribute, data[] fails to trigger the corresponding getter to collect dependencies when the myWatch method is executed. Data[] cannot access the gift attribute, data[box][gift] can only be used, so myWatch must be rewritten as follows:
function myWatch(exp, fn) { target = fn let pathArr, obj = data if (/\./.test(exp)) { pathArr = ('.') (p => { obj = obj[p] }) return } data[exp] }
If the field you want to read includes . , then divide it into arrays by . , and then use a loop to read the property values of the nested object.
At this time, after executing the code, it was found that = 'guitar' still failed to trigger the corresponding dependency, that is, the message 'I am the observer of gift' was printed. After debugging, find the problem:
myWatch('', () => { ('I am the observer of gift') })
When executing the above code, pathArr is ['box', 'gift']. In the loop, obj = obj[p] is actually obj = data[box]. The box is read once, the corresponding getter is triggered, and the dependencies are collected:
() => { ('I am the observer of gift') }
After collecting, the global variable target is set to an empty function, and then the loop continues to execute and reads the value of gift. However, at this time, the target is already an empty function, causing the getter corresponding to the property gift to collect an "empty dependency". Therefore, the operation = 'guitar' cannot trigger the expected dependency.
There are two problems with the above code:
- Modifying the box will trigger the dependency "I am the observer of gift"
- Modify gift failed to trigger the dependency of "I am the observer of gift"
The first problem is that when reading gift, it will inevitably go through the process of reading box, so it is inevitable to trigger the corresponding getter of box, so the dependency of box corresponding getter collecting gift is also inevitable. But it is reasonable to think about it, because when box is modified, gift belonging to box is also considered a modification. From this point of view, the problem does not count as a problem, so it is removed.
The second problem, I think it can be solved like this:
function myWatch(exp, fn) { let pathArr, obj = data if (/\./.test(exp)) { pathArr = ('.') (p => { target = fn obj = obj[p] }) return } target = fn data[exp] } = 'guitar' // 'I am the observer of gift' = {gift: 'basketball'} // 'I am the observer of box' // 'I am the observer of gift'
Ensure that target = fn is required when the attribute is read.
So:
const data = { box: { gift: 'book' } } let target = null function walk(data) { for (let key in data) { const dep = [] let value = data[key] if ((value) === '[object Object]') { walk(value) } (data, key, { set (newVal) { if (newVal === value) return value = newVal (f => { f() }) }, get () { (target) target = () => {} return value } }) } } walk(data) function myWatch(exp, fn) { let pathArr, obj = data if (/\./.test(exp)) { pathArr = ('.') (p => { target = fn obj = obj[p] }) return } target = fn data[exp] } myWatch('box', () => { ('I am the observer of the box') }) myWatch('', () => { ('I am the observer of gift') })
Now I think, if I have the following data:
const data = { player: 'James Harden', team: 'Houston Rockets' }
Execute the following code:
function render() { = `The last season's MVP is ${}, he's from ${}` } render() myWatch('player', render) myWatch('team', render) = 'Kobe Bryant' = 'Los Angeles Lakers'
Is it possible to map data to the page and respond to data changes?
Execution code found that = 'Kobe Bryant' reported an error. The reason is that when the render method is executed, the values of \ and , will be obtained. However, at this time, the target is null, so the corresponding dependency collector dep collected null when reading the player, causing the player's setter call to depend on the dependency.
Then I think that if you take the initiative to collect dependencies when render is executed, it will not cause null to be collected in dep.
If you look closely at myWatch, what this method does is actually to help getter collect dependencies. Its first parameter is the attribute to access, whose getter is to be triggered, and the second parameter is the corresponding dependency to be collected.
In this way, the render method can not only help getter collect dependencies (player team is read when render is executed), but it is itself the dependency to be collected. So, can I modify the implementation of myWatch to support such writing:
myWatch(render, render)
The first parameter is executed as a function and the first parameter has the effect of the previous parameter. The second parameter still needs to be collected dependencies. Well, it is reasonable.
Then, myWatch is rewritten as follows:
function myWatch(exp, fn) { target = fn if (typeof exp === 'function') { exp() return } let pathArr, obj = data if (/\./.test(exp)) { pathArr = ('.') (p => { target = fn obj = obj[p] }) return } data[exp] }
However, the modification to team failed to trigger page updates. I think the render performs reading player collection dependencies and target becomes an empty function, resulting in the empty function being collected when reading team collection dependencies. Everyone’s dependence here is render, so you can delete the sentence target = () => {}.
There is another advantage in implementing myWatch in this way. If there are many properties in data that need to be rendered to the page through render, just say myWatch(render, render) without being complicated:
myWatch('player', render) myWatch('team', render) myWatch('number', render) myWatch('height', render) ...
Then in the end:
const data = { player: 'James Harden', team: 'Houston Rockets' } let target = null function walk(data) { for (let key in data) { const dep = [] let value = data[key] if ((value) === '[object Object]') { walk(value) } (data, key, { set (newVal) { if (newVal === value) return value = newVal (f => { f() }) }, get () { (target) return value } }) } } walk(data) function myWatch(exp, fn) { target = fn if (typeof exp === 'function') { exp() return } let pathArr, obj = data if (/\./.test(exp)) { pathArr = ('.') (p => { target = fn obj = obj[p] }) return } data[exp] } function render() { = `The last season's MVP is ${}, he's from ${}` } myWatch(render, render)
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.