手写一套mvvm框架

1 MVVM是什么

MVVM 设计模式,是由 MVC、MVP 等设计模式进化而来。

  1. M - 数据模型(Model),简单的JS对象
  2. VM - 视图模型(ViewModel),连接Model与View
  3. V - 视图层(View),呈现给用户的DOM渲染界面

通过以上的MVVM模式图,我们可以看出最核心的就是ViewModel,它主要的作用:对View中DOM元素的监听和对Model中的数据进行绑定,当View变化会引起Modal中数据的改动,Model中数据的改动会触发View视图重新渲染,从而达到数据双向绑定的效果,该效果也是Vue最为核心的特性。


2 Object.defineProperty的用法

下面介绍如何实现一个完整MVVM框架,首先介绍一下一个核心api,Object.defineProperty的使用。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法:

Object.defineProperty(obj, prop, descriptor)

  1. obj:要在其上定义属性的对象
  2. prop:要定义或修改的属性的名称
  3. descriptor:将被定义或修改的属性描述符

返回值:

被传递给函数的对象

使用方法:

var obj = {};
obj.name = 'hunger';
obj['age'] = 3;
Object.defineProperty(obj, 'intro', {
  value: 'hello world',
});
console.log(obj);  // {name: 'hunger', age: 3, intro: 'hello world'}

以上三种方法都可以用来定义 / 修改一个属性,Object.defineProperty 的方法貌似看起来有些小题大做。没关系,且往下看它更复杂的用法。

var obj = {};
Object.defineProperty(obj, 'intro', {
  configurable: false,
  value: 'hello world'
});
obj.intro = 'jirengu';
console.log(obj.intro);   // "helloworld"
delete obj.intro;           // false, 删除失败
console.log(obj.intro);  // "helloworld"
var obj = {};
Object.defineProperty(obj, 'name', {
  configurable: true,
  value: 'hunger'
});
console.log(obj.name);  // hunger
delete obj.name;   // true  , 成功删除
console.log(obj.name); // undefind

 通过上面的例子可以看出,属性描述对象中 configurable 的值设置为 false 后(如果没设置,默认就是 false) ,以后就不能再次通过 Object.defineProperty修改属性,也无法删除该属性。

var obj = { name: 'hello' };
Object.defineProperty(obj, 'age', {
  value: 3,
  enumerable: false
});
for (var key in obj) {
  console.log(key);   // 只输出 'name', 不输出'age'
}

设置 enumerable 属性为 false 后,遍历对象的时候会忽略当前属性(如果未设置,默认就是 false不可遍历) 。

var obj = { name: 'hello' }
Object.defineProperty(obj, 'age', {
  value: 3,
  writable: false
});
obj.age = 4;
console.log(obj.age);   // 3, writable为 false 时,修改对象的当前属性值无效

value 和 writable 叫数据描述符,具有以下可选键值:

  1. value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
  2. writable:当且仅当该属性的writable为true时,该属性才能被赋值运算符改变。默认为 false
  3. configurable:true 和 wriable:true 的区别是,前者是设置属性能删除,后者是设置属性能修改
var obj = {};
var age;
Object.defineProperty(obj, 'age', {
  get: function () {
    console.log('get age...')
    return age;
  },
  set: function (val) {
    console.log('set age...')
    age = val;
  }
});
obj.age = 100;  // 'set age...'
console.log(obj.age); // 'get age...', 100

get 和 set 叫存取描述符,有以下可选键值:

  1. get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined
  2. set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined
  3. 数据描述符和存取描述符是不能同时存在的
var obj = {};
var age;
Object.defineProperty(obj, 'age', {
  value: 100,
  get: function () {
    console.log('get age...')
    return age;
  },
  set: function (val) {
    console.log('set age...')
    age = val;
  }
});

因为有 value,又有 get,上述代码会报错。

浏览器报:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>at Function.defineProperty (<anonymous>)

总结:

