安东尼罗宾视频,种子搜索神器2012,顺德日新
mvvm 框架
近年来前端一个明显的开发趋势就是架构从传统的 mvc 模式向 mvvm 模式迁移。在传统的 mvc 下,当前前端和后端发生数据交互后会刷新整个页面,从而导致比较差的用户体验。因此我们通过 ajax 的方式和网关 rest api 作通讯,异步的刷新页面的某个区块,来优化和提升体验。
mvvm 框架基本概念
在 mvvm 框架中,view(视图) 和 model(数据) 是不可以直接通讯的,在它们之间存在着 viewmodel 这个中间介充当着观察者的角色。当用户操作 view(视图),viewmodel 感知到变化,然后通知 model 发生相应改变;反之当 model(数据) 发生改变,viewmodel 也能感知到变化,使 view 作出相应更新。这个一来一回的过程就是我们所熟知的双向绑定。
mvvm 框架的应用场景
mvvm 框架的好处显而易见:当前端对数据进行操作的时候,可以通过 ajax 请求对数据持久化,只需改变 dom 里需要改变的那部分数据内容,而不必刷新整个页面。特别是在移动端,刷新页面的代价太昂贵。虽然有些资源会被缓存,但是页面的 dom、css、js 都会被浏览器重新解析一遍,因此移动端页面通常会被做成 spa 单页应用。由此在这基础上诞生了很多 mvvm 框架,比如 react.js、vue.js、angular.js 等等。
mvvm 框架的简单实现
模拟 vue 的双向绑定流,实现了一个简单的mvvm 框架,从上图中可以看出虚线方形中就是之前提到的 viewmodel 中间介层,它充当着观察者的角色。另外可以发现双向绑定流中的 view 到 model 其实是通过 input 的事件监听函数实现的,如果换成 react(单向绑定流) 的话,它在这一步交给状态管理工具(比如 redux)来实现。另外双向绑定流中的 model 到 view 其实各个 mvvm 框架实现的都是大同小异的,都用到的核心方法是 object.defineproperty(),通过这个方法可以进行数据劫持,当数据发生变化时可以捕捉到相应变化,从而进行后续的处理。
mvvm(入口文件) 的实现
一般会这样调用 mvvm 框架
const vm = new mvvm({ el: '#app', data: { title: 'mvvm title', name: 'mvvm name' }, })
但是这样子的话,如果要得到 title 属性就要形如 vm.data.title 这样取得,为了让 vm.title 就能获得 title 属性,从而在 mvvm 的 prototype 上加上一个代理方法,代码如下:
function mvvm (options) { this.data = options.data const self = this object.keys(this.data).foreach(key => self.proxykeys(key) ) } mvvm.prototype = { proxykeys: function(key) { const self = this object.defineproperty(this, key, { get: function () { // 这里的 get 和 set 实现了 vm.data.title 和 vm.title 的值同步 return self.data[key] }, set: function (newvalue) { self.data[key] = newvalue } }) } }
实现了代理方法后,就步入主流程的实现
function mvvm (options) { this.data = options.data // ... observe(this.data) new compile(options.el, this) }
observer(观察者) 的实现
observer 的职责是监听 model(js 对象) 的变化,最核心的部分就是用到了 object.defineproperty() 的 get 和 set 方法,当要获取 model(js 对象) 的值时,会自动调用 get 方法;当改动了 model(js 对象) 的值时,会自动调用 set 方法;从而实现了对数据的劫持,代码如下所示。
let data = { number: 0 } observe(data) data.number = 1 // 值发生变化 function observe(data) { if (!data || typeof(data) !== 'object') { return } const self = this object.keys(data).foreach(key => self.definereactive(data, key, data[key]) ) } function definereactive(data, key, value) { observe(value) // 遍历嵌套对象 object.defineproperty(data, key, { get: function() { return value }, set: function(newvalue) { if (value !== newvalue) { console.log('值发生变化', 'newvalue:' + newvalue + ' ' + 'oldvalue:' + value) value = newvalue } } }) }
运行代码,可以看到控制台输出 值发生变化 newvalue:1 oldvalue:0,至此就完成了 observer 的逻辑。
dep(订阅者数组) 和 watcher(订阅者) 的关系
观测到变化后,我们总要通知给特定的人群,让他们做出相应的处理吧。为了更方便地理解,我们可以把订阅当成是订阅了一个微信公众号,当微信公众号的内容有更新时,那么它会把内容推送(update) 到订阅了它的人。
那么订阅了同个微信公众号的人有成千上万个,那么首先想到的就是要 new array() 去存放这些人(html 节点)吧。于是就有了如下代码:
// observer.js function dep() { this.subs = [] // 存放订阅者 } dep.prototype = { addsub: function(sub) { // 添加订阅者 this.subs.push(sub) }, notify: function() { // 通知订阅者更新 this.subs.foreach(function(sub) { sub.update() }) } } function observe(data) {...} function definereactive(data, key, value) { var dep = new dep() observe(value) // 遍历嵌套对象 object.defineproperty(data, key, { get: function() { if (dep.target) { // 往订阅器添加订阅者 dep.addsub(dep.target) } return value }, set: function(newvalue) { if (value !== newvalue) { console.log('值发生变化', 'newvalue:' + newvalue + ' ' + 'oldvalue:' + value) value = newvalue dep.notify() } } }) }
初看代码也比较顺畅了,但可能会卡在 dep.target 和 sub.update,由此自然而然地将目光移向 watcher,
// watcher.js function watcher(vm, exp, cb) { this.vm = vm this.exp = exp this.cb = cb this.value = this.get() } watcher.prototype = { update: function() { this.run() }, run: function() { // ... if (value !== oldval) { this.cb.call(this.vm, value) // 触发 compile 中的回调 } }, get: function() { dep.target = this // 缓存自己 const value = this.vm.data[this.exp] // 强制执行监听器里的 get 函数 dep.target = null // 释放自己 return value } }
从代码中可以看到当构造 watcher 实例时,会调用 get() 方法,接着重点关注 const value = this.vm.data[this.exp] 这句,前面说了当要获取 model(js 对象) 的值时,会自动调用 object.defineproperty 的 get 方法,也就是当执行完这句的时候,dep.target 的值传进了 observer.js 中的 object.defineproperty 的 get 方法中。同时也一目了然地在 watcher.prototype 中发现了 update 方法,其作用即触发 compile 中绑定的回调来更新界面。至此解释了 observer 中 dep.target 和 sub.update 的由来。
来归纳下 watcher 的作用,其充当了 observer 和 compile 的桥梁。
1 在自身实例化的过程中,往订阅器(dep) 中添加自己
2 当 model 发生变动,dep.notify() 通知时,其能调用自身的 update 函数,并触发 compile 绑定的回调函数实现视图更新
最后再来看下生成 watcher 实例的 compile.js 文件。
compile(编译) 的实现
首先遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成 fragment(文档碎片) 进行解析编译,解析完成,再将 fragment 添加回原来的真实 dom 节点中。代码如下:
function compile(el, vm) { this.vm = vm this.el = document.queryselector(el) this.fragment = null this.init() } compile.prototype = { init: function() { if (this.el) { this.fragment = this.nodetofragment(this.el) // 将节点转为 fragment 文档碎片 this.compileelement(this.fragment) // 对 fragment 进行编译解析 this.el.appendchild(this.fragment) } }, nodetofragment: function(el) { const fragment = document.createdocumentfragment() let child = el.firstchild // △ 第一个 firstchild 是 text while(child) { fragment.appendchild(child) child = el.firstchild } return fragment }, compileelement: function(el) {...}, }
这个简单的 mvvm 框架在对 fragment 编译解析的过程中对 {{}} 文本元素、v-on:click 事件指令、v-model 指令三种类型进行了相应的处理。
compile.prototype = { init: function() { if (this.el) { this.fragment = this.nodetofragment(this.el) // 将节点转为 fragment 文档碎片 this.compileelement(this.fragment) // 对 fragment 进行编译解析 this.el.appendchild(this.fragment) } }, nodetofragment: function(el) {...}, compileelement: function(el) {...}, compiletext: function (node, exp) { // 对文本类型进行处理,将 {{abc}} 替换掉 const self = this const inittext = this.vm[exp] this.updatetext(node, inittext) // 初始化 new watcher(this.vm, exp, function(value) { // 实例化订阅者 self.updatetext(node, value) }) }, compileevent: function (node, vm, exp, dir) { // 对事件指令进行处理 const eventtype = dir.split(':')[1] const cb = vm.methods && vm.methods[exp] if (eventtype && cb) { node.addeventlistener(eventtype, cb.bind(vm), false) } }, compilemodel: function (node, vm, exp) { // 对 v-model 进行处理 let val = vm[exp] const self = this this.modelupdater(node, val) node.addeventlistener('input', function (e) { const newvalue = e.target.value self.vm[exp] = newvalue // 实现 view 到 model 的绑定 }) }, }
在上述代码的 compiletest 函数中看到了期盼已久的 watcher 实例化,对 watcher 作用模糊的朋友可以往上回顾下 watcher 的作用。另外在 compilemodel 函数中看到了本文最开始提到的双向绑定流中的 view 到 model 是借助 input 监听事件变化实现的。
项目地址
本文记录了些阅读 mvvm 框架源码关于双向绑定的心得,并动手实践了一个简版的 mvvm 框架,不足之处在所难免,欢迎指正。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。
如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复
VUE+elementui组件在table-cell单元格中绘制微型echarts图
Vue通过getAction的finally来最大程度避免影响主数据呈现问题
vue 路由懒加载中给 Webpack Chunks 命名的方法
网友评论