媒介
谈起当前前端最热点的 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
基于字符串的模板引擎,本质上依旧是字符串拼接的情势,只是平常的库做了封装和优化,供应了更多轻易的语法简化了我们的事变。基本原理以下:
典范的库:
之前的一篇文章中我引见了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
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.js
S中定义了如许一个要领:
/**
* 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 举行理会,关于存在差别的部份加以申明。
监听对象更改
// 视察者组织函数
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
结果以下:
监听数组更改
上面我们经由历程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.set
和 Vue.delete
替代了,后续我们再理会。
定义一个数组作为数据模子,并对这个数组挪用变异的七个要领完成监听。
let skills = ['JavaScript', 'Node.js', 'html5']
// 原型指针指向具有变异要领的数组对象
skills.__proto__ = arrayMethods
skills.push('java')
// 数组更改
skills.pop()
// 数组更改
结果以下:
我们将须要监听的数组的原型指针指向我们定义的数组对象,如许我们的数组在挪用上面七个数组的变异要领时,可以监听到更改从而完成对数组举行跟踪。
关于__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])
}
}
原型链
关于不相识原型链的朋侪可以看一下我这里画的一个基本关联图:
- 原型对象是组织函数的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>
本文目的不是为了造一个轮子,而是在进修优异框架完成的历程当中去提拔本身,搞清楚框架生长的来龙去脉,由浅及深去进修基本,本文参考了网上许多优异博主的文章,因为时候关联,有些内容没有做深切讨论,以为照样有些遗憾,在后续的进修中会更多的独立思考,提出更多本身的主意。
参考文档
- 前端模板手艺面面观
- Object.defineProperty()
- Vue.js 源码进修笔记
- vue初期源码进修系列
- 理会最简朴的observer和watcher
- 理会Vue完成原理 – 怎样完成双向绑定mvvm
申明
本文的完全代码及图片可以在这里下载:learn-javascript/mvvm
原文首发于 GitChat :http://gitbook.cn/books/593fa…,迎接关注我的新话题:JavaScript 进阶之 Vue.js + Node.js 入门实战开辟。
我在segmentfault上有两期讲座,迎接来围观:
html5+ App开辟工程化实践之路
html5+ App开辟之 Android 平台离线集成 5+ SDK