Git Product home page Git Product logo

Comments (58)

errorrik avatar errorrik commented on July 3, 2024

问题1根据现有标准描述,问题2从使用角度考虑,我觉得:

  1. 允许调用,不允许异常
  2. 要显示为Hello World。

from esui.

otakustay avatar otakustay commented on July 3, 2024

那么意味着所有的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.

otakustay avatar otakustay commented on July 3, 2024

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的时候会给同步回去造成错乱

但依旧不觉得这样很顺,甚至感觉更混乱,主要原因:

  • setContentsetText对应的其实是一个东西,前后要相互覆盖,在没有main的情况下如何保存
  • 在没有main的时候,使用setContent之后,再getText,这里set和get的目标不一样,但对应的是一个字段,转换很恶心
  • 仅一个东西,就要在setXxxrender中加很多分支,更复杂的控件,更多的getset接口要怎么办,比如DialogsetBodysetTitlesetFoot以及这3个的setXxxText版本,控件要有多少精力在处理这之间的同步?

from esui.

otakustay avatar otakustay commented on July 3, 2024

补充一下,render中的数据同步逻辑是无论如何都要有的,和这个话题其实没关系(因为构造函数的时候能传东西进来)。但最主要的问题就是像setContentsetText两个方法对应一个东西的时候,其同步问题很严重,以及set了HTML后get文本问题同样很严重。

from esui.

DDDBear avatar DDDBear commented on July 3, 2024

控件开发里对于main的创建时机的考虑确实也让我很头疼。太多地方依赖这个main。总是要做各种判断。所以我在想,如果main这样必须,莫不如在构造函数里就直接创建了。两种情况:

  1. 参数中包含main,使用参数main
  2. 参数中不包含main,创建空div,赋为控件的main。

其实没有必要把main的创建延迟到appendTo中执行。appendTo可以只专心的关注把控件塞到哪个位置就好了。

浅见,求拍。

from esui.

otakustay avatar otakustay commented on July 3, 2024

+1,我认为构造函数中统一处理,有main的直接用,没有main的直接createMain()创建出来,保证构造函数之后main肯定到位,不要再在各个阶段折腾这事了。

这样构造函数的过程是:

  1. initOptions把参数按各优先级合在一起
  2. 有必要的话调用createMain()main弄出来
  3. 调用syncProperties一次把数据同步到main上(无论一开始有没有main这步肯定要做)

再之后全部是有main的情况,另外render()中会调用syncProperties

from esui.

errorrik avatar errorrik commented on July 3, 2024

莫不如在构造函数里就直接创建了

+1

from esui.

otakustay avatar otakustay commented on July 3, 2024

那这里要再理一下构造函数的抽象级流程,切成几块让子控件各自实现1-N块,比如initOptionscreateMainsyncProperties这些

from esui.

errorrik avatar errorrik commented on July 3, 2024

syncProperties貌似没办法简单解决吧,还有,这货貌似不需要独立实现

from esui.

otakustay avatar otakustay commented on July 3, 2024

syncProperties貌似没办法简单解决吧,还有,这货貌似不需要独立实现

setProperties会调用repaint(),但repaint只在生命周期处于 RENDERED 的时候才会调用render,为了保证在new以后、render()以前的时间里,属性也能同步到main上,需要这么个东西。然后render默认会调用一下这东西

from esui.

errorrik avatar errorrik commented on July 3, 2024
  1. input的name是同步不了的
  2. 同步不是简单的setAttribute,不同属性的同步逻辑是不同的
  3. render前为啥需要同步到main上

from esui.

otakustay avatar otakustay commented on July 3, 2024

syncProperties是各控件实现自己写的,能同步哪些由TextBox、Dialog等的开发者自己写,不是基类方法。

from esui.

otakustay avatar otakustay commented on July 3, 2024

设想这样的代码:

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.

errorrik avatar errorrik commented on July 3, 2024

以control上的text属性为准呀

from esui.

otakustay avatar otakustay commented on July 3, 2024

那为什么我明明setText过了,渲染之后竟然变回构造函数给的东西了呢,我觉得解释不通啊。

from esui.

errorrik avatar errorrik commented on July 3, 2024

setText没写this.text = value吧

from esui.

otakustay avatar otakustay commented on July 3, 2024

没写,而且我觉得不写比较好,同样getText也不拿自己的text属性。要是setText写自己的属性,同时又同步maininnerText,会有这样的问题:

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.

errorrik avatar errorrik commented on July 3, 2024

个人觉得,一切数据以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.

otakustay avatar otakustay commented on July 3, 2024

每个setXxx都要一个if分支很头疼啊……

那么标准的set(prop, value)要不要也加上生命周期的判断?

from esui.

otakustay avatar otakustay commented on July 3, 2024

另外还有一个问题,比如setTextsetHTML对应的其实是同一个东西(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.

errorrik avatar errorrik commented on July 3, 2024

那么标准的set(prop, value)要不要也加上生命周期的判断?

repaint里+了

from esui.

errorrik avatar errorrik commented on July 3, 2024

setText调用:

this.setHTML()

from esui.

otakustay avatar otakustay commented on July 3, 2024

重新整理了一下流程,我提议改成这样:

控件提供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.

otakustay avatar otakustay commented on July 3, 2024

现在的情况就简单了,不存在main有没有的问题,只有控件是不是已经render过是两个阶段,需要一定的if分支来支持,还不算太难。

from esui.

errorrik avatar errorrik commented on July 3, 2024

果然还是得有模板方法。因为之前说不希望控件对象上有太多东西,我竭力避免使用模板方法了来着。
其实,renderSelf就干了两件事情,一个renderMain(初始化main的行为),一个repaint(属性体现到视图)。Select这类控件多一个renderLayer

from esui.

otakustay avatar otakustay commented on July 3, 2024

renderMain也应该抽象在模板之中,renderSelf只处理属性到视图这块就行了吧?

from esui.

errorrik avatar errorrik commented on July 3, 2024

我原来抽象的repaint方法就是用来干这个的。。。

from esui.

otakustay avatar otakustay commented on July 3, 2024

但是repaint不是调用的render吗……

from esui.

otakustay avatar otakustay commented on July 3, 2024

@errorrik 这个模板方法的抽象是你来还是我来?

helper那东西,提供的是与控件本身生命周期关系不大的辅助方法,并不是指一定要干得控件上一个protected方法都没有……

from esui.

otakustay avatar otakustay commented on July 3, 2024

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.

otakustay avatar otakustay commented on July 3, 2024

再一个问题,前面已经提出过,但没讨论清楚:

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.

otakustay avatar otakustay commented on July 3, 2024

控件从helper.beforeXxxhelper.afterXxx这种形式,改为模板方法,这个我已经完成了,现在的结构是这样:

constructor(options)
    -> 初始化一堆东西
    -> initOptions(options) // 子类实现
    -> createMain() // 如果没给main
    -> 创建id
    -> helper.initViewContext(this
    -> helper.initExtensions(this)
    -> this.fire('init')

构造函数过程中,各控件自己实现initOptionscreateMain即可

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.

otakustay avatar otakustay commented on July 3, 2024

重新整理了一下,记录下我现在的看法供后续讨论。

首先,控件有很多属性,但整体上,这些属性要分为3类:

  • 样式/状态 类属性:如widthheighttitlealtdisabled,这些属性的特点是通过main上的 属性或样式 操纵,不涉及到HTML的变化
  • 内容/结构 类属性:如textcontentfoot,这些属性的特点是通过拼接HTML形成新的内容,然后刷新自身的视图来实现
  • 交互/值 类属性:如valuechecked,这些属性的特点是 用户的行为会导致其变化,且控件自身保持的属性无法保证同步

其中 交互/值 类属性最为特殊,参考以下代码:

var textbox = new TextBox({ value: 'abc' });
textbox.render();
// 然后用户在文本框中输入了xyz
textbox.setProperties({ title: 'changed' });

在上面的代码中,setProperties的默认实现为将title放到实例上,然后调用repaint。但是repaint由于不知道setProperties更新了哪些属性,所以要同步 所有属性 。这时他会发现value的值是 abc ,但不知道main.valuethis.value哪一个才是正确的, 无法处理这个逻辑

样式/状态内容/结构 这两类属性,由于其修改方式不同,如果混用,则会在 任何属性更新时都刷新整个视图 导致 效率的低下 ,如仅仅修改width会导致一个Dialog把自己从headfoot都渲染一次,这种性能的损失在一些特定的情况下 是无法接受的 ,比如业务系统需要给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.

otakustay avatar otakustay commented on July 3, 2024

另外关于renderrepaint的关系,现在是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.

otakustay avatar otakustay commented on July 3, 2024

具体的我先push了一份上来,随时准备有反对意见后回滚(不上来没办法给人看到真实的样子啊),Control的流程改了,另外Label和Panel相应实现,个人感觉还行……

from esui.

errorrik avatar errorrik commented on July 3, 2024

在n天以后,回来看到这么多条,我看到了挣扎的过程。不过有很多东西梳理清楚了。那我马后炮下,说说我的想法:

constructor过程

  1. 初始化一堆东西
  2. initOptions(options) // 子类实现
  3. createMain() // 如果没给main
  4. 创建id
  5. helper.initViewContext(this)
  6. helper.initExtensions(this)
  7. this.fire('init')

这个过程我第一眼看觉得ok,没有问题。但是,待会请看属性的视图刷新那节。

额外要提的一点是,id是用户没传时创建。虽然这么提有点多余,还是怕漏了。

render过程

灰大给的代码里:

if (this.lifeCycle === Control.LifeCycle.INITED) {
    this.fire('beforerender');
}

其实就是我实现时候的helper.beforeRender。所以总结下:

  1. fire beforerender
  2. repaint
  3. stage rendered
  4. fire afterrender

但是,我个人觉得,这个过程有点单薄。主要是缺少了初始化控件基础结构这个环节。

  • 对于Dialog控件,第一次render的时候,是要创建head的。后面repaint的时候,是不用创建的。
  • 同理,有的子控件是只有第一次render才会创建的。

属性的视图刷新

原先的设想是:

  1. repaint负责刷新控件属性到视图上。所以,repaint不关心任何控件基础结构的事情。
  2. repaint刷新视图的依据是控件属性。这和数据->视图单向映射是一个道理。

所以,这涉及到控件属性如何保持最新鲜。保鲜这种事情虽然女人最擅长,但我尝试分解下,有下面几个点:

  1. setXxx的时候:无论如何,需要更新控件属性。
  2. set(xxx,value)的时候:如果有setXxx,则有保障;如果没有,也必须this.xxx = value。
  3. setProperties的时候:extend实现,可以保障。
  4. constructor的时候:如果有main,并且用户没传相应的属性,则相应属性应该从main上读出来。

根据上面所说,和之前的讨论,constructor的过程里,createMain应该在initOptions前面。initOptions应该负责从main读出必要的属性值,并写到控件属性上。

render和repaint的关系

我脑海里其实一直是repaint负责刷新属性到视图上的,所以应该也必须是render调用repaint。

我也不知道为啥当时写的repaint调用render,估计是写快了脑子抽了。给大家带来困惑了,对不起。

repaint({Array=} changes)

可选择属性的刷新视图,我的问题是:

  1. 这么设计,是不是每个视图都要有一个刷新视图的方法?比如repaintValue,repaintTitle......
  2. 如果是的话setTitle是不是就变成this.title = title;this.repaintTitle();

from esui.

otakustay avatar otakustay commented on July 3, 2024

看到一堆粗又黑的字的时候,我明白这件事离讨论出结果不远了……

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>元素
}

属性的视图刷新

关于createMaininitOptions的顺序,我调整过好几次,最后决定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);
        }
    }
}

会出现这种代理掉属性更新的。因此不考虑在控件抽象层面上做attributePropertiesstyleProperties这种东西,各控件自己去实现。

from esui.

errorrik avatar errorrik commented on July 3, 2024

render过程与初始化控件基础结构

现在的方案,有两个小问题:

  1. 用户可能会多次使用控件的render,也就是会多次运行无参的repaint
  2. changes属性选择是否初始化结构其实是两个逻辑,用一个参数来描述有些不对劲

建议是render里直接调用initStructure,这么个调法(伪马请意会):

if ( isInStage( 'INITED' ) ) {
    initStructure();
}

createMain和initOptions的顺序

首先,这是个比较细节的问题,无伤大雅。但是既然提出来,也可以讨论下,以后追究时有凭据。

根据之前的讨论,createMain放在constructor里是为了能initOptions的时候不至于面对this.main可能为空,而要做一堆多余的判断。

所以,如果initOptions在前,两个问题,如果能接受,我觉得没问题:

  1. initOptions就可能会有个if判断,我猜大概这么写。这个if是否能接受?
    var optionsInMain = {};
    if ( options.main ) {
    // 往optionsInMain上读
    }
    lib.extend( this, defaultOptions, optionsInMain, options );
  2. createMain是否就可以放回去到render里面做?

setProperties中的repaint

TextBox是一个特例,但也是一个典型。

我一直认为,原则是以控件属性为准。如果这个原则合理,那我们在这里要解决的问题是如何保证TextBox的value是最新鲜的。基于这货,我想到两招:

  1. TextBox在实现上,input/propertyChange时更新value属性
  2. repaint使用get方法取属性值

还有,setProperties中是直接调用无参repaint还是有参的部分属性repaint,我觉得都行。

repaint实现

区分属性的视图刷新行为是一个好主意!并且repaint需要知道全刷新是刷新哪些东西。下面是我在 @otakustay 的抽象基础上进一步做的抽象,主要更新点是:

  1. 砍掉attributePropertiesstyleProperties
  2. allProperties原来纯name的数组配置,里面每项包含repaint行为
  3. 预置常见的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.

Protected avatar Protected commented on July 3, 2024

So, what's all this then?

from esui.

otakustay avatar otakustay commented on July 3, 2024

@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.

Protected avatar Protected commented on July 3, 2024

No worries, it happens all the time ;) Also I reported it to github years ago and they never bothered to fix it...

from esui.

otakustay avatar otakustay commented on July 3, 2024

render过程与初始化控件基础结构

这个OK,对于 用户多次调用render 这个问题事实上我没有想过也不觉得该这么做。不过既然renderpublic 的,那就考虑一下这个事,把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更新TextBoxvalue了:

TextBox.prototype.getValue = function () {
    return this.main.value;
};

TextBox.prototype.repaint = function () {
    // ...
    this.main.value = this.get('value');
}

显而易见……

TextBox上玩监控用户输入 的实现成本并不低,真要做好了其实就能玩双向绑定了。我反对双向绑定的一个很大因素就是对基础建设的要求太高……当然如果真觉得能100%做出同步来,无疑是好的(要考虑几乎所有的<input>类型及<textarea>,包括daterange这种很可能用户根本不手动输入的东西)

setProperties调用有参部分属性的paint无非就是为了:

  • 区分属性视图刷新,可以进行局部刷新提高效率
  • 解决value这种奇怪的东西的同步问题,按我前面说的,我觉得 始终保持value属性同步 并不是那么好玩的事

repaint实现

我觉得可以,不过要我玩的话会变成函数式编程:

var painters = [
    style('width'),
    style('height'),
    attribute('title'),
    {
        name: 'text',
        paint: ...
    }
];

果然我的编程理念还是比较奇怪的吧~

from esui.

otakustay avatar otakustay commented on July 3, 2024

建议是render里直接调用initStructure

我又思考了一下,在createMain中完成这些事,同时无论是否传了main,都会调用createMain,这个思路如何

createMain = function () {
    if (!this.main) {
        this.main = document.createElement('div');
    }

    this.main.innerHTML = ...;
}

from esui.

errorrik avatar errorrik commented on July 3, 2024

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.

otakustay avatar otakustay commented on July 3, 2024

initStructure和createMain

initStructure里调用createMain的话,那constructor里不调用了?

我的意思是没有initStructure,让createMain承担这个任务,createMain的描述是 创建/构造完整的main元素 ,而不再像以前一样只弄一个<div>出来

