JavaScript 进阶之深入理解数据双向绑定

aibj4887 3年前
   <h2>前言</h2>    <p>谈起当前前端最热门的 js 框架,必少不了 Vue、React、Angular,对于大多数人来说,我们更多的是在使用框架,对于框架解决痛点背后使用的基本原理往往关注不多,近期在研读 Vue.js 源码,也在写源码解读的系列文章。和多数源码解读的文章不同的是,我会尝试从一个初级前端的角度入手,由浅入深去讲解源码实现思路和基本的语法知识,通过一些基础事例一步步去实现一些小功能。</p>    <p>本场 Chat 是系列 Chat 的开篇,我会首先讲解一下数据双向绑定的基本原理,介绍对比一下三大框架的不同实现方式,同时会一步步完成一个简单的mvvm示例。读源码不是目的,只是一种学习的方式,目的是在读源码的过程中提升自己,学习基本原理,拓展编码的思维方式。</p>    <h2>模板引擎实现原理</h2>    <p>对于页面渲染,一般分为服务器端渲染和浏览器端渲染。一般来说服务器端吐html页面的方式渲染速度更快、更利于SEO,但是浏览器端渲染更利于提高开发效率和减少维护成本,是一种相关舒服的前后端协作模式,后端提供接口,前端做视图和交互逻辑。前端通过Ajax请求数据然后拼接html字符串或者使用js模板引擎、数据驱动的框架如Vue进行页面渲染。</p>    <p>在ES6和Vue这类框架出现以前,前端绑定数据的方式是动态拼接html字符串和js模板引擎。模板引擎起到数据和视图分离的作用,模板对应视图,关注如何展示数据,在模板外头准备的数据, 关注那些数据可以被展示。模板引擎的工作原理可以简单地分成两个步骤:模板解析 / 编译(Parse / Compile)和数据渲染(Render)两部分组成,当今主流的前端模板有三种方式:</p>    <ul>     <li> <p>String-based templating (基于字符串的parse和compile过程)</p> </li>     <li> <p>Dom-based templating (基于Dom的link或compile过程)</p> </li>     <li> <p>Living templating (基于字符串的parse 和 基于dom的compile过程)</p> </li>    </ul>    <p>String-based templating</p>    <p>基于字符串的模板引擎,本质上依然是字符串拼接的形式,只是一般的库做了封装和优化,提供了更多方便的语法简化了我们的工作。基本原理如下:</p>    <p><img src="https://simg.open-open.com/show/5a21bf5f8d53946ac53d37dd4476a006.png"></p>    <p>典型的库:</p>    <ul>     <li> <p><a href="/misc/goto?guid=4959750253189426042" rel="nofollow,noindex">art-template</a></p> </li>     <li> <p><a href="/misc/goto?guid=4958341604691389041" rel="nofollow,noindex">mustache.js</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959615951708644650" rel="nofollow,noindex">doT</a></p> </li>    </ul>    <p>之前的一篇文章中我介绍了js模板引擎的实现思路,感兴趣的朋友可以看看这里: <a href="/misc/goto?guid=4959750253353258420" rel="nofollow,noindex">JavaScript进阶学习(一)—— 基于正则表达式的简单js模板引擎实现</a> 。这篇文章中我们利用正则表达式实现了一个简单的js模板引擎,利用正则匹配查找出模板中 {{}} 之间的内容,然后替换为模型中的数据,从而实现视图的渲染。</p>    <pre>  <code class="language-javascript">var template = function(tpl, data) {    var re = /{{(.+?)}}/g,      cursor = 0,      reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,          code = 'var r=[];\n';      // 解析html    function parsehtml(line) {      // 单双引号转义,换行符替换为空格,去掉前后的空格      line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,"");      code +='r.push("' + line + '");\n';    }      // 解析js代码            function parsejs(line) {         // 去掉前后的空格      line = line.replace(/(^\s+)|(\s+$)/g,"");      code += line.match(reExp)? line + '\n' : 'r.push(' + 'this.' + line + ');\n';    }          // 编译模板    while((match = re.exec(tpl))!== null) {      // 开始标签  {{ 前的内容和结束标签 }} 后的内容      parsehtml(tpl.slice(cursor, match.index));      // 开始标签  {{ 和 结束标签 }} 之间的内容      parsejs(match[1]);      // 每一次匹配完成移动指针      cursor = match.index + match[0].length;    }    // 最后一次匹配完的内容    parsehtml(tpl.substr(cursor, tpl.length - cursor));    code += 'return r.join("");';    return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);  }</code></pre>    <p>源代码: <a href="/misc/goto?guid=4959750253442664292" rel="nofollow,noindex">http://jsrun.net/yaYKp/embedded/all/light/</a></p>    <p>现在ES6支持了模板字符串,我们可以用比较简单的代码就可以实现类似的功能:</p>    <pre>  <code class="language-javascript">const template = data => `    <p>name: ${data.name}</p>    <p>age: ${data.profile.age}</p>    <ul>      ${data.skills.map(skill => `        <li>${skill}</li>      `).join('')}    </ul>`    const data = {    name: 'zhaomenghuan',    profile: { age: 24 },    skills: ['html5', 'javascript', 'android']  }    document.body.innerHTML = template(data)</code></pre>    <p>Dom-based templating</p>    <p><img src="https://simg.open-open.com/show/947e295b7ca4f321c313f4b254b3212d.png"></p>    <p>Dom-based templating 则是从DOM的角度去实现数据的渲染,我们通过遍历DOM树,提取属性与DOM内容,然后将数据写入到DOM树中,从而实现页面渲染。一个简单的例子如下:</p>    <pre>  <code class="language-javascript">function MVVM(opt) {    this.dom = document.querySelector(opt.el);    this.data = opt.data || {};    this.renderDom(this.dom);  }    MVVM.prototype = {    init: {      sTag: '{{',      eTag: '}}'    },    render: function (node) {      var self = this;      var sTag = self.init.sTag;      var eTag = self.init.eTag;        var matchs = node.textContent.split(sTag);      if (matchs.length){        var ret = '';        for (var i = 0; i < matchs.length; i++) {          var match = matchs[i].split(eTag);          if (match.length == 1) {              ret += matchs[i];          } else {              ret = self.data[match[0]];          }          node.textContent = ret;        }      }    },    renderDom: function(dom) {      var self = this;        var attrs = dom.attributes;      var nodes = dom.childNodes;        Array.prototype.forEach.call(attrs, function(item) {        self.render(item);      });        Array.prototype.forEach.call(nodes, function(item) {        if (item.nodeType === 1) {          return self.renderDom(item);        }        self.render(item);      });    }  }    var app = new MVVM({    el: '#app',    data: {      name: 'zhaomenghuan',      age: '24',      color: 'red'    }  });</code></pre>    <p>源代码: <a href="/misc/goto?guid=4959750253535510913" rel="nofollow,noindex">http://jsrun.net/faYKp/embedded/all/light/</a></p>    <p>页面渲染的函数 renderDom 是直接遍历DOM树,而不是遍历html字符串。遍历DOM树节点属性(attributes)和子节点(childNodes),然后调用渲染函数render。当DOM树子节点的类型是元素时,递归调用遍历DOM树的方法。根据DOM树节点类型一直遍历子节点,直到文本节点。</p>    <p>render的函数作用是提取 {{}} 中的关键词,然后使用数据模型中的数据进行替换。我们通过textContent获取Node节点的nodeValue,然后使用字符串的split方法对nodeValue进行分割,提取 {{}} 中的关键词然后替换为数据模型中的值。</p>    <p>DOM 的相关基础</p>    <p>注:元素类型对应NodeType</p>    <table>     <thead>      <tr>       <th>元素类型</th>       <th>NodeType</th>      </tr>     </thead>     <tbody>      <tr>       <td>元素</td>       <td>1</td>      </tr>      <tr>       <td>属性</td>       <td>2</td>      </tr>      <tr>       <td>文本</td>       <td>3</td>      </tr>      <tr>       <td>注释</td>       <td>8</td>      </tr>      <tr>       <td>文档</td>       <td>9</td>      </tr>     </tbody>    </table>    <p>childNodes 属性返回包含被选节点的子节点的 NodeList。childNodes包含的不仅仅只有html节点,所有属性,文本、注释等节点都包含在childNodes里面。children只返回元素如input, span, script, div等,不会返回TextNode,注释。</p>    <h2>数据双向绑定实现原理</h2>    <p>js模板引擎可以认为是一个基于MVC的结构,我们通过建立模板作为视图,然后通过引擎函数作为控制器实现数据和视图的绑定,从而实现实现数据在页面渲染,但是当数据模型发生变化时,视图不能自动更新;当视图数据发生变化时,模型数据不能实现更新,这个时候双向数据绑定应运而生。检测视图数据更新实现数据绑定的方法有很多种,目前主要分为三个流派,Angular使用的是脏检查,只在特定的事件下才会触发视图刷新,Vue使用的是Getter/Setter机制,而React则是通过 Virtual DOM 算法检查DOM的变动的刷新机制。</p>    <p>本文限于篇幅和内容在此只探讨一下 Vue.js 数据绑定的实现,对于 angular 和 react 后续再做说明,读者也可以自行阅读源码。Vue 监听数据变化的机制是把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Vue 2.x 对 Virtual DOM 进行了支持,这部分内容后续我们再做探讨。</p>    <p>引子</p>    <p>为了更好的理解Vue中视图和数据更新的机制,我们先看一个简单的例子:</p>    <pre>  <code class="language-javascript">var o = {    a: 0   }  Object.defineProperty(o, "b", {     get: function () {       return this.a + 1;     },    set: function (value) {       this.a = value / 2;     }  });  console.log(o.a); // "0"  console.log(o.b); // "1"    // 更新o.a  o.a = 5;  console.log(o.a); // "5"  console.log(o.b); // "6"    // 更新o.b  o.b = 10;   console.log(o.a); // "5"  console.log(o.b); // "6"</code></pre>    <p>这里我们可以看出对象o的b属性的值依赖于a属性的值,同时b属性值的变化又可以改变a属性的值,这个过程相关的属性值的变化都会影响其他相关的值进行更新。反过来我们看看如果不使用Object.defineProperty()方法,上述的问题通过直接给对象属性赋值的方法实现,代码如下:</p>    <pre>  <code class="language-javascript">var o = {    a: 0  }      o.b = o.a + 1;  console.log(o.a); // "0"  console.log(o.b); // "1"    // 更新o.a  o.a = 5;  o.b = o.a + 1;  console.log(o.a); // "5"  console.log(o.b); // "6"    // 更新o.b  o.b = 10;   o.a = o.b / 2;  o.b = o.a + 1;  console.log(o.a); // "5"  console.log(o.b); // "6"</code></pre>    <p>很显然使用 Object.defineProperty() 方法可以更方便的监听一个对象的变化。当我们的视图和数据任何一方发生变化的时候,我们希望能够通知对方也更新,这就是所谓的数据双向绑定。既然明白这个道理我们就可以看看Vue源码中相关的处理细节。</p>    <p><a href="/misc/goto?guid=4959632029316817377" rel="nofollow,noindex">Object.defineProperty()</a></p>    <p>Object.defineProperty()方法可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。</p>    <p>语法:Object.defineProperty(obj, prop, descriptor)</p>    <p>参数:</p>    <ul>     <li> <p>obj:需要定义属性的对象。</p> </li>     <li> <p>prop:需被定义或修改的属性名。</p> </li>     <li> <p>descriptor:需被定义或修改的属性的描述符。</p> </li>    </ul>    <p>返回值:返回传入函数的对象,即第一个参数obj.</p>    <p>该方法重点是描述,对象里目前存在的属性描述符有两种主要形式: <strong>数据描述符</strong> 和 <strong>存取描述符</strong> 。 <strong>数据描述符</strong> 是一个拥有可写或不可写值的属性。 <strong>存取描述符</strong> 是由一对 getter-setter 函数功能来描述的属性。描述符必须是两种形式之一;不能同时是两者。</p>    <p>数据描述符和 <strong>存取描述符</strong> 均具有以下可选键值:</p>    <ul>     <li> <p>configurable:当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false。</p> </li>     <li> <p>enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。</p> </li>    </ul>    <p>数据描述符同时具有以下可选键值:</p>    <ul>     <li> <p>value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。</p> </li>     <li> <p>writable:当且仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false。</p> </li>    </ul>    <p>存取描述符同时具有以下可选键值:</p>    <ul>     <li> <p>get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为undefined。</p> </li>     <li> <p>set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。</p> </li>    </ul>    <p>我们可以通过Object.defineProperty()方法精确添加或修改对象的属性。比如,直接赋值创建的属性默认情况是可以枚举的,但是我们可以通过Object.defineProperty()方法设置enumerable属性为false为不可枚举。</p>    <pre>  <code class="language-javascript">var obj = {    a: 0,    b: 1  }  for (var prop in obj) {    console.log(`obj.${prop} = ${obj[prop]}`);  }    结果:  "obj.a = 0"  "obj.b = 1"</code></pre>    <p>我们通过Object.defineProperty()修改如下:</p>    <pre>  <code class="language-javascript">var obj = {    a: 0,    b: 1  }  Object.defineProperty(obj, 'b', {    enumerable: false  })  for (var prop in obj) {    console.log(`obj.${prop} = ${obj[prop]}`);  }    结果:  "obj.a = 0"</code></pre>    <p>这里需要说明的是我们使用Object.defineProperty()默认情况下是enumerable属性为false,例如:</p>    <pre>  <code class="language-javascript">var obj = {    a: 0  }  Object.defineProperty(obj, 'b', {    value: 1  })  for (var prop in obj) {    console.log(`obj.${prop} = ${obj[prop]}`);  }    结果:  "obj.a = 0"</code></pre>    <p>其他描述属性使用方法类似,不做赘述。Vue源码 core/util/lang.js S中定义了这样一个方法:</p>    <pre>  <code class="language-javascript">/**   * Define a property.   */  export function def (obj: Object, key: string, val: any, enumerable?: boolean) {    Object.defineProperty(obj, key, {      value: val,      enumerable: !!enumerable,      writable: true,      configurable: true    })  }</code></pre>    <p><a href="/misc/goto?guid=4959632029658528571" rel="nofollow,noindex">Object.getOwnPropertyDescriptor()</a></p>    <p>Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性) 语法:Object.getOwnPropertyDescriptor(obj, prop)</p>    <p>参数:</p>    <ul>     <li> <p>obj:在该对象上查看属性。</p> </li>     <li> <p>prop:一个属性名称,该属性的属性描述符将被返回。</p> </li>    </ul>    <p>返回值:如果指定的属性存在于对象上,则返回其属性描述符(property descriptor),否则返回 undefined。可以访问“属性描述符”内容,例如前面的例子:</p>    <pre>  <code class="language-javascript">var o = {    a: 0   }    Object.defineProperty(o, "b", {     get: function () {       return this.a + 1;     },    set: function (value) {       this.a = value / 2;     }  });    var des = Object.getOwnPropertyDescriptor(o,'b');  console.log(des);  console.log(des.get);</code></pre>    <p>Vue源码分析</p>    <p>本次我们主要分析一下Vue 数据绑定的源码,这里我直接将 <a href="/misc/goto?guid=4959750253700623377" rel="nofollow,noindex">Vue.js 1.0.28</a> 版本的代码稍作删减拿过来进行,2.x 的代码基于 flow 静态类型检查器书写的,代码除了编码风格在整体结构上基本没有太大改动,所以依然基于 1.x 进行分析,对于存在差异的部分加以说明。</p>    <p><img src="https://simg.open-open.com/show/e6f8aff2297d4afe0caef4a486776cdc.png"></p>    <p>监听对象变动</p>    <pre>  <code class="language-javascript">// 观察者构造函数  function Observer (value) {    this.value = value    this.walk(value)  }    // 递归调用,为对象绑定getter/setter  Observer.prototype.walk = function (obj) {    var keys = Object.keys(obj)    for (var i = 0, l = keys.length; i < l; i++) {      this.convert(keys[i], obj[keys[i]])    }  }    // 将属性转换为getter/setter  Observer.prototype.convert = function (key, val) {    defineReactive(this.value, key, val)  }    // 创建数据观察者实例  function observe (value) {    // 当值不存在或者不是对象类型时,不需要继续深入监听    if (!value || typeof value !== 'object') {      return    }    return new Observer(value)  }    // 定义对象属性的getter/setter  function defineReactive (obj, key, val) {    var property = Object.getOwnPropertyDescriptor(obj, key)    if (property && property.configurable === false) {      return    }      // 保存对象属性预先定义的getter/setter    var getter = property && property.get    var setter = property && property.set      var childOb = observe(val)    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get: function reactiveGetter () {        var value = getter ? getter.call(obj) : val        console.log("访问:"+key)        return value      },      set: function reactiveSetter (newVal) {        var value = getter ? getter.call(obj) : val        if (newVal === value) {          return        }        if (setter) {          setter.call(obj, newVal)        } else {          val = newVal        }        // 对新值进行监听        childOb = observe(newVal)        console.log('更新:' + key + ' = ' + newVal)      }    })  }</code></pre>    <p>定义一个对象作为数据模型,并监听这个对象。</p>    <pre>  <code class="language-javascript">let data = {    user: {      name: 'zhaomenghuan',      age: '24'    },    address: {      city: 'beijing'    }  }  observe(data)    console.log(data.user.name)   // 访问:user   // 访问:name    data.user.name = 'ZHAO MENGHUAN'  // 访问:user  // 更新:name = ZHAO MENGHUAN</code></pre>    <p>效果如下:</p>    <p><img src="https://simg.open-open.com/show/4d95fb29aa0250f6ad29953a056d7d2e.gif"></p>    <p>监听数组变动</p>    <p>上面我们通过Object.defineProperty把对象的属性全部转为 getter/setter 从而实现监听对象的变动,但是对于数组对象无法通过Object.defineProperty实现监听。Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。</p>    <pre>  <code class="language-javascript">const arrayProto = Array.prototype  const arrayMethods = Object.create(arrayProto)    function def(obj, key, val, enumerable) {    Object.defineProperty(obj, key, {      value: val,      enumerable: !!enumerable,      writable: true,      configurable: true    })  }    // 数组的变异方法  ;[    'push',    'pop',    'shift',    'unshift',    'splice',    'sort',    'reverse'  ]  .forEach(function (method) {    // 缓存数组原始方法    var original = arrayProto[method]    def(arrayMethods, method, function mutator () {      var i = arguments.length      var args = new Array(i)      while (i--) {        args[i] = arguments[i]      }      console.log('数组变动')      return original.apply(this, args)    })  })</code></pre>    <p>Vue.js 1.x 在Array.prototype原型对象上添加了 $set 和 $remove 方法,在2.X后移除了,使用全局 API Vue.set 和 Vue.delete 代替了,后续我们再分析。</p>    <p>定义一个数组作为数据模型,并对这个数组调用变异的七个方法实现监听。</p>    <pre>  <code class="language-javascript">let skills = ['JavaScript', 'Node.js', 'html5']  // 原型指针指向具有变异方法的数组对象  skills.__proto__ = arrayMethods    skills.push('java')  // 数组变动  skills.pop()  // 数组变动</code></pre>    <p>效果如下:</p>    <p><img src="https://simg.open-open.com/show/851cc35ac70074cb9debfc0e007c7403.gif"></p>    <p>我们将需要监听的数组的原型指针指向我们定义的数组对象,这样我们的数组在调用上面七个数组的变异方法时,能够监听到变动从而实现对数组进行跟踪。</p>    <p>对于 __proto__ 属性,在ES2015中正式被加入到规范中,标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,所以 Vue 是先进行了判断,当 __proto__ 属性存在时将原型指针 __proto__ 指向具有变异方法的数组对象,不存在时直接将具有变异方法挂在需要追踪的对象上。</p>    <p>我们可以在上面Observer观察者构造函数中添加对数组的监听,源码如下:</p>    <pre>  <code class="language-javascript">const hasProto = '__proto__' in {}  const arrayKeys = Object.getOwnPropertyNames(arrayMethods)    // 观察者构造函数  function Observer (value) {    this.value = value    if (Array.isArray(value)) {      var augment = hasProto        ? protoAugment        : copyAugment      augment(value, arrayMethods, arrayKeys)      this.observeArray(value)    } else {      this.walk(value)    }  }    // 观察数组的每一项  Observer.prototype.observeArray = function (items) {    for (var i = 0, l = items.length; i < l; i++) {      observe(items[i])    }  }    // 将目标对象/数组的原型指针__proto__指向src  function protoAugment (target, src) {    target.__proto__ = src  }    // 将具有变异方法挂在需要追踪的对象上  function copyAugment (target, src, keys) {    for (var i = 0, l = keys.length; i < l; i++) {      var key = keys[i]      def(target, key, src[key])    }  }</code></pre>    <p>原型链</p>    <p>对于不了解原型链的朋友可以看一下我这里画的一个基本关系图:</p>    <p><img src="https://simg.open-open.com/show/5459374260a657a2d4bcfb5cdd15716b.png"></p>    <ul>     <li> <p>原型对象是构造函数的prototype属性,是所有实例化对象共享属性和方法的原型对象。</p> </li>     <li> <p>实例化对象通过new构造函数得到,都继承了原型对象的属性和方法。</p> </li>     <li> <p>原型对象中有个隐式的constructor,指向了构造函数本身。</p> </li>    </ul>    <p>Object.create</p>    <p>Object.create 使用指定的原型对象和其属性创建了一个新的对象。</p>    <pre>  <code class="language-javascript">const arrayProto = Array.prototype  const arrayMethods = Object.create(arrayProto)</code></pre>    <p>这一步是通过 Object.create 创建了一个原型对象为Array.prototype的空对象。然后通过Object.defineProperty方法对这个对象定义几个变异的数组方法。有些新手可能会直接修改 Array.prototype 上的方法,这是很危险的行为,这样在引入的时候会全局影响Array 对象的方法,而使用Object.create实质上是完全了一份拷贝,新生成的arrayMethods对象的原型指针 __proto__ 指向了Array.prototype,修改arrayMethods 对象不会影响Array.prototype。</p>    <p>基于这种原理,我们通常会使用Object.create 实现类式继承。</p>    <pre>  <code class="language-javascript">// 实现继承  var extend = function(Child, Parent) {      // 拷贝Parent原型对象      Child.prototype = Object.create(Parent.prototype);      // 将Child构造函数赋值给Child的原型对象      Child.prototype.constructor = Child;  }    // 实例  var Parent = function () {      this.name = 'Parent';  }  Parent.prototype.getName = function () {      return this.name;  }  var Child = function () {      this.name = 'Child';  }  extend(Child, Parent);  var child = new Child();  console.log(child.getName())</code></pre>    <p>发布-订阅模式</p>    <p>在上面一部分我们通过Object.defineProperty把对象的属性全部转为 getter/setter 以及 数组变异方法实现了对数据模型变动的监听,在数据变动的时候,我们通过console.log打印出来提示了,但是对于框架而言,我们相关的逻辑如果直接写在那些地方,自然是不够优雅和灵活的,这个时候就需要引入常用的设计模式去实现,vue.js采用了发布-订阅模式。发布-订阅模式主要是为了达到一种“高内聚、低耦合"的效果。</p>    <p>Vue的Watcher订阅者作为Observer和Compile之间通信的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。</p>    <pre>  <code class="language-javascript">/**   * 观察者对象   */  function Watcher(vm, expOrFn, cb) {      this.vm = vm      this.cb = cb      this.depIds = {}      if (typeof expOrFn === 'function') {          this.getter = expOrFn      } else {          this.getter = this.parseExpression(expOrFn)      }      this.value = this.get()  }    /**   * 收集依赖   */  Watcher.prototype.get = function () {      // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者      Dep.target = this      // 触发getter,将自身添加到dep中      const value = this.getter.call(this.vm, this.vm)      // 依赖收集完成,置空,用于下一个Watcher使用      Dep.target = null      return value  }    Watcher.prototype.addDep = function (dep) {      if (!this.depIds.hasOwnProperty(dep.id)) {          dep.addSub(this)          this.depIds[dep.id] = dep      }  }    /**   * 依赖变动更新   *   * @param {Boolean} shallow   */  Watcher.prototype.update = function () {      this.run()  }    Watcher.prototype.run = function () {      var value = this.get()      if (value !== this.value) {          var oldValue = this.value          this.value = value          // 将newVal, oldVal挂载到MVVM实例上          this.cb.call(this.vm, value, oldValue)      }  }    Watcher.prototype.parseExpression = function (exp) {      if (/[^\w.$]/.test(exp)) {          return      }      var exps = exp.split('.')        return function(obj) {          for (var i = 0, len = exps.length; i < len; i++) {              if (!obj) return              obj = obj[exps[i]]          }          return obj      }  }</code></pre>    <p>Dep 是一个数据结构,其本质是维护了一个watcher队列,负责添加watcher,更新watcher,移除watcher,通知watcher更新。</p>    <pre>  <code class="language-javascript">let uid = 0    function Dep() {      this.id = uid++      this.subs = []  }    Dep.target = null    /**   * 添加一个订阅者   *   * @param {Directive} sub   */  Dep.prototype.addSub = function (sub) {      this.subs.push(sub)  }    /**   * 移除一个订阅者   *   * @param {Directive} sub   */  Dep.prototype.removeSub = function (sub) {      let index = this.subs.indexOf(sub);      if (index !== -1) {          this.subs.splice(index, 1);      }  }    /**   * 将自身作为依赖添加到目标watcher   */  Dep.prototype.depend = function () {      Dep.target.addDep(this)  }    /**   * 通知数据变更   */  Dep.prototype.notify = function () {      var subs = toArray(this.subs)      // stablize the subscriber list first      for (var i = 0, l = subs.length; i < l; i++) {          // 执行订阅者的update更新函数          subs[i].update()      }  }</code></pre>    <p>模板编译</p>    <p>compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。</p>    <pre>  <code class="language-javascript">function Compile(el, value) {      this.$vm = value      this.$el = this.isElementNode(el) ? el : document.querySelector(el)      if (this.$el) {          this.compileElement(this.$el)      }  }    Compile.prototype.compileElement = function (el) {      let self = this      let childNodes = el.childNodes        ;[].slice.call(childNodes).forEach(node => {          let text = node.textContent          let reg = /\{\{((?:.|\n)+?)\}\}/          // 处理element节点          if (self.isElementNode(node)) {              self.compile(node)          } else if (self.isTextNode(node) && reg.test(text)) { // 处理text节点              self.compileText(node, RegExp.$1.trim())          }          // 解析子节点包含的指令          if (node.childNodes && node.childNodes.length) {              self.compileElement(node)          }      })  }    Compile.prototype.compile = function (node) {      let nodeAttrs = node.attributes      let self = this        ;[].slice.call(nodeAttrs).forEach(attr => {          var attrName = attr.name          if (self.isDirective(attrName)) {              let exp = attr.value              let dir = attrName.substring(2)              if (self.isEventDirective(dir)) {                  compileUtil.eventHandler(node, self.$vm, exp, dir)              } else {                  compileUtil[dir] && compileUtil[dir](node, self.$vm, exp)              }              node.removeAttribute(attrName)          }      });  }    Compile.prototype.compileText = function (node, exp) {      compileUtil.text(node, this.$vm, exp);  }    Compile.prototype.isDirective = function (attr) {      return attr.indexOf('v-') === 0  }    Compile.prototype.isEventDirective = function (dir) {      return dir.indexOf('on') === 0;  }    Compile.prototype.isElementNode = function (node) {      return node.nodeType === 1  }    Compile.prototype.isTextNode = function (node) {      return node.nodeType === 3  }    // 指令处理集合  var compileUtil = {      text: function (node, vm, exp) {          this.bind(node, vm, exp, 'text')      },      html: function (node, vm, exp) {          this.bind(node, vm, exp, 'html')      },      model: function (node, vm, exp) {          this.bind(node, vm, exp, 'model')            let self = this, val = this._getVMVal(vm, exp)          node.addEventListener('input', function (e) {              var newValue = e.target.value              if (val === newValue) {                  return              }              self._setVMVal(vm, exp, newValue)              val = newValue          });      },      bind: function (node, vm, exp, dir) {          var updaterFn = updater[dir + 'Updater']          updaterFn && updaterFn(node, this._getVMVal(vm, exp))          new Watcher(vm, exp, function (value, oldValue) {              updaterFn && updaterFn(node, value, oldValue)          })      },      eventHandler: function (node, vm, exp, dir) {          var eventType = dir.split(':')[1],              fn = vm.$options.methods && vm.$options.methods[exp];            if (eventType && fn) {              node.addEventListener(eventType, fn.bind(vm), false);          }      },      _getVMVal: function (vm, exp) {          var val = vm          exp = exp.split('.')          exp.forEach(function (k) {              val = val[k]          })          return val      },      _setVMVal: function (vm, exp, value) {          var val = vm;          exp = exp.split('.')          exp.forEach(function (k, i) {              // 非最后一个key,更新val的值              if (i < exp.length - 1) {                  val = val[k]              } else {                  val[k] = value              }          })      }  }    var updater = {      textUpdater: function (node, value) {          node.textContent = typeof value == 'undefined' ? '' : value      },      htmlUpdater: function (node, value) {          node.innerHTML = typeof value == 'undefined' ? '' : value      },      modelUpdater: function (node, value, oldValue) {          node.value = typeof value == 'undefined' ? '' : value      }  }</code></pre>    <p>这种实现和我们讲到的Dom-based templating类似,只是更加完备,具有自定义指令的功能。在遍历节点属性和文本节点的时候,可以编译具备 {{}} 表达式或 v-xxx 的属性值的节点,并且通过添加 new Watcher() 及绑定事件函数,监听数据的变动从而对视图实现双向绑定。</p>    <p>MVVM实例</p>    <p>在数据绑定初始化的时候,我们需要通过 new Observer() 来监听数据模型变化,通过 new Compile() 来解析编译模板指令,并利用Watcher搭起Observer和Compile之间的通信桥梁。</p>    <pre>  <code class="language-javascript">/**   * @class 双向绑定类 MVVM   * @param {[type]} options [description]   */  function MVVM(options) {      this.$options = options || {}      // 简化了对data的处理      let data = this._data = this.$options.data      // 监听数据      observe(data)      new Compile(options.el || document.body, this)  }    MVVM.prototype.$watch = function (expOrFn, cb) {      new Watcher(this, expOrFn, cb)  }</code></pre>    <p>为了能够直接通过实例化对象操作数据模型,我们需要为MVVM实例添加一个数据模型代理的方法:</p>    <pre>  <code class="language-javascript">MVVM.prototype._proxy = function (key) {      Object.defineProperty(this, key, {          configurable: true,          enumerable: true,          get: () => this._data[key],          set: (val) => {              this._data[key] = val          }      })  }</code></pre>    <p>至此我们可以通过一个小例子来说明本文的内容。</p>    <p><img src="https://simg.open-open.com/show/4dc6d559127b901f7b499bf00415c1b5.gif"></p>    <pre>  <code class="language-javascript"><div id="app">      <h3>{{user.name}}</h3>      <input type="text" v-model="modelValue">      <p>{{modelValue}}</p>  </div>  <script>      let vm = new MVVM({          el: '#app',          data: {              modelValue: '',              user: {                  name: 'zhaomenghuan',                  age: '24'              },              address: {                  city: 'beijing'              },              skills: ['JavaScript', 'Node.js', 'html5']          }      })        vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`))  </script></code></pre>    <p>本文目的不是为了造一个轮子,而是在学习优秀框架实现的过程中去提升自己,搞清楚框架发展的前因后果,由浅及深去学习基础,本文参考了网上很多优秀博主的文章,由于时间关系,有些内容没有做深入探讨,觉得还是有些遗憾,在后续的学习中会更多的独立思考,提出更多自己的想法。</p>    <h2>参考文档</h2>    <ul>     <li> <p><a href="/misc/goto?guid=4959750253785115655" rel="nofollow,noindex">前端模板技术面面观</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959632029316817377" rel="nofollow,noindex">Object.defineProperty()</a></p> </li>     <li> <p><a href="/misc/goto?guid=4958964911428859279" rel="nofollow,noindex">Vue.js 源码学习笔记</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959750253917073360" rel="nofollow,noindex">vue早期源码学习系列</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959750254019397639" rel="nofollow,noindex">解析最简单的observer和watcher</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959750254096034902" rel="nofollow,noindex">剖析Vue实现原理 - 如何实现双向绑定mvvm</a></p> </li>    </ul>    <p>来自:http://gitbook.cn/books/593faaf7d3845323661a4cec/index.html</p>    <p> </p>