baidu / san

A fast, portable, flexible JavaScript component framework
https://baidu.github.io/san/
MIT License
4.73k stars 551 forks source link

bug: obj.checked 后续无法绑定到 checkbox #685

Closed John0King closed 3 years ago

John0King commented 3 years ago
<div s-for=“item,index in list”> <input type="checkbox" checked="{{item.checked}}"  /></div>

initData:{
    list:[]
},
attached:function(){
   this.data.set('list',{  checked:true });

  setInterval(()=> {  this.toggleAll() })
},
toggleAll:function()
{
   var list = this.data.get('list');
   var isAllChecked= this.data.get('isAllChecked');
   for(var i =0;i<list.length;i++){
     this.set('list['+i+'].checked', !isAllChecked)
  }
},

computed:{
    isAllChecked:function(){
     var list = this.data.get('list');
     var c = list.length;
     var c2 = this.filter(x=>x.checked == true).length;
    return c == c2
  }
}

预期的结果

循环中的复选框跟随 列表项的 checked 选中和补选中

实际结果

列表中的项 不会跟随 item.checked ,

其他的说明

item.checked 改为 item.isChecked , 该问题得到修复, 所以这应该属于bug, 推测可能跟 san内部的跟踪机制有一定关系, 建议内部的跟踪属性修改成 ____$checked__

errorrik commented 3 years ago

这示例代码怎么一堆问题,引号、this.filterthis.set ......

我试了下没这问题,san 没有用 checked 做什么内部跟踪机制。我怀疑是你代码哪里有问题。下面是我的测试代码,有问题留言


var MyApp = san.defineComponent({
  template: `<div>
    <div s-for="item,index in list"> <input type="checkbox" checked="{{item.checked}}"  /></div>
  </div>`,

  attached() {
    setInterval(() => {
      var list = this.data.get('list');
      var isAllChecked= this.data.get('isAllChecked');
      for (var i =0; i<list.length; i++) {
        this.data.set('list['+i+'].checked', !isAllChecked)
      }
    }, 1000)
  },

  computed:{
    isAllChecked() {
      var list = this.data.get('list');
      var len = list.length;
      while (len--) {
        if (!list[len].checked) {
          return false;
        }
      }

      return true;
    }
  }
})
var myApp = new MyApp({
  data: {
    list: [
      {checked: true}, 
      {checked: true}
    ]
  }
});
myApp.attach(document.body)
John0King commented 3 years ago

这个问题可能是其他领域(change detector)的问题导致的, 我目前没有完全重现这个问题 根据我的测试, san 的变更检测 在视图层和 逻辑层使用的变更检测不一致 我简单的理解为

然后问题导致了 在 computed 等地方(或者 视图绑定的方法上) 业务逻辑的数据是正确的, 但是视图的状态跟它脱节了

https://github.com/John0King/san-demo-check 这是我的 demo, 里面app.js 的第一行代码 控制了两种 对 toggleAll 方法 变更数据的方式, 两种方法, 逻辑层的 data总是正确的, 但是 视图层却不会更新


我这个还是没有把我遇到的问题测全, 实际上我之前提交过一个 bug 关于双向绑定和数组的, 不过因为网络问题,好像提交没有成功, 在这个测试里面没有测出来, 因为测试那个需要至少 父级和子级两个组件。

推测的问题:

  1. 父级组件双向绑定 的 data 可能是 null | undefined , 而不是数组
  2. array的双向绑定 没有检测null, 直接调用了 contact push pop 等方法
  3. 与上面的测试一样, 组件更新的状态,它的 “value” 值更新了 , 但是ref 值 没有更新
John0King commented 3 years ago

@errorrik 我简略的看了下文件结构(和非常小的一部分代码), 我觉得这个 changeDetector 的设计问题, 设计之初可能你们想要跟 vue一样 尽可能的局部更新,所以你们利用值是否相等来判断两个值是否相等, 但是与 vue不同的是, vue 会把 所有 data 里面描述的字段,全部变更为 Object Proxy 属性, 比如:
list[3].name = 3, 首先 list 本身是继承数组的 子类数组,它模拟了几乎数组的所有操作,比如 pop, push, shift 等等, 但是无法模拟 list[99] = { } 这样的添加( 当然vue3 利用了 ObjectProxy 已经 不存在此限制) 他的这种方式保证了 变更通知和发起变更事件总是同步的, 但是 san采用的另外一种 proxy, 所有的操作都采用 data 这个代理来委托变更, 但是这造成了, 它不知道变更的来源是 下级还是父级 , 比如 data.set('list',list) , 他必须去检测每个元素有没有变更(子集变更) 同时要向 watch 提交 父级事件 (但是问题出现在 这个 相等性比较上,或者没有向子集去变更检测), 所以我感觉 他还不如使用 this.detectChange() 这种批处理 统一去管理变更 (视图更新和 watch事件), 而且也不用再去处理字符串命名 和 变量名的双重变量名问题。(当然这一点得看你们的主体设计方向了)


考虑下这样的操作


san.defineComponent({
   initState(){  //为了兼容老版本,data本身重命名为 state,  initData 仍然是 变更委托 ,  initData 跟他一样,
                     //这里是觉得如果 state 变更是推荐的,所以这里也跟着改了名
       return { 
        list:[]
      }
  },
  attached(){
     this.state.list.push({  name:'1'  });
     this.detectChange();
  },
 someClick(){
     //老方法理论上可以兼容理论上可以兼容
      this.data.push('list', {  name: 'old-name' });  // 给 state.list  push 并且执行 detectChange 方法
   }
})
John0King commented 3 years ago

@errorrik 能否解答下,是不是如我所想, 除了 后端代码 有 变更检测, 视图又有自己独立的变更检测? 如果是的话,我觉得应该学习一下 angular 的方式由统一的 变更检测 发起对 watch 和 视图的更新, 至少不能让状态跟视图完全脱节

errorrik commented 3 years ago

这里的原因没有那么复杂。通过方法操作数据变化,数据的变化肯定是不会丢的,不会出现老 vue 的问题。

但是为什么会出现你说的这种情况?因为我们认为,当 数据没有发生变化的时候,视图就不应该更新。那什么叫 数据没有发生变化 ?我们认为 immutable 是个好的想法,大家也比较好理解,毕竟社区一直在谈论,也这么多年了。data 的操作就是基于这个想法去封装的,所以你例子里的现象,是 by design 的。

我觉得,这个文章可能对你有所帮助 https://efe.baidu.com/blog/san-perf/#immutable

John0King commented 3 years ago

@errorrik 完全用 immutable 来当然是没有问题的, 问题是在没有类型辅助的情况下,immutable很难做到, 不过 typescript 有个 Readonly helper , type 应该使用这个来帮助阻止非 immutable 更新 eg.

export class Data<TData=any>
{
    // 本来想 Readonly<Parital<TData>> 来着,不过exp会多层
    get<T=any>(exp:string,option?:{ force?:boolean,slience?:boolean }): Readonly<T> {

        //... do somthing
    }
}

immutable 的变更检测是自下而上的(child->root),子更新导致从子更新到root完全更新,而“下”不够彻底就是这个问题的根源(变更发生在开始这个“下”的更下面), 可以考虑增加一个 this.detechChange() 方法, 强制 变更检测 重新 自上而下 检测变更,同步视图状态。

errorrik commented 3 years ago

immutable 作用是在变更时,不是检测时。变更时自下而上的引用全变,从而检测只需要 ===。

和类型辅助没啥关系。也无需增加方法强制触发,强制触发已经有 force 参数了。

另外,immutable 还有另外的好处。

// data: {person: {name: 'foo'}}
let person = this.data.get('person'); 
person.name; // foo

this.data.set('person.name', 'bar');
person.name; // 如果不是 immutable,就会莫名其妙影响到 person 对象,就会是 bar。期望应该不被影响,是foo
John0King commented 3 years ago

@errorrik force 参数只会触发自己,而不会触发 子级, Vue 就有个 $forceupdate() 来帮助解决 视图与状态不对应的问题。

另外来说下 我上面提到的 Readonly<T> 这个 helper type, 这个是 typescript 内置的帮助类型之一, 用来将一个 可变对象 当成不可变对象来出来(他没有实现真正的不可变,而是在typescript 类型基础上 不能变更) 比如: type Foo = { a:string; b:number }type ImmutableFoo = Readonly<Foo> 意思是

type ImmutableFoo = {  
    readonly a:string;  
    readonly b:number 
}

image

可以看到任何对成员的赋值都是不允许的,

errorrik commented 3 years ago

@John0King 我觉得,我们在讨论的点可能有些偏差

关于 Readonly helper

你可以从 repo 里看到新的(还没 release)的 .d.ts ,里面增加了对 data 的类型推断,我们希望用 ts 的开发项目能更便捷。

但我之前直接回避了这个东西。原因有2:

  1. 我们在讨论的是视图更新机制,讨论 helper 会不聚焦
  2. 有很大一部分开发者是用 js 的,ts 的支持本身就是增强,但不是开发的必选项

至于 Readonly 要不要应用于 data.get ,如果想讨论可以单开一个 issue

关于更新机制

为什么说不需要一个方法,只要 force 参数就行?这是整个机制决定的。

force 参数确实只会发出当前变更数据项的事件,不包含子项,但是视图认为 什么变更数据项影响了我 ,这个实现才是视图更新的决定因素。下面这个简单的例子可以更清楚的展示效果。

var MyComponent = san.defineComponent({
    template: '<div>{{person.name}}</div>',
});

var myComponent = new MyComponent({
    data: {
        person: {name: 'one'}
    }
});
myComponent.attach(document.body);

setTimeout(function () {
    var p = myComponent.data.get('person');
    p.name = '1';

    // 看看下面两行效果有什么不同
    myComponent.data.set('person', p, {force: true});
    // myComponent.data.set('person', p);
}, 1000);

可以看到,person 的 force,让 person.name 引用的视图发生了变更, 即使没有 immutable。

当然,当次变更没有问题,但在复杂的业务流中,可能会玩脱,造成意料之外的影响。所以,还是建议细粒度走数据方法来更新数据