属性默认值说明
configurablefalse描述属性是否被删除,默认为false
enumerablefalse描述属性是否可以被for..in...或Objects.keys枚举,默认为false
writablefalse描述属性是否可以修改,默认为false
getundefind当访问属性时触发该方法,默认为undefined
setundefind当属性被修改时触发该方法,默认为undefined
valueundefind属性值,默认为undefined

3 数据的劫持

现在我们利用 Object.defineProperty方法动态监听数据。

var data = {
  name: 'hunger',
  friends: [1, 2, 3]
};
observe(data);

console.log(data.name);
data.name = 'valley';
data.friends[0] = 4;

function observe(data) {
  if (!data || typeof data !== 'object') {
    return;
  }
  for (var key in data) {
    let val = data[key];
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function() {
        console.log(`get ${val}`);
        return val;
      },
      set: function(newVal) {
        console.log(`changes happen: ${val} => ${newVal}`);
        val = newVal;
      }
    });
    if (typeof val === 'object'){
      observe(val);
    }
  }
}

上面的 observe 函数实现了一个数据监听,当监听某个对象后,我们可以在用户读取或者设置属性值的时候做个拦截,做我们想做的事


4 观察者模式

发布 - 订阅模式(Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。

一个典型的观察者模式应用场景是用户在一个网站订阅主题

  1. 多个用户(观察者,Observer)都可以订阅某个主题(Subject)
  2. 当主题内容更新时订阅该主题的用户都能收到通知

以下是代码实现:

Subject是构造函数,new Subject() 创建一个主题对象,该对象内部维护订阅当前主题的观察者数组。主题对象上有一些方法,如添加观察者(addObserver)、删除观察者(removeObserver)、通知观察者更新(notify)。 当notify 时实际上调用全部观察者 observer 自身的 update 方法。

Observer 是构造函数,new Observer() 创建一个观察者对象,该对象有一个 update 方法。

// ES5写法
function Subject() {
  this.observers = []
}
Subject.prototype.addObserver = function(observer) {
  this.observers.push(observer)
}
Subject.prototype.removeObserver = function(observer) {
  var index = this.observers.indexOf(observer)
  if(index > -1){
    this.observers.splice(index, 1)
  }
}
Subject.prototype.notify = function() {
  this.observers.forEach(function(observer){
    observer.update()
  })
}
function Observer(name) {
  this.name = name
  this.update = function(){
    console.log(name + ' update...')
  }
}  

// 创建主题
var subject = new Subject();
//创建观察者1
var observer1 = new Observer('hunger');
//主题添加观察者1
subject.addObserver(observer1);
//创建观察者2
var observer2 = new Observer('valley');
//主题添加观察者2
subject.addObserver(observer2);
//主题通知所有的观察者更新
subject.notify();


// ES6写法
class Subject {
  constructor() {
    this.observers = []
  }
  addObserver(observer) {
    this.observers.push(observer)
  }
  removeObserver(observer) {
    var index = this.observers.indexOf(observer)
    if(index > -1){
      this.observers.splice(index, 1)
    }
  }
  notify() {
    this.observers.forEach(observer=> {
      observer.update()
    })
  }
}

class Observer{
  constructor() {
    this.update = function() {}
  }
}  

let subject = new Subject();
let observer1 = new Observer();
observer1.update = function() {
  console.log('observer1 update')
}
subject.addObserver(observer1);
let observer2 = new Observer('valley');
observer2.update = function() {
  console.log('observer2 update')
}
subject.addObserver(observer2);
subject.notify();

上面的代码中,主题被观察者订阅的写法是 subject.addObserver(observer), 不是很直观,给观察者增加订阅方法。

class Observer{
  constructor() {
    this.update = function() {}
  }
  subscribeTo(subject) {
    subject.addObserver(this)
  }
} 


let subject = new Subject()
let observer = new Observer()
observer.update = function() {
  console.log('observer update');
}
observer.subscribeTo(subject);  //观察者订阅主题
subject.notify();

5 MVVM单向绑定的实现

思考:假设有如下代码,data 里的name会和视图中的{{name}}一一映射,修改 data 里的值,会直接引起视图中对应数据的变化。

<body>
  <div id="app" >{{name}}</div>

  <script>
    function mvvm(){
        //todo...
    }
    var vm = new mvvm({
      el: '#app',
      data: { 
          name: 'hello world' 
      }
    })
  </script>
<body>

如何实现上述 mvvm 呢?

一起回想之前讲的观察者模式和数据监听:

  1. 主题(subject)是什么?
  2. 观察者(observer)是什么?
  3. 观察者何时订阅主题?
  4. 主题何时通知更新?

上面的例子中,主题应该是data的 name 属性,观察者是视图里的{{name}},当一开始执行mvvm初始化(根据 el 解析模板发现{{name}})的时候订阅主题,当data.name发生改变的时候,通知观察者更新内容。 我们可以在一开始监控 data.name (Object.defineProperty(data, 'name', {...})),当用户修改 data.name 的时候调用主题的 subject.notify。

<div id="app" >
  <h1>{{name}} 's age is {{age}}</h1>
</div>

function observe(data) {
  if(!data || typeof data !== 'object') return
  for(var key in data) {
    let val = data[key]
    let subject = new Subject()
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function() {
        console.log(`get ${key}: ${val}`)
        if(currentObserver){
          console.log('has currentObserver')
          currentObserver.subscribeTo(subject)
        }
        return val
      },
      set: function(newVal) {
        val = newVal
        console.log('start notify...')
        subject.notify()
      }
    })
    if(typeof val === 'object'){
      observe(val)
    }
  }
}

