简体   繁体   English

使对象可见

[英]Making objects observable

I've been looking into JavaScript frameworks such as Angular and Meteor lately, and I was wondering how they know when an object property has changed so that they could update the DOM. 最近,我一直在研究Angular和Meteor等JavaScript框架,我想知道他们如何知道何时更改了对象属性,以便可以更新DOM。

I was a bit surprised that Angular used plain old JS objects rather than requiring you to call some kind of getter/setter so that it could hook in and do the necessary updates. Angular使用普通的旧JS对象而不是要求您调用某种getter / setter,以便它可以挂接并进行必要的更新,这让我感到有些惊讶。 My understanding is that they just poll the objects regularly for changes. 我的理解是,它们只是定期轮询对象以进行更改。

But with the advent of getters and setters in JS 1.8.5 , we can do better than that, can't we? 但是随着JS 1.8.5中getter和setter方法的出现,我们可以做得更好,不是吗?

As a little proof-of-concept, I put together this script: 作为一点概念验证,我整理了以下脚本:

( Edit: updated code to add dependent-property/method support) 编辑:更新代码以添加从属属性/方法支持)

function dependentProperty(callback, deps) {
    callback.__dependencies__ = deps;
    return callback;
}

var person = {
    firstName: 'Ryan',
    lastName: 'Gosling',
    fullName: dependentProperty(function() {
        return person.firstName + ' ' + person.lastName;
    }, ['firstName','lastName'])
};

function observable(obj) {
    if (!obj.__properties__) Object.defineProperty(obj, '__properties__', {
        __proto__: null,
        configurable: false,
        enumerable: false,
        value: {},
        writable: false
    });
    for (var prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            if(!obj.__properties__[prop]) obj.__properties__[prop] = {
                value: null,
                dependents: {},
                listeners: []
            };
            if(obj[prop].__dependencies__) {
                for(var i=0; i<obj[prop].__dependencies__.length; ++i) {
                    obj.__properties__[obj[prop].__dependencies__[i]].dependents[prop] = true;
                }
                delete obj[prop].__dependencies__;
            }
            obj.__properties__[prop].value = obj[prop];
            delete obj[prop];
            (function (prop) {
                Object.defineProperty(obj, prop, {
                    get: function () {
                        return obj.__properties__[prop].value;
                    },
                    set: function (newValue) {
                        var oldValue = obj.__properties__[prop].value;
                        if(oldValue !== newValue) {
                            var oldDepValues = {};
                            for(var dep in obj.__properties__[prop].dependents) {
                                if(obj.__properties__[prop].dependents.hasOwnProperty(dep)) {
                                    oldDepValues[dep] = obj.__properties__[dep].value();
                                }
                            }
                            obj.__properties__[prop].value = newValue;
                            for(var i=0; i<obj.__properties__[prop].listeners.length; ++i) {
                                obj.__properties__[prop].listeners[i](oldValue, newValue);
                            }
                            for(dep in obj.__properties__[prop].dependents) {
                                if(obj.__properties__[prop].dependents.hasOwnProperty(dep)) {
                                    var newDepValue = obj.__properties__[dep].value();
                                    for(i=0; i<obj.__properties__[dep].listeners.length; ++i) {
                                        obj.__properties__[dep].listeners[i](oldDepValues[dep], newDepValue);
                                    }
                                }
                            }
                        }
                    }
                });
            })(prop);
        }
    }
    return obj;
}

function listen(obj, prop, callback) {
    if(!obj.__properties__) throw 'object is not observable';
    obj.__properties__[prop].listeners.push(callback);
}

observable(person);

listen(person, 'fullName', function(oldValue, newValue) {
    console.log('Name changed from "'+oldValue+'" to "'+newValue+'"');
});

person.lastName = 'Reynolds';

Which logs: 哪个日志:

Name changed from "Ryan Gosling" to "Ryan Reynolds" 名称从“ Ryan Gosling”更改为“ Ryan Reynolds”

The only problem I see is with defining methods such as fullName() on the person object which would depend on the other two properties. 我看到的唯一问题是要在person对象上定义诸如fullName()方法,这将取决于其他两个属性。 This requires a little extra markup on the object to allow developers to specify the dependency. 这需要在对象上添加一些额外的标记,以允许开发人员指定依赖项。

Other than that, are there any downsides to this approach? 除此之外,这种方法还有什么缺点吗?

JsFiddle JsFiddle

advent of getters and setters in JS 1.8.5 - are there any downsides to this approach? JS 1.8.5中的getter和setter方法的问世-这种方法有什么缺点吗?

  • You don't capture any property changes apart from the observed ones. 除了观察到的以外,您不会捕获任何属性更改。 Sure, this is enough for modeled entity objects, and for anything else we could use Proxies. 当然,对于建模实体对象以及我们可以使用代理的任何其他对象,这已经足够。
  • It's limited to browsers that support getters/setters, and maybe even proxies. 它仅限于支持getter / setter的浏览器,甚至支持代理。 But hey, who does care about outdated browsers? 但是,谁在乎过时的浏览器? :-) And in restricted environments (Node.js) this doesn't hold at all. :-)在受限环境(Node.js)中,这根本不成立。
  • Accessor properties (with getter and setter) are much slower than real get/set methods. 访问器属性(带有getter和setter)比实际的get / set方法要慢得多。 Of course I don't expect them to be used in critical sections, and they can make code looking much fancier. 当然,我不希望它们在关键部分中使用,它们会使代码看起来更奇特。 Yet you need to keep that in the back of your mind. 但是,您需要牢记这一点。 Also, the fancy-looking code can lead to misconceptions - normally you would expect property assignment/accessing to be a short ( O(1) ) operation, while with getters/setters there might be a lot of more happening. 同样,看似花哨的代码可能会导致误解-通常,您希望属性分配/访问是一个简短的( O(1) )操作,而使用getter / setter方法可能会发生更多的事情。 You will need to care not forgetting that, and the use of actual methods could help. 您需要注意不要忘记这一点,使用实际方法可能会有所帮助。

So if we know what we are doing, yes, we can do better. 因此,如果我们知道自己在做什么,是的,我们可以做得更好。

Still, there is one huge point we need to remember: the synchronity/asynchronity (also have a look at this excellent answer ). 尽管如此,我们仍然需要记住一个重要的观点:同步/异步(也可以看看这个出色的答案 )。 Angular's dirty checking allows you to change a bunch of properties at once, before the event fires in the next event loop turn. Angular的脏检查允许您在下一个事件循环回合触发事件之前立即更改一堆属性。 This helps to avoid (the propagation of) semantically invalid states. 这有助于避免语义无效状态(的传播)。

Yet I see the synchronous getters/setters as a chance as well. 然而,我也认为同步吸气剂/吸气剂也是一个机会。 They do allow us to declare the dependencies between properties and define the valid states by this. 它们确实允许我们声明属性之间的依赖关系,并由此定义有效状态。 It will automatically ensure the correctness of the model, while we only have to change one property at a time (instead of changing firstName and fullName all the time, firstName is enough). 它将自动确保模型的正确性,而我们一次只需要更改一个属性(而不是始终更改firstNamefullNamefirstName就足够了)。 Nevertheless, during dependency resolving that might not hold true so we need to care about it. 但是,在依赖关系解析过程中,这可能并不成立,因此我们需要对此加以注意。

So, the listeners that are not related to the dependencies management should be fired asynchronous. 因此,与依赖项管理无关的侦听器应异步触发。 Just setImmediate their loop. 只需setImmediate他们的循环。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM