MVVM中的bug

本文专门记录我再做MVVM时遇到的难点和bug

无法触发刷新

场景

<h1>{{tasks.length - activeCount}} remain</h1>

为什么会无法触发呢?触发的条件是属性修改(add, update, delete). 而需要注意的是model仅仅observe自己的属性, 并不会知道嵌套属性的修改. 因此, activeCount修改, h1的内容会轻松的变化, 然而tasks length属性的变化(删除task)却无法触发刷新, 因为length已经属于tasks这个scope的东西了

解决方法:

  1. 不再以model作为刷新节点的单位, 只要是属性修改, 全部节点刷新, 这势必带来不必要的性能损失, 我个人非常喜欢model控制一片的概念, 这个方法必然否决
  2. 是否可以手动触发父model的刷新?
  3. 是否可以改一种写法, 放弃子model刷新父model, 增加手动触发刷新的接口, 如model.refresh

子scope无法获得父scope的值

场景

<li data-repeat='tasks' class="" data-bind="editing: editing">
  <div class="view">
    <input class="toggle" type="checkbox" data-bind="completed: toggle" data-on="click: toggleTask">
    <label data-on="dblclick: editTask">{{content}}</label>
    <button class="destroy" data-on="click: destroyTask"></button>
  </div>
  <input class="edit" value="<article>
  <h1>博客自动分标题のCSS Counter</h1>
  
  <span class="post-category">
    <span class="label"></span>
  </span>
  <div class="post-meta">
    <time class="post-date">2014-03-19 08:57</time>
  </div>
  <div class="post">
  <h2 id="大标题一">大标题一</h2>

<h3 id="中标题一">中标题一</h3>

<h3 id="中标题二">中标题二</h3>

<h4 id="小标题一">小标题一</h4>
<h4 id="小标题二">小标题二</h4>

<h2 id="段落标题编号css实现">段落标题编号CSS实现</h2>

<h3 id="markdown写法">markdown写法</h3>

<div class="highlighter-rouge"><pre class="highlight"><code>大标题一
---

### 中标题一

### 中标题二

#### 小标题一
#### 小标题二
</code></pre>
</div>

<h3 id="css-counter">CSS Counter</h3>

<p><a href="https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Counters">CSS Counter</a>简直就是为了markdown写文章准备的, 不过你同样也可以用来自定义ol序号的样式</p>

<p>我的段落标题CSS:</p>

<div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="nt">h1</span> <span class="p">{</span>
  <span class="nl">counter-reset</span><span class="p">:</span> <span class="n">big</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">h2</span> <span class="p">{</span>
  <span class="nl">counter-increment</span><span class="p">:</span> <span class="n">big</span><span class="p">;</span>
  <span class="nl">counter-reset</span><span class="p">:</span> <span class="n">mid</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">h2</span><span class="nd">:before</span> <span class="p">{</span>
  <span class="nl">content</span><span class="p">:</span> <span class="n">counter</span><span class="p">(</span><span class="n">big</span><span class="p">);</span>
<span class="p">}</span>

<span class="nt">h3</span> <span class="p">{</span>
  <span class="nl">counter-increment</span><span class="p">:</span> <span class="n">mid</span><span class="p">;</span>
  <span class="nl">counter-reset</span><span class="p">:</span> <span class="n">small</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">h3</span><span class="nd">:before</span> <span class="p">{</span>
  <span class="nl">content</span><span class="p">:</span> <span class="n">counter</span><span class="p">(</span><span class="n">big</span><span class="p">)</span><span class="s2">'.'</span><span class="n">counter</span><span class="p">(</span><span class="n">mid</span><span class="p">);</span>
<span class="p">}</span>

<span class="nt">h4</span> <span class="p">{</span>
  <span class="nl">counter-increment</span><span class="p">:</span> <span class="n">small</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">h4</span><span class="nd">:before</span> <span class="p">{</span>
  <span class="nl">content</span><span class="p">:</span> <span class="n">counter</span><span class="p">(</span><span class="n">big</span><span class="p">)</span><span class="s2">'.'</span><span class="n">counter</span><span class="p">(</span><span class="n">mid</span><span class="p">)</span><span class="s2">'.'</span><span class="n">counter</span><span class="p">(</span><span class="n">small</span><span class="p">);</span>
<span class="p">}</span>

<span class="nt">h2</span><span class="nd">:before</span><span class="o">,</span> <span class="nt">h3</span><span class="nd">:before</span><span class="o">,</span> <span class="nt">h4</span><span class="nd">:before</span> <span class="p">{</span>
  <span class="nl">padding-right</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>

<span class="c">/* 缩进 */</span>
<span class="nt">h1</span><span class="o">,</span> <span class="nt">h2</span><span class="o">,</span> <span class="nt">h3</span> <span class="p">{</span>
  <span class="nl">text-indent</span><span class="p">:</span> <span class="m">-10px</span><span class="p">;</span>
<span class="p">}</span>

</code></pre>
</div>

<h4 id="小标题一-1">小标题一</h4>
<h4 id="小标题二-1">小标题二</h4>
<h4 id="小标题三">小标题三</h4>

<h3 id="中标题三">中标题三</h3>

<h4 id="小标题一-2">小标题一</h4>
<h4 id="小标题二-2">小标题二</h4>
<h4 id="小标题三-1">小标题三</h4>


  </div>
  <div class="post-meta">
    <span>Tags: </span>
    
      <span style="margin: 5px"><a href="/tags.html#css">css</a></span>
    
  </div>
  <!-- baidu share -->
  <div class="bdsharebuttonbox"><a href="#" class="bds_more" data-cmd="more"></a><a href="#" class="bds_tsina" data-cmd="tsina" title="分享到新浪微博"></a><a href="#" class="bds_qzone" data-cmd="qzone" title="分享到QQ空间"></a><a href="#" class="bds_tqq" data-cmd="tqq" title="分享到腾讯微博"></a><a href="#" class="bds_renren" data-cmd="renren" title="分享到人人网"></a><a href="#" class="bds_t163" data-cmd="t163" title="分享到网易微博"></a></div>
  <script>window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"0","bdSize":"16"},"share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=86835285.js?cdnversion='+~(-new Date()/36e5)];</script>
  <!-- end baidu share -->
  <div class="section-nav">
    <div class="left align-left">
      
      <a href="/2014/03/16/tdd.html" class="prev">上一篇:<b class="mobile-ignore"> TDD单元测试</b></a>
      
    </div>
    <div class="right align-right">
      
      <a href="/2014/03/21/mvvm-bugs.html" class="next">下一篇:<b class="mobile-ignore"> MVVM中的bug</b></a>
      
    </div>
  </div>
</article>
<!-- Duoshuo Comment BEGIN -->
<div class="ds-thread"></div>
<script type="text/javascript">
var duoshuoQuery = {short_name:"biedalian"};
  (function() {
    var ds = document.createElement('script');
    ds.type = 'text/javascript';ds.async = true;
    ds.src = 'http://static.duoshuo.com/embed.js';
    ds.charset = 'UTF-8';
    (document.getElementsByTagName('head')[0] 
    || document.getElementsByTagName('body')[0]).appendChild(ds);
  })();
  </script>
<!-- Duoshuo Comment END -->

" data-bind="completed: edit"> <!-- onblur change class -->
</li>

在data-repeat中, model就已经属于tasks[i]了, 也就是说那样才可以直接用completed, content这些值, 问题就是子model获取不到父model的key(editing), 这并不合理, 参照js本身, 子函数的变量都是自己的scope加上全部父scope的. 但在我mvvm设计之初根本没有scope的概念, render时with的对象直接就是model

加上scope的概念是不可少的, 而且这个scope将完全是一个新的对象(不会污染model)

可喜的是我在model中已经加入了$parent属性, 不用专门传scope对象就可以达到类似的效果

function getScope(model) {
  if (!model.$parent) return model
  var scope = {}, $parent = model
  while ($parent) {
    if (!Array.isArray($parent)) extend(scope, $parent) // skip array
    $parent = data[$parent.$parent]
  }
  return scope
}

父model的修改无法触发子model

场景同上, 就算子model通过scope认识了editing这个父元素的属性, 但仍然没法获取它修改的动态

这个错误的产生是因为我将data-bind{{ key }}一样处理了, 但他们是不同的, 后者属于单纯的渲染, 而data-bind更像是订阅, 属于全局

修改方式如下

<div data-bind="editing: editingFunc"></div>
keyBind: {
  'editing': [editingFunc]
}

observe(changes) {
  if (keyBind[changes.name]) {
    var handlers = keyBind[changes.name]
    // exec handlers...
  }
}

做完发现有问题, 子model绑定一个key, 导致全部函数都在keyBind中了, 改一个值全部都变了. 像completed这种确实应该属于自己, 那如何区分自己scope的bind和全局scope的bind呢?

继续解决, 对全局bind和子bind进行区分, 如果bind的name在model中, 则为该model的bind, 否则就是全局的bind

最后解决竟然是通过事件触发的model和model对应的dom实现的

mvvm如果对data-bind加上model和node的传值, 很多操作会简单很多, 但问题是获取这个显然会造成性能的降低

不过至此todomvc已经没有明显bug, 可以在这里看到demo

接下来要做的事情主要有三个

  1. 加入defineProperty, 这样我们获取model和node的时候可以get的时候再操作

  2. 两种模式切换, 一个是支持defineProperty模式, 一种是不支持, 保持写法一样

  3. 彻底使用data, model和element以及event一进来就转换成data类型