let id = 0
let currentObserver = null

class Subject {
  constructor() {
    this.id = id++
    this.observers = []
  }
  addObserver(observer) {
    this.observers.push(observer)
  }
  removeObserver(observer) {
    var index = this.observers.indexOf(observer)
    if(index > -1){
      this.observers.splice(index, 1)
    }
  }
  notify() {
    this.observers.forEach(observer=> {
      observer.update()
    })
  }
}

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.bind(this.vm)(value, oldVal)
    }
  }
  subscribeTo(subject) {
    if(!this.subjects[subject.id]){
      console.log('subscribeTo.. ', subject)
       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(opts) {
    this.init(opts)
    observe(this.$data)
    this.compile()
  }
  init(opts){
    this.$el = document.querySelector(opts.el)
    this.$data = opts.data
    this.observers = []
  }
  compile(){
    this.traverse(this.$el)
  }
  traverse(node){
    if(node.nodeType === 1){
      node.childNodes.forEach(childNode=>{
        this.traverse(childNode)
      })
    }else if(node.nodeType === 3){ //文本
      this.renderText(node)
    }
  }
  renderText(node){
    let reg = /{{(.+?)}}/g
    let match
    while(match = reg.exec(node.nodeValue)){
      let raw = match[0]
      let key = match[1].trim()
      node.nodeValue = node.nodeValue.replace(raw, this.$data[key])
      new Observer(this, key, function(val, oldVal){
        node.nodeValue = node.nodeValue.replace(oldVal, val)
      })
    }    
  }
    
}

let vm = new mvvm({
  el: '#app',
  data: { 
    name: 'zhangfei',
    age: 3
  }
})

setInterval(function(){
  vm.$data.age++
}, 1000)

6 MVVM双向绑定实现

<div id="app" >
  <input v-model="name" type="text">
  <h1>{{name}} 's age is {{age}}</h1>
</div>

function observe(data) {
  if(!data || typeof data !== 'object') return
  for(var key in data) {
    let val = data[key]
    let subject = new Subject()
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function() {
        console.log(`get ${key}: ${val}`)
        if(currentObserver){
          console.log('has currentObserver')
          currentObserver.subscribeTo(subject)
        }
        return val
      },
      set: function(newVal) {
        val = newVal
        console.log('start notify...')
        subject.notify()
      }
    })
    if(typeof val === 'object'){
      observe(val)
    }
  }
}

