声明:ITeye资讯文章的版权属于ITeye网站所有,严禁任何网站转载本文,否则必将追究法律责任!
前言
谈起当前前端最热门的 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过程)
基于字符串的模板引擎,本质上依然是字符串拼接的形式,只是一般的库做了封装和优化,提供了更多方便的语法简化了我们的工作。基本原理如下:
典型的库:
之前的一篇文章中我介绍了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://jsrun.net/yaYKp/embedded/all/light/
现在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://jsrun.net/faYKp/embedded/all/light/
页面渲染的函数 renderDom 是直接遍历DOM树,而不是遍历html字符串。遍历DOM树节点属性(attributes)和子节点(childNodes),然后调用渲染函数render。当DOM树子节点的类型是元素时,递归调用遍历DOM树的方法。根据DOM树节点类型一直遍历子节点,直到文本节点。
render的函数作用是提取{{}}中的关键词,然后使用数据模型中的数据进行替换。我们通过textContent获取Node节点的nodeValue,然后使用字符串的split方法对nodeValue进行分割,提取{{}}中的关键词然后替换为数据模型中的值。
DOM 的相关基础
注:元素类型对应NodeType
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(obj, prop, descriptor)
参数:
- obj:需要定义属性的对象。
- prop:需被定义或修改的属性名。
- descriptor:需被定义或修改的属性的描述符。
该方法重点是描述,对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个拥有可写或不可写值的属性。存取描述符是由一对 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。
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()
参数:
- obj:在该对象上查看属性。
- prop:一个属性名称,该属性的属性描述符将被返回。
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 使用指定的原型对象和其属性创建了一个新的对象。
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
发表评论
相关推荐
-
asp.net页面引用外部js文件无效
<br />我在asp.net页面引用外部js文件无效,把js代码放到asp.net页面上就可以!<br />js文件没有正确加载,你浏览器刷新几次,清下缓存试下看看<br />aspx不一定放在网站上哪一个目录下呢,所以写“js/....”这是不能保证访问到网站下的js目录的。应该使用类似这样的语句<br />其实不仅仅是aspx,在ascx,自定义控件,甚至最普通的类库代码中你也可能需要声明引用js文件,所以应该熟悉 RegisterClientScriptInclude 这个方法。<br />嗯,请试
-
Asp.net页面中引用js文件无效的问题的解决方法
在BS项目中,某个aspx页面需要引用外部脚本文件,通过在页面head节方式引用指定的js之后,仍然无效。通过alert方式调试,发现是由于js文件编码与js文件内容不符。由于js文件中包含中文注释,所以需要设置js文件为可识别中文的gb2312编码。其方法在网上也讲述,以下为网摘内容: 在.net中通过这种方式引用js文件 然后在页面中调用setday0.js文件中的方法往往
-
javascript在asp.net中使用时要注意!
google_ad_client = "pub-2947489232296736";/* 728x15, 创建于 08-4-23MSDN */google_ad_slot = "3624277373";google_ad_width = 728;google_ad_height = 15;//<script type="text/javascript"
-
asp.net页面中调用js文件
src="../Js/a.js">这样的方式加载js教本会出错,但如果把js文件中的代码抄到页面里就不会出错。最后解决方案:Web.config文件中 utf-8" responseEncoding="utf-8" />中的utf-8改成GB2312就搞定了,如果把js文件保存成utf8格式也应该可以。
-
怎么在ASP.NET中引用JS文件
ASP.NET中引用JS文件
-
如何在ASP.NET中使用JavaScript
ASP.NET中JavaScript 在ASP.NET页面中使用JavaScript可以使您的应用程序看起来运行更快,并防止对服务器的不必要调用。 JavaScript可用于在用户浏览器中执行客户端功能,而无需回叫服务器。 从而节省资源。 下面的示例演示如何使用ASP.NET按钮来触发修改ASP.NET TextBoxJavaScript调用。 例 以下示例将向您显示: 如何向页面动...
-
ASP.NET里masterpage的javascript问题
masterpage是vs2005中的新功能,可以实现以前用模板页和框架页的效果,使页面的开发和维护据效率大大提高了,可是前天在做客户端验证时发现masterpage里面不能使用javascript,页面虽然可以显示,但是浏览器提示页面存在错误。 我们知道,masterpage不能独立运行,需要其他页面继承它才能运行。打开浏览器里面的HTML源文件发现服务器端的ASP控件(客户端被
-
ASP.NET中引用JS不能调用JQuery问题 解决
问自己时是该走哪条路,问他人时是悬崖如何爬
-
ASP.Net_WebForm实现c#后台对js函数的调用
后台调用前台 定义: if (SaveOpetions.webType == null) SaveOpetions.webType = this.GetType(); ClientScript.RegisterStartupScript(SaveOpetions.webType, "0", "<script type='text/javascript'>var map = new Amap();function Draw(){ <%--内部实现
-
解决.net后台调用js弹窗后,前台界面样式乱掉问题
在程序中经常会使用到弹窗:Response.Write("<script>alert('弹窗内容');</script>"); 但是点击确定按钮后导致前台样式乱掉,在这里记录下解决方法。 解决方法一: 加上一句解析格式的语句: Response.Write("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 ...