JavaScript 进阶之深切明白数据双向绑定

媒介

谈起当前前端最热点的 js 框架,必少不了 Vue、React、Angular,关于大多半人来讲,我们更多的是在运用框架,关于框架处置惩罚痛点背地运用的基本原理每每关注不多,近期在研读 Vue.js 源码,也在写源码解读的系列文章。和多半源码解读的文章差别的是,我会尝试从一个低级前端的角度入手,由浅入深去解说源码完成思绪和基本的语法学问,经由历程一些基本事例一步步去完成一些小功用。

本场 Chat 是系列 Chat 的开篇,我会起首解说一下数据双向绑定的基本原理,引见对照一下三大框架的差别完成体式格局,同时会一步步完成一个简朴的mvvm示例。读源码不是目的,只是一种进修的体式格局,目的是在读源码的历程当中提拔本身,进修基本原理,拓展编码的头脑体式格局。

模板引擎完成原理

关于页面衬着,平常分为服务器端衬着和浏览器端衬着。平常来讲服务器端吐html页面的体式格局衬着速率更快、更利于SEO,然则浏览器端衬着更利于进步开辟效力和削减保护本钱,是一种相干惬意的前后端合作情势,后端供应接口,前端做视图和交互逻辑。前端经由历程Ajax要求数据然后拼接html字符串或许运用js模板引擎、数据驱动的框架如Vue举行页面衬着。

在ES6和Vue这类框架涌现之前,前端绑定数据的体式格局是动态拼接html字符串和js模板引擎。模板引擎起到数据和视图星散的作用,模板对应视图,关注怎样展现数据,在模板外头预备的数据, 关注那些数据可以被展现。模板引擎的事变原理可以简朴地分红两个步骤:模板理会 / 编译(Parse / Compile)和数据衬着(Render)两部份构成,现今主流的前端模板有三种体式格局:

  • String-based templating (基于字符串的parse和compile历程)
  • Dom-based templating (基于Dom的link或compile历程)
  • Living templating (基于字符串的parse 和 基于dom的compile历程)

String-based templating

基于字符串的模板引擎,本质上依旧是字符串拼接的情势,只是平常的库做了封装和优化,供应了更多轻易的语法简化了我们的事变。基本原理以下:

《JavaScript 进阶之深切明白数据双向绑定》

典范的库:

之前的一篇文章中我引见了js模板引擎的完成思绪,感兴趣的朋侪可以看看这里:JavaScript进阶进修(一)—— 基于正则表达式的简朴js模板引擎完成。这篇文章中我们应用正则表达式完成了一个简朴的js模板引擎,应用正则婚配查找出模板中{{}}之间的内容,然后替代为模子中的数据,从而完成视图的衬着。

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);
}

源代码:http://jsfiddle.net/zhaomengh…

如今ES6支撑了模板字符串,我们可以用比较简朴的代码就可以完成相似的功用:

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)

Dom-based templating

《JavaScript 进阶之深切明白数据双向绑定》

Dom-based templating 则是从DOM的角度去完成数据的衬着,我们经由历程遍历DOM树,提取属性与DOM内容,然后将数据写入到DOM树中,从而完成页面衬着。一个简朴的例子以下:

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'
  }
});

源代码:http://jsfiddle.net/zhaomengh…

页面衬着的函数 renderDom 是直接遍历DOM树,而不是遍历html字符串。遍历DOM树节点属性(attributes)和子节点(childNodes),然后挪用衬着函数render。当DOM树子节点的范例是元素时,递归挪用遍历DOM树的要领。依据DOM树节点范例一向遍历子节点,直到文本节点。

render的函数作用是提取{{}}中的关键词,然后运用数据模子中的数据举行替代。我们经由历程textContent猎取Node节点的nodeValue,然后运用字符串的split要领对nodeValue举行支解,提取{{}}中的关键词然后替代为数据模子中的值。

DOM 的相干基本

注:元素范例对应NodeType

元素范例NodeType
元素1
属性2
文本3
解释8
文档9

childNodes 属性返回包括被选节点的子节点的 NodeList。childNodes包括的不单单议只要html节点,一切属性,文本、解释等节点都包括在childNodes内里。children只返回元素如input, span, script, div等,不会返回TextNode,解释。

数据双向绑定完成原理

js模板引擎可以以为是一个基于MVC的组织,我们经由历程竖立模板作为视图,然后经由历程引擎函数作为控制器完成数据和视图的绑定,从而完成完成数据在页面衬着,然则当数据模子发生变化时,视图不能自动更新;当视图数据发生变化时,模子数据不能完成更新,这个时刻双向数据绑定应运而生。检测视图数据更新完成数据绑定的要领有许多种,现在重要分为三个派别,Angular运用的是脏搜检,只在特定的事宜下才会触发视图革新,Vue运用的是Getter/Setter机制,而React则是经由历程 Virtual DOM 算法搜检DOM的更改的革新机制。