所以这里的流程是

  1. constructor
  2. createMain(options) <-- 我认为把optionscreateMain足够使createMain在前,initOptions在后,呼应后面的顺序问题
  3. initOptions
  4. render <-- render不再有“第一次”这种区别,createMain负责把完整的DOM构建出来
  5. repaint

setProperties中的repaint

覆盖够全面(用setInterval检测之类的)确实可以,我不在意去做这样的实现,那么我们就要求所有InputControl实现value的绝对同步吧

setXxx与视图刷新

createMain越晚就让各方法的实现越复杂,为什么我们一定要让每一个setXxx都判断一下是否渲染呢……我觉得initOptionscreateMain的顺序调整得到的收益比不过每个方法加一个if分支的代价

repaint的实现

本来是这样的:

var paint = require('./painter');
var painters = [
    paint.style('width'),
    paint.attribute('title')
];

我更喜欢读起来像标准的句式的,比如 paint style width 就感觉很通畅。当然这个真心无所谓,用class的实现也很干净利索,等其它问题都清楚了这个随便定一下就好

from esui.

otakustay avatar otakustay commented on July 3, 2024

还是不对,createMain无论放在哪,肯定有个地方要对options.main是否存在判断,不在createMain里就是在initOptions里,找不到啥办法可以没有这个if分支。

所以我现在还是回到以前的看法,即initOptions -> createMain这个流程,在initOptions里要求有if分支。

另外createMain还是希望在构造函数中搞定,很难接受每个setXxx都要写个if

from esui.

errorrik avatar errorrik commented on July 3, 2024

我的意思是没有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。

  1. setXxx写法是设置属性 -> 调用相应painter更新视图
  2. painter需要判断控件是否处于RENDERED。也就是说,就算main存在,控件处于INITED,也是不应该执行更新视图的行为的。而且,更新视图的行为可能更新的不是main,而可能是调用子控件的某个方法,以及等等。所以是否要执行更新视图行为main是否存在无关,只和控件当前所属阶段有关。
  3. 对于视图更新的if,是可以包装的,如灰大之前写的style('height')

from esui.

otakustay avatar otakustay commented on July 3, 2024

那是否构造子控件(比如Select的Layer)

由控件决定,我的Select是这样的:

  • createMain中,建立一个div > span的结构,建立一个隐藏的<select>元素
  • 将Layer的建立做懒加载,在第一次open的时候建立的,如果不用懒加载,我会放到createMain
  • render中,根据自身的selectedIndexvalue,给<span>元素设innerHTML

createMain 只建立稳定的结构 ,不同步数据,而render则是把自己的属性和这个DOM结构中绑在一起。而一些 有数据才能生成的结构 则是在render中做的。比如Dialogfoot依赖于其配置,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操作。

不过如果前面的createMaininitOptions顺序确定是createMain在前,那么createMain无论如何也是在构造函数里调用了,这个话题似乎也自然有了结论?

from esui.

errorrik avatar errorrik commented on July 3, 2024

控件越复杂,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渲染时:

  1. 如果控件实例具有name属性,创建具有name attribute的input[type=hidden]
  2. 创建div做为控件主元素,执行后续渲染逻辑

控件使用者传入div元素,调用render渲染时:

  1. 如果控件实例不具有name属性,读取div元素的name attribute,做为控件实例的name属性
  2. 如果控件实例具有name属性,创建具有name attribute的input[type=hidden]
  3. 使用传入的div元素做为控件主元素,执行后续渲染逻辑

控件使用者传入input[type=text]元素,调用render渲染时:

  1. 如果控件实例不具有name属性,读取input[type=text]元素的name attribute,做为控件实例的name属性
  2. 如果控件实例具有name属性,创建具有name attribute的input[type=hidden]
  3. 移除传入的input[type=text]元素
  4. 创建div做为控件主元素,执行后续渲染逻辑

from esui.

otakustay avatar otakustay commented on July 3, 2024