let id = 0
let currentObserver = null

class Subject {
  constructor() {
    this.id = id++
    this.observers = []
  }
  addObserver(observer) {
    this.observers.push(observer)
  }
  removeObserver(observer) {
    var index = this.observers.indexOf(observer)
    if(index > -1){
      this.observers.splice(index, 1)
    }
  }
  notify() {
    this.observers.forEach(observer=> {
      observer.update()
    })
  }
}

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.bind(this.vm)(value, oldVal)
    }
  }
  subscribeTo(subject) {
    if(!this.subjects[subject.id]){
      console.log('subscribeTo.. ', subject)
       subject.addObserver(this)
       this.subjects[subject.id] = subject
    }
  }
  getValue(){
    currentObserver = this
    let value = this.vm.$data[this.key]
    currentObserver = null
    return value
  }
} 


class Compile {
  constructor(vm){
    this.vm = vm
    this.node = vm.$el
    this.compile()
  }
  compile(){
    this.traverse(this.node)
  }
  traverse(node){
    if(node.nodeType === 1){
      this.compileNode(node)   //解析节点上的v-bind 属性
      node.childNodes.forEach(childNode=>{
        this.traverse(childNode)
      })
    }else if(node.nodeType === 3){ //处理文本
      this.compileText(node)
    }
  }
  compileText(node){
    let reg = /{{(.+?)}}/g
    let match
    console.log(node)
    while(match = reg.exec(node.nodeValue)){
      let raw = match[0]
      let key = match[1].trim()
      node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key])
      new Observer(this.vm, key, function(val, oldVal){
        node.nodeValue = node.nodeValue.replace(oldVal, val)
      })
    }    
  }

  //处理指令
  compileNode(node){
    let attrs = [...node.attributes] //类数组对象转换成数组,也可用其他方法
    attrs.forEach(attr=>{
      //attr 是个对象,attr.name 是属性的名字如 v-model, attr.value 是对应的值,如 name
      if(this.isDirective(attr.name)){
        let key = attr.value       //attr.value === 'name'
        node.value = this.vm.$data[key]  
        new Observer(this.vm, key, function(newVal){
          node.value = newVal
        })
        node.oninput = (e)=>{
          this.vm.$data[key] = e.target.value  //因为是箭头函数,所以这里的 this 是 compile 对象
        }
      }
    })
  }
  //判断属性名是否是指令
  isDirective(attrName){
     return attrName === 'v-model'
  }

}

class mvvm {
  constructor(opts) {
    this.init(opts)
    observe(this.$data)
    new Compile(this)
  }
  init(opts){
    this.$el = document.querySelector(opts.el)
    this.$data = opts.data
  }
}

let vm = new mvvm({
  el: '#app',
  data: { 
    name: 'zhangfei',
    age: 3
  }
})

setInterval(function(){
  vm.$data.age++
}, 1000)

7 增加事件

<div id="app" >
  <input v-model="name" v-on:click="sayHi" type="text">
  <h1>{{name}} 's age is {{age}}</h1>
</div>


function observe(data) {
  if(!data || typeof data !== 'object') return
  for(var key in data) {
    let val = data[key]
    let subject = new Subject()
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function() {
        console.log(`get ${key}: ${val}`)
        if(currentObserver){
          console.log('has currentObserver')
          currentObserver.subscribeTo(subject)
        }
        return val
      },
      set: function(newVal) {
        val = newVal
        console.log('start notify...')
        subject.notify()
      }
    })
    if(typeof val === 'object'){
      observe(val)
    }
  }
}

let id = 0
let currentObserver = null