本文限于篇幅和内容在此只讨论一下 Vue.js 数据绑定的完成,关于 angular 和 react 后续再做申明,读者也可以自行浏览源码。Vue 监听数据变化的机制是把一个一般 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象一切的属性,并运用 Object.defineProperty 把这些属性悉数转为 getter/setter。Vue 2.x 对 Virtual DOM 举行了支撑,这部份内容后续我们再做讨论。

引子

为了更好的邃晓Vue中视图和数据更新的机制,我们先看一个简朴的例子:

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"

这里我们可以看出对象o的b属性的值依靠于a属性的值,同时b属性值的变化又可以转变a属性的值,这个历程相干的属性值的变化都邑影响其他相干的值举行更新。反过来我们看看假如不运用Object.defineProperty()要领,上述的题目经由历程直接给对象属性赋值的要领完成,代码以下:

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"

很显然运用Object.defineProperty()要领可以更轻易的监听一个对象的变化。当我们的视图和数据任何一方发生变化的时刻,我们愿望可以关照对方也更新,这就是所谓的数据双向绑定。既然邃晓这个原理我们就可以看看Vue源码中相干的处置惩罚细节。

Object.defineProperty()

Object.defineProperty()要领可以直接在一个对象上定义一个新属性,或许修正一个已存在的属性, 并返回这个对象。

语法:Object.defineProperty(obj, prop, descriptor)

参数:

  • obj:须要定义属性的对象。
  • prop:需被定义或修正的属性名。
  • descriptor:需被定义或修正的属性的形貌符。

返回值:返回传入函数的对象,即第一个参数obj

该要领重点是形貌,对象里现在存在的属性形貌符有两种重要情势:数据形貌符存取形貌符数据形貌符是一个具有可写或不可写值的属性。存取形貌符是由一对 getter-setter 函数功用来形貌的属性。形貌符必需是两种情势之一;不能同时是二者。

数据形貌符存取形貌符均具有以下可选键值:

  • configurable:当且仅当该属性的 configurable 为 true 时,该属性才可以被转变,也可以被删除。默以为 false。
  • enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才可以涌如今对象的罗列属性中。默以为 false。

数据形貌符同时具有以下可选键值:

  • value:该属性对应的值。可所以任何有用的 JavaScript 值(数值,对象,函数等)。默以为 undefined。
  • writable:当且仅当仅当该属性的writable为 true 时,该属性才被赋值运算符转变。默以为 false。

存取形貌符同时具有以下可选键值:

  • get:一个给属性供应 getter 的要领,假如没有 getter 则为 undefined。该要领返回值被用作属性值。默以为undefined。
  • set:一个给属性供应 setter 的要领,假如没有 setter 则为 undefined。该要领将接收唯一参数,并将该参数的新值分配给该属性。默以为undefined。

我们可以经由历程Object.defineProperty()要领准确增加或修正对象的属性。比方,直接赋值建立的属性默许状况是可以罗列的,然则我们可以经由历程Object.defineProperty()要领设置enumerable属性为false为不可罗列。

var obj = {
  a: 0,
  b: 1
}
for (var prop in obj) {
  console.log(`obj.${prop} = ${obj[prop]}`);
}

结果:
"obj.a = 0"
"obj.b = 1"

我们经由历程Object.defineProperty()修正以下:

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"

这里须要申明的是我们运用Object.defineProperty()默许状况下是enumerable属性为false,比方:

var obj = {
  a: 0
}
Object.defineProperty(obj, 'b', {
  value: 1
})
for (var prop in obj) {
  console.log(`obj.${prop} = ${obj[prop]}`);
}

结果:
"obj.a = 0"

其他形貌属性运用要领相似,不做赘述。Vue源码core/util/lang.jsS中定义了如许一个要领:

