早期的web
开发是由后端为主导的MVC
开发,前端的任务只是在本地开发好相应的页面,然后交付给后端开发进行嵌套,这种开发模式下前端需要依赖后端环境,导致开发效率并不高效。从web
诞生至2005年,一直处于后端重,前端轻的状态。
2004年,Google
发布了一个名为Gmail
的web
应用,里面用到了大量的ajax
技术,能够使页面不刷新向服务器发送请求。加上jQuery
的封装,ajax
技术很快风靡全球,前后端逐渐分离。但是这种架构还是存在一定的问题:前端的开发业务逐渐增多,整体的内容都放到一起,前端的代码会变得越来越难以维护,而此时前端开发者缺少的是一种具有可行性的开发模式和框架。因此,前端的MV*
也随之而来。
一、MVC和MVVM
MVC
:前端的MVC
和后端类似,具有Model
、Controller
和View
。其中,Model
负责储存应用的数据,提供数据处理逻辑的接口;Controller
负责业务逻辑,根据用户行为对调用Model
中的接口;View
负责试图展示,将Model
中的数据可视化。
MVVM
: 即Model
、View
、ViewModel
。和MVC
相似,Model
负责数据,View
负责视图,不同之处在于MVVM
中的Model
和View
可以向ViewModel
发送变化,ViewModel
一旦接受改变,会同步数据到Model
和View
中。
二、主流框架的实现原理
单向绑定非常简单,就是在Model
数据发生改变时同时改变View
,当我们通过JavaScript
来对Model
的数据进行更改时会自动更新View
。在很久之前就已经有框架实现了这种方式,如backbone.js
、sproutcore.js
、sammy.js
等,而双向绑定也并不复杂,用户修改视图的方式无非就是填写表单,所以只需要监听表单的输入,用户输入的同时通过发送更改到ViewModel
中来对Model
进行数据更新。
脏值检测(angular.js
): 当应用触发了一些特定的条件后,会遍历$watchList
中的属性,进行快照对比,来检查这些属性值是否有发生变化,如果有变化,就用一个变量dirty
记录为true
,再次进行遍历,如此往复,直到某一个遍历完成时,这些属性值都没有变化时,结束遍历执行更新。 这种方式很直观,但是同时存在很多问题: 脏值检测由于需要涉及到对比,所以不得不保存两份变量,而且还要遍历对象比较每一个属性,对内存和运算的要求较高; 脏值检测还需要预知可能会触发状态变更的时机,意味着需要对部分原生接口进行封装,包括setTimeout
、requestAnimationFrame
等。
数据劫持(vue.js
):在JavaScript
中,可以通过Object.defineProperty
来实现对象的劫持,该函数接受三个参数(对象,属性,描述),其中描述包括有六个选项(value
,writable
,get
,set
,configurable
,enumerable
),描述中的set
和get
方法就是当修改或访问对象属性的时候会触发的函数,我们只要在对象属性发生改变时,进行一些额外的操作,就是所谓的数据劫持了。
const user = {} Object.defineProperty(user, 'name', { get () { console.log(`get user\'s name`) }, set (newVal) { console.log(`set user\'s name to ${newVal}`) } }) user.name // get user's name user.name = 'test' // set user's name to test
要注意该方法每次只能设置一个属性,要配置所有属性,必须通过遍历来完成;如果后续需要扩展对象,则必须手动对新属性进行设置,这就是为什么不在data
中声明的属性无法拥有双向绑定的原因;该方法对数组是无效的,需要另外再封装监听数组的方法。在ES6
中这些问题都可以通过Proxy
对象解决,这也是vue3.0
中会做的一处修改。
三、手写MVVM
主要分为三大模块:
Observer
– 数据监听器:能够监听对象的所有属性,如有变动可以拿到最新值并发送通知Compiler
– 指令解析器:将节点中的指令(v-model
、{{}}
等)替换成数据,并添加数据的订阅,一旦数据变动,收到通知,更新视图Watcher
– 订阅发布者:作为连接Observer
和Compiler
的桥梁,接受一个update
函数作为参数,在自身实例化时往订阅器(dep
)中添加自己,等到数据变动发送通知时,能够调用自身的update
方法
Observer.js:
class Observer { constructor (data) { this.observer(data) } /** * 数据劫持 * @param data * @returns {*} */ observer (data) { if (data && typeof data === 'object') { for (const key in data) { let value = data[key] this.observer(value) // 递归嵌套对象 const dep = new Dep() // 为该属性创建订阅 Object.defineProperty(data, key, { get () { if (Dep.target) dep.addSub(Dep.target) // 如果节点中有引用,则添加订阅 return value }, set: (newVal) => { if (newVal === value) return // 如果新值没有变化就return,以避免不必要的节点渲染 this.observer(newVal) // 可能新值是对象 value = newVal dep.notify() // 通知所有订阅更新 } }) } } } }
Compiler.js:
class Compiler { constructor (el, vm) { this.vm = vm const root = document.querySelector(el) const fragment = this.nodeToFragment(root) this.compile(fragment, vm) root.appendChild(fragment) // 最后把fragment添加回节点中 } /** * 节点转为fragment * @param root 根节点 * @returns {DocumentFragment} */ nodeToFragment (root) { const fragment = document.createDocumentFragment() // fragment不是真实DOM树的一部分,它的变化不会触发DOM树的重新渲染,且不会导致性能等问题 let child while (child = root.firstChild) { fragment.appendChild(child) // appendChild具有可移动性,root中child会被移动到fragment中,而不是复制 } return fragment } /** * 解析节点 * @param node 节点 */ compile (node) { Array.from(node.childNodes).forEach(child => { if (child.nodeType === 1) { // nodeType为1是ELEMENT_NODE this.compileElement(child) this.compile(child) // 递归解析子节点 } else if (child.nodeType === 3) { // nodeType为3是TEXT_NODE this.compileText(child) } }) } /** * 解析ELEMENT_NODE * @param node ELEMENT_NODE节点 */ compileElement (node) { Array.from(node.attributes).forEach(({ name, value: exp }) => { if (name === 'v-model') { // 处理v-model node.addEventListener('input', e => { this.setValue(exp, e.target.value) }) // 监听手动输入,修改数据 new Watcher(() => { node.value = this.getValue(exp) }) // 订阅数据,动态修改value } else if (name.startsWith(':')) { // 处理动态属性 new Watcher(() => { node.setAttribute(name.slice(1), this.getValue(exp)) }) // 订阅数据,动态修改属性 } }) } /** * 解析TEXT_NODE * @param node TEXT_NODE节点 */ compileText (node) { let text = node.textContent if (/\{\{(.+?)\}\}/.test(text)) { new Watcher(() => { node.textContent = text.replace(/\{\{(.+?)\}\}/g, (full, exp) => { return this.getValue(exp) }) // 替换所有的双大括号语法 }) // 订阅数据,动态修改文本内容 } } /** * 获取数据 * @param exp 表达式 * @returns {string} * * @example getValue(' user.name ') -> this.vm['user']['name'] */ getValue (exp) { return exp.trim().split('.').reduce((data, key) => { return data[key] }, this.vm) } /** * 设置数据 * @param exp 表达式 * @param value 新的值 * * @example setValue(' user.name ', 'new name') -> this.vm['user']['name'] = 'new name' */ setValue (exp, value) { exp.split('.').reduce((data, key, index, arr) => { if (index === arr.length - 1) { return data[key] = value } return data[key] }, this.vm) } }
Watcher.js:
class Watcher { constructor (fn) { Dep.target = this this.update = fn this.update() // 初始化时先进行一次数据渲染,当然真正的订阅发布者模式不会这样做,这里执行是为了避免等到数据更新时才渲染数据 Dep.target = null } } class Dep { subs = [] addSub (sub) { this.subs.push(sub) } notify () { this.subs.forEach(sub => sub.update()) } }
最后用一个类来整合上面的内容:
class MVVM { constructor (options) { this.$el = options.el this.$data = options.data this.$proxyData() new Observer(this.$data) new Compiler(this.$el, this) } /** * 数据代理 * @returns {*} * * @example vm.user -> vm.$data.user */ $proxyData () { for (const key in this.$data) { Object.defineProperty(this, key, { configurable: true, enumerable: true, get () { return this.$data[key] }, set (newVal) { this.$data[key] = newVal } }) } } }
实例:
<div id="app"> <div> <input type="text" v-model="user.name"> <input type="number" v-model="user.age"> </div> <p :title="user.gender">My name is {{ user.name }}, I am {{ user.age }} years old.</p> </div> <script src="js/Observer.js"></script> <script src="js/Watcher.js"></script> <script src="js/Compiler.js"></script> <script src="js/MVVM.js"></script> <script> const app = new MVVM({ el: '#app', data: { user: { name: 'ysm', age: 19, gender: 'male' } } }) </script>
效果:
可以看出无论是通过input或是控制台修改数据,页面的数据都会动态响应。
欢迎评论区讨论( 根本没人看啊 )。
哈哈哈还是有人看的 如果你写个硬币系统我还可以每期给你投两个硬币?
啊哈哈下次贴个收款码