自己实现MVVM(Vue源码解析)

vevd6292 8年前
   <h2>前言</h2>    <p>本文会带大家手动实现一个双向绑定过程(仅仅涵盖一些简单的指令解析,如: v-text , v-model ,插值),当然借鉴的是Vue1的源码,相信大家在阅读完本文后对Vue1会有一个更好的理解, <a href="/misc/goto?guid=4959728678591112440" rel="nofollow,noindex">源代码</a> 放到了github,由于本人水平有限,理解不到位的地方还请大家指出。</p>    <h2>MVVM</h2>    <p>MVVM 使开发可以更加关注于数据,减少了很大的工作量,也使代码可读性,可维护性更高, MVVM 核心的思想就是视图是状态的函数: <strong>View = ViewModel(Model)</strong> ,所以当Model发生改变时,ViewModel会来操作View来怎么做,而非是自己写代码来做。无论是双向绑定还是单向绑定,都是符合 MVVM 思想的。Vue提倡的是双向绑定,也就是允许View到Model的变化,其实这个场景出现在的也就是表单操作上, 看个例子 ,例子中分别利用了Vue和React实现了一下表单 value 变化,影响页面与其相关的 dom 节点发生变化, 可以发现的是双向绑定的Vue是 input 的 value 发生变化则 h1 的 innerText 就发生了变化,变化是由View->Model,而提倡单向数据流的 React 需要手动监听事件,事件触发后,更改Model的值,从而使 input 的 value 发生了变化。看了Vue的源码后不难发现Vue的双向绑定的实现也就是在表单元素上添加了 input 事件,可以说双向绑定是单向绑定的一个语法糖。</p>    <h2>实现思路</h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/20f4e6df69db90c9638d596851958b6e.png"></p>    <p>上图是一个大体的流程,下面按照流程来实现下:</p>    <ul>     <li> <p>利用 observer 对 data 进行了监听,并且提供订阅某个数据项的变化的能力</p> </li>    </ul>    <p>这点的实现,需要借助的是 Object.defineProperty() 来为对象的属性绑定 get/set 特性(由于利用了 Object.defineProperty() ,所以Vue不支持ie8), observer 需要将 data 的所有属性都绑定 get/set ,很容易想到的就是利用递归来实现 。</p>    <ul>     <li> <p>利用 Compile 对模板进行解析</p> </li>    </ul>    <p>这点实现的是将我们的模板转化为 html ,过程中会将数据与View中的节点相关联起来,最终会将编译好的 html 页面替换到页面上。首先来看解析,首先从根节点开始,根据不同的节点类型采用不同的解析方式:</p>    <pre>  <code class="language-javascript">function compileNode(node, vm) {      const type = node.nodeType;      if (type === 1 && !isScript(node)) {          compileElement(node, vm);      } else if (type === 3 && node.data.trim()) {          compileTextNode(node, vm);      } else {          return null;      }  }</code></pre>    <p>对于文本节点来说,可能存在情况只有两种:</p>    <ol>     <li> <p>与数据不相关不用操作</p> </li>     <li> <p>含有插值,需要与数据进行关联</p>      <ul>       <li> <p>{{}} 文本插值</p> </li>       <li> <p>{{{}}} 纯 html 插值</p> </li>      </ul> </li>    </ol>    <p>利用下面正就可以将插值找出:</p>    <pre>  <code class="language-javascript">/\{\{\{(.*?)\}\}\}|\{\{(.*?)\}\}/g</code></pre>    <p>采用下面函数来对文本节点的内容解析:</p>    <pre>  <code class="language-javascript">function parseText(node) {      var text = node.wholeText;      if (!tagRE.test(text)) {          return void 0;      }      const tokens = [];      var lastIndex = tagRE.lastIndex = 0,          match, index, html, value;      while (match = tagRE.exec(text)) {          index = match.index;          if (index > lastIndex) {              tokens.push({                  value: text.slice(lastIndex, index)              })          }          html = htmlRE.test(match[0]);          value = html ? match[1] : match[2];          tokens.push({              value: value,              tag: true,              html: html          });          lastIndex = index + match[0].length;      }      if (lastIndex < text.length) {          tokens.push({              value: text.slice(lastIndex)          })      }      return tokens;  }</code></pre>    <p>返回了 tokens ,里面存储了每一个块内容,一个插值or一个普通文本, tag 来标记是否为插值, html 来标记是否为纯 html 插值。遍历返回的 tokens ,根据不同的类型,来采用不同的方式将其添加到其父节点上:</p>    <pre>  <code class="language-javascript">function compileTextNode(node, vm) {      const tokens = parseText(node);      if (tokens == null) return void 0;      var frag = document.createDocumentFragment();      tokens.forEach(token => {          var el;          if (token.tag) {              if (token.html) {                  el = document.createDocumentFragment();                  el.$parent = node.parentNode;                  el.$oneTime = true;                  dirCollection["html"](el, vm, token.value);              } else {                  el = document.createTextNode(" ");                  dirCollection["text"](el, vm, token.value);              }          } else {              el = document.createTextNode(token.value);          }           el && frag.appendChild(el);      });      return replace(node, frag);  }</code></pre>    <p>dirCollection 是一个指令集合,也就是决定了如何初始化以及如何更新该节点。对于 nodeType 为 1 的节点来说,指令全部存储在其属性中,遍历属性,假若指令中含有 v-html,v-model,v-text ,则停止遍历其子树,直接将调用相应指令即可,否则,则需要遍历其子节点,对其子节点应用 compileNode 进行解析:</p>    <pre>  <code class="language-javascript">function compileNodeList(nodes, vm) {      for (let val of nodes) {          compileNode(val, vm);      }  }  function compileElement(node, vm) {      var flag = false;      const attrs = Array.prototype.slice.call(node.attributes);      attrs.forEach((val) => {          const name = val.name,              value = val.value;          if (dirRE.test(name)) {              var dir;              // 事件指令              if (                  (dir = name.match(eventRE)) &&                   (dir = dir[1])              ) {                  dirCollection["eventDir"](node, dir, vm, value);              } else {                  dir = name.match(dirRE)[1];                  dirCollection[dir](node, vm, value);              }              // 指令中为v-html or v-text or v-model终止递归              flag = flag ||                   name === vhtml ||                   name === vtext;                  node.removeAttribute(name);          }          });      const childs = node.childNodes;      if (!flag && childs && childs.length) {          compileNodeList(childs, vm);      }  }</code></pre>    <p>在 dirCollections 中还会做的就是将数据与View的 dom 节点相关联,利用的就是 Dep 与 Watcher ,页面上每一个与数据相关联的节点都含有一个 Watcher ,当数据发生变化是 Watcher 用于计算,是否需要更新该节点;数据的每一个属性都有一个 Dep ,当该属性发生变化时, Dep 会通知与该数据相关联的 Watcher 来进行计算是否需要更新对应页面。 <a href="/misc/goto?guid=4959728678694354260" rel="nofollow,noindex">Dep代码</a> , <a href="/misc/goto?guid=4959728678773057087" rel="nofollow,noindex">Watcher代码</a> 。</p>    <ul>     <li> <p>异步更新队列</p> </li>    </ul>    <p>异步更新队列,是一个优化,将更新 dom 的操作变为异步的,放到下一个事件循环来做,这样做可以减少不必要的 dom 更新,看下面情况:</p>    <pre>  <code class="language-javascript">vm.value++;  vm.value++;  vm.value++;</code></pre>    <p>三次数据改变,假若同步更新的话,则每次数据改变会立即更新 dom ,而异步更新的话,可以先将更新推入一个队列中,由于是异步,也可以保证每一个 Watcher 只被推入到一次,这样就避免了不必要的更新,异步更新主要利用的是 nextTick ,这个函数会优先使用 Promise ,不兼容则利用 MutationObserver ,再不兼容的话会利用 setTimeout 。</p>    <h2>写在后面</h2>    <p>看过了Vue的源码不得不感叹Vue的优美,而Vue2又增加了虚拟dom,这样就可以做到服务端渲染,给了我们更多的可能!</p>    <p> </p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007741904</p>    <p> </p>