手写一个MVVM

作者 likaiqiang 日期 2019-04-21
手写一个MVVM

基本原理

Object.defineProperty

vue是通过数据劫持来实现模型到视图的更新,使用了ES5的Object.defineProperty,这个api无法shim,所以vue不支持IE8

function observe(data){
for(var key in data){
if(typeof data[key] == 'object') observe(data[key])
else {
var val = data[key]
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get(){
return val
},
set(value){
if(val!==value){
val = value
}
}
})
}
}
}

发布订阅模式

B和C订阅A,A在更新时通知B和C,这就是发布-订阅模式。javascript里面最常用的发布-订阅模式即对事件的处理。

btn.addEventListener('click',()=>{
console.log(111)
})
btn.addEventListener('click',()=>{
console.log(222)
})
btn.click() //111 222

MVVM 单向绑定

假如有一个Watcher类(订阅者),用来存放vue的依赖。有一个Dep类(发布者),用来存放watcher以及通知所有的watcher更新数据

class Watcher{
constructor(data,exp,fn){
this.data = data
this.exp = exp
this.fn = fn
}
update(){
this.fn()
}
}
class Dep{
constructor(){
this.subs = []
}
addSub(sub){
this.subs.push(sub)
}
notify(){
this.subs.forEach(item=>{
item.update()
})
}
}

vue会在初始化实例的时候进行依赖收集,创建watcher实例

function complie(el,vm){
if(typeof el == 'string')
el = document.querySelector(el)

Array.from(el.childNodes).forEach(node=>{
if(node.nodeType ==1 && node.childNodes.length) complie(node,vm)
else if(node.nodeType == 3) complieText(vm._data,node)
})
}
function complieText(data,node){
var text = node.textContent
var reg = /{{(.*)}}/
if(reg.test(text)){
var match = reg.exec(text)
var arr = match[1].split('.')
var val = data
arr.forEach(key=>{
val = val[key]
})
node.textContent = node.textContent.replace(reg,val)
new watcher(data,match[1],function(val,oldValue){
node.textContent = node.textContent.replace(oldValue,val)
})
}
}

通过不停遍历el的子孙节点,找到所有符合reg的匹配,每一个匹配就是一个watcher

watcher订阅dep的更新,这里有个技巧,在watcher的constructor中,通过取值触发该watcher对应数据的getter函数,从而将自己(watcher)添加到dep中

class Watcher{
constructor(data,exp,fn){
this.data = data
this.exp = exp
this.fn = fn
this.value = this.getValue() //add
}
update(){
let oldVal = this.value
let value = this.getValue()
if(value !== oldVal) {
this.fn(value,oldVal)
}
}
getValue(){
Dep.target = this
var value = this.data
var arr = this.exp.split('.')
arr.forEach(k=>{
value = value[k]
})
Dep.target = null
return value
}
}
function observe(data){
var dep = new Dep() //add
for(var key in data){
if(typeof data[key] == 'object') observe(data[key])
else {
var val = data[key]
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get(){
Dep.target && dep.addSub( Dep.target) //add
return val
},
set(value){
if(val!==value){
val = value
}
}
})
}
}
}

当数据变更时,触发setter函数,调用dep.notify(),从而触发所有watcher更新

function observe(data){
var dep = new Dep() //add
for(var key in data){
if(typeof data[key] == 'object') observe(data[key])
else {
var val = data[key]
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get(){
Dep.target && dep.addSub( Dep.target) //add
return val
},
set(value){
if(val!==value){
val = value
dep.notify() //add
}
}
})
}
}
}

class Watcher{
constructor(data,exp,fn){
this.data = data
this.exp = exp
this.fn = fn
this.value = this.getValue() //add
}
update(){ //update
var oldValue = this.value
var value = expVal(this.data,this.exp)
if(this.value !== value){
this.value = value
}
this.fn(value,oldValue)
}
getValue(){
Dep.target = this
var value = this.data
var arr = this.exp.split('.')
arr.forEach(k=>{
value = value[k]
})
Dep.target = null
return value
}
}

这就是模型向视图的单向绑定

function mvvm(options = {}) {
this.$options = options
this._data = options.data
observe(this._data)
complie(options.el,this)
}

var vm = new mvvm({
el:'#app',
data:{
a:{
x:1,
y:2
},
b:100
}
})

MVVM 双向绑定

模型向视图的单向绑定

功能类似上面的complie函数,解析html模板,找到dom节点上的v-model属性,然后 new Watcher(代码后续补充)

视图向模型的单向绑定

监听input 事件,从而触发input的回调函数,更新模型,触发setter,更新所有watcher,一个完美的闭环。。。(代码后续补充)