早期的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或是控制台修改数据,页面的数据都会动态响应。

欢迎评论区讨论( 根本没人看啊 )。
哈哈哈还是有人看的 如果你写个硬币系统我还可以每期给你投两个硬币?
啊哈哈下次贴个收款码