[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"content-the-boundary-of-mvvm":3,"$f1nlk23ltx8f2e":19},{"id":4,"type":5,"slug":6,"title":7,"date":8,"category":9,"tags":10,"body_markdown":11,"permalink":12,"excerpt_src":12,"media_type":12,"media_title":12,"media_author":12,"media_url":12,"rating":12,"layout":12,"pv":13,"admin_only":13,"created_at":14,"updated_at":14,"deleted_at":12,"status":15,"html":16,"excerpt":17,"cover":18},177,"article","the-boundary-of-mvvm","思考MVVM的边界","2017-07-11 13:27","web",[],"\n## 前提\n\nMVVM 的概念由来已久，一开始，伴随着非常多的争议，MVC 还是 MVP 还是 MVVM ？MVVM 中什么是 VM ？不过随着时间的推移，大家也不再纠结到底是 MV 什么鬼了，于是也有人叫它们为 MV* 。为行文方便，下文的 MVVM 并不特指 Angular.js 之类双向绑定的框架，更多是 MV* 的意义。\n\n不管它有什么争议，核心的数据绑定机制上，大家是没有太多争议的。Angular.js 带来的数据双向绑定至今让人印象深刻。而 React 带来的单向数据流\n\n![UI=f(state)](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F01.jpg)\n\n在今天已经几乎成为主流，在其它框架中都能找到它的影子。在 Vue 中也能找到它的身影，尤其是在引入 Vuex 之后，几乎就是 Flux 的翻版，概念几乎完全一样。\n\n今天要讨论的主题就是上面这个公式的边界，为避免 MVVM 名称带来的争议，下文直接使用“框架”一词。\n\n\u003C!-- more -->\n\n## 数据与UI的映射\n\n![math function](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F02.jpg)\n\n我们在数学中都学过 y = f(x) ，表示对每一个确定的 x ，都有唯一的 y 值与它对应。例如 y = x + 1，不论 x 取什么值，都能找到确定的 y 与之对应。而反过来理解，y 值的不同正是因为 x 取值不同导致的。\n\nUI = F(state)\n\n这也是一个典型的函数。它表示每一种 state 都有唯一与之对应的 UI 表现。反过来，UI 表现的不同正是因为 state 的不同导致的。\n\n正是因为这种映射的确定性，带来了极易理解的 UI 开发方法：我们只需要编写好映射关系，然后注意力就可以全部放到 state 上面了，因为 state 的任何变动都会体现到 UI 上，而 UI 是不会在 state 不变的情况下发生变更的。而因为同样的 state 对应同样的 UI ，我们甚至可以完全将用户当前的 UI 复制到另一台设备上，只要保证 state 是完全相同的即可。\n\n## 初窥边界\n\n如果世界真的这么简单，那也许就不会有前端工程师一职了。\n\n回想一个真实的案例，如果我们要将一个字符串显示在界面上，那么可能是这么写：\n\n```html\n\u003Cspan>{{text}}\u003C\u002Fspan>\n```\n\n```javascript\nthis.setState({\n  text:'hello world'\n});\n```\n\n此时 UI 完全由 state 决定。但是，如果这是一个输入框呢？\n\n```html\n\u003Cinput value=\"{{text}}\" \u002F>\n```\n\n这样写吗？那输入框输入的时候会发生什么？熟悉 React 的朋友应该知道，在 React 中，有一种 input 叫作 Controlled Input ，它需要监听输入框的事件，当内容变更的时候，实时改变 state 的值，从而再次达到 state 和 UI 一致的目的。然而这种情况下与其说是 state 控制 UI ，倒不如说是 UI 在控制 state 了。\n\n![controlled input](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F03.jpg)\n\n如果说内容变更还可以勉强通过 Controlled Input 的方式来保持 state 到 UI 的映射，那光标的控制就只能说是无能为力了。\n\n理论上来说，当我们输入的时候，光标也在同步变更，此时如果我们继续应用 state 到 UI 的映射，是有可能导致输入框被重新渲染的，而此时 input 中的光标是无法保证位置的。这样会导致非常奇怪的输入体验，或者说其实是没法用的。框架在处理输入框上下了非常多的功夫，其实是避免了 state 变更的时候重新渲染 input 的，最多只是改变它的属性（值）。\n\n至此，我们其实已经看到了，所谓 state 控制 UI 也只是在我们的理想中存在，现实中有非常多的细节是无法通过 state 到 UI 的映射关系照顾到的。\n\n## 再探边界\n\n上面的例子，为什么在设计 state 的时候，不把光标也设计进去呢？这样不就可以进行精准的 state 到 UI 的控制了么？\n\n我们在脑海中尝试一下：首先，需要在 state 中为每个输入框的值增加一个光标位置。接下来，需要在每一次值变更的时候计算光标位置，并通过光标 API 维护界面上光标的位置。然后，需要监听 focus \u002F blur 事件，重建、移除光标。需要监听点击事件重算光标位置，需要监听键盘事件，使光标位置发生跳跃。除此之外，还需要处理选区，此时是否还需要在 state 中添加一个选区呢？\n\n我们只是想控制一下光标而已……\n\n而如果我们不控制光标（也就是主流框架的现状），你会发现事情要更容易得多，我们上述种种光标的行为浏览器都有对应的处理和反应。也就是说，浏览器把这些行为都已经封装到了 input 组件中，而我们在大部分情况下，并不需要处理这些。这个封装行为，实际上就划出了一条边界：框架应该只管理封装组件对外暴露的部分，未暴露的部分不应该介入。\n\n再举一例，当我们引入一个文本编辑器组件（例如 AceEditor ）时，这个编辑器除了内容之外，还会有非常多的行为，例如是否显示 查找\u002F替换 的界面，是否显示行号、参考线，使用不同的语法进行高亮等等。以高亮这一行为来说，本质上它是由一堆带样式的`\u003Cspan>`元素堆起来的，例如`var a=123`这样一句简单的代码，它实际的结构可能是这样的：\n\n```html\n\u003Cspan class=\"keyword\">var \u003C\u002Fspan>\u003Cspan class=\"variable\">a\u003C\u002Fspan>\u003Cspan class=\"operate\">=\u003C\u002Fspan> \u003Cspan class=\"const\">123\u003C\u002Fspan>\n```\n\n此时我们需要将这个高亮的结构通过 state 来映射吗？还是在 state 中直接保存 var a=123 这样一个字符串就好了？答案不言自明。\n\n![highlight](\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F04.jpg)\n\n至此，我们已经能非常明白地对 MVVM 的边界有一个认知。这条边界，就是组件的封装，面对一个封装的组件，我们不应该跨过封装的边界。\n\n但，问题又来了，如果组件的行为并不能通过 MVVM 的 state 来决定，那么，由谁决定？\n",null,0,"2026-08-28 04:37:17","published","\u003Ch2>前提\u003C\u002Fh2>\n\u003Cp>MVVM 的概念由来已久，一开始，伴随着非常多的争议，MVC 还是 MVP 还是 MVVM ？MVVM 中什么是 VM ？不过随着时间的推移，大家也不再纠结到底是 MV 什么鬼了，于是也有人叫它们为 MV* 。为行文方便，下文的 MVVM 并不特指 Angular.js 之类双向绑定的框架，更多是 MV* 的意义。\u003C\u002Fp>\n\u003Cp>不管它有什么争议，核心的数据绑定机制上，大家是没有太多争议的。Angular.js 带来的数据双向绑定至今让人印象深刻。而 React 带来的单向数据流\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F01.jpg\" alt=\"UI=f(state)\">\u003C\u002Fp>\n\u003Cp>在今天已经几乎成为主流，在其它框架中都能找到它的影子。在 Vue 中也能找到它的身影，尤其是在引入 Vuex 之后，几乎就是 Flux 的翻版，概念几乎完全一样。\u003C\u002Fp>\n\u003Cp>今天要讨论的主题就是上面这个公式的边界，为避免 MVVM 名称带来的争议，下文直接使用“框架”一词。\u003C\u002Fp>\n\u003Ch2>数据与UI的映射\u003C\u002Fh2>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F02.jpg\" alt=\"math function\">\u003C\u002Fp>\n\u003Cp>我们在数学中都学过 y = f(x) ，表示对每一个确定的 x ，都有唯一的 y 值与它对应。例如 y = x + 1，不论 x 取什么值，都能找到确定的 y 与之对应。而反过来理解，y 值的不同正是因为 x 取值不同导致的。\u003C\u002Fp>\n\u003Cp>UI = F(state)\u003C\u002Fp>\n\u003Cp>这也是一个典型的函数。它表示每一种 state 都有唯一与之对应的 UI 表现。反过来，UI 表现的不同正是因为 state 的不同导致的。\u003C\u002Fp>\n\u003Cp>正是因为这种映射的确定性，带来了极易理解的 UI 开发方法：我们只需要编写好映射关系，然后注意力就可以全部放到 state 上面了，因为 state 的任何变动都会体现到 UI 上，而 UI 是不会在 state 不变的情况下发生变更的。而因为同样的 state 对应同样的 UI ，我们甚至可以完全将用户当前的 UI 复制到另一台设备上，只要保证 state 是完全相同的即可。\u003C\u002Fp>\n\u003Ch2>初窥边界\u003C\u002Fh2>\n\u003Cp>如果世界真的这么简单，那也许就不会有前端工程师一职了。\u003C\u002Fp>\n\u003Cp>回想一个真实的案例，如果我们要将一个字符串显示在界面上，那么可能是这么写：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;span&gt;{{text}}&lt;\u002Fspan&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode>this.setState({\n  text:'hello world'\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>此时 UI 完全由 state 决定。但是，如果这是一个输入框呢？\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;input value=&quot;{{text}}&quot; \u002F&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这样写吗？那输入框输入的时候会发生什么？熟悉 React 的朋友应该知道，在 React 中，有一种 input 叫作 Controlled Input ，它需要监听输入框的事件，当内容变更的时候，实时改变 state 的值，从而再次达到 state 和 UI 一致的目的。然而这种情况下与其说是 state 控制 UI ，倒不如说是 UI 在控制 state 了。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F03.jpg\" alt=\"controlled input\">\u003C\u002Fp>\n\u003Cp>如果说内容变更还可以勉强通过 Controlled Input 的方式来保持 state 到 UI 的映射，那光标的控制就只能说是无能为力了。\u003C\u002Fp>\n\u003Cp>理论上来说，当我们输入的时候，光标也在同步变更，此时如果我们继续应用 state 到 UI 的映射，是有可能导致输入框被重新渲染的，而此时 input 中的光标是无法保证位置的。这样会导致非常奇怪的输入体验，或者说其实是没法用的。框架在处理输入框上下了非常多的功夫，其实是避免了 state 变更的时候重新渲染 input 的，最多只是改变它的属性（值）。\u003C\u002Fp>\n\u003Cp>至此，我们其实已经看到了，所谓 state 控制 UI 也只是在我们的理想中存在，现实中有非常多的细节是无法通过 state 到 UI 的映射关系照顾到的。\u003C\u002Fp>\n\u003Ch2>再探边界\u003C\u002Fh2>\n\u003Cp>上面的例子，为什么在设计 state 的时候，不把光标也设计进去呢？这样不就可以进行精准的 state 到 UI 的控制了么？\u003C\u002Fp>\n\u003Cp>我们在脑海中尝试一下：首先，需要在 state 中为每个输入框的值增加一个光标位置。接下来，需要在每一次值变更的时候计算光标位置，并通过光标 API 维护界面上光标的位置。然后，需要监听 focus \u002F blur 事件，重建、移除光标。需要监听点击事件重算光标位置，需要监听键盘事件，使光标位置发生跳跃。除此之外，还需要处理选区，此时是否还需要在 state 中添加一个选区呢？\u003C\u002Fp>\n\u003Cp>我们只是想控制一下光标而已……\u003C\u002Fp>\n\u003Cp>而如果我们不控制光标（也就是主流框架的现状），你会发现事情要更容易得多，我们上述种种光标的行为浏览器都有对应的处理和反应。也就是说，浏览器把这些行为都已经封装到了 input 组件中，而我们在大部分情况下，并不需要处理这些。这个封装行为，实际上就划出了一条边界：框架应该只管理封装组件对外暴露的部分，未暴露的部分不应该介入。\u003C\u002Fp>\n\u003Cp>再举一例，当我们引入一个文本编辑器组件（例如 AceEditor ）时，这个编辑器除了内容之外，还会有非常多的行为，例如是否显示 查找\u002F替换 的界面，是否显示行号、参考线，使用不同的语法进行高亮等等。以高亮这一行为来说，本质上它是由一堆带样式的\u003Ccode>&lt;span&gt;\u003C\u002Fcode>元素堆起来的，例如\u003Ccode>var a=123\u003C\u002Fcode>这样一句简单的代码，它实际的结构可能是这样的：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;span class=&quot;keyword&quot;&gt;var &lt;\u002Fspan&gt;&lt;span class=&quot;variable&quot;&gt;a&lt;\u002Fspan&gt;&lt;span class=&quot;operate&quot;&gt;=&lt;\u002Fspan&gt; &lt;span class=&quot;const&quot;&gt;123&lt;\u002Fspan&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>此时我们需要将这个高亮的结构通过 state 来映射吗？还是在 state 中直接保存 var a=123 这样一个字符串就好了？答案不言自明。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F04.jpg\" alt=\"highlight\">\u003C\u002Fp>\n\u003Cp>至此，我们已经能非常明白地对 MVVM 的边界有一个认知。这条边界，就是组件的封装，面对一个封装的组件，我们不应该跨过封装的边界。\u003C\u002Fp>\n\u003Cp>但，问题又来了，如果组件的行为并不能通过 MVVM 的 state 来决定，那么，由谁决定？\u003C\u002Fp>\n","\u003Ch2>前提\u003C\u002Fh2>\n\u003Cp>MVVM 的概念由来已久，一开始，伴随着非常多的争议，MVC 还是 MVP 还是 MVVM ？MVVM 中什么是 VM ？不过随着时间的推移，大家也不再纠结到底是 MV 什么鬼了，于是也有人叫它们为 MV* 。为行文方便，下文的 MVVM 并不特指 Angular.js 之类双向绑定的框架，更多是 MV* 的意义。\u003C\u002Fp>\n\u003Cp>不管它有什么争议，核心的数据绑定机制上，大家是没有太多争议的。Angular.js 带来的数据双向绑定至今让人印象深刻。而 React 带来的单向数据流\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F01.jpg\" alt=\"UI=f(state)\">\u003C\u002Fp>\n\u003Cp>在今天已经几乎成为主流，在其它框架中都能找到它的影子。在 Vue 中也能找到它的身影，尤其是在引入 Vuex 之后，几乎就是 Flux 的翻版，概念几乎完全一样。\u003C\u002Fp>\n\u003Cp>今天要讨论的主题就是上面这个公式的边界，为避免 MVVM 名称带来的争议，下文直接使用“框架”一词。\u003C\u002Fp>\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2017\u002Fthe-boundary-of-mvvm\u002F01.jpg",{"total":13,"totalRoots":13,"comments":20,"pv":13},[]]