dcl学习
I'm incredibly honored to have Eugene Lazutkin author for David Walsh Blog. Eugene has written much of the vector graphic code for the Dojo Toolkit's dojox/gfx (and subsequent charting and drawing resources) library, a library I consider to be mind-blowingly awesome. Eugene chose to write about dcl
, an ultra-flexible, tiny OOP JS library.
能够为David Walsh Blog的作者Eugene Lazutkin感到非常荣幸。 尤金(Eugene)为Dojo Toolkit的dojox / gfx(以及随后的制图和绘图资源)库编写了许多矢量图形代码,该库我认为令人赞叹不已。 Eugene选择撰写有关dcl
, dcl
是一种超灵活的小型OOP JS库。
dcl
is a minimalistic yet complete JavaScript package for node.js and modern browsers. It implements OOP with mixins + AOP at both "class" and object level, and works in strict and non-strict modes.
dcl
是适用于node.js和现代浏览器的简约但完整JavaScript程序包。 它在“类”和对象级别上用mixins + AOP实现OOP,并在严格和非严格模式下工作。
The simplest way to learn something is to dive right in. Let's implement a simple widget based on reactive templating: when we change parameters of a widget, they are immeditely reflected in a web page.
学习某些东西的最简单方法是直接学习。让我们实现一个基于React性模板的简单小部件:当我们更改小部件的参数时,它们会立即反映在网页中。
Assuming that we run our code using AMD format in browser, our "code shell" will look like that:
假设我们在浏览器中使用AMD格式运行代码,则“代码外壳”将如下所示:
require(
["dcl", "dcl/bases/Mixer", "dcl/mixins/Cleanup", "dcl/advices/memoize"],
function(dcl, Mixer, Cleanup, memoize){
// our code goes here
}
);
As the first step let's code our data model:
第一步,让我们对数据模型进行编码:
var Data = dcl(Mixer, {
declaredClass: "Data",
updateData: function(data){
dcl.mix(this, data);
}
});
We derived our class using single inheritance from Mixer, which comes with dcl
. Mixer
is a very simple base. All it does is it copies properties of the first constructor argument to an instance.
我们使用dcl
随附的Mixer的单继承来派生我们的类。 Mixer
是一个非常简单的基础。 它所做的只是将第一个构造函数参数的属性复制到实例。
Obviously in this simple example we could just call updateData()
from our constructor, but let's assume that a constructor and an updater can do (slightly) different things and we want to keep them separately.
显然,在这个简单的示例中,我们可以从构造函数中调用updateData()
,但让我们假设构造函数和更新程序可以(略)做不同的事情,并且我们希望将它们分开保存。
declaredClass
is completely optional, yet recommended to be specified (any unique human-readable name is fine), because it is used by debugging helpers included with `dcl`.
declaredClass
是完全可选的,但建议指定它(任何人类可读的唯一名称都是可以的),因为它由dcl附带的调试助手使用。
Now let's code our nano-sized template engine, which substitutes strings like this: ${abc}
with properties taken directly from an instance (this.abc
in this case). Something like that:
现在,让我们编写纳米级模板引擎的代码,该引擎将这样的字符串替换为: ${abc}
,其属性直接取自实例(在本例中为this.abc
)。 像这样:
var Template = dcl(null, {
declaredClass: "Template",
render: function(templateName){
var self = this;
return this[templateName].replace(/\$\{([^\}]+)\}/g, function(_, prop){
return self[prop];
});
}
});
We specify what template to use by name, which is a property name on an object instance, and it fills out a template string using properties specified on an object.
我们通过名称指定要使用的模板,这是对象实例上的属性名称,它使用对象上指定的属性填充模板字符串。
This is another demonstration of single inheritance: our Template
is based on a plain vanilla Object
, like any JavaScript's object, which is indicated by using null
as a base.
这是单继承的另一示例:我们的Template
基于普通的香草Object
,就像任何JavaScript的对象一样,使用null
作为基础来表示。
What else do we need? We need a way to manage our DOM node:
我们还需要什么? 我们需要一种管理DOM节点的方法:
var Node = dcl([Mixer, Cleanup], {
show: function(text){
if(this.node){
this.node.innerHTML = text;
}
},
destroy: function(){
if(this.node){
this.node.innerHTML = "";
}
}
});
The code above provides a way to show some HTML, and clears out its presentation when we destroy()
a widget.
上面的代码提供了一种显示HTML的方法,并在我们destroy()
小部件时清除了其表示。
It uses two bases: already mentioned Mixer
is used to get a property in during initialization (node
in this case), and Cleanup, which again comes with dcl
. The latter chains all destroy()
methods together and provides a simple foundation for clean up management, so all resources can be properly disposed of.
它有两个基础:已经提到的Mixer
用于在初始化过程中获取属性(在本例中为node
),以及Cleanup ,它也是dcl
附带的。 后者将所有destroy()
方法链接在一起,并为清理管理提供了简单的基础,因此可以正确处理所有资源。
What we did up to this point is we came up with very small manageable orthogonal components, which reflect different sides of our widget, and can be combined together in different configurations. Let's put them all together now:
到目前为止,我们要做的是,我们提出了非常小的可管理的正交组件,这些组件反映了小部件的不同面,并且可以在不同的配置中组合在一起。 现在将它们放在一起:
var NameWidget0 = dcl([Data, Template, Node], {
declaredClass: "NameWidget0",
template: "Hello, ${firstName} ${lastName}!"
});
var x = new NameWidget0({
node: document.getElementById("name"),
firstName: "Bob",
lastName: "Smith"
});
x.show(x.render("template")); // Hello, Bob Smith!
x.updateData({firstName: "Jill"});
x.show(x.render("template")); // Hello, Jill Smith!
It works, but it is not very coherent, and way too verbose. Don't worry, we will fix it soon.
它可以工作,但不是很协调,而且太冗长。 不用担心,我们会尽快修复。
Some readers probably noticed that we have three bases now: Data
, Template
, and Node
, and two of them (Data
, and Node
) are based on Mixer
. How does it work? It works fine, because underneath dcl
uses C3 superclass linearization algorithm (the same one used by Python), which removes duplicates, and sorts bases to ensure that their requested order is correct. In this case a single copy of Mixin
should go before both Data
and Node
. Read more on that topic in dcl() documentation.
一些读者可能已经注意到我们现在有三个基础: Data
, Template
和Node
,其中两个( Data
和Node
)基于Mixer
。 它是如何工作的? 之所以能正常工作,是因为dcl
之下使用了C3超类线性化算法 (Python使用了相同的算法 ),该算法删除了重复项,并对基数进行排序以确保其请求的顺序正确。 在这种情况下,应在Data
和Node
之前添加一个Mixin
副本。 在dcl()文档中阅读有关该主题的更多信息 。
Now let's address deficiencies of our implementation #0:
现在让我们解决实现#0的缺陷:
- As soon as a widget is constructed, we should show text. 构造小部件后,我们应该显示文本。
- As soon as data is updated, we should show text. 数据更新后,我们应该显示文本。
Both requirements are simple and seem to call for good old-fashioned supercalls:
这两个要求都很简单,似乎要求良好的老式超级调用:
var NameWidget1 = dcl([Data, Template, Node], {
declaredClass: "NameWidget1",
template: "Hello, ${firstName} ${lastName}!",
constructor: function(){
this.showData();
},
updateData: dcl.superCall(function(sup){
return function(){
sup.apply(this, arguments);
this.showData();
};
}),
showData: function(){
var text = this.render("template");
this.show(text);
}
});
var x = new NameWidget1({
node: document.getElementById("name"),
firstName: "Bob",
lastName: "Smith"
});
// Hello, Bob Smith!
x.updateData({firstName: "Jill"}); // Hello, Jill Smith!
Much better!
好多了!
Let's take a look at two new things: constructor and a supercall. Both are supposed to be supercalls, yet look differently. For example, constructor doesn't call its super method. Why? Because dcl
chains constructors automatically.
让我们看一下两个新事物:构造函数和超级调用。 两者都应该是超级通话,但外观有所不同。 例如,构造函数不调用其super方法。 为什么? 因为dcl
自动链接构造函数。
updateData()
is straightforward: it calls a super first, then a method to update a visual. But it is declared using a double function pattern. Why? For two reasons: run-time efficience, and ease of debugging. Read all about it in dcl.superCall() documentation, and Supercalls in JS.
updateData()
很简单:首先调用一个super,然后调用一个更新视觉效果的方法。 但这是使用双重功能模式声明的。 为什么? 出于两个原因:运行时效率高,调试方便。 在dcl.superCall()文档中阅读所有相关内容,并在JS中阅读Supercalls 。
While this implementation looks fine, it is far from "fine". Let's be smart and look forward: in real life our implementation will be modified and augmented by generations of developers. Some will try to build on top of it.
尽管此实现看起来不错,但远非“优良”。 让我们变得聪明并期待:在现实生活中,我们的实现将被几代开发人员修改和增强。 有些人会尝试在此基础上构建。
Our call to
showData()
in construct is not going to be the last code executed, as we expected. Constructors of derived classes will be called after it.正如我们所期望的那样,在构造函数中对
showData()
调用不会成为最后执行的代码。 派生类的构造函数将在其后被调用。updateData()
will be overwritten, and some programmers may forget to call a super. Again, they may update data in their code after our code calledshowData()
resulting in stale data shown.updateData()
将被覆盖,某些程序员可能会忘记调用super。 同样,在我们的代码showData()
导致显示过时的数据之后,他们可能会更新其代码中的数据。
Obviously we can write lengthy comments documenting our "implementation decisions", and suggesting future programmers ways to do it right, but who reads docs and comments especially when writing "industrial" code in a crunch time?
显然,我们可以写一些冗长的注释来记录我们的“实现决策”,并建议未来的程序员正确地做这些事情,但是谁能读文档和注释,特别是在紧要关头编写“工业”代码时呢?
It would be nice to solve those problems in a clean elegant way. Is it even possible? Of course. That's why we have AOP.
干净利落地解决这些问题将是很好的。 可能吗 当然。 这就是为什么我们有AOP。
Let's rewrite our attempt #1:
让我们重写尝试1:
var NameWidget2 = dcl([Data, Template, Node], {
declaredClass: "NameWidget2",
template: "Hello, ${firstName} ${lastName}!",
constructor: dcl.after(function(){
this.showData();
}),
updateData: dcl.after(function(){
this.showData();
}),
showData: function(){
var text = this.render("template");
this.show(text);
}
});
var x = new NameWidget2({
node: document.getElementById("name"),
firstName: "Bob",
lastName: "Smith"
});
// Hello, Bob Smith!
x.updateData({firstName: "Jill"}); // Hello, Jill Smith!
Not only we got a (slightly) smaller code, now we are guaranteed, that showData()
is called after all possible constructors, and after every invokation of updateData()
, which can be completely replaced with code that may use supercalls. We don't really care --- we just specified code, which will be executed *after* whatever was put there by other programmers.
不仅我们得到了一个(稍微)小的代码,而且现在可以保证,在所有可能的构造函数之后以及每次调用updateData()
之后都会调用showData()
updateData()
,可以完全将其替换为可以使用超级调用的代码。 我们并不在乎-我们只是指定了代码,这些代码将在其他程序员放置的所有内容之后*执行。
Now imagine that our user wants to click on a name, and get a pop-up with more detailed information, e.g., an HR record of that person. It would make sense to keep the information in one place, yet render it differently. And we already have a provision for that: we can add another template property, and call render()
with its name:
现在,假设我们的用户想要单击一个名称,然后弹出一个包含更详细信息的弹出窗口,例如该人的HR记录。 将信息保留在一个地方,而以不同的方式呈现,这是有道理的。 并且我们已经有一个规定:我们可以添加另一个模板属性,并使用其名称调用render()
:
var PersonWidget1 = dcl(NameWidget2, {
declaredClass: "PersonWidget1",
detailedTemplate: "..."
});
var x = new PersonWidget1({
node: document.getElementById("name"),
firstName: "Bob",
lastName: "Smith",
position: "Programmer",
hired: new Date(2012, 0, 1) // 1/1/2012
});
// Hello, Bob Smith!
var detailed = x.render("detailedTemplate");
In the example above I skipped the definition of a detailed template for brevity. But you can see that we can add more information about person, and we can define different templates when a need arises.
在上面的示例中,为简洁起见,我跳过了详细模板的定义。 但是您可以看到我们可以添加有关人员的更多信息,并且可以在需要时定义不同的模板。
Imagine that we profiled our new implementation and it turned out that we call render()
method directly and indirectly very frequently, and it introduces some measurable delays. We can pre-render a template eagerly on every data update, yet it sounds like a lot of work for several complex templates, and some of them are not even going to be used. Better solution is to implement some kind of lazy caching: we will invalidate cache on every update, yet we will build a string only when requested.
想象一下,我们介绍了新的实现,结果发现我们非常频繁地直接和间接调用render()
方法,并且引入了一些可测量的延迟。 我们可以在每次数据更新时急切地预渲染模板,但是对于多个复杂的模板来说,这似乎需要大量工作,并且其中一些甚至将不再使用。 更好的解决方案是实现某种惰性缓存:我们将在每次更新时使缓存无效,但仅在请求时才构建字符串。
Obviously such changes involve both Data
and Template
. Or it can be done downstream in NameWidget
or PersonWidget
. Now look above and please refrain from doing those changes: so far we tried to keep our "classes" orthogonal, and caching is clearly an orthogonal business.
显然,此类更改涉及到Data
和Template
。 或者可以在NameWidget
或PersonWidget
下游完成。 现在看上面,请不要进行这些更改:到目前为止,我们试图使“类”保持正交,并且缓存显然是正交的。
dcl
already provides a simple solution: memoize advice. Let's use it in our example:
dcl
已经提供了一个简单的解决方案: 记住建议 。 在我们的示例中使用它:
var PersonWidget2 = dcl(NameWidget2, {
declaredClass: "PersonWidget2",
detailedTemplate: "...",
// memoization section:
render: dcl.advise(memoize.advice("render")),
updateData: dcl.advise(memoize.guard ("render"))
});
With these two lines added our render()
result is cached for every first parameter value ("template" or "detailedTemplate" in our case), and the cache will be invalidated every time we call updateData()
.
添加这两行后,将为每个第一个参数值(在本例中为“ template”或“ detailedTemplate” render()
缓存render()
结果,并且每次调用updateData()
时,缓存都会失效。
In this article we presented dcl
package. If you plan to use it in your Node.js project install it like this:
在本文中,我们介绍了dcl
软件包。 如果您打算在Node.js项目中使用它,请像这样安装它:
npm install dcl
For your browser-based projects I suggest to use volo.js:
对于基于浏览器的项目,我建议使用volo.js :
volo install uhop/dcl
The code is an open source on github.com/uhop/dcl under New BSD and AFL v2 licenses.
该代码是github.com/uhop/dcl上的新BSD和AFL v2许可下的开源代码。
This article didn't cover a lot of other things provided by dcl
:
本文未涵盖dcl
提供的许多其他内容:
Avoid the double function pattern in your legacy projects using
inherited()
supercalls.在您的旧项目中,请使用
inherited()
超级调用避免使用双重功能模式。- Use AOP on object-level --- add and remove advices dynamically in any order. 在对象级别上使用AOP ---以任意顺序动态添加和删除建议。
- Specify "before" and "after" automatic chaining for any method. 为任何方法指定自动链接的“之前”和“之后”。
Use debug helpers that come with
dcl
.使用
dcl
随附的调试助手。Leverage a small library of canned advices and mixins provided by
dcl
.利用
dcl
提供的小型罐装建议和mixin库。
If you want to learn more about it, or just curious, you can find a lot of information in the documentation.
如果您想了解更多或只是好奇,可以在文档中找到很多信息。
Happy DRY coding!
DRY编码愉快!
dcl学习