class Subject {
  constructor() {
    this.id = id++
    this.observers = []
  }
  addObserver(observer) {
    this.observers.push(observer)
  }
  removeObserver(observer) {
    var index = this.observers.indexOf(observer)
    if(index > -1){
      this.observers.splice(index, 1)
    }
  }
  notify() {
    this.observers.forEach(observer=> {
      observer.update()
    })
  }
}

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.bind(this.vm)(value, oldVal)
    }
  }
  subscribeTo(subject) {
    if(!this.subjects[subject.id]){
      console.log('subscribeTo.. ', subject)
       subject.addObserver(this)
       this.subjects[subject.id] = subject
    }
  }
  getValue(){
    currentObserver = this
    let value = this.vm[this.key]   //等同于 this.vm.$data[this.key]
    currentObserver = null
    return value
  }
} 


class Compile {
  constructor(vm){
    this.vm = vm
    this.node = vm.$el
    this.compile()
  }
  compile(){
    this.traverse(this.node)
  }
  traverse(node){
    if(node.nodeType === 1){
      this.compileNode(node)   //解析节点上的v-bind 属性
      node.childNodes.forEach(childNode=>{
        this.traverse(childNode)
      })
    }else if(node.nodeType === 3){ //处理文本
      this.compileText(node)
    }
  }
  compileText(node){
    let reg = /{{(.+?)}}/g
    let match
    console.log(node)
    while(match = reg.exec(node.nodeValue)){
      let raw = match[0]
      let key = match[1].trim()
      node.nodeValue = node.nodeValue.replace(raw, this.vm[key])
      new Observer(this.vm, key, function(val, oldVal){
        node.nodeValue = node.nodeValue.replace(oldVal, val)
      })
    }    
  }

  //处理指令
  compileNode(node){
    let attrs = [...node.attributes] //类数组对象转换成数组,也可用其他方法
    attrs.forEach(attr=>{
      //attr 是个对象,attr.name 是属性的名字如 v-model, attr.value 是对应的值,如 name
      if(this.isModelDirective(attr.name)){
        this.bindModel(node, attr)
      }else if(this.isEventDirective(attr.name)){
        this.bindEventHander(node, attr)
      }
    })
  }
  bindModel(node, attr){
    let key = attr.value       //attr.value === 'name'
    node.value = this.vm[key]  
    new Observer(this.vm, key, function(newVal){
      node.value = newVal
    })
    node.oninput = (e)=>{
      this.vm[key] = e.target.value  //因为是箭头函数,所以这里的 this 是 compile 对象
    }
  }
  bindEventHander(node, attr){       //attr.name === 'v-on:click', attr.value === 'sayHi'
    let eventType = attr.name.substr(5)       // click
    let methodName = attr.value
    node.addEventListener(eventType, this.vm.$methods[methodName]) 
  }

  //判断属性名是否是指令
  isModelDirective(attrName){
     return attrName === 'v-model'
  }

  isEventDirective(attrName){
    return attrName.indexOf('v-on') === 0
  }

}


class mvvm {
  constructor(opts) {
    this.init(opts)
    observe(this.$data)
    new Compile(this)
  }
  init(opts){
    this.$el = document.querySelector(opts.el)
    this.$data = opts.data || {}
    this.$methods = opts.methods || {}

    //把$data 中的数据直接代理到当前 vm 对象
    for(let key in this.$data) {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get: ()=> {  //这里用了箭头函数,所有里面的 this 就指代外面的 this 也就是 vm
          return this.$data[key]
        },
        set: newVal=> {
          this.$data[key] = newVal
        }        
      })
    }

    //让 this.$methods 里面的函数中的 this,都指向当前的 this,也就是 vm
    for(let key in this.$methods) {
      this.$methods[key] = this.$methods[key].bind(this)
    }
  }

}

let vm = new mvvm({
  el: '#app',
  data: { 
    name: 'jirengu',
    age: 3
  },
  methods: {
    sayHi(){
      alert(`hi ${this.name}` )
    }
  }
})

let clock = setInterval(function(){
  vm.age++   //等同于 vm.$data.age, 见 mvvm init 方法内的数据劫持

  if(vm.age === 10) clearInterval(clock)
}, 1000)

参考

mdn:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

饥人谷:https://xiedaimala.com/

慕课网:https://www.imooc.com/

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值