上一篇写了实现 MVVM 框架的一些基本概念node
本篇用代码来实现一个完整的 MVVM 框架segmentfault
假设有以下代码,data
里面的name
会和试图中的{{name}}
——一一映射,修改data
的值,会直接引发试图中对应数据的变化数组
<body> <div id='app'>{{name}}</div> <script> function MVVM(){ //todo... } var vm = new MVVM({ el:'#app', data:{ name:'zhangsan' } }) </script> </body>
如何实现上述 MVVM 呢?app
回想下这篇讲的观察者模式和数据监听:框架
简单回答下:
上面例子中,主题应该是data
的name
属性,观察者是试图里的{{name}}
,当一开始执行 MVVM 初始化(根据el
解析模板发现{{name}}
)的时候订阅主题,当data.name
发生改变的时候,通知观察者更新内容,咱们能够在一开始监控data.name
,当用户修改data.name
的时候调用主题的subject.ontify
。mvvm
有以下 HTML函数
<div id="app"> <h1>{{name}}'is age is {{age}}</h1> </div>
从上面 HTML 中咱们看出,操做的节点是div#app
,须要的数据是name
和age
,因此实例化 MVVM 能够须要传递两个参数element
和data
this
let vm = MVVM({ element:'#app', data:{ name:'zhangsan', age:20 } }) setInterval(function(){ vm.data.age++ },2000)
咱们 MVVM 的构造函数应该怎么写呢?咱们只须要作两件事情:双向绑定
初始化是必须作的,将实例化的数据存在自身上面,后面要用,这里就不叙述了。code
class MVVM{ constructor(options){ init(options) observe(this.data) this.compile() } init(options){ this.element = document.querySelector(options.element) this.data = options.data } }
先看compile
这个方法,它就是在编译页面中的节点,若是节点里还有孩子,须要再去遍历这些孩子,若是遍历到文本,就进行下一步文本替换。
compile(){ //虽然这里能够直接对节点进行遍历,但最好仍是分开来比较好点 this.traverse(this.el) } traverse(node){ //对节点进行遍历,若是遇到元素节点,用递归继续遍历直到遍历到都是文本为止,进行下一步页面渲染 node.childNodes.forEach(childNode=>{ if(childNode.nodeType === 1){ this.traverse(childNode) }else if(childNode.nodeType === 3){ this.renderText(childNode) } }) } renderText(textNode){ //到这一步,已经获取到页面中的文本了,用正则去匹配 let reg = /{{([^}]*)}}/g //正则或者能够写称/{{(.+?)}}/g let match while(match = reg.exec(textNode.textContent)){ //将匹配到的内容赋值给match,match是一个数组 let raw = match[0] let key = match[1].trim() textNode.textContent = textNode.textContent.replace(raw,this.data[key]) //页面渲染 new Observer(this,key,function(val,oldVal){ textNode.textContent = textNode.textContent.replace(oldVal,val) }) //建立一个观察者 } }
假设用户去修改数据时,那数据该如何进行实时的变更呢?
这里就引入了观察者和主题的概念,咱们在解析的过程当中建立一个个观察者,这个观察者就观察这个属性,解析到下个属性在建立一个观察者,并观察这个属性。
观察这个属性就是订阅这个主题,咱们在this.compile()
解析完后建立一个观察者,它有个方法,若是这个属性变更,我就会修改页面。
function observe(data){ if(!data || typeof data !== 'object')return for(let key in data){ let val = data[key] let subject = new Subject() //建立主题 if(typeof val === 'object'){ observe(val) } Object.defineProperty(data,key,{ configurable:true, enumerable:true, get(){ return val }, set(newVal){ val = newVal subject.notify() } }) } }
问题是建立了观察者后何时去观察这个主题?
在建立后马上观察这个主题,但是主题在哪?观察者有了,就是刚刚new
的时候。主题是在observe
遍历属性时建立的。主题存在在observe
局部变量中,外面是访问不到的,那观察者怎样订阅这个主题呢?
思考到这里发现行不通了,就须要换种思路了。
当建立观察者时,会调用getValue()
,它作什么事情呢,把我设置为场上权限最高的观察者,由于页面中有不少观察者,此时this.key
,就是我要订阅的主题,当我调用this.vm.data[this.key]
就等于调用了observe
的get
方法,由于刚刚我已经把观察者设置为场上权限最高者,此时currentObserver
是存在的,这时观察者就开始订阅主题,订阅的以后在把权限去掉
let currentObserver = null class Observer{ constructor(vm,key,cb){ this.subjects = {} this.vm = vm this.key = key this.cb = cb this.value = this.getValue() } getValue(){ currentObserver = this let value = this.vm.data[this.key] currentObserver = null return value } }
经过currentObserver
去订阅主题,由于在建立观察者时调用了getValue
方法,把currentObserver
设置为Observer
,经过它去订阅主题
get:function(){ if(currentObserver){ currentObserver.subscribeTo(subject) } }
主题的构造函数
let id = 0 class Subject{ constructor(){ this.id = id++ this.observers = [] } addObserver(observer){ this.observers.push(observer) } notify(){ this.observers.forEach(observer=>{ observer.update() }) } }
添加观察者
subscribeTo(subject){ if(!this.subjects[subject.id]){ subject.addObserver(this) this.subjects[subject.id] = subject } }
更新页面数据,旧值经过自身属性获取,新值经过getValue
方法获取
update(){ let oldVal = this.value let value = this.getValue() if(value !== oldVal){ this.value = value this.cb.call(this.vm,value,oldVal) } }
最后贴上完整的单向绑定的代码
function observe(data){ if(!data || typeof data !== 'object')return for(let key in data){ let val = data[key] let subject = new Subject() if(typeof val === 'object'){ observe(val) } Object.defineProperty(data,key,{ configurable:true, enumerable:true, get(){ if(currentObserver){ currentObserver.subscribeTo(subject) } return val }, set(newVal){ val = newVal subject.notify() } }) } } let id = 0 class Subject{ constructor(){ this.id = id++ this.observers = [] } addObserver(observer){ this.observers.push(observer) } notify(){ this.observers.forEach(observer=>{ observer.update() }) } } let currentObserver = null class Observer{ constructor(vm,key,cb){ this.subjects = {} this.vm = vm this.key = key this.cb = cb this.value = this.getValue() } update(){ let oldVal = this.value let value = this.getValue() if(value !== oldVal){ this.value = value this.cb.call(this.vm,value,oldVal) } } subscribeTo(subject){ if(!this.subjects[subject.id]){ subject.addObserver(this) this.subjects[subject.id] = subject } } getValue(){ currentObserver = this let value = this.vm.data[this.key] currentObserver = null return value } } class mvvm{ constructor(options){ this.init(options) observe(this.data) this.compile() } init(options){ this.el = document.querySelector(options.el) this.data = options.data } compile(){ this.traverse(this.el) } traverse(node){ node.childNodes.forEach(childNode=>{ if(childNode.nodeType === 1){ this.traverse(childNode) }else if(childNode.nodeType === 3){ this.renderText(childNode) } }) } renderText(textNode){ let reg = /{{([^}]*)}}/g let match while(match = reg.exec(textNode.textContent)){ let raw = match[0] let key = match[1].trim() textNode.textContent = textNode.textContent.replace(raw,this.data[key]) new Observer(this,key,function(val,oldVal){ textNode.textContent = textNode.textContent.replace(oldVal,val) }) } } } let vm = new mvvm({ el:'#app', data:{ name:'uccs', age:20 } }) setInterval(function(){ vm.data.age++ },2000)
本篇详细讲述了 MVVM 单项绑定的原理,下一篇讲述双向绑定
用原生 JS 实现 MVVM 框架MVVM 框架系列:
用原生 JS 实现 MVVM 框架1——观察者模式和数据监控