Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Ability to disable Vue observation #2637

Closed
rpkilby opened this issue Apr 7, 2016 · 64 comments
Closed

[feature] Ability to disable Vue observation #2637

rpkilby opened this issue Apr 7, 2016 · 64 comments

Comments

@rpkilby
Copy link

rpkilby commented Apr 7, 2016

Update:
If anyone ends up needing this functionality, I've released it as vue-nonreactive with the appropriate admonitions and everything.


We have some non-plain models where we need to disable Vue's observation and walking. An example is a resource model that has access to a cache so that it can lookup related resources. This causes all of the objects in the cache to become watched (probably inefficient), as well as some additional interactions with other code. Currently, we get around this by setting a dummy Observer on the cache. Something similar to...

import get from 'http';
import Resource from 'resource';


new Vue({
    data: { instance: {}, },
    ready() { this.fetch(); },

    methods: {
        fetch() {
            const Observer = Object.getPrototypeOf(this.instance.__ob__).constructor;

            get('/api/frobs')
            .then(function(data) {
                // initialize Resource w/ JSON document
                const resource = new Resource(data);

                // Protect cache with dummy observer
                resource.cache.__ob__ = new Observer({});

                this.instance = resource;
            });
        },
    },
});

This does work, but

  • relies on vue's internals
  • requires an already observed object since we cannot import the Observer class directly.

Proposal:
Add an official method for explicitly disabling Vue's observation/walking. eg, something like...

const someThing = {
  nestedThing: {},
};

// make entire object non-reactive
Vue.nonreactive(someThing);

// make nested object non-reactive
Vue.nonreactive(someThing.nestedThing);
vm.$set('key.path', someThing);