/**
 * 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
  })
}

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性形貌符。(自有属性指的是直接给予该对象的属性,不须要从原型链上举行查找的属性)

语法:Object.getOwnPropertyDescriptor(obj, prop)

参数:

  • obj:在该对象上检察属性
  • prop:一个属性称号,该属性的属性形貌符将被返回

返回值:假如指定的属性存在于对象上,则返回其属性形貌符(property descriptor),不然返回 undefined。可以接见“属性形貌符”内容,比方前面的例子:

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);

Vue源码理会

本次我们重要理会一下Vue 数据绑定的源码,这里我直接将 Vue.js 1.0.28 版本的代码稍作删减拿过来举行,2.x 的代码基于 flow 静态范例搜检器誊写的,代码除了编码作风在团体组织上基本没有太大修改,所以依旧基于 1.x 举行理会,关于存在差别的部份加以申明。

《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)
    }
  })
}

定义一个对象作为数据模子,并监听这个对象。

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

结果以下:
《JavaScript 进阶之深切明白数据双向绑定》

监听数组更改

上面我们经由历程Object.defineProperty把对象的属性悉数转为 getter/setter 从而完成监听对象的更改,然则关于数组对象没法经由历程Object.defineProperty完成监听。Vue 包括一组视察数组的变异要领,所以它们也将会触发视图更新。

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)
  })
})

Vue.js 1.x 在Array.prototype原型对象上增加了$set$remove要领,在2.X后移除了,运用全局 API Vue.setVue.delete替代了,后续我们再理会。

定义一个数组作为数据模子,并对这个数组挪用变异的七个要领完成监听。

let skills = ['JavaScript', 'Node.js', 'html5']
// 原型指针指向具有变异要领的数组对象
skills.__proto__ = arrayMethods

skills.push('java')
// 数组更改
skills.pop()
// 数组更改

结果以下:
《JavaScript 进阶之深切明白数据双向绑定》

我们将须要监听的数组的原型指针指向我们定义的数组对象,如许我们的数组在挪用上面七个数组的变异要领时,可以监听到更改从而完成对数组举行跟踪。

关于__proto__属性,在ES2015中正式被加入到范例中,规范明确规定,只要浏览器必需布置这个属性,其他运转环境不一定须要布置,所以 Vue 是先举行了推断,当__proto__属性存在时将原型指针__proto__指向具有变异要领的数组对象,不存在时直接将具有变异要领挂在须要追踪的对象上。

我们可以在上面Observer视察者组织函数中增加对数组的监听,源码以下:

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])
  }
}

原型链

关于不相识原型链的朋侪可以看一下我这里画的一个基本关联图:
《JavaScript 进阶之深切明白数据双向绑定》

  • 原型对象是组织函数的prototype属性,是一切实例化对象同享属性和要领的原型对象;
  • 实例化对象经由历程new组织函数获得,都继续了原型对象的属性和要领;
  • 原型对象中有个隐式的constructor,指向了组织函数本身。

Object.create

Object.create 运用指定的原型对象和其属性建立了一个新的对象。

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

这一步是经由历程 Object.create 建立了一个原型对象为Array.prototype的空对象。然后经由历程Object.defineProperty要领对这个对象定义几个变异的数组要领。有些新手可能会直接修正 Array.prototype 上的要领,这是很风险的行动,如许在引入的时刻会全局影响Array 对象的要领,而运用Object.create实质上是完全了一份拷贝,新天生的arrayMethods对象的原型指针__proto__指向了Array.prototype,修正arrayMethods 对象不会影响Array.prototype。

基于这类原理,我们通常会运用Object.create 完成类式继续。

// 完成继续
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())

宣布-定阅情势

在上面一部份我们经由历程Object.defineProperty把对象的属性悉数转为 getter/setter 以及 数组变异要领完成了对数据模子更改的监听,在数据更改的时刻,我们经由历程console.log打印出来提醒了,然则关于框架而言,我们相干的逻辑假如直接写在那些处所,自然是不够文雅和天真的,这个时刻就须要引入经常使用的设想情势去完成,vue.js采用了宣布-定阅情势。宣布-定阅情势重要是为了到达一种“高内聚、低耦合”的结果。

Vue的Watcher定阅者作为Observer和Compile之间通讯的桥梁,可以定阅并收到每一个属性更改的关照,实行指令绑定的响应回调函数,从而更新视图。

/**
 * 视察者对象
 */
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
    }
}

Dep 是一个数据组织,其本质是保护了一个watcher行列,担任增加watcher,更新watcher,移除watcher,关照watcher更新。

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()
    }
}

模板编译

compile重要做的事变是理会模板指令,将模板中的变量替代成数据,然后初始化衬着页面视图,并将每一个指令对应的节点绑定更新函数,增加监听数据的定阅者,一旦数据有更改,收到关照,更新视图。

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
    }
}

这类完成和我们讲到的Dom-based templating相似,只是越发完全,具有自定义指令的功用。在遍历节点属性和文本节点的时刻,可以编译具有{{}}表达式或v-xxx的属性值的节点,而且经由历程增加 new Watcher()及绑定事宜函数,监听数据的更改从而对视图完成双向绑定。

MVVM实例

在数据绑定初始化的时刻,我们须要经由历程new Observer()来监听数据模子变化,经由历程new Compile()来理会编译模板指令,并应用Watcher搭起Observer和Compile之间的通讯桥梁。

/**
 * @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)
}

为了可以直接经由历程实例化对象操纵数据模子,我们须要为MVVM实例增加一个数据模子代办的要领:

MVVM.prototype._proxy = function (key) {
    Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get: () => this._data[key],
        set: (val) => {
            this._data[key] = val
        }
    })
}

至此我们可以经由历程一个小例子来讲明本文的内容:

<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>

本文目的不是为了造一个轮子,而是在进修优异框架完成的历程当中去提拔本身,搞清楚框架生长的来龙去脉,由浅及深去进修基本,本文参考了网上许多优异博主的文章,因为时候关联,有些内容没有做深切讨论,以为照样有些遗憾,在后续的进修中会更多的独立思考,提出更多本身的主意。

参考文档

申明

本文的完全代码及图片可以在这里下载:learn-javascript/mvvm

原文首发于 GitChat :http://gitbook.cn/books/593fa…,迎接关注我的新话题:JavaScript 进阶之 Vue.js + Node.js 入门实战开辟

我在segmentfault上有两期讲座,迎接来围观:
html5+ App开辟工程化实践之路
html5+ App开辟之 Android 平台离线集成 5+ SDK

    原文作者:匠心
    原文地址: https://segmentfault.com/a/1190000010456158
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