Dialog现在是 @DDDBear 在做,所以我比较熟悉(什么逻辑),Dialog的现状是这样的:

  • widthheight单独通过style设置
  • 剩余的所有属性,不管是变了1个,还是变了N个,统统重写main.innerHTML

这样来看,Dialog认为所有的结构都是动态的,这个真的更多是取决与控件实现者自己的想法。

当然其实我不认为Dialog应该这么做,这是后话和本话题没关系……

另外,Select控件应该放一个隐藏的input[type=hidden],是我误搞成隐藏的<select>了,这属于实现上的错误。不过传达的概念是,像这个隐藏的input[type=hidden],就是一个 稳定的结构 ,无论其它属性怎么变都会一直存在,所以在createMain中搞定。

精确定义一下,所谓 稳定结构 ,就是指 render中只会使用不会覆盖或销毁 的那些元素,具体是哪些元素,控制权交由控件实现者应该没什么问题?

from esui.

errorrik avatar errorrik commented on July 3, 2024

从实现来说,我觉得,创建input[type=hidden]的逻辑应该是InputControl这个抽象类去overrideControlcreateMain。其他所有具体类应该不关心这个创建过程。

可见,之前我对创建input[type=hidden]的定义是:控件主元素创建逻辑。这个定义的出发点在于:

  1. 创建控件结构,是控件开发者的事情
  2. 控件开发者基本不需要override render方法
  3. 控件开发者基本不需要override createMain方法

对于Dialog:

  1. @DDDBear 现在的实现,我首先认为repaint时候重写main的innerHTML是合理的。但这种实现下,Dialog不包含任何内部稳定结构,所以对我们现在的讨论没有帮助
  2. 假设要区分稳定结构,并且区分权交由实现者控制:实现者把head、body当成稳定结构,foot当成不稳定结构。那么,直接重写main的innerHTML的实现就不行了。并且同时会出现我刚才提到的问题:
  • override createMain,在createMain中create head div,并append到main
  • override render,在render中create foot div,并append到main

所以,我认为,抽象initStructurerender调用initStructureinitStructure由控件实现者override。这个机制是必要的。

from esui.

errorrik avatar errorrik commented on July 3, 2024

我很担心并相信的一点是:讨论了这么多,其实是没人看,没人理解的……

from esui.

otakustay avatar otakustay commented on July 3, 2024

我们能定下来让别人跟着做就行了,不发言者没人权……

关于input[type=hidden]的问题,如果InputControl来控制,则TextBoxTextLine需要重写掉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.

otakustay avatar otakustay commented on July 3, 2024

现在感觉问题都已经有结论了,我明天整理一份新的流程,并相应把ControlInputControl改了

from esui.

errorrik avatar errorrik commented on July 3, 2024

嗯,看来这个issue的问题都有结论了。真不容易~

Dialog的问题。我觉得稳定结构是更好的。额外有一点,期望能支持渲染已有的dom,并自动识别head和body等:

<div data-ui-type="Dialog">
    <h3>MyTitle</h3>
    <div>我是一堆文字或者内部的dom结构</div>
</div>

我就一提,不在这讨论Dialog了。等 @DDDBear 写完标准文档,在评审issue里讨论吧。

from esui.

otakustay avatar otakustay commented on July 3, 2024

我已经在 b3aa5ab 中把整理后的生命周期实现了,请参考ControlInputControl

另外LabelPanel也跟着实现了,不过这两个控件很简单不需要initStructure所以不具备啥参考价值就是了

painter这个东西还没做,没确定是用对象构造还是函数构造,谁有兴趣就自己决定下给做了,没人做的话过几天我就做成函数式了- -

InputControl中我没有实现默认的加input[type="hidden"]的逻辑,因为遇到一些问题,回头我整理一下再发上来

TextBox现在是完全不可用了,麻烦 @errorrik 再看一下跟着新的模式实现一次吧

from esui.

Justineo avatar Justineo commented on July 3, 2024

这里有这么多讨论...等有空了要来爬一下楼...

from esui.

otakustay avatar otakustay commented on July 3, 2024

这个Issue已经分散为多个子Issue,本身要讨论的事已经讨论清楚了,关

from esui.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.