Comments (58)
问题1根据现有标准描述,问题2从使用角度考虑,我觉得:
- 允许调用,不允许异常
- 要显示为Hello World。
from esui.
那么意味着所有的set
类方法都要增加main
是否存在的逻辑,如果不存在则找地方把内容存下来,当render
的时候做一次setProperties
操作,这个 判断main
-> 找地方存下来 的过程能不能抽象出来……抽象出来后可能会变成这么写:
helper.definePropertySetter = function (name, fn) {
return function (value) {
if (!this.main) {
this.syncProperties[name] = value;
}
else {
fn(value);
}
};
};
TextBox.prototype.setValue = helper.definePropertySetter('value', function(value) {
this.main.value = value;
});
当然也可以换个方式helper
只提供前置判断,在setValue
里多写几行。
from esui.
在Label
中试验了一下,相关的实现要这样:
Label.prototype.setText = function (text) {
if (this.main) {
this.disposeChildren();
this.main.innerHTML = require('./lib').encodeHTML(text);
// 用完要去掉保留的数据,不然会造成同步混乱
this.text = null;
}
else {
// 如果元素还没有则先保留着,
// 为了`setText`和`setContent`连续调用时,保证时序性没问题,
// 这里不用2个字符分别存放`content`和`text`,而是使用一个字段
this.content = {
mode: 'text',
content: text
};
}
};
Label.prototype.setContent = function (html) {
if (this.main) {
this.disposeChildren();
this.main.innerHTMl = html;
this.initChildren(this.main);
}
else {
this.content = {
mode: 'html',
content: html
};
}
};
Label.prototype.render = function () {
helper.beforeRender(this);
helper.renderMain(this);
helper.afterRender(this);
// 同步相关属性
if (this.content) {
if (this.content.mode === 'text') {
this.setText(this.content.content);
}
else {
this.setContent(this.content.content);
}
this.content = null;
}
};
Label.prototype.getText = function () {
if (this.main) {
return require('./lib').getText(this.main);
}
if (!this.content) {
return '';
}
if (this.content.mode === 'text') {
return this.content.content;
}
else {
// 这里非常的糟糕,因为原本保存的是一段HTML,
// 但又没有main来给你生成`innerText`
var div = document.createElement('div');
div.innerHTML = this.content.content;
return require('./lib').getText(div);
}
};
重点有以下几个:
- 在
setText
的时候要关心是否有main
,没有的话要把东西存起来 - 在
render
的时候要额外加逻辑,把这些数据同步一下 - 同步之后千万记得要把存着的东西删掉,不然下次再
render
的时候会给同步回去造成错乱
但依旧不觉得这样很顺,甚至感觉更混乱,主要原因:
setContent
和setText
对应的其实是一个东西,前后要相互覆盖,在没有main
的情况下如何保存- 在没有
main
的时候,使用setContent
之后,再getText
,这里set和get的目标不一样,但对应的是一个字段,转换很恶心 - 仅一个东西,就要在
setXxx
和render
中加很多分支,更复杂的控件,更多的get
和set
接口要怎么办,比如Dialog
的setBody
、setTitle
、setFoot
以及这3个的setXxxText
版本,控件要有多少精力在处理这之间的同步?
from esui.
补充一下,render
中的数据同步逻辑是无论如何都要有的,和这个话题其实没关系(因为构造函数的时候能传东西进来)。但最主要的问题就是像setContent
和setText
两个方法对应一个东西的时候,其同步问题很严重,以及set了HTML后get文本问题同样很严重。
from esui.
控件开发里对于main的创建时机的考虑确实也让我很头疼。太多地方依赖这个main。总是要做各种判断。所以我在想,如果main这样必须,莫不如在构造函数里就直接创建了。两种情况:
- 参数中包含main,使用参数main
- 参数中不包含main,创建空div,赋为控件的main。
其实没有必要把main的创建延迟到appendTo中执行。appendTo可以只专心的关注把控件塞到哪个位置就好了。
浅见,求拍。
from esui.
+1,我认为构造函数中统一处理,有main的直接用,没有main的直接createMain()
创建出来,保证构造函数之后main
肯定到位,不要再在各个阶段折腾这事了。
这样构造函数的过程是:
initOptions
把参数按各优先级合在一起- 有必要的话调用
createMain()
把main
弄出来 - 调用
syncProperties
一次把数据同步到main
上(无论一开始有没有main
这步肯定要做)
再之后全部是有main
的情况,另外render()
中会调用syncProperties
from esui.
莫不如在构造函数里就直接创建了
+1
from esui.
那这里要再理一下构造函数的抽象级流程,切成几块让子控件各自实现1-N块,比如initOptions
、createMain
、syncProperties
这些
from esui.
syncProperties貌似没办法简单解决吧,还有,这货貌似不需要独立实现
from esui.
syncProperties貌似没办法简单解决吧,还有,这货貌似不需要独立实现
setProperties
会调用repaint()
,但repaint
只在生命周期处于 RENDERED 的时候才会调用render
,为了保证在new
以后、render()
以前的时间里,属性也能同步到main
上,需要这么个东西。然后render
默认会调用一下这东西
from esui.
- input的name是同步不了的
- 同步不是简单的setAttribute,不同属性的同步逻辑是不同的
- render前为啥需要同步到main上
from esui.
syncProperties
是各控件实现自己写的,能同步哪些由TextBox、Dialog等的开发者自己写,不是基类方法。
from esui.
设想这样的代码:
var label = new Label({ text: 'abc' }); // 这时创建了main,且没同步text
label.setText('xyz'); // setText直接改了main.innerText
label.render(); // render的时候有setProperties
注意最后这个,当render()
的时候,他不知道 作为属性的abc 和 在main上的innerText 到底哪个是最新的,那么render()
的时候到底要不要把main.innerText
改成 abc 呢:
- 改了,显然就错了。
- 不改吧,如果没有
setText
那一行,这个Label就没文字了,也是错的。
所以,在构造函数里,得同步一次
from esui.
以control上的text属性为准呀
from esui.
那为什么我明明setText
过了,渲染之后竟然变回构造函数给的东西了呢,我觉得解释不通啊。
from esui.
setText没写this.text = value吧
from esui.
没写,而且我觉得不写比较好,同样getText
也不拿自己的text
属性。要是setText
写自己的属性,同时又同步main
的innerText
,会有这样的问题:
var label = new Label({ text: 'abc' });
label.render(); // 这时显示的是abc
label.setText('xyz'); // 这时显示的是xyz,且`text`属性也是xyz
label.setProperties({
xxx: 123
}); // 这里会同步xxx属性
最后的setProperties
,同步xxx属性之外,因为控件本身上有个text
属性,他也会去同步,但text
本身就和innerText
已经同步了的,这一次同步是浪费的。
from esui.
个人觉得,一切数据以control上的属性为准。
对于setText,我想法的实现大概是:
this.text = value;
if (this.isInStage('RENDER')) {
this.main.innerText = this.text;
}
因为set(prop,value)方法的实现本来就是
this.prop = value;
this.repaint();
所以setXxx应该从逻辑上保持一致。
from esui.
每个setXxx
都要一个if
分支很头疼啊……
那么标准的set(prop, value)
要不要也加上生命周期的判断?
from esui.
另外还有一个问题,比如setText
和setHTML
对应的其实是同一个东西(main.innerHTML
),那么
var label = new Label();
label.setText('abc');
label.setHTML('<a href="http://www.baidu.com">xyz</a>');
label.render();
在render()
之后到底是 abc 还是 xyz 呢?多对一的属性怎么控制优先级,无论你认为哪个优先,上面2和3行代码对换总能不符合要求
from esui.
那么标准的set(prop, value)要不要也加上生命周期的判断?
repaint里+了
from esui.
setText调用:
this.setHTML()
from esui.
重新整理了一下流程,我提议改成这样:
控件提供3个抽象方法:
createMain()
:在构造函数中,没有main
时,创建一个空的main
出来initOptions()
:处理各种选项,全部放到自己身上renderSelf()
:控件本身自有的渲染逻辑
控件基类的构造函数:
function Control(options) {
if (!options.main) {
options.main = this.createMain();
}
this.initOptions();
}
控件基类的render方法:
Control.prototype.render = function () {
this.fire('beforerender');
// helper.renderMain的逻辑
this.renderSelf();
this.fire('afterrender');
}
把render这块的东西放基类里来,不在helper
模块中。几乎每一个控件都要有自己的渲染逻辑,不抽象一个renderSelf()
出来,就要重写render()
,重写之后又要写调用helper.beforeRender()
之类的方法,这是重复工作没啥意思。虽然说希望把更多的东西放helper
中,但也不好让写控件的人太痛苦
from esui.
现在的情况就简单了,不存在main
有没有的问题,只有控件是不是已经render过是两个阶段,需要一定的if分支来支持,还不算太难。
from esui.
果然还是得有模板方法。因为之前说不希望控件对象上有太多东西,我竭力避免使用模板方法了来着。
其实,renderSelf就干了两件事情,一个renderMain(初始化main的行为),一个repaint(属性体现到视图)。Select这类控件多一个renderLayer
from esui.
renderMain
也应该抽象在模板之中,renderSelf
只处理属性到视图这块就行了吧?
from esui.
我原来抽象的repaint
方法就是用来干这个的。。。
from esui.
但是repaint
不是调用的render
吗……
from esui.
@errorrik 这个模板方法的抽象是你来还是我来?
helper那东西,提供的是与控件本身生命周期关系不大的辅助方法,并不是指一定要干得控件上一个protected方法都没有……
from esui.
setText调用:this.setHTML()
看这代码:
var label = new Label();
label.setContent('<span>abc</span>');
console.log(label.getText());
由于控件没有渲染,这个setContent
不会直接反应到main
上,而是保存在this.content
这个属性上,等着渲染的时候同步。
那么这个label.getText()
你觉得应该返回什么?按main.innerText
的逻辑来,应该是 abc ,但这里main
不可用,所以只能返回 abc 这一串,但这是错误的(显然这不是 文字 而是 HTML )
from esui.
再一个问题,前面已经提出过,但没讨论清楚:
var label = new Label();
label.appendTo(document.body);
label.setText('abc');
label.setProperties({ title: 'xyz' });
在setProperties
的时候,调用过程是:setProperties -> repaint -> render -> renderSelf
。在renderSelf
中,会把自己的属性和main
的视图同步一下。
这个时候就有一个问题了:可以读到this.text
属性是 abc ,但是没办法知道这是已经同步过的还是没有同步过的,因此最坏的打算,就是执行this.main.innerText = this.text
来完成同步。但事实上,看上面的代码,显然setProperties
里是没有text
属性的,也就是说这个innerText
的赋值,以及因这个赋值产生的重渲染(甚至重布局)都是浪费的。
我最初的设想是:
Label.prototype.setText = function (text) {
// 不管是不是渲染的,统统放到`main`上
this.main.innerText = text;
};
Label.prototype.renderSelf = function () {
if (this.text) {
this.setText(this.text);
// 用完了删掉,避免下次又浪费性地同步
this.text = null;
}
};
Control.prototype.setProperties = function (properties) {
lib.extend(this, properties);
// 最终会调用renderSelf同步属性,同步完会删掉
this.repaint();
};
通过不在对象上保留属性,来避免这种浪费性地渲染。
from esui.
控件从helper.beforeXxx
及helper.afterXxx
这种形式,改为模板方法,这个我已经完成了,现在的结构是这样:
constructor(options)
-> 初始化一堆东西
-> initOptions(options) // 子类实现
-> createMain() // 如果没给main
-> 创建id
-> helper.initViewContext(this
-> helper.initExtensions(this)
-> this.fire('init')
构造函数过程中,各控件自己实现initOptions
和createMain
即可
render()
-> this.fire('beforerender')
-> 给main加上id
-> helper.addClass(this, this.main)
-> this.setDisabled(this.disabled)
-> this.renderSelf() // 子类实现
-> this.fire('afterrender')
-> this.lifeCycle = Control.LifeCycle.RENDERED
render
方法中,各控件自己实现renderSelf
即可
不过上面的几个问题没讨论清楚,我暂时不把这代码提交上来(因为测试过不了,又不清楚控件怎么改是最好的),问题讨论清楚了我就放代码上来, @errorrik 不用实现这些了。
请其他参与者也注意一下,控件的构造的渲染流程大概就会改成上面所说的,不再像以前一样反复调helper
的N多方法了,届时麻烦大家跟进自己手上的控件。
from esui.
重新整理了一下,记录下我现在的看法供后续讨论。
首先,控件有很多属性,但整体上,这些属性要分为3类:
- 样式/状态 类属性:如
width
、height
、title
、alt
、disabled
,这些属性的特点是通过main
上的 属性或样式 操纵,不涉及到HTML的变化 - 内容/结构 类属性:如
text
、content
、foot
,这些属性的特点是通过拼接HTML形成新的内容,然后刷新自身的视图来实现 - 交互/值 类属性:如
value
、checked
,这些属性的特点是 用户的行为会导致其变化,且控件自身保持的属性无法保证同步
其中 交互/值 类属性最为特殊,参考以下代码:
var textbox = new TextBox({ value: 'abc' });
textbox.render();
// 然后用户在文本框中输入了xyz
textbox.setProperties({ title: 'changed' });
在上面的代码中,setProperties
的默认实现为将title
放到实例上,然后调用repaint
。但是repaint
由于不知道setProperties
更新了哪些属性,所以要同步 所有属性 。这时他会发现value
的值是 abc ,但不知道main.value
和this.value
哪一个才是正确的, 无法处理这个逻辑 。
而 样式/状态 和 内容/结构 这两类属性,由于其修改方式不同,如果混用,则会在 任何属性更新时都刷新整个视图 导致 效率的低下 ,如仅仅修改width
会导致一个Dialog
把自己从head
到foot
都渲染一次,这种性能的损失在一些特定的情况下 是无法接受的 ,比如业务系统需要给Dialog
做一个resize功能,通过setProperties({ width: xxx, height: xxx })
来随鼠标移动改变大小。
基于以上的考虑,我们需要解决的问题是 根据更新的属性,进行有判断可掌握地刷新 。考虑到更新控件属性只有3个入口:
setXxx(value)
这个本身会控制单个属性的刷新,但往往是调用repaint()
解决问题set(name, value)
这个会默认调用repaint()
setProperties(properties)
这个也会 默认调用repaint()
从这方面来看,事实上最后更新视图全是在repaint
中完成的,因此为了可以做到 有判断地刷新 ,需要在这个入口进行处理。
我的提议是:修改repaint
的签名为{void} repaint({Array=} changes)
,把 更新过的属性 交给repaint
方法,以使控件开发者可以进行判断,如果changes
参数不存在,则是一次 全部刷新 ,即所有属性都要更新(第一次render的状态)。
因此setProperties
的实现就是这样:
Control.prototype.setProperties = function(properties) {
var changes = [];
for (var key in properties) {
if (properties.hasOwnProperty(key)) {
var oldValue = this[key];
var newValud = properties[key];
if (oldValue !== newValue) {
this[key] = newValue;
var record = {
name: key,
oldValue: oldValue,
newValue: newValue
};
changes.push(record);
}
}
}
this.repaint(changes);
}
另外的setXxx(value)
和set(name, value)
也可以按照这个思路来实现,比如:
Control.prototype.set = function (name, value) {
var oldValue = this[name];
if (oldValue !== value) {
this[name] = value;
this.repaint([{ name: name, oldValue: oldValue, newValue: value ]};
}
}
from esui.
另外关于render
和repaint
的关系,现在是repaint
调用render
,但我认为应该反过来:
Control.prototype.render = function () {
if (this.lifeCycle === Control.LifeCycle.INITED) {
this.fire('beforerender');
}
this.repaint();
this.lifeCycle = Control.LifeCycle.RENDERED;
if (this.lifeCycle === Control.LifeCycle.INITED) {
this.fire('afterrender');
}
}
而repaint
才是真正处理视图更新的那个。原因是repaint
是一个 @Protected 的方法,而render
是 @public 的,正常的设计下,可重写的、核心实现的,应该在 @Protected 方法上, @public 方法由于会被外部调用,应该成为一个模板
from esui.
具体的我先push了一份上来,随时准备有反对意见后回滚(不上来没办法给人看到真实的样子啊),Control的流程改了,另外Label和Panel相应实现,个人感觉还行……
from esui.
在n天以后,回来看到这么多条,我看到了挣扎的过程。不过有很多东西梳理清楚了。那我马后炮下,说说我的想法:
constructor过程
- 初始化一堆东西
- initOptions(options) // 子类实现
- createMain() // 如果没给main
- 创建id
- helper.initViewContext(this)
- helper.initExtensions(this)
- this.fire('init')
这个过程我第一眼看觉得ok,没有问题。但是,待会请看属性的视图刷新
那节。
额外要提的一点是,id是用户没传时创建。虽然这么提有点多余,还是怕漏了。
render过程
灰大给的代码里:
if (this.lifeCycle === Control.LifeCycle.INITED) {
this.fire('beforerender');
}
其实就是我实现时候的helper.beforeRender。所以总结下:
- fire beforerender
- repaint
- stage rendered
- fire afterrender
但是,我个人觉得,这个过程有点单薄。主要是缺少了初始化控件基础结构
这个环节。
- 对于Dialog控件,第一次render的时候,是要创建head的。后面repaint的时候,是不用创建的。
- 同理,有的子控件是只有第一次render才会创建的。
属性的视图刷新
原先的设想是:
- repaint负责刷新控件属性到视图上。所以,
repaint
不关心任何控件基础结构
的事情。 - repaint刷新视图的依据是
控件属性
。这和数据->视图
单向映射是一个道理。
所以,这涉及到控件属性
如何保持最新鲜。保鲜这种事情虽然女人最擅长,但我尝试分解下,有下面几个点:
- setXxx的时候:无论如何,需要更新控件属性。
- set(xxx,value)的时候:如果有setXxx,则有保障;如果没有,也必须this.xxx = value。
- setProperties的时候:extend实现,可以保障。
- constructor的时候:如果有main,并且用户没传相应的属性,则相应属性应该从main上读出来。
根据上面所说,和之前的讨论,constructor的过程里,createMain应该在initOptions前面。initOptions应该负责从main读出必要的属性值,并写到控件属性上。
render和repaint的关系
我脑海里其实一直是repaint负责刷新属性到视图上的,所以应该也必须是render调用repaint。
我也不知道为啥当时写的repaint
调用render
,估计是写快了脑子抽了。给大家带来困惑了,对不起。
repaint({Array=} changes)
可选择属性的刷新视图,我的问题是:
- 这么设计,是不是每个视图都要有一个刷新视图的方法?比如repaintValue,repaintTitle......
- 如果是的话setTitle是不是就变成
this.title = title;this.repaintTitle();
from esui.
看到一堆粗又黑的字的时候,我明白这件事离讨论出结果不远了……
render过程
初始化控件基础结构 这个事,事实上我也遇到了,在做Select
控件的时候,第一次渲染要生成一个隐藏的<select>
元素,要对datasource
中的每一项生成一个<span>
元素。
在现在的设计中, 第一次调用repaint
有一个独有的特征,那就是不会传递函数的参数changes
。所以我在Select
的实现是这样的:
Select.prototype.repaint = function (changes) {
if (!changes) {
initDOMStructure(this);
}
// 同步自己的属性
}
function initDOMStructure(control) {
for (var i = 0; i < control.datasource.length; i++) {
// 生成对应的<span>元素
}
// 生成隐藏的<select>元素
}
属性的视图刷新
关于createMain
和initOptions
的顺序,我调整过好几次,最后决定initOptions
在前,原因是有些控件,createMain
需要options
中的一些选项,比如TextBox
控件需要mode
参数来决定创建<input>
还是<textarea>
。反之,initOptions
中可以通过options.main
来拿到构造函数中传的main
元素,所以不会受限。
setProperties
中使用extend
把传入的参数放到自身,然后调用repaint
,绝对会出现某些特殊的属性无法同步的问题,原因是正常的repaint
根本不知道setProperties
更新了哪些属性,加之有些属性是会在用户的操作之后丢失同步的(典型的value
之类),我上面说的TextBox
的问题就是典型,再放一次代码:
var textbox = new TextBox({ value: 'abc' });
textbox.render();
// 然后用户在文本框中输入了xyz
textbox.setProperties({ title: 'changed' });
// 这时候要不要执行`this.main.value = this.value;`呢
也正因为这个死结,我才设计了repaint({Array=} changes)
这个东西,不知 @errorrik 对这个有没有更好的解决办法?
repaint的设计
我在Label
中的实现是这样的:
Label.prototype.setText = function (text) {
this.setProperties({ text: text });
};
Label.prototype.repaint = function (changes) {
for (var i = 0; i < this.changes.length; i++) {
var record = this.changes[i];
if (record.name === 'text') {
// 说明text有更新
this.main.innerHTML = lib.encodeHTML(this.text);
}
if (record.name === 'title') {
// 说明title有更新
this.main.title = lib.encodeHTML(this.title);
}
}
};
这种方案是让repaint
实现所有属性的绘制,所有的setXxx
统一到repaint
上去,具有高度的一致性。
当然在上面的每个if
,你都可以抽成paintXxx
方法,这是控件实现者的自由,同时与控件的复杂性有关系,不作规定。
推荐的repaint实现
首先第一次调用的时候是没有changes
这参数的,这时认为要 更新所有属性 ,所以要知道自己有哪些属性,然后统一地更新就行。
如前所言,把属性分为2种:
- 修改
main
的样式或者属性的 - 修改
main
的HTML的
设定哪些属性在哪一种中,并对2种属性作不同处理:
- 修改样式或属性的,立刻修改生效
- 修改HTML的,通知一下 要刷新HTML ,同时无论更新了几个要修改HTML的属性,都把HTML全刷一次就好
典型实现方法:
var allProperties = [
{ name: 'title' },
{ name: 'alt' },
{ name: 'width' },
{ name: 'height' },
{ name: 'border' },
{ name: 'text' },
];
var attributeProperties = {
title: true,
alt: true
};
var styleProperties = {
width: true,
height: true,
border: true
};
repaint = function (changes) {
// 如果没传,就认为更新所有属性好了,这样就统一实现了
changes = changes || allProperties;
// 如果有遇上需要更新HTML的属性,就变成true来控制重绘innerHTML
var shouldRepaintContent = false;
for (var i = 0; i < changes.length; i++) {
var record = changes[i];
var name = record.name;
if (attributeProperties[name]) {
this.main[name] = this[name];
}
else if (styleProperties[name]) {
this.main.style[name] = this[name];
}
else {
shouldRepaintContent = true;
}
}
if (shouldRepaintContent) {
this.main.innerHTML = getHTML(this);
}
}
function getHTML (control) {
// 通过模板把HTML弄出来
}
不过这模型依旧不具有普遍性,比如Dialog
这种复杂控件,会有其它基础控件组成,所以
Dialog.prototype.repaint = function (changes) {
for (var i = 0; i < changes.length; i++) {
var record = changes[i];
var name = record.name;
if (name === 'bodyContent') {
this.body.setContent(this.bodyContent);
}
}
}
会出现这种代理掉属性更新的。因此不考虑在控件抽象层面上做attributeProperties
和styleProperties
这种东西,各控件自己去实现。
from esui.
render过程与初始化控件基础结构
现在的方案,有两个小问题:
- 用户可能会多次使用控件的render,也就是会多次运行无参的
repaint
changes属性选择
和是否初始化结构
其实是两个逻辑,用一个参数来描述有些不对劲
建议是render里直接调用initStructure
,这么个调法(伪马请意会):
if ( isInStage( 'INITED' ) ) {
initStructure();
}
createMain和initOptions的顺序
首先,这是个比较细节的问题,无伤大雅。但是既然提出来,也可以讨论下,以后追究时有凭据。
根据之前的讨论,createMain
放在constructor里是为了能initOptions的时候不至于面对this.main可能为空,而要做一堆多余的判断。
所以,如果initOptions
在前,两个问题,如果能接受,我觉得没问题:
- initOptions就可能会有个if判断,我猜大概这么写。这个if是否能接受?
var optionsInMain = {};
if ( options.main ) {
// 往optionsInMain上读
}
lib.extend( this, defaultOptions, optionsInMain, options ); - createMain是否就可以放回去到render里面做?
setProperties中的repaint
TextBox是一个特例,但也是一个典型。
我一直认为,原则是以控件属性为准
。如果这个原则合理,那我们在这里要解决的问题是如何保证TextBox的value是最新鲜的
。基于这货,我想到两招:
- TextBox在实现上,input/propertyChange时更新value属性
- repaint使用get方法取属性值
还有,setProperties
中是直接调用无参repaint还是有参的部分属性repaint,我觉得都行。
repaint实现
区分属性的视图刷新行为是一个好主意!并且repaint需要知道全刷新是刷新哪些东西。下面是我在 @otakustay 的抽象基础上进一步做的抽象,主要更新点是:
- 砍掉
attributeProperties
和styleProperties
- 将
allProperties
原来纯name的数组配置,里面每项包含repaint行为 - 预置常见的repaint行为类
代码描述如下:
var propertyRepainters = [
new StyleRepainter( 'width' ),
new StyleRepainter( 'height' ),
new AttributeRepainter( 'title' ),
{
name: 'text',
repaint: function ( control ) {
control.main.innerHTML = control.getText();
}
}
];
// 使用默认repaint可以这样
xx.prototype.repaint = createControlRepainter( propertyRepainters );
// 子类(如复杂的table等)override时可以这样
var superRepaint = createControlRepainter( propertyRepainters );
xx.prototype.repaint = function () {
superRepaint();
// do something
};
from esui.
So, what's all this then?
from esui.
@Protected sorry, we are discussing about method accessibility and we are not aware that @Protected would notify you, going to find another way to write our public / protected / private synonyms
from esui.
No worries, it happens all the time ;) Also I reported it to github years ago and they never bothered to fix it...
from esui.
render过程与初始化控件基础结构
这个OK,对于 用户多次调用render
这个问题事实上我没有想过也不觉得该这么做。不过既然render
是 public 的,那就考虑一下这个事,把initStruture
抽象出来。
当然render
在调用repaint
时,依旧是无参的,意为 同步所有属性
createMain和initOptions的顺序
createMain
越往后,各方法要做的事越多:
var label = new Label();
label.setText('abc');
lalbel.appendTo(document.body);
label.setText('xyz');
在render
之前和之后各个方法的逻辑要有分支(决定是否同步到main
上),导致开发控件成本增加不少,我觉得并不合适。
另一个方法就是createMain
的时候把options
丢过去,应该能解决问题。
事实上,把createMain
放在构造函数中的目的是解决上面代码的问题,而不是initOptions
……
setProperties中的repaint
如果 repaint
中使用get
方法取值 ,估计这辈子也不能通过setProperties
更新TextBox
的value
了:
TextBox.prototype.getValue = function () {
return this.main.value;
};
TextBox.prototype.repaint = function () {
// ...
this.main.value = this.get('value');
}
显而易见……
而 TextBox上玩监控用户输入 的实现成本并不低,真要做好了其实就能玩双向绑定了。我反对双向绑定的一个很大因素就是对基础建设的要求太高……当然如果真觉得能100%做出同步来,无疑是好的(要考虑几乎所有的<input>
类型及<textarea>
,包括date
、range
这种很可能用户根本不手动输入的东西)
setProperties
调用有参部分属性的paint
无非就是为了:
- 区分属性视图刷新,可以进行局部刷新提高效率
- 解决
value
这种奇怪的东西的同步问题,按我前面说的,我觉得 始终保持value属性同步 并不是那么好玩的事
repaint实现
我觉得可以,不过要我玩的话会变成函数式编程:
var painters = [
style('width'),
style('height'),
attribute('title'),
{
name: 'text',
paint: ...
}
];
果然我的编程理念还是比较奇怪的吧~
from esui.
建议是render里直接调用initStructure
我又思考了一下,在createMain
中完成这些事,同时无论是否传了main
,都会调用createMain
,这个思路如何
createMain = function () {
if (!this.main) {
this.main = document.createElement('div');
}
this.main.innerHTML = ...;
}
from esui.
initStructure和createMain
initStructure里调用createMain的话,那constructor里不调用了?
setProperties中的repaint
我理解,esui大多数控件都不是键盘输入的,比如Calendar,Select都是模拟的。那就不存在value不同步的问题。剩下的,也就是TextBox对应的input[type=text|password]或者textarea了。这个我们已经有了很好的办法,不是么。
setXxx与视图刷新(createMain和initOptions顺序 的 补充讨论)
setXxx方法的实现我觉得应该是这样:
setText = function ( text ) {
this.text = text;
findPainters( 'text' ).paint( this );
};
那么paint方法的实现应该判断控件是处于RENDERED:
if ( control.isInStage( 'RENDERED' ) ) {
control.main.innerText = control.text;
}
这样,createMain应该就不是必须
在constructor里调用,也就不存在顺序问题了。这也呼应上initStructure和createMain
上面讨论的问题。
repaint的实现
灰大的风格无疑是清爽的,我觉得这么实现挺好。但实际代码可能就没那么清爽,可能会变成这样:
var painter = require( './controlHelper').painter;
var painters = [
painter.style('width'),
painter.style('height'),
painter.attribute('title'),
{
name: 'text',
paint: ...
}
];
当然,可以var style = painter.style;
from esui.
initStructure和createMain
initStructure里调用createMain的话,那constructor里不调用了?
我的意思是没有initStructure
,让createMain
承担这个任务,createMain
的描述是 创建/构造完整的main
元素 ,而不再像以前一样只弄一个<div>
出来
所以这里的流程是
- constructor
- createMain(options) <-- 我认为把
options
给createMain
足够使createMain
在前,initOptions
在后,呼应后面的顺序问题 - initOptions
- render <-- render不再有“第一次”这种区别,
createMain
负责把完整的DOM构建出来 - repaint
setProperties中的repaint
覆盖够全面(用setInterval
检测之类的)确实可以,我不在意去做这样的实现,那么我们就要求所有InputControl
实现value
的绝对同步吧
setXxx与视图刷新
createMain
越晚就让各方法的实现越复杂,为什么我们一定要让每一个setXxx
都判断一下是否渲染呢……我觉得initOptions
和createMain
的顺序调整得到的收益比不过每个方法加一个if
分支的代价
repaint的实现
本来是这样的:
var paint = require('./painter');
var painters = [
paint.style('width'),
paint.attribute('title')
];
我更喜欢读起来像标准的句式的,比如 paint style width 就感觉很通畅。当然这个真心无所谓,用class的实现也很干净利索,等其它问题都清楚了这个随便定一下就好
from esui.
还是不对,createMain
无论放在哪,肯定有个地方要对options.main
是否存在判断,不在createMain
里就是在initOptions
里,找不到啥办法可以没有这个if
分支。
所以我现在还是回到以前的看法,即initOptions -> createMain
这个流程,在initOptions
里要求有if
分支。
另外createMain
还是希望在构造函数中搞定,很难接受每个setXxx
都要写个if
from esui.
我的意思是没有initStructure,让createMain承担这个任务,createMain的描述是 创建/构造完整的main元素 ,而不再像以前一样只弄一个div出来
那是否构造子控件(比如Select的Layer)?
- 如果是的话,构造函数其实就已经render了
- 如果不是的话,render方法留下来干嘛?触发beforerender和afterrender事件?
还是不对,createMain无论放在哪,肯定有个地方要对options.main是否存在判断,不在createMain里就是在initOptions里,找不到啥办法可以没有这个if分支。
createMain是必须判断this.main是否存在的。也就是说:
- 如果createMain在前面,则initOptions可以不用判断options.main是否存在。
- 如果initOptions在前面,initOptions需要判断options.main,createMain也要判断this.main
createMain还是希望在构造函数中搞定,很难接受每个setXxx都要写个if
我觉得可以。但是,即使不在构造函数中搞定,也不是每个setXxx都要写个if。
- setXxx写法是
设置属性
->调用相应painter更新视图
- painter需要判断控件是否处于
RENDERED
。也就是说,就算main存在,控件处于INITED,也是不应该执行更新视图的行为的。而且,更新视图的行为可能更新的不是main,而可能是调用子控件的某个方法,以及等等。所以是否要执行更新视图行为
和main是否存在
无关,只和控件当前所属阶段有关。 - 对于视图更新的if,是可以包装的,如灰大之前写的style('height')
from esui.
那是否构造子控件(比如Select的Layer)
由控件决定,我的Select是这样的:
- 在
createMain
中,建立一个div > span
的结构,建立一个隐藏的<select>
元素 - 将Layer的建立做懒加载,在第一次
open
的时候建立的,如果不用懒加载,我会放到createMain
中 - 在
render
中,根据自身的selectedIndex
和value
,给<span>
元素设innerHTML
但createMain
只建立稳定的结构 ,不同步数据,而render
则是把自己的属性和这个DOM结构中绑在一起。而一些 有数据才能生成的结构 则是在render
中做的。比如Dialog
的foot
依赖于其配置,closeButton
也依赖于配置,则是在render
中建立的,因为这些可能根据配置的变化在多次render
中创建或者销毁,是动态的,而createMain
负责静态的。
如果initOptions在前面,initOptions需要判断options.main,createMain也要判断this.main
所以我其实建议在createMain
的时候给构造函数接受的options
对象,这样createMain
可以判断options.main
是否有(决定是否要创建),再根据其它参数决定怎么创建,这样createMain
在前面,initOptions
不需要再行判断
painter需要判断控件是否处于RENDERED
控件越复杂,painter
的用武之地越小,像Table这种控件有太多东西还是要自己写函数,则if
依旧会大量存在,当然为了效率,确实复杂控件无论有没有main
,都会判断是在 RENDERED 状态以减少DOM操作。
不过如果前面的createMain
和initOptions
顺序确定是createMain
在前,那么createMain
无论如何也是在构造函数里调用了,这个话题似乎也自然有了结论?
from esui.
控件越复杂,painter的用武之地越小,像Table这种控件有太多东西还是要自己写函数,则if依旧会大量存在,当然为了效率,确实复杂控件无论有没有main,都会判断是在 RENDERED 状态以减少DOM操作。
在我看来,这种复杂控件的repaint方法,无论如何肯定是需要override的。其做不到每个属性都刷新的是局部视图
,Table是整体视图取决于多个属性的综合
。当然,其bodyHeight之类的属性可以局部刷新。类似我之前的例子的描述:
// 子类(如复杂的table等)override时可以这样
var painters = { name: 'bodyHeight', paint: function ( control ) { //..... } };
var superRepaint = createControlRepainter( painters );
xx.prototype.repaint = function () {
superRepaint();
// do something
};
不过如果前面的createMain和initOptions顺序确定是createMain在前,那么createMain无论如何也是在构造函数里调用了,这个话题似乎也自然有了结论?
嗯,可以在构造函数里调用。我们的郁结点已经从在构造函数还是在render里调用
变成了createMain的职责
。
createMain只建立稳定的结构,是有问题的。拿Dialog举例,foot是动态结构,head是固定结构,那么,我们有可能:
- 在createMain中create head div,并append到main
- 在render中create foot div,并append到main
这是一个很奇怪的事情。开发Dialog时,我们要override两个方法,并且这两个方法里都要create一些控件内部的结构
。
所以,我的建议是:createMain在constructor里调用,只负责main。控件内部结构由initStructure负责。这样更清晰,而且Dialog的开发者就能只override一个方法。
另外,对于Select来讲,建立一个隐藏的input[type=hidden]
这个事情,应该是createMain负责。因为标准里控件主元素章节规定了,这个逻辑是控件主元素逻辑。以下内容选自标准文档:
复杂输入控件的控件主元素
本章节描述了对复杂输入控件渲染阶段对控件主元素
的处理。
当控件使用者
未传入控件主元素
,调用appendTo渲染时:
- 如果控件实例具有
name
属性,创建具有name attribute的input[type=hidden] - 创建div做为
控件主元素
,执行后续渲染逻辑
当控件使用者
传入div元素
,调用render渲染时:
- 如果控件实例不具有
name
属性,读取div元素
的name attribute,做为控件实例的name
属性 - 如果控件实例具有
name
属性,创建具有name attribute的input[type=hidden] - 使用传入的
div元素
做为控件主元素
,执行后续渲染逻辑
当控件使用者
传入input[type=text]元素
,调用render渲染时:
- 如果控件实例不具有
name
属性,读取input[type=text]元素
的name attribute,做为控件实例的name
属性 - 如果控件实例具有
name
属性,创建具有name attribute的input[type=hidden] - 移除传入的
input[type=text]元素
- 创建div做为
控件主元素
,执行后续渲染逻辑
from esui.
Dialog
现在是 @DDDBear 在做,所以我比较熟悉(什么逻辑),Dialog
的现状是这样的:
width
和height
单独通过style
设置- 剩余的所有属性,不管是变了1个,还是变了N个,统统重写
main.innerHTML
这样来看,Dialog
认为所有的结构都是动态的,这个真的更多是取决与控件实现者自己的想法。
当然其实我不认为Dialog
应该这么做,这是后话和本话题没关系……
另外,Select
控件应该放一个隐藏的input[type=hidden]
,是我误搞成隐藏的<select>
了,这属于实现上的错误。不过传达的概念是,像这个隐藏的input[type=hidden]
,就是一个 稳定的结构 ,无论其它属性怎么变都会一直存在,所以在createMain
中搞定。
精确定义一下,所谓 稳定结构 ,就是指 render中只会使用不会覆盖或销毁 的那些元素,具体是哪些元素,控制权交由控件实现者应该没什么问题?
from esui.
从实现来说,我觉得,创建input[type=hidden]
的逻辑应该是InputControl
这个抽象类去overrideControl
的createMain
。其他所有具体类应该不关心这个创建过程。
可见,之前我对创建input[type=hidden]
的定义是:控件主元素创建逻辑。这个定义的出发点在于:
- 创建控件结构,是控件开发者的事情
- 控件开发者基本不需要override render方法
- 控件开发者基本不需要override createMain方法
对于Dialog:
- @DDDBear 现在的实现,我首先认为repaint时候重写main的innerHTML是合理的。但这种实现下,Dialog不包含任何内部
稳定结构
,所以对我们现在的讨论没有帮助 - 假设要区分
稳定结构
,并且区分权交由实现者控制:实现者把head、body当成稳定结构
,foot当成不稳定结构
。那么,直接重写main的innerHTML的实现就不行了。并且同时会出现我刚才提到的问题:
- override createMain,在createMain中create head div,并append到main
- override render,在render中create foot div,并append到main
所以,我认为,抽象initStructure
,render
调用initStructure
,initStructure
由控件实现者override。这个机制是必要的。
from esui.
我很担心并相信的一点是:讨论了这么多,其实是没人看,没人理解的……
from esui.
我们能定下来让别人跟着做就行了,不发言者没人权……
关于input[type=hidden]
的问题,如果InputControl
来控制,则TextBox
和TextLine
需要重写掉createMain
方法,如果是这样的话我认同。
Dialog这事,我认为Dialog应该是:
- head是个
Label
控件 - body和foot是个
Panel
控件 setHead
->head.setText
setBody
/setFoot
->body/foot.setContent
这样要求Dialog的结构是稳定的(有head、body、foot共3个子控件,仅仅是显示和隐藏而已),而不是用innerHTML
来处理,理由我和 @DDDBear 有讨论过不少次,因为是单个控件的实现问题所以就没在这里记录。
关于initStructure
,有这个也是合理的,在render
中调用OK,我只是在想是否createMain
能搞定而省掉一个方法,既然createMain
处理这些会有副作用,并引起来些歧义,那么就让initStructure
存在吧
from esui.
现在感觉问题都已经有结论了,我明天整理一份新的流程,并相应把Control
和InputControl
改了
from esui.
嗯,看来这个issue的问题都有结论了。真不容易~
Dialog的问题。我觉得稳定结构是更好的。额外有一点,期望能支持渲染已有的dom,并自动识别head和body等:
<div data-ui-type="Dialog">
<h3>MyTitle</h3>
<div>我是一堆文字或者内部的dom结构</div>
</div>
我就一提,不在这讨论Dialog了。等 @DDDBear 写完标准文档,在评审issue里讨论吧。
from esui.
我已经在 b3aa5ab 中把整理后的生命周期实现了,请参考Control
和InputControl
另外Label
和Panel
也跟着实现了,不过这两个控件很简单不需要initStructure
所以不具备啥参考价值就是了
painter
这个东西还没做,没确定是用对象构造还是函数构造,谁有兴趣就自己决定下给做了,没人做的话过几天我就做成函数式了- -
InputControl
中我没有实现默认的加input[type="hidden"]
的逻辑,因为遇到一些问题,回头我整理一下再发上来
TextBox
现在是完全不可用了,麻烦 @errorrik 再看一下跟着新的模式实现一次吧
from esui.
这里有这么多讨论...等有空了要来爬一下楼...
from esui.
这个Issue已经分散为多个子Issue,本身要讨论的事已经讨论清楚了,关
from esui.
Related Issues (20)
- 日程投放控件 HOT 4
- InputCollection 应当继承 ControlCollection HOT 1
- Table 的 overflowX 属性为非 hidden 的时候多出一个横向滚动条 HOT 3
- BoxGroup有一处事件没使用addDOMEvent绑定
- 解决set和setProperties触发change的问题
- Table的依赖不全
- 控件初始化子控件时的valueReplacer管理
- 控件初始化子控件时的valueReplacer管理
- 控件初始化子控件时的valueReplacer管理
- 指定元素的销毁子控件
- Select 控件对于value比较判断的兼容性问题 HOT 2
- 希望能添加一些布局相关的组件 HOT 4
- 对于带有数据源的控件是否应该支持外部不提供数据源的场景的表决 HOT 19
- addChild的时候添加校验 HOT 6
- viewContext的疑问 HOT 3
- 加个Lisence HOT 1
- MonthView的年月下拉框格式可调 HOT 1
- 渐变背景的问题
- Panel控件的addContent方法不适用table布局 HOT 2
- 关于拓展组件
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from esui.