Considerations:

  • What should happen if a user set a reactive key path to an non-reactive object? Should vue warn the user? eg,

    vm.$set('a', Vue.nonreactive({});
    
    // different from..
    vm.$set('a', {
        someKey: Vue.nonreactive({}),
    });
  • Should an already reactive object warn the user if attempted to be made non-reactive? eg,

// error
Vue.nonreactive(vm.$data.a)

// valid
Vue.nonreactive(_.clone(vm.$data.a));
@azamat-sharapov
Copy link

Won't Object.freeze() not work in your case? It's supported since v1.0.18

@yyx990803
Copy link
Member

  1. If you need to skip observation for an object/array in data, use Object.freeze() on it;
  2. You don't need to put an object in data in order to access it on this. If you simply attach it to this in the created() hook, it doesn't get observed at all.

@rpkilby
Copy link
Author

rpkilby commented Apr 7, 2016

  • Object.freeze doesn't work here. The cache is updated over time.
  • The main resource is reactive. I'm mostly interested in making the nested cache object non-reactive.

@yyx990803
Copy link
Member

Then maybe it's time to rethink your model design. Why nest those things under something to be observed?

@rpkilby
Copy link
Author

rpkilby commented Apr 7, 2016

Because the cache is used to dynamically lookup related resources.

eg, we could have Author and Post models. The author model defines a to-many relationship called posts to the post model. This cache contains the relationship data, as well as the related collection.

calling author.posts gets the posts from the cache.

@yyx990803
Copy link
Member

By design, Vue discourages putting complex objects with their own state-mutating mechanism into Vue instance's data. You should be only putting pure state as observed data into Vue instances. You may manipulate these state anyway you want, but the objects responsible for such manipulations should not be part of Vue instance's state.

@rpkilby
Copy link
Author

rpkilby commented Apr 8, 2016

First, clarifying question - what exactly do you mean by pure state? We have two types of state:

  • model state (permanent, data synchronized with a store. eg, a todo)
  • vue state (temporary, data that controls view behavior. eg, collapse/show the todo list)

But anyway:
That's fair. The model is definitely 'complex', so this request goes against current best practices. Also, my initial example isn't very good - it's just what worked to disable observation. This is more representative of our current setup w/ possible usage:

<!-- layout -->
<post :post="post"></post>
<author :author="author" ><author>
<comments :comments="comments"></comments>
import post from 'components/post';
import author from 'components/author';
import comments from 'components/comments';
/* post = {
 *     template: '...'
 *     props: ['post'],
 *     data: () => {collapsed: false},
 *     ...
 * };  */

new Vue({
    el: 'body',
    data() { 
        instance = postStore.fetch({include: ['author', 'comments.author']})
        Vue.nonreactive(instance.cache)

        return {post: instance, },
    },
    components: {
        post,
        author,
        comments,
    },
    ...
});

Basically, we have a parent vue responsible for placing reusable components in a layout and fetching/binding data to the associated components. The child components don't fetch their own data because the data is different in different contexts. eg, a list of a user's comments vs. a list of a post's comments.

The model is fairly 'dumb' with the exception that related objects are not nested ({post: {author: {}, comments: []}}), but are instead looked up from the cache. eg, post.comments[2].author may be the same exact object as post.author. So, instead of having multiple copies of the author object, we just have one that's looked up from the cache. There isn't any mutation in the above - all data is made on the initial fetch.

Also, not that the request is relevant any more, but an alternative might be to not observe 'private' object members. This could be members with a leading single or maybe double underscore. Downside to this approach is that it would be breaking change.

@rpkilby
Copy link
Author

rpkilby commented Apr 15, 2016

If anyone ends up needing this functionality, I've released it as vue-nonreactive with the appropriate admonitions and everything.

@yyx990803
Copy link
Member

@rpkilby thanks for sharing!

@jsgv
Copy link

jsgv commented Aug 10, 2017

@rpkilby One way I copy an object and remove the observable/reactivity

var newObj = JSON.parse(JSON.stringify(obj))

Really useful since I want to keep an array of "states" and implement a state history object in vuex.

Edit: This solution was specific to my case. I had an object where I only needed a copy of property values at a moment in time. I did not care about references, dynamic updates etc.

@Mechazawa
Copy link

Mechazawa commented Sep 5, 2017

Right now freezing the object is not a long-term solution. Vue-nonreactive has Vue as a dependency which is overkill when it comes to doing something that straight forward. Wouldn't a simple check in the code such as instance.__ob__ !== false be enough? This'd allow libraries to ensure that things like caches won't be observed.

class Unobservable {
  construtor() {
    Object.defineProperty(this, '__ob__', {  
      enumerable: false,  configurable: false,
      writable: false, value: false,
    });
  }
}

This is mostly an issue for libraries used in Vue applications (at least for me).

@fritx
Copy link

fritx commented Sep 25, 2017

How to tell Vue to only watch (defineProperty to) the 1-level depth of data?

My case is, I want Vue to get notified when data.curObj changed,
but do not with curObj.position, curObj.rotation, etc.

I used to use Object.freeze, but in this case, it would cause Error when three.js tries to assign values to the object.

Do I have to do the below?
(actually I did it in another similar place)

data () {
  return {
    wrapper: Object.freeze({
      actual: [bigData]
    })
  }
},
methods: {
  operation () {
    this.wrapper = Object.freeze({
      actual: [newBigData]
    })
  }
}

// core/observer/watch.js
function _traverse (val: any, seen: ISet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
    return
  }
  // ...
// core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  // ...
> curObj PerspectiveCamera {uuid: "BD3C14DF-8C2B-4B96-9900-B3DD0EAC1163", name: "PerspectiveCamera", type: "PerspectiveCamera", parent: null, children: Array(0), …}

> Lodash.isPlainObject(curObj) false
> Vue.isPlainObject(curObj) true
  1. Can we add another condition for user to disable observing than just by Object.isExtensible (Object.freeze)?
  2. Can we improve the Vue.isPlainObject detection?

@marceloavf
Copy link

You can use destructuring

var newObj = { ...obj };

@Mechazawa
Copy link

Mechazawa commented Nov 23, 2017

This should fix it. It will make the isPlainObject method return false.

/**
 * Makes an object and it's children unobservable by frameworks like Vuejs
 */
class Unobservable {
  /**
   * Overrides the `Object.prototype.toString.call(obj)` result
   * @returns {string} - type name
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag}
   */
  get [Symbol.toStringTag]() {
    // Anything can go here really as long as it's not 'Object'
    return 'ObjectNoObserve';
  }
}
>> Object.prototype.toString.call(new Unobservable());
   "[object ObjectNoObserve]"

@rpkilby
Copy link
Author

rpkilby commented Nov 25, 2017

Hi all, one point that has been lost in the responses is that the data in the original comment is not pure state. In my case, I have a model instance with a private reference to a relationship lookup cache. For example, an article can lookup its author or comments. When I call article.author, this is a dynamic property lookup into that relationship cache and is not a simple attribute access. A few things to consider:

  • The cache is not pure state, so I don't want it to be observed by Vue, as it's a waste of resources.
  • The cache cannot be discarded, as I still need a reference to perform these dynamic lookups/updates.
  • The cache is effectively a singleton and may be updated externally. The app updates accordingly.

In response to some suggestions:

  • Using JSON stringify/parse or object destructuring is not sufficient, as this both duplicates the object & cache, as well as breaking the reference to the original cache. Updating the original cache reference will no longer update the app's instances. Without these dynamic lookups and updates the cache is basically pointless, and it would be better to make the related objects simple attributes on the original model. Also worth noting, these suggestions break the instance methods of these objects.
  • The suggestions by @Mechazawa make sense if you control type creation, and the types are built for use with Vue. In my case, this is an external library that is not tied to Vue, and I don't want to go to the trouble of altering the types in the application. It's much more straightforward for the vue layer to simply mark certain known properties as unobservable.

My only criticism:

Vue-nonreactive has Vue as a dependency which is overkill when it comes to doing something that straight forward.

I'm not sure why this would be a bad thing? You're already using Vue in your application, and the plugin is specific to Vue. If I'm not mistaken, most build tools are smart enough to not create bundles with duplicate dependencies. Regardless, this isn't correct. There is a dev dependency, but not a runtime dependency.


Anyway, I'm glad to see this post has garnered some interest, and I'm sure some of these other solutions will work in a variety of other cases. I simply want to highlight the requirements from my original comment and why the suggestions aren't suitable alternatives for that case.

@Morgul
Copy link

Morgul commented Jan 30, 2018

So, I recently ran into this, and discovered that there's a much easier way to short-circuit Vue's observation logic: Define a property as non-configurable.

Background

In my application, I have to work with a 3rd party library (OpenLayers), which creates class instances that hold data, and doesn't support any reactivity system. Trying to shoehorn one in has caused so many headaches, let me tell you. So, the only viable solution for a large scale application using this library is to let OpenLayers have things the way it wants, and for me to make Vue play nicer with these horribly nested, uber objects of doom. Prior to finding this issue, my application was using about 3 gigs of ram (on our largest dataset), all of it caused by Vue making these objects reactive. Also, it was really slow when loading. I tried Vue-nonreactive, and it helped, but only to get us down to about 1 gig. Prior to using Vue, the application was sitting around 350megs.

Solution

Anything you don't want to be reactive, simply mark as configurable: false. It's as simple as:

Object.defineProperty(target, 'nested', { configurable: false });

(This stops the nested property, and all of it's properties from being observed.)

That's it! No Vue dependency, and arguably not even incorrect. With that, my application is down to 200megs with our largest dataset. It's simple, and easy, and requires only a documentation change on Vue's side to make it an 'official' way of making something non-reactive.

@rpkilby
Copy link
Author

rpkilby commented Jan 30, 2018

Interesting - definitely seems like a viable alternative.

@intijk
Copy link

intijk commented Mar 12, 2018

Is there a way to temporarily pause the observation reactive and unpause it later?

I have a prop watcher, within that I update a huge object where I don't want to trigger the DOM update only after the whole data preparation is finished.

@Morgul
Copy link

Morgul commented Mar 13, 2018

@intijk Not exactly. See, it depends on what you're trying to do; Vue eventually has to apply your state, so simply pausing while it's calculated doesn't help much. If, instead, you're trying to skip intermediate states, and only apply the final state, just start with a new object, and then assign that object at the end.

For example (psuedocode):

doUpdate()
{
   const state = _.cloneDeep(this.myState);

  // Do intermediate state updates
  
  this.myState = state;
}

(Normal Vue caveats about object reactivity apply.)

My recommendation would be to use the above configurable trick to skip the sections of your large object that doesn't need to be reactive. If all of it does need to be reactive, I recommend using something like vuex.

@intijk
Copy link

intijk commented Mar 13, 2018

@Morgul I already used this trick for long, but the fact is that I no longer want to use this trick anymore.
In my case, the data object is now kind of big, range from 2M to 100M, perform deep copy on such an object is quite painful.

@Morgul
Copy link

Morgul commented Mar 13, 2018

@intijk That sounds incredibly complex for something to bind Vue to. What's the use case here?

@intijk
Copy link

intijk commented Mar 13, 2018

@Morgul
I don't think the case is complex, case itself is simple, just data is kind of big. Each time the network will load some indexed visualization log file, and I have a visualization component to display it.

@networkimprov
Copy link

Anyone have thoughts on defining a non-reactive field within a computed property? My first idea depends on non-reactivity of assignment to array...

template: '<div v-html="markdown.render(input, env)"></div>',
props: ['id', 'input'],
computed: {
  env:      function() { return { reactive:this.id, non_reactive:[] } },
  markdown: function() { return Markdown },
},

// within markdown.render():
  env.non_reactive[0] = internal_data;

But that's not exactly self-documenting :-)

@LeeYoung624
Copy link

LeeYoung624 commented Mar 26, 2018

Hey guys. I just found this issue and found that I'm facing a problem which is pretty like rpkilby's problem: my project construct a series of Vue Virtual DOMs(or called vnode) from a JSON object. I will use this JSON object to construct a android app. Anyway, this JSON object can be of great size, and when I use this JSON in Vue, it will be observed by Vue. I tried the rpkilby's and Morgul's way, but it doesn't work.(BTW I am in a team, while some guys may not pretty familiar with Vue, and they will probably cause the JSON observed, and my Vue Version is 2.5.16). I'm wondering if we can do this in Vue traverse:
function _traverse (val, seen) {
var i, keys;
var isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode
|| (val && val['vueNonReactive'])) {
return
}
...
As you can see, I add the "val && val['vueNonReactive']". Then I modify my JSON object to have the "vueNonReactive = true" with the root node of JSON, and this solve my problem.
I'm wondering if this may cause any problem? And will this be considered as a new feature in Vue that enable the developer can configure a object to be observed by Vue or not by configuring a property of the object?(Object.freeze may change the object to be a immutable object, so it can't fit every situation)

@magicdawn
Copy link

consider this https://github.com/vuejs/vue/blob/v2.5.16/src/core/observer/index.js#L121
set val._isVue = true can escape from vue observe procedures.

Today I met a case that, Vue observe a map instance of mapbox-gl, then weird things happed, the map get lighter. But the map instance need to be passed between vue instances. After I add map._isVue = true, problem resolved.

@gierschv
Copy link

gierschv commented Jun 6, 2018

+1 to support that officially. I'm using a large object that don't need reactivity in a component, and disabling unused reactvity reduced the memory object size from 800MB to 43MB
I'm using @magicdawn solution for compatibility issue, but @Mechazawa is the best solution here I think.
For the solution of setting __ob__'s configurable to false works but makes Vue crash when it tries to set the real __ob__.

@samuelantonioli
Copy link

samuelantonioli commented Jul 9, 2018

I've build a Vue plugin that makes it possible to make Vue variables non-reactive (it uses the beforeCreate hook).

This is cleaner than vue-nonreactive - @rpkilby, please look at this comment - your solution won't work for the next version of Vue.


Please look at Vue-Static for a way to make variables non-reactive.

<script>
export default {
    static() {
        return {
            map: null,
        };
    },
    mounted() {
        this.map = new mapboxgl.Map({...}); /* something heavy */
    },
};
</script>

@magicdawn
Copy link

to make __ob__ none enumerable, use defineProperty

vue-free.js

import Vue from 'vue'

const Observer = new Vue().$data.__ob__.constructor

function prevent(val) {
	if (val) {
		// Set dummy observer on value
		Object.defineProperty(val, '__ob__', {
			value: new Observer({}),
			enumerable: false,
			configurable: true,
		})
	}

	return val
}

// vue global
Vue.VUE_FREE = prevent

// window
global.VUE_FREE = prevent

// default export
export default prevent

@d1820
Copy link

d1820 commented May 16, 2019

Figure i would give my 2 cents and solution on this.

I also had similar issues implementing both the Freeze concept and fake Observer. My data comes from the server and is a recursive TreeNode scenario, My project is also using vuex in this case which added a layer to the issues seen. I constantly got Maximum call stack size exceeded due to vues object.keys loop. I had tried Freeze and setting data inside a fake VNode, but neither seemed to stop the recursion issues.

I finally stepped back and wrapped my "non-reactive" properties using the classic Revealing Module Pattern

this is the class (ES6/typescript) but same can be applied in plain vue as well

import {  first, forEach } from 'lodash';

export class TreeNode {
    internalRefsInstance: () => { getParent: () => TreeNode; setParent: (parent: TreeNode) => void; getChildNodes: () => TreeNode[]; setChildNode: (childNode: TreeNode) => number; };

    get visitedDate(): string | undefined {
        return this._visitedDates.get(this.id) || undefined;
    }
    
    isSelectedTreeNode: boolean = false;
    showSubheader: boolean = false;
    showHelp: boolean = false;
    treeNodeIconName: string = 'empty';
    childTreeNodeCount: number = 0;

    constructor(public id: string,
        public componentName: string,
        private _visitedDates: Map<string, string>,
        public isActive: boolean = true,
        public nextFlow?: string,
        public prevFlow?: string,
        parent: TreeNode | undefined = undefined) {

        //invoke the internal refs module to create our static instance func to get the values from
        this.internalRefsInstance = this.nonReactiveModule();
        this.internalRefsInstance().setParent(parent);
    }
    nonReactiveModule = () => {
        let _parent: TreeNode | undefined = undefined;
        let _childNodes: TreeNode[] = [];
        const _getParent = (): TreeNode | undefined => {
            return _parent;
        };
        const _setParent = (parent: TreeNode | undefined): void => {
            _parent = parent;
        };
        const _getChildNodes = (): TreeNode[] => {
            return _childNodes || [];
        };
        const _setChildNode = (childNode: TreeNode): number => {
            if (!_childNodes) {
                _childNodes = [];
            }
            _childNodes.push(childNode);
            return _childNodes.length;
        };
        const returnObj = {
            getParent: _getParent,
            setParent: _setParent,
            getChildNodes: _getChildNodes,
            setChildNode: _setChildNode,
        };
        return () => { return returnObj; };
    }

    getParent(): TreeNode | undefined {
        return this.internalRefsInstance().getParent();
    }

    getChildNodes(): TreeNode[] {
        return this.internalRefsInstance().getChildNodes();
    }

    setChildNode(childFlow: TreeNode): void {
        this.childTreeNodeCount = this.internalRefsInstance().setChildNode(childFlow);
    }

    clone(parent: TreeNode | undefined = undefined): TreeNode {
        const newInstance = new TreeNode(this.id, this.componentName, this._visitedDates, this.isActive, this.nextFlow, this.prevFlow, parent);
        newInstance.showHelp = this.showHelp;
        newInstance.showSubheader = this.showSubheader;
        newInstance.isSelectedTreeNode = this.isSelectedTreeNode;
        forEach(this.getChildNodes(), (flow: TreeNode) => {
            newInstance.childTreeNodeCount = newInstance.internalRefsInstance().setChildNode(flow.clone(newInstance));
        });
        return newInstance;
    }

    setVisitedDates(visitedDates: Map<string, string>): void {
        this._visitedDates = visitedDates;
        forEach(this.getChildNodes(), (flow: TreeNode) => {
            flow.setVisitedDates(visitedDates);
        });
    }

    setAsSelected(setParent: boolean = true, setAllFirstChildren: boolean = true): void {
        this.isSelectedTreeNode = true;
        if (setAllFirstChildren) {
            const firstChildFlow = first(this.getChildNodes());
            if (firstChildFlow) {
                firstChildFlow.setAsSelected(false, true);
            }
        }

        if (setParent && this.getParent()) {
            this.getParent()!.setAsSelected(setParent);
        }
    }
    resetSelected(resetChildren: boolean = true): void {
        this.isSelectedTreeNode = false;
        if (resetChildren) {
            forEach(this.getChildNodes(), (flow: TreeNode) => {
                flow.resetSelected(resetChildren);
            });
        }
    }
}

Computed did not work in my case cause i still needed the full object and not a handler sending me the change on the computed. At least if im understanding how a deep watcher would work against a computed subsetted result.

I think taking this a next level up would be to create a injected nonrecursive helper or decorator to speed up the process. Any useful feed back would be great.

@hereiscasio
Copy link

seems like somebody solved this issue already,

hope u check all the details at #4384

@colin-guyon
Copy link

Hi, I created #10265 especially as I was not aware of this former issue.
I'm just wondering what would be the advised solution to be future proof and stay compatible with Vue when it will use Proxy.
Using Object.defineProperty with configurable: false works well (but it does not prevent a property having existing setter/getter to become reactive).
Will this technique still be usable with Vue 3 ?
Thanks

@iamahuman
Copy link

iamahuman commented Nov 8, 2019

@colin-guyon configurable: false would probably not work, especially since there seems to be https://github.com/vuejs/vue-next/blob/d9c6ff372c10dde8b496ee32f2b9a246edf66a35/packages/reactivity/src/reactive.ts#L159. If it ends up in Vue 3.x, there would be an official API to mark an object non-reactive.

Note that just like the new proposed Vue.observable, when a property is set on a reactive object, that new value wouldn't be tainted and just be left as-is. Instead, the getter would return a reactive proxy for it, creating one if it doesn't already exist in the cache.

A fortunate news is that, as long as you don't do much on that reactive proxy you should probably be fine. If memory hog or interoperability is a concern then you certainly don't need to worry about it since whatever the object is — a giant collection of data or some foreign object from a library which behavior is unpredictable should reactivity be applied on it, you say — nothing about it gets touched, after all. In this sense Vue 3.x actually solves a lot of corner cases where this feature would otherwise be useful.

At the moment, the vue-next code seems to exclude symbol keys from being reactive, just like what the current version of Vue does.

@decademoon
Copy link
Contributor

decademoon commented Oct 15, 2020

consider this https://github.com/vuejs/vue/blob/v2.5.16/src/core/observer/index.js#L121
set val._isVue = true can escape from vue observe procedures.

Today I met a case that, Vue observe a map instance of mapbox-gl, then weird things happed, the map get lighter. But the map instance need to be passed between vue instances. After I add map._isVue = true, problem resolved.

I was using this method until I got bitten by Vue dev tools bugging out because it sees _isVue is true and it thinks the object is a Vue component instance but it isn't.

The only hack I've seen with no serious side effects seems to be OP's approach with the vue-nonreactive library.

@HunderlineK
Copy link

Are there alternative solutions in V3 for this?

@SnosMe
Copy link

SnosMe commented Nov 17, 2020

@HunderlineK shallowRef, shallowReactive, markRaw

@WhereJuly
Copy link

WhereJuly commented Nov 18, 2021

I wonder why nobody mentioned here the Object.seal() method.

It is more convenient when need to make entire nested objects non-reactive (in fact immutable) than Object.defineProperty(). You encapsulate seal() (the object behaviour) within the object so it only cares / knows whether it should be immuable or not (seal() could be even applied conditionally, though I see it is a bad design). Its parent is carefree on it then.

This way you can plan your nested data model to be reactive as needed object by object, nest level by nest level.

If you need to make only some properties immutable, @Morgul's Object.defineProperty() approach combined with seal() approach for entire objects would give you the required flexibility as well as saving browser resources otherwise waisted.

@decademoon
Copy link
Contributor

I wonder why nobody mentioned here the Object.seal() method.

Because that approach, like may others, has side effects (namely sealing the object of course, which may not be desired).

We just want a way to inform Vue not to observe an object and leave it at that. If we want to seal/freeze the object then that should be up to us to do if we desire that behavior.

@decademoon
Copy link
Contributor

This could easily be implemented by Vue maintaining a WeakSet of unobservable objects and exposing a public API to add an object to that set. Vue then simply skips observing objects in that set.

The observe function isn't exported so we can't even monkey patch it if we wanted to (although I wouldn't necessarily recommend that approach either).

@WhereJuly
Copy link

@decademoon

We just want a way to inform Vue not to observe an object and leave it at that

We do not need this capability. We consider seal() is quite enough, is not a side effect, is a straight desired behaviour. Having seal() and defineProperty() in place we do not consider there is any use of polluting Vue API in view of existing robust solution.

@decademoon
Copy link
Contributor

@WhereJuly In my case I tried using seal and other approaches but it didn't work. It isn't enough, and it does have side effects which I cannot work around.

Consider something like this:

data() {
  return {
    foo: new Foo()
  }
}

Imagine Foo is a huge object and if it were to be observed then it would consume a lot of memory and performance would greatly suffer. So we have to prevent Vue from observing it. Also we don't need the properties of the foo object to be observable anyway, but we do need the this.foo property to be observable such that if we were to reassign this.foo to a different object (or null) then we want the view to update.

Freezing/sealing the object would break it. Foo is a mutable object and operates under the assumption that it isn't affected in that way.

We could tag the object in such a way that Vue doesn't observe it. This can be done by adding a _isVue property to it (but see #2637 (comment)).

So then one solution could be to wrap the object in another which is then frozen:

data() {
  return {
    foo: Object.freeze({
      value: new Foo()
    })
  }
}

But now we have to do this.foo.value everywhere and it's messy. So we could probably hide all this detail behind a computed property with a getter and setter that unwraps the value for us. Not to mention foo might be passed around to other components which then inadvertently make it reactive which we didn't want in the first place.

But really something like this would be so much simpler:

data() {
  return {
    foo: Vue.unobservable(new Foo())
  }
}

@Mechazawa
Copy link

@WhereJuly
[...]
But really something like this would be so much simpler:

data() {
  return {
    foo: Vue.unobservable(new Foo())
  }
}

In our codebase we're currently using this helper for that purpose

import Vue from 'vue';

// Vue doesn't publicly export this constructor
const Observer = Object.getPrototypeOf(Vue.observable({}).__ob__).constructor;

export function blockObserver (obj) {
  if (obj && !obj.hasOwnProperty('__ob__')) {
    Object.defineProperty(obj, '__ob__', {
      value: new Observer({}),
      enumerable: false,
      configurable: false,
      writeable: false,
    });
  }

  return obj;
}

// example
blockObserver(new Foo())

@decademoon
Copy link
Contributor

@Mechazawa I'm also doing a similar thing in my codebase:

const Observer = new Vue().$data.__ob__.constructor;

export function unobservable(obj) {
  if (!Object.prototype.hasOwnProperty.call(obj, '__ob__')) {
    // Set dummy observer on value
    Object.defineProperty(obj, '__ob__', {
      value: new Observer({}),
      enumerable: false,
    });
  }

  return obj;
}

@WhereJuly
Copy link

@decademoon , @Mechazawa Thanks for clarification. If I got you right you just want to watch the change of the pointer to your new Foo() that is stored in this.$data.foo.

I do not see how it does not work. It does with either frozen or sealed object as the change of pointers in this.$data is the most basic intention of the reactivity. Look at this (the example is on TS and vue-property-decorator but this does not change the outcome):

    // ...
    mounted() {
        let count = 1;
        window.setInterval(() => {
            this.$data.temp = Object.freeze({ count: count });
            count++;
        }, 5000);
    }
    // ...
    data(): IData {
        return {
            temp: Object.freeze({ count: 0 })
        }
    }

    @Watch('temp')
    onTempChanged(newValue: any): void {
        console.dir(newValue);
    }

This works fine as follows: every 5 seconds the new object is printed in the console. You could change freeze to seal and it is still works. You can use this.$data.temp.count in your template and it would work as well printing the count.

If I got your explanations right your use case should work fine. You replace the one frozen/sealed object with another and you get notified of it from your watch handler or in the template.

@decademoon
Copy link
Contributor

@WhereJuly Yes your example works but at the expense of freezing the object. Freezing objects has consequences, namely none of the object's properties can be modified.

Here's maybe a better less-abstract example. Assume we have a Window class which represents some kind of GUI window.

class Window {
  x = 0
  y = 0

  moveTo(x, y) {
    this.x = x
    this.y = y
  }
}

For the sake of simplicity, I've just given it two internal properties x and y which represents the window's position on screen, but a more realistic Window class would probably have numerous other properties, some private.

Now imagine we want to store a reference to a Window instance in our component:

data() {
  return {
    win: Object.freeze(new Window())
  }
}

I've frozen it because I want to prevent Vue from traversing through all the properties of the Window instance and converting them into reactive getters and setters (for performance reasons).

Now later on we call some method on the object:

methods: {
  moveWindow() {
    this.win.moveTo(500, 500)
  }
}

But this throws an exception because x is now a readonly property (because the object got frozen) and the moveTo method assigns to this property.

The only way to make this work is to wrap the Window instance inside another object which instead is frozen (which I already explained here):

data() {
  return {
    win: Object.freeze({ value: new Window() })
  }
}

@decademoon
Copy link
Contributor

decademoon commented Dec 3, 2021

Granted, the burden of wrapping the object can be eased with something like this:

data() {
  return {
    _win: Object.freeze({ value: null })
  }
},

computed: {
  win: {
    get() {
      return this._win.value
    },
    set(value) {
      this._win = Object.freeze({ value })
    }
  }
}

@WhereJuly
Copy link

WhereJuly commented Dec 3, 2021

@decademoon I understand. But you use cases contradict one another.

Your first use case clearly says you have a huge deep nested object you need reactivity only on its pointer, not the object's properties. I put a working solution with freeze/seal. This does exactly what it is requested to though you declared that it did not. Now. This is not the expense. This is the savings on unnecessary reactivity + unnecessary code you put in your example. Just one liner. This is it.

Now in your latter post you try to use freeze/seal for the completely opposite use case where you now need to change the object. This is crystal clear freeze/seal cannot be the soultion here so why try to apply it at all.

Finally this use case has nothing to do with the point of the entire thread - be able to disable Vue reactivity on objects. And as I have shown before, freeze/seal is a great simple transparent flexible and thus effective way to do this.

@decademoon
Copy link
Contributor

@WhereJuly

Your first use case clearly says you have a huge deep nested object you need reactivity only on its pointer, not the object's properties. I put a working solution with freeze/seal. This does exactly what it is requested to though you declared that it did not.

I'm not sure why you think my use cases are contradicting. In my first example I never mentioned that I didn't intend to mutate the object in some way (in fact I pointed out that freezing the object would prevent me from doing so which is why it won't work), all I require that is mutating the object is allowed without triggering Vue's reactivity system (typically because of performance reasons).

You're right that I only need reactivity on the pointer and not the object's properties, but freezing the object achieves this and goes one step further by disallowing mutations on the object's properties -- I still need that aspect to work!

@decademoon
Copy link
Contributor

Maybe the misunderstanding arose from "not needing reactivity on the object's properties" - by this I mean I do not want Vue to make those properties "reactive" (you know how Vue traverses all the properties of an object and converts the properties into reactive getters and setters?). I'm using Vue's definition of "reactive", maybe you're confusing it with "mutable" (hence why you suggested freezing)?

@WhereJuly
Copy link

WhereJuly commented Dec 4, 2021

You're right that I only need reactivity on the pointer and not the object's properties, but freezing the object achieves this and goes one step further by disallowing mutations on the object's properties -- I still need that aspect to work!

@decademoon I see. Then, if you need only partially mutated (by you) and reactive (notifying you changes) object you have to use different approach. First, your object has to comprise several inner objects that are given responsibilties according to the domain (as in DDD) requirements. Some of those objects (immutable, non-reactive) should be frozen/sealed for performance savings, some not (mutable, reactive). Now you may observe its changes via putting in $data.

Generally, in this use case you have to carefully select which nested objects / properties to make immutable. This is the key decision leading to performance/functional balance you have to take here. Do not forget you can freeze single properties as well as discussed above.

maybe you're confusing it with "mutable" (hence why you suggested freezing)?

In our context I cannot see a reason for using mutability (be able to change an object content) with no reactivity (be able to detect those changes are made). This is the single basic reason to exist for a reactve framework.

all I require that is mutating the object is allowed without triggering Vue's reactivity system

Then just do not make it reactive. Do not put it in $data or make it reactive any other way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests