Without further ado, let’s get straight to it:
demand
The interface requests a JSON object to be displayed on the page. The logic of Vue is:
- Initialize jsonData in state to
{}
- JsonData for state is updated in mutation after a successful request
- Page rerendering
The birth of the pit
The basic architecture of this project was generated through the customized Vue scaffolding within the group. When I looked at the code, I found that my colleague used a library called deep-assign in Mutaion to change the state. Then I checked Github and found that the author of this library said that the new version had problems but no longer maintained. Lodash.merge is recommended. I thought, “I’m familiar with this, so lodash. Merge.” (too youny too simple!)
So I started coding happily. The key code after simplification is as follows:
<! --app.vue-->
<template>
<div>
{{ jsonData }}
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState({
jsonData: state= > state.jsonData
})
}
}
</script>
Copy the code
// state.js
export default {
isLoading: false.Merge = {// merge = {// merge = {// merge = {// merge = {// merge = {// merge = {// merge = {// merge = {// merge = null
jsonData: {}
}
// mutation.js
export default {
[Types.M_CONTENTS_DETAIL_PACKAGE__SUCCESS]: (state, payload) = > {
// Update state with lodash/merge
merge(state, {
isLoading: false.jsonData: payload.jsonData }); }}Copy the code
Following the logic in the above requirements, the expected result is that after the merge, the component’s jsonData data is updated and the page is re-rendered. However, I found through vue-devtool that the component’s jsonData data has indeed been changed to the latest data, but the page has not been re-rendered. Why is that? 🤔 ️
Then I replaced Lodash. Merge with Object.assign just to try it out
Object.assign(state, {
isLoading: false.jsonData: payload.jsonData
});
Copy the code
The 🙀 page actually rendered properly! Why is that? So I had two questions:
- How does Vue’s responsive change View work?
- What is the difference between Object.assign and lodash.merge?
The responsive principle of Vue
First, I took a close look at the Vue documentation and came up with three key points:
- At initialization, use Object.defineProperty to turn all of these properties into getters/setters
- When the setter for the dependency is called
- Tell Watcher to recalculate
Then a brief look at the Vue source code:
/** * Observer/index.js * iterates over each property of type object and converts it to getter/setter **/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
export function defineReactive (obj: Object, key: string, val: any, ...) {...// Use object.defineProperty to convert all of these properties to getters/setters
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
...
return value
},
set: function reactiveSetter (newVal) {
When the jsonData set function is triggered, type the relevant information **/
if (key === 'jsonData') {
console.log('set value of ' + key, newVal);
}
const value = getter ? getter.call(obj) : val
...
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal)// Notify Watcher to update the view
dep.notify()
}
})
}
Copy the code
As we can clearly see from the source code, Vue iterates through all properties of type object at page initialization, converts them to getters/setters, and notifies the observer to update the view when the property is set. So the component data here has been changed, but the view has not been updated. What is the problem in these three links?
It is normal to initialize the first section as a getter/setter by testing. Then I try to print out the jsonData information in the set function in the Vue source code (such as the comments in the above code) to determine if the set function is fired when jsonData is updated. Merge jsonData’s set function was not triggered when lodash.merge was used. Assign was triggered when object. assign was used
What is the difference between Object-Assgin and Lodash?
Object.assign
As described in the document,
The object.assign () method is used to copy the values of all enumerable properties from one or more source objects to target objects. It will return the target object.
Yeah, that’s all I remember. As we move on,
This method uses the source object’s [[Get]] and the target object’s [[Set]], so it calls the relevant getter and setter
Hmmm calls the set of the target object. Moving on to Polyfill (a piece of code used to provide this functionality on older browsers that didn’t originally support it, which can be read as source code), we see that Object.assign() iterates through the enumerable properties of the source Object and then assigns them directly to the target Object, at which point the target Object’s set is fired.
Object.assign is not a deep copy.
Object.defineProperty(Object."assign", {
value: function assign(target, varArgs) {
'use strict'; . if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; }}}}return to;
},
writable: true.configurable: true
});
Copy the code
lodash.merge
After looking at Object.assign(), we move on to lodash.merge. Merge The merge function recursively merges the source object’s own enumerable properties into the target object. Here we see that the word “recursively” is added to the object.assign () document. To find out how merge works recursively, I looked at the lodash source code. The main source code and its explanation are as follows:
BaseMerge is recursively called in baseMergeDeep. The object is no longer the target object, but a property of the target object. This property is of object type **/
function baseMerge(object, source, srcIndex, customizer, stack) {... baseFor(source, (srcValue, key) => {// Recursive deep copy values are attributes of the object, until the attributes are non-objects, else directly assign
if (isObject(srcValue)) {
...
baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack)
} else. }, keysIn) }function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
/** * Key: On the first baseMergeDeep, object[key] corresponds to state.jsonData in my code, Object [key] is an empty object * whereas objValue * is assigned directly to objValue * so objValue and object[key] reference address is the same **/
const objValue = object[key]
...
let newValue = customizer
? customizer(objValue, srcValue, `${key}`, object, source, stack)
: undefined
let isCommon = newValue === undefined
if (isCommon) {
...
// Determine that an attribute value of the source object is an object, then assign objValue to newValue
else if (isPlainObject(srcValue) || isArguments(srcValue)) {
/** * Key: ObjValue refers to the same address as state.jsonData * here we assign objValue to newValue again, So newValue and state.jsonData also refer to the same address *, which means that all subsequent property merging operations on newValue will result in the property of state.jsonData having been changed **/
newValue = objValue
...
}
if (isCommon) {
...
/** * The baseMerge function is used here to determine whether the deeper child attributes are objects. If so, the same baseMergeDeep processing is performed **/
mergeFunc(newValue, srcValue, srcIndex, customizer, stack)
...
}
/** * Finally the newValue merge changes to the object that has all the attributes of the latest jsonData object * now the first object[key], The state. JsonData in my code has changed along with the newValue. Eq (Object [key], value) is false, baseAssignValue * is no longer executed to pass the breakpoint test, indeed baseAssignValue **/ was not executed when jsonData was finally merged
assignMergeValue(object, key, newValue)
}
/** * assignMergeValue. Js * assigns values to the attributes of the target object **/
function assignMergeValue(object, key, value) {
/** * here is a key judgment --! Eq (object[key], value) * Use eq function to determine whether the value of the attribute key of the target object is equal to the value we are going to assign **/
if((value ! = =undefined && !eq(object[key], value)) || (value === undefined && !(key inobject))) { baseAssignValue(object, key, value); }}function baseAssignValue(object, key, value) {.../** * Set the key attribute of object to value * if the state jsonData attribute is merged, the state jsonData attribute will be merged. This step * triggers the set modifier Vue initialized for jsonData, which triggers the next step - notifting Wachter to update the view * But lodash.merge handles the merging of the state. State.jsondata has been changed to the latest data * therefore, no jsonData set modifier **/ is triggeredobject[key] = value; . }Copy the code
Lodash. merge is a process for handling deep object merges.
conclusion
At this point, we know exactly why lodash.merge merges Vue data without triggering page updates. Briefly summarize a few points:
- It is important to initialize all required data in the Vue, because only after initialization can the Vue listen for and respond to changes to the View
- Lodash. merge Merges objects without firing the set of the outermost object
- Lodash. merge is a deep copy
- Object.assign is not a deep copy
This trip to the pit is over!
The author is Yu Jie, the front end team of Lilac Garden