作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
声明式编程是, 目前, 广泛而多样的领域(如数据库)的主导范式, 模板和配置管理.
简而言之, 声明性编程 由指令程序组成 什么 需要去做,而不是说出来 如何 去做. 在实践中,这种方法需要提供用于表达的领域特定语言(DSL) 什么 用户想要, 并保护它们不受低级结构(循环)的影响, 条件, 作业)实现期望的最终状态.
虽然这种范式相对于它所取代的命令式方法是一个显著的改进, 我认为声明式编程有很大的局限性, 这是我在本文中探讨的限制. 此外, 我提出了一种双重方法,既能抓住声明性编程的优点,又能克服它的局限性.
警告: 这篇文章是我多年来与声明性工具斗争的结果. 我在这里提出的许多说法都没有得到彻底的证实, 有些甚至以表面价值呈现. 对声明式编程进行适当的批评需要花费相当多的时间, 努力, 和 I would have to go back 和 use many of these 工具s; my heart is 不 in such an undertaking. 本文的目的是与您分享一些想法, 毫不留情, 向我展示什么对我有用. 如果您一直在与声明性编程工具作斗争,您可能会找到喘息的机会和替代方案. 如果你喜欢这个范例和它的工具,不要把我的话太当真.
如果声明式编程适合您, 我没资格跟你说别的.
在我们探讨声明性编程的局限性之前,有必要了解它的优点.
可以说,最成功的声明性编程工具是关系数据库(RDB)。. 它甚至可能是第一个声明性工具. 无论如何, rdb展示了我认为典型的声明性编程的两个属性:
rdb之前, 大多数数据库系统都是通过命令式代码访问的, 这在很大程度上依赖于底层细节,比如记录的顺序, 索引和数据本身的物理路径. 因为这些元素会随着时间而改变, 代码经常因为数据结构的一些底层变化而停止工作. 结果代码难以编写,难以调试,难以阅读,难以维护. 我大胆地说,大部分代码都在, 所有的可能性, 长, 充满了众所周知的条件句的老鼠窝, 重复和微妙, 依赖错误.
面对这种情况,rdb为系统开发人员提供了巨大的生产力飞跃. 现在, 而不是成千上万行命令式代码, 你有一个明确定义的数据方案, 加上数百个(甚至只是数十个)查询. 结果是, 应用程序只需要处理抽象, 有意义且持久的数据表示, 并通过一个强大的, 然而简单的查询语言. RDB可能提高了程序员的生产力, 以及雇佣他们的公司, 一个数量级.
声明式编程通常列出的优点是什么?
虽然以上都是声明性编程的常用优点, 我想把它们概括为两点, 当我提出另一种方法时,哪些将作为指导原则.
在接下来的两节中,我将介绍声明式编程的两个主要问题: 分离 和 缺乏展开. 每一种批评都需要它的妖魔鬼怪, 因此,我将使用HTML模板系统作为声明式编程缺点的具体例子.
假设您需要编写一个包含大量视图的web应用程序. 将这些视图硬编码到一组HTML文件中是不可行的,因为这些页面的许多组件都会发生变化.
最直接的解决方案, 这是通过连接字符串生成HTML, 看起来很可怕,你会很快寻找另一个选择. 标准的解决方案是使用模板系统. 虽然有不同类型的模板系统, 为了进行分析,我们将回避它们之间的差异. 我们可以认为它们都是相似的,因为模板系统的主要任务是提供一种替代使用条件和循环连接HTML字符串的代码, 就像rdb作为循环遍历数据记录的代码的替代方案而出现一样.
Let’s suppose we go with a st和ard templating system; you will encounter three sources of friction, 我将按重要性的先后顺序列出. 首先,模板必须驻留在与代码分开的文件中. 因为模板系统使用DSL,所以语法是不同的,所以它不能在同一个文件中. 在简单的项目中, 哪里的文件数量少, 需要保留单独的模板文件可能会使文件数量重复或增加两倍.
我为嵌入式Ruby模板(ERB)打开一个异常,因为 那些 集成到Ruby源代码中. 对于用其他语言编写的受erbb启发的工具,情况并非如此,因为这些模板也必须作为不同的文件存储.
第二个原因是DSL有自己的语法, 与你的编程语言不同. 因此,修改DSL(更不用说编写自己的DSL了)相当困难. 到引擎盖下面去换工具, 您需要学习标记化和解析, 哪个有趣又有挑战性, 但是困难. 我恰好认为这是一种劣势.
您可能会问,“您究竟为什么要修改您的工具?? 如果您正在做一个标准项目,那么编写良好的标准工具应该符合要求.“也许是,也许不是.
DSL永远不具备编程语言的全部功能. 如果是这样的话,它就不再是DSL了,而是一种完整的编程语言.
但这不正是DSL的意义所在吗? To 不 是否具备编程语言的全部功能, 这样我们就可以实现抽象并消除大多数bug的来源? 也许,是的. 然而, 大多数 dsl开始时很简单,然后逐渐包含越来越多的编程语言的功能,直到, 事实上, it 成为一个. 模板系统就是一个很好的例子. 让我们来看看模板系统的标准特性,以及它们是如何与编程语言功能相关联的:
这个论点, DSL是有限的,因为它同时觊觎和拒绝编程语言的能力, 与DSL的特性直接映射到编程语言的特性的程度成正比. 就SQL而言, 这个论证很弱,因为SQL提供的大多数功能都与普通编程语言中所提供的功能完全不同. 在光谱的另一端, 我们发现在模板系统中,几乎所有的特性都使DSL趋同 基本.
现在让我们退后一步,思考这三个摩擦的典型来源, 的概念总结 分离. 因为它是独立的, a DSL needs to be located on a separate file; it is harder to modify (和 even harder to write your own), 和(通常, 但并不总是)需要你添加, 一个接一个, 真正的编程语言所缺少的特性.
分离性是任何DSL的固有问题,无论设计得多么好.
现在我们转向声明性工具的第二个问题,它很普遍,但不是固有的.
如果我几个月前写这篇文章,这一节就会被命名 大多数声明性工具都是#@!$#@! 复杂但我不知道为什么. 在写这篇文章的过程中,我发现了一种更好的表达方式: 大多数声明性工具都比它们需要的复杂得多. 我将在本节的其余部分解释原因. 为了分析工具的复杂性,我提出了一种称为 复杂性的差距. 复杂性差距是使用工具解决给定问题与在较低级别(假设是)解决问题之间的差异, 该工具打算替换的纯命令式代码. 当前一种解决方案比后一种解决方案更复杂时,我们就存在复杂性差距. By 更复杂的, 我的意思是更多的代码行, 难以阅读的代码, 更难修改,更难维护, 但并非所有这些都是同时发生的.
请注意,我们并没有将较低级别的解决方案与最好的工具进行比较, 而是反对 no 工具. 这与医学原理相呼应 “首先,不要伤害他人。”.
具有较大复杂性差距的工具的标志是:
由于模板系统并非如此,我可能在这里受到了情绪的影响 那。 复杂的, 但是这种相对较小的复杂性差距并不是它们设计的优点, 而是因为适用的领域非常简单(记住, 这里只是生成HTML). 当同样的方法用于更复杂的领域(比如配置管理)时,复杂性的差距可能很快将您的项目变成一个泥潭.
也就是说, it is 不 necessarily unacceptable for a 工具 to be some什么 更复杂的 than the lower level it intends to replace; if the 工具 yields code 那。 is more readable, 简明、正确, 这是值得的. It’s an issue when the 工具 is several times 更复杂的 than the problem it replaces; this is flat-out unacceptable. Brian Kernighan说过一句名言:控制复杂性是计算机程序设计的本质.“如果一个工具给你的项目增加了显著的复杂性,为什么还要使用它呢?
问题是,为什么一些声明性工具比它们需要的复杂得多? 我认为将其归咎于糟糕的设计是错误的. 这样一个笼统的解释,对这些工具的作者进行人身攻击,是不公平的. 必须有一个更准确、更有启发性的解释.
我的观点是,任何提供高级接口来抽象低级接口的工具都必须 展开 这个高的层次从低的层次开始. 的概念 展开 来自克里斯托弗·亚历山大的巨著 秩序的本质 - -特别是第二卷. It is (hopelessly) beyond the scope 本文简介 (不 to mention my underst和ing) to summarize the implications of this monumental work for software design; I believe its impact will be huge in years to come. 提供展开过程的严格定义也超出了本文的范围. 我将在a中使用这个概念 启发式方法.
展开的过程是一个, 逐步地, 创建新的结构,但不否定现有结构. 每一步, 每个变化(或分化), 用亚历山大的话来说)与之前的结构保持和谐, 当先前的结构为, 简单的, 过去变化的结晶顺序.
有趣的是, Unix 是一个从较低层次展开到更高层次的好例子吗. 在Unix中, 操作系统的两个复杂特征, 批处理作业和协程(管道), 仅仅是基本命令的扩展吗. 因为某些基本的设计决策, 比如把所有东西都变成字节流, 壳是一个 用户态程序 和 标准I/O文件, Unix能够以最小的复杂性提供这些复杂的特性.
为了强调为什么这些都是很好的展开例子, 我想引用几段 1979年的论文 Dennis Ritchie, Unix的作者之一:
关于批处理作业:
… the new 过程 control scheme instantly rendered some very valuable features trivial to implement; for example detached 过程es (with
&
)和递归使用shell作为命令. 大多数系统必须提供某种特殊的批处理作业提交
工具和一个特殊的命令解释器,用于不同于交互式使用的文件.
在协同程序:
Unix管道的天才之处在于,它是由经常以单形方式使用的相同命令构建而成的.
我认为,这种优雅和简单来自于 展开 过程. 批处理作业和协程是从以前的结构中展开的(在用户shell中运行的命令). 我相信这是因为极简主义的理念和创建Unix的团队有限的资源, 这个系统是逐步发展的, 因此, 是否能够整合高级功能而不会因为没有足够的资源而放弃基本功能.
在没有展开过程的情况下, 高层将比必要的复杂得多. 换句话说, 大多数声明性工具的复杂性源于这样一个事实,即它们的高层并没有从它们打算取代的低层展开.
这种缺乏 展开ance, 请原谅我的用词, 是否有必要保护用户不受较低层次的影响. 这种对poka-yoke(保护用户不受低级错误的影响)的强调是以巨大的复杂性差距为代价的,这是弄巧成拙的,因为额外的复杂性将产生新的错误类别. 雪上加霜, 这类错误与问题域无关,而是与工具本身有关. 如果我们把这些误差描述为 医源性.
声明式模板工具, 至少在应用于生成HTML视图的任务时是这样, 是否有一个典型的高级别的案例背弃了它想要取代的低级别的案例. 怎么这么? 因为 生成任何重要的视图都需要逻辑, 模板系统, 尤其是那些没有逻辑的, 把逻辑从正门驱逐出去,然后从猫门偷偷带回来一些.
注意: 对于大复杂性差距的一个更弱的理由是,当一个工具被营销为 魔法之类的 只是工作, 低层次的不透明性被认为是一种资产,因为一个神奇的工具总是应该在你不知道为什么或如何工作的情况下工作. 根据我的经验, 一种工具声称的魔力就越大, 它越快把我的热情转化为沮丧.
但是关注点的分离呢? 视图和逻辑不应该分开? 这里的核心错误是将业务逻辑和表示逻辑放在同一个袋子中. 业务逻辑当然在模板中没有位置,但是表示逻辑仍然存在. 从模板中排除逻辑会将表示逻辑推到服务器中,而服务器本来就不适合它. 我把这一点的明确表述归功于阿列克谢·博罗宁,他对此提出了极好的理由 在本文中.
我的感觉是,模板大约三分之二的工作都在它的表示逻辑中, 而另外三分之一处理诸如连接字符串之类的通用问题, 结束标签, 转义特殊字符, 等等......。. 这是生成HTML视图的两种低级性质. 模板系统可以很好地处理后半部分,但它们不能很好地处理前半部分. 没有逻辑的模板完全背弃了这个问题,迫使您笨拙地解决它. 其他模板系统的问题在于,它们确实需要提供一种重要的编程语言,这样它们的用户才能真正编写表示逻辑.
To sum up; declarative templating 工具s suffer because:
我想用一个逻辑上与本文主题无关的论点来结束我的评论, 但却与它的情感核心产生了深刻的共鸣:我们学习的时间有限. 人生苦短,最重要的是,我们需要工作. 面对我们的局限, 我们需要把时间花在学习有用和经得起时间考验的东西上, 即使面对快速变化的技术. 这就是为什么我建议您使用的工具不仅提供解决方案,而且实际上为其自身的适用性领域提供了明亮的光芒. rdb教你关于数据的知识, Unix教你操作系统的概念, 但用的是不令人满意的无法展开的工具, 我总是觉得自己在学习一个次优解决方案的复杂性的同时,却对它想要解决的问题的本质一无所知.
我建议你们考虑的启发式是, 能够阐明问题领域的价值工具, 而不是那些将问题领域隐藏在所谓的特性背后的工具.
克服声明式编程的两个问题, 我在这里展示的, 我提出了两种方法:
数据结构DSL (dsDSL)是一种DSL 用编程语言的数据结构构建的. 核心思想是使用可用的基本数据结构, 比如字符串, 数字, 数组, 对象和函数, 并将它们组合起来以创建处理特定领域的抽象.
我们希望保持声明结构或操作的能力(高级),而不必指定实现这些结构的模式(低级)。. 我们希望克服DSL和编程语言之间的分离,以便在需要时可以自由地使用编程语言的全部功能. 通过dsdsl,这不仅是可能的,而且是直接的.
如果你一年前问我, 我本以为dsDSL的概念是新颖的, 然后有一天, 我意识到 JSON 它本身就是这种方法的一个完美例子! 解析后的JSON对象由数据结构组成,这些数据结构以声明方式表示数据条目,以便获得DSL的优点,同时也使其易于在编程语言中进行解析和处理. (可能还有其他的dsdsl,但到目前为止我还没有遇到过. 如果你知道一个,我将非常感谢你在评论部分提到它.)
与JSON一样,dsDSL具有以下属性:
解析
和 stringify
.但是dsdsl在很多方面都超越了JSON. 让我们创建一个dsDSL,用于使用Javascript生成HTML. 稍后我将讨论这种方法是否可以扩展到其他语言(剧透:它绝对可以在Ruby和Python中完成), 但可能不是C项。.
HTML是一种由 标签
以尖括号(<
和 >
). 这些标记可能具有可选的属性和内容. 属性只是键/值属性的列表,内容可以是文本或其他标记. 对于任何给定的标记,属性和内容都是可选的. 我稍微简化了一下,但它是准确的.
在dsDSL中表示HTML标签的一种直接方法是使用包含三个元素的数组:
—标签:字符串.
—属性:一个对象(plain, key/value类型)或 未定义的
(如果没有必要的属性).
—内容:字符串(文本)、数组(另一个标签)或 未定义的
(如果没有内容).
例如, Index
可以写成 ['a', {href: 'views'}, 'Index']
.
如果我们想把这个锚元素嵌入到 div
与类 链接
,我们可以这样写: [" div "{类:“链接”},[a, {href:“观点”},“索引”]]
.
要列出同一级别的多个html标签,我们可以将它们包装在一个数组中:
[
(h1,你好!'],
['a', {href: 'views'}, 'Index']
]
同样的原理也适用于在一个标签内创建多个标签:
(“身体”,(
(h1,你好!'],
['a', {href: 'views'}, 'Index']
]]
当然,如果我们不从中生成HTML,这个dsDSL也不会让我们走得太远. 我们需要一个 生成
函数,它将接受我们的dsDSL并产生一个带有HTML的字符串. 所以如果我们运行 生成(['a', {href: 'views'}, 'Index'])
,我们将得到字符串 Index
.
任何DSL背后的思想都是指定一些具有特定结构的结构,然后将其传递给函数. 在这种情况下, 构成dsDSL的结构是这个数组, which has one to three 元素s; these 数组 have a specific structure. If 生成
彻底验证其输入(彻底验证输入既简单又重要), 因为这些验证规则是DSL语法的精确模拟), 它会准确地告诉你输入哪里出错了. 过了一会儿, 您将开始认识到dsDSL中有效结构的区别, 这个结构将高度暗示它所产生的潜在的东西.
现在,与DSL相比,dsDSL的优点是什么?
现在,最后一个主张是强有力的,所以我将用本节剩下的时间来支持它. 我的意思是什么 正确地使用? 来看看这是怎么回事, 让我们考虑一个示例,在这个示例中,我们希望构造一个表来显示来自名为 DATA
.
var DATA = [
{id: 1,描述:'产品1',价格:20,onSale: true,类别:['a']},
{id: 2,描述:'产品2',价格:60,onSale: 假,类别:['b']},
{id: 3,描述:'产品3',价格:120,onSale: 假,类别:['a', 'c']},
{id: 4,描述:'产品4',价格:45,onSale: true,类别:['a', 'b']}
]
在实际应用中, DATA
会从数据库查询动态生成吗.
此外,我们有 过滤器
变量,初始化后,它将是一个包含我们想要显示的类别的数组.
我们希望我们的桌子:
id
字段,但将其添加为 id
属性设置为每行. 备选版本:添加一个 id
每个属性 tr
元素.onSale
如果产品在打折.过滤器
是空数组,我们会显示所有的产品吗. 否则,我们将只显示包含该产品类别的产品 过滤器
.我们可以在大约20行代码中创建符合此要求的表示逻辑:
函数drawTable (DATA, 过滤器) {
var printableFields = ['description', 'price', 'categories'];
DATA.Sort(函数(a, b){返回a.价格- b.价格});
返回['table', []
(tr,戴尔.do (printableFields, function (field) {
返回['th',字段];
})],
戴尔.do (DATA, function (product) {
Var 匹配 = (! Filter || Filter.长度=== 0)|| 戴尔.停止(产品.类别,true, function (category) {
回流过滤器.indexOf(一类) !== -1;
});
返回匹配 === 假 ? [] : ['tr', {
id:产品.id,
类:产品.onSale ? 'onsale':未定义
},戴尔.do (printableFields, function (field) {
返回['td', product [field]];
})];
})
]];
}
我承认这不是一个简单的例子, 然而, 它代表了持久存储的四个基本功能的一个相当简单的视图, 也被称为 CRUD. 任何重要的web应用程序都会有比这更复杂的视图.
现在让我们看看这段代码在做什么. 首先,它定义了一个函数, drawTable
,以包含绘制产品表的表示逻辑. 这个函数接收 DATA
和 过滤器
作为参数,因此它可以用于不同的数据集和过滤器. drawTable
完成部分和助手的双重角色.
var drawTable = function (DATA, 过滤器) {
内部变量, printableFields
, 是唯一需要指定哪些字段是可打印字段的地方吗, 面对不断变化的需求,避免重复和不一致.
var printableFields = ['description', 'price', 'categories'];
然后进行排序 DATA
根据其产品的价格. 请注意,不同的和更复杂的排序标准可以直接实现,因为我们可以使用整个编程语言.
DATA.Sort(函数(a, b){返回a.价格- b.价格});
Here we return an object literal; an array which contains 现在我们用表头创建一行. 要创建其内容,我们使用 戴尔.do 函数是什么样的 数组.map,但它也适用于对象. 我们会迭代 注意,我们刚刚实现了迭代, 生成HTML的主力, 和 we didn’t need any DSL constructs; we only needed a function to iterate a data structure 和 return dsDSLs. 类似的本机或用户实现的函数也可以达到同样的效果. 中包含的产品进行迭代 我们检查这个产品是否被遗漏了 Notice the intricacy of the 条件al; it is precisely tailored to our requirement 和 we have total freedom for expressing it because we are in a programming language rather than a DSL. If 我们当然会关闭所有我们打开的东西. 语法不是很有趣吗?? 现在,我们如何将这张表整合到更广泛的背景中? 我们写一个函数 如果你不喜欢上面代码的样子,我说什么也说服不了你. 这是最好的dsDSL. 你最好停止阅读这篇文章(并写下刻薄的评论,因为如果你已经读到这里了,你已经有权利这样做了!). 但是说真的,如果上面的代码不能让您觉得优雅,那么本文中的其他内容也不会. 为那些还和我在一起的人, 我想回到这一节的主要主张, 那就是 dsDSL具有高层次和低层次的优点: 但是这和纯粹的命令式代码有什么区别呢? 我认为dsDSL方法的优雅最终归结为这样一个事实 以这种方式编写的代码主要由表达式组成,而不是语句. 更准确地说,使用dsDSL的代码几乎完全由以下部分组成: 大部分由表达式组成并将大部分语句封装在函数中的代码非常简洁,因为所有的重复模式都可以很容易地抽象出来. 您可以编写任意代码,只要该代码返回的文字符合非常特定的, 专制的形式. dsdsl的另一个特征(我们在这里没有时间进行探讨)是可以使用类型来增加文字结构的丰富性和简洁性. 我将在以后的文章中详细阐述这个问题. 是否有可能在Javascript(唯一真正的语言)之外创建dsdsl? 我认为这是可能的,只要语言支持: 我认为这意味着dsdsl在任何现代动态语言中都是成立的.e.(Ruby, Python, Perl, PHP),但可能不会使用C或Java. 在本节中,我将尝试展示一种从其领域展开高级工具的方法. 简而言之,该方法包括以下步骤 现在,到底是什么 表征模式 和 生成模式? 我很高兴你这么问. 表示模式是您应该能够表达属于与您的工具相关的领域的问题的模式. 它是一个结构的字母表,允许您在其适用范围内编写您可能希望表达的任何模式. 在DSL中,这些就是产生规则. 让我们回到生成HTML的dsDSL. HTML的表示模式如下: 这些实例可以用我们在前一节中确定的dsDSL符号表示. 这就是表示可能需要的HTML所需的全部内容. 更复杂的模式, 如条件迭代通过一个对象来生成一个表, 可以用返回上述表示模式的函数实现吗, 这些模式直接映射到HTML标签. 如果表征模式是你用来表达你想要的东西的结构, 生成模式是工具将用于将表示模式转换为较低级结构的结构. 对于HTML,它们如下: 信不信由你, 这些是创建生成HTML的展开dsDSL层所需的模式. 可以找到用于生成CSS的类似模式. 事实上, 结石 在250行代码中,两者都有吗. 最后一个有待回答的问题是:我所说的……是什么意思 先走,再滑? 当我们处理一个问题域时, 我们希望使用一种工具将我们从该领域的讨厌细节中解脱出来. 换句话说,我们想要把低水平扫到地毯下面,越快越好. 的 先走,再滑 Approach的建议正好相反:花一些时间在低层次上. 拥抱它的怪癖, 并了解哪些是必要的,哪些是可以避免的,面对一组真实的, 多种多样的, 和有用的问题. 在低水平行走一段时间后,解决了有用的问题, 你将对他们的领域有足够深入的了解. 的 表征模式 和 generation will then arise naturally; they are wholly derived from the nature of the problem they intend to solve. 然后,您可以编写使用它们的代码. 如果它们有效,你将能够快速地解决你最近不得不解决的问题. Sliding means many things; it implies speed, precision 和 lack of friction. 也许更重要的是, this quality can be felt; when solving problems with this 工具, 你是否觉得自己正在解决问题, 或者你觉得你在滑过它? 也许关于一个展开的工具最重要的事情并不是它让我们免于处理底层的问题. 而, 通过捕捉低水平重复的经验模式, 一个好的高级工具可以让我们充分理解应用领域. 展开的工具不仅能解决问题,还能让你了解问题的结构. 所以,不要逃避有价值的问题. 先绕着它走一圈,然后滑过去. 极简软件制造商. 设计、编写、测试、部署和维护真实的系统,解决真实的(如果无聊的话)问题. 12 世界级的文章,每周发一次. 世界级的文章,每周发一次.table
作为它的第一个元素,它的内容作为第二个元素. 的dsDSL表示 我们想要创造.
返回['table', []
printableFields
并为每个表生成表头: (tr,戴尔.do (printableFields, function (field) {
返回['th',字段];
})],
DATA
. 戴尔.do (DATA, function (product) {
过滤器
. If 过滤器
是空的,我们会打印产品吗. If 过滤器
不是空的, 我们将遍历产品的类别,直到找到其中包含的类别 过滤器
. 我们使用 戴尔.停止. Var 匹配 = (! Filter || Filter.长度=== 0)|| 戴尔.停止(产品.类别,true, function (category) {
回流过滤器.indexOf(一类) !== -1;
});
匹配
is 假
,则返回一个空数组(因此不打印此乘积). 否则,返回a 使用适当的id和类,然后遍历 printableFields
打印字段.
返回匹配 === 假 ? [] : ['tr', {
id:产品.id,
类:产品.onSale ? 'onsale':未定义
},戴尔.do (printableFields, function (field) {
返回['td', product [field]];
})];
})
]];
}
drawAll
它将调用生成视图的所有函数. 除了 drawTable
,我们可能也有 拉拔机机头
, drawFooter
以及其他类似的函数,所有这些 会退回dsdsl吗.var drawwall = function () {
返回生成([
拉拔机机头(),
drawTable (DATA, 过滤器),
drawFooter ()
]);
}
走,然后滑:如何从低点展开高点
['标签']
['TAG', {attribute1: value1, attribute2: value2, ...}]
(“标签”,“内容”)
['TAG', {attribute1: value1, ...},“内容”)
(“标签1”,(“标签2”, ...]]
[[“标签1”, ...]、[标签2, ...]]
条件 ? (“标签”, ...] : []
/根据条件,放置属性或不放置属性: ['标签',{类:条件。 ? “someClass”:未定义的}, ...]
,自动关闭).
和
标签).
关于总博客的进一步阅读:
作者简介
专业知识
工作经验