Skip to content

pppcode/MVVM

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 

Repository files navigation

MVVM 框架

项目介绍

原生 JS 实现一个 MVVM 框架(Vue 的核心)

技术栈

  • Object.defineProperty 的用法
  • 数据劫持的写法
  • 观察者模式
  • 单向数据流
  • 双向数据流

效果:

  • 页面内容就是 data 中的数据
  • 修改内容时,会有提示(事件的绑定,点击的时候调用'sayHi' , 也就是 methods 中的方法,再去调用 data 中的数据)
  • 双向绑定:修改内容时,页面显示的内容也跟着发生变化

具体实现

流程

  1. 数据的劫持:使用 Object.defineProperty 实现数据的拦截
  2. 发布订阅模式:拦截数据后,在合适的时间去订阅数据的变动,在数据变动时再去触发修改

Object.defineProperty 的用法

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

语法

Object.defineProperty(obj, prop, descriptor)

  • obj:要处理的目标对象
  • prop:要定义或修改的属性名
  • descriptor:将被定义或修改的属性描述符

使用方法

var obj = {}
obj.name = 'zhangsan'
obj['age'] = 3
//对对象里面的属性和值操作的方法(一种新的)
Object.defineProperty(obj, 'intro', {
  value: 'hello world'
})
console.log(obj) //{name: "zhangsan", age: 3, intro: "hello world"}

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

var obj = {}
Object.defineProperty(obj, 'intro', {
  configurable: false,
  value: 'hello world'
})
obj.intro = 'zhangsan'
console.log(obj.intro) //'hello world'
delete obj.intro //false,删除失败
console.log(obj.intro) //'hello world'

Object.defineProperty(obj, 'intro', {
  configurable: true,
  value: 'hello world'
})
delete obj.intro //true,删除成功
console.log(obj.intro) //undefined

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

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

设置 enumerable 属性为 false 后,遍历对象的时候回忽略通过 defineProperty 设置的属性(如果未设置,默认就是 false 不可遍历),设为 true 后,对象中的属性全部可遍历。

var obj = {name: 'zhangsan'}
Object.defineProperty(obj, 'age', {
  value: 3,
  writable: false
})
obj.age = 4
console.log(obj.age) //3

设置 writable 为 false 时,修改对象的 defineProperty 中设置的属性值无效。

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

  • value: 该属性对应的值,可以是任何有效的 JavaScript 值(数值,对象,函数等),默认为 undefined.
  • writable: 当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变,默认为 false.

configurable: true 和 writable: 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 = 200 //'set age ...' age = val = 200,此处的 age 是值,等于 get 中的 age
obj.age //'get age ...' 200 

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

  • get: 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。改方法返回值被用作属性值,默认为 undefined。
//没有 getter 
var obj = {}
Object.defineProperty(obj, 'age', {})
obj.age //undefined

//getter 的返回值被用做属性值,默认为 undefined
var obj = {}
var age 
Object.defineProperty(obj, 'age', {
  get: function() {
    console.log('get age ...')
    return age
  }
})
obj.age //undefined 先从里面找,没有的话,再去全局找到 age = undefined
  • set: 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性,默认为 undefined。

数据描述符和存取描述符是不能同时存在的

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/writable 不能和 get/set 同时存在,因为这些都是对数据的描述,所以这种写法是不行的。

通过 get 和 set 可以做数据的劫持了

  • 当用户通过一个对象调用它的属性得到它的值得时候,中间就会被拦截了一道,调用 get ,
  • 当用户去设置这个值得时候,中间也会变被拦截,调用 set。

数据的劫持

现在我们利用 Object.defineProperty 这个方法动态监听数据,data 里的数据被监听了,当用户操作数据时,observe() 会去做对应的事情,这就实现了数据的拦截

//定义数据
var data = {
  name: 'zhangsan',
  friends: [1, 2, 3]
}

//定义监听函数
function observe(data) {
  if(!data || typeof data !== 'object') return 
  for(var key in data) {
    //遍历时,得到每个属性的值,延伸:1.这里是 let 而不是 var ,为什么
    let val = data[key]
    Object.defineProperty(data, key, {
      enumerable: true, //可遍历
      configurable: true, //可删除
      get: function() {
        console.log(`get ${val}`)
        return val //延伸:2.val 能否替换成 data[key]
      },
      //调用set 时, 把值作为参数传递进来,把新值 赋值给 旧值
      set: function(newVal) {
        console.log(`changes happen: ${val} => ${newVal}`)
        val = newVal
      }
    })
    //如果值是对象,再次执行 obsrve ,递归操作,递归到了一定的层级,传递的值不是对象,就会停掉
    if(typeof val === 'object') {
      observe(val) 
    }
  }
}

//监听数据
observe(data)

//操作数据
console.log(data.name) //'get zhangsan' zhangsan
data.name = 'lisi' //'changes happen: zhangsan => lisi' lisi
data.friends[0] //'get 1' 1
data.friends[0] = 4 //'changes happen: 1 => 4' 4

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

延伸

  1. 为什么用 let 而不是 var ?

let 有块级作用域 {},每次遍历,都能存贮当前的状态,每次的 val 是不一样的,当用户调用 get 时,获取的是当前作用域下的 val,如果用 var 的话,遍历完成后,val 是最后一次的值,用户再次获取 val 的值是都是获取的最后一次的值,data.name //get 3 ,不符合需求。

若用 var 的话,则需要这样写, 利用 forEach 提供的回调函数

function observe(data) {
  if (!data || typeof data !== 'object') return
  var keys = Object.keys(data)
  keys.forEach(key => {
    //每个 val 是不一样的, 因为他处于这个回调函数里面
    var 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)
    }
  })
}

或者用闭包

function observe(data) {
  if (!data || typeof data !== 'object') return
  for (let key in data) {
    //闭包,用立即执行函数存贮每次遍历的 key
    (function (key) {
      var 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)
      }
    })(key)
  }
}

操作数据data.name //'get zhangsan' zhangsan,均符合需求。

  1. get 中的 val 能否替换成 data[key]?

不可以,会陷入死循环,例如当用户调用 data[name] 执行 get(), 执行的过程中又需要 data[name],当需要得到 data[name] 的值时,又需要调用 get(),调用get() 时,又需要...

观察者模式

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

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

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

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

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('zhangsan')
//主题添加观察者1
subject.addObserver(observer1)
//创建观察者2
var observer2 = new Observer('lisi')
//主题添加观察者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(name) {
    this.name = name
  }
  update() {
    console.log(this.name + ' update...')
  }
}

let subject = new Subject()
let observer1 = new Observer('zhangsan')
subject.addObserver(observer1)
let observer2 = new Observer('lisi')
subject.addObserver(observer2)

subject.notify()

有个小问题

用户订阅博客,是用户来操作,用户发起的,而不是主题去发起的,做一些修改

class Observer {
  constructor(name) {
    this.name = name
  }
  update() {
    console.log(this.name + ' update...')
  }
  subscribeTo(subject) {
    subject.addObserver(this)
    console.log('订阅成功!')
  }
}

let subject = new Subject()
let observer = new Observer('zhangsan')
observer.subscribeTo(subject)

subject.notify()

增加 subscribeTO 方法,我是观察者,我要订阅你,当订阅 subject 时,调用 subject.addObserver(),把我加进去,其实就是加了一层壳 从外部调用来看,更符合逻辑,更直观了。

总结

我去订阅某个主题,可以订阅不同的主题,当主题发出通知后,我就能收到,执行我的方法去更新。

MVVM 单项绑定的实现

MVVM (Model-View-ViewModel) 是一种用于把数据和 UI 分离的设计模式。

MVVM 中的 Model 表示应用程序使用的数据,比如一个用户账户信息(名字、头像、电子邮件等)。Model 保存信息,但通常不处理行为,不会对信息进行再次加工。数据的格式化是由View 处理的。行为一般认为是业务逻辑,封装再 ViewModel 中。

View 是与用户进行交互的桥梁。

ViewModel 充当数据转换器,讲 Model 信息转换为 View 的信息,将命令从 View 传递到 Model。

实现的效果:

data 里的name会和视图中的{{name}}一一映射,修改 data 里的值,会直接引起视图中对应数据的变化。

  1. 解析并编译模板,字符串替换成数据中对应的值。

    首先进行初始化,数据暂存起来,执行 compile() 编译模板,遍历节点,如果有子元素的话,再去遍历子元素,直到里面的内容是文本,开始处理文本,把文本做个替换: 正则匹配模板中的{{name}},{{age}},一个个匹配,匹配完后,把里面的 name 换成 以 name 作为属性的值,整体会被换掉,此时用户看到的就是真实的数据了。

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

<script>
  class mvvm {
    constructor(opts) {
      this.init(opts)
      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) { //div
        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)) { //循环两次,拿到 [{{name}}, name], [{{age}}, age]
        let raw = match[0] //{{name}} {{age}}
        let key = match[1].trim() //name age,删除字符串两端的空白字符
        node.nodeValue = node.nodeValue.replace(raw, this.$data[key]) //字符串替换成对应的数据
      }
    }
  }

  let vm = new mvvm({
    el: '#app',
    data: {
      name: 'zhangsan',
      age: 3
    }
  })
</script>

页面显示:由{{name}} 's is {{age}}变为zhangsan 's is 3

问题:用户修改数据后,模板中的 {{name}},{{age}} 怎么进行实时的变动呢?

这里用到上面提到的观察者模式和数据监听。

  • 主题(subject): data 中的 name 和 age 属性
  • 观察者(observer): 模板中的 {{name}} 和 {{age}}
  • 观察者何时订阅主题: 当一开始执行 mvvm 初始化(根据 el 解析模板发现 {{name}})的时候订阅主题
  • 主题何时通知更新: 当data.name发生改变的时候,通知观察者更新内容。 我们可以在一开始监控 data.name (Object.defineProperty(data, 'name', {...})),当用户修改 data.name 的时候调用主题的 subject.notify。
  1. 修改数据,模板中的内容进行实时变动

    在模板解析的过程中,解析到 {{name}} 了,创建一个观察者,这个观察者就观察 name 这个属性,解析到 {{age}} 了,又创建一个观察者,这个观察者就观察 age 这个属性

      renderText(node) {
          ...
          //解析模板时,遇到了变量,就创建一个观察者,观察这个属性的变化,去执行 cb
          new Observer(this, key, cb)  
        }
      }
    

    创建观察者后,什么时候去观察这个主题呢?

    创建了 observer 后立刻去观察这个主题,可是主题在哪,主题什么时候创建的呢

    主题是 observe(data) 时,遍历这个数据时创建的,主题在这里,问题是怎么找到对应的主题呢(找不到,都存在内存中,都是局部变量,外面访问不到),所以不可能创建完成后,立刻去订阅这个主题

    所以在创建这个观察者时,通过调用 this.getValue()

    1. 把我设置成独一无二的场外最高的权限的一个观察者,全球唯一的观察者,因为观察者很多,每遇到个这个符号,就创建一个,所以到了我时,把我设置为全局最高的。
    2. 此时key 就是我需要订阅的这个属性,调用 this.vm.$data[this.key] 时,就是调用了 get ,currentObserver是存在的,这个观察者开始订阅这个主题,那么这个主题在哪,使用了闭包,这个主题往上找,找到之前创造的主题,否则的话根本就找不到这里创建的主题(若还有递归的话,主题会嵌套很多层,每个属性都有个主题),找到的原因就是:在创建的过程中 调用一次 get ,好了,同时也得到了这个值,完成了订阅主题。
    3. 订阅过之后,设置为 null ,不是最高观察者了,因为我已经订阅过主题了,主题已经在我的 this.subjects 里了,同时 subjects 里也有我了。
    //监听数据
    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 () {
            //如果当前观察者存在,去订阅主题
            if (currentObserver) {
              currentObserver.subscribeTo(subject)
            }
            return val
          }
        })
        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)
        }
      }
    }
    
    //创建观察者
    class Observer {
      constructor(vm, key, cb) {
        this.subjects = {} //我订阅的主题
        this.vm = vm //mvvm 对象
        this.key = key //属性
        this.cb = cb //订阅之后去做的事情
        this.value = this.getValue() //获取到真实的值时,去订阅这个主题
      }
      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] //调用 observe(data) 中的 get 方法
        currentObserver = null //我已经订阅过了,所以去掉当前观察者的身份
        return value
      }
    }
    
    class mvvm {
      constructor(opts) {
        ...
        observe(this.$data)
      }
      init(opts) {
        ...
        this.observers = []
      }
      renderText(node) {
        ...
        new Observer(this, key, cb)
      }
    }
    

    以上是解析 {{name}} 时,创建了一个观察者所做的事情

    解析 {{age}} 时,创建一个观察者,创建的过程中,调用 this.value 时,把这个观察者设置为最高观察者, 获取 this.vm.$data[this.key] 再调用 get ,把他订阅到了当前的 age 主题。

    现在,所有的这些符号都订阅了自己的主题(数据),当主题(数据)更新时,调用 set 方法,再调用 subject.notify(),再调用 observer.update(),在调用 update(),执行回调,执行了 new Observer() 中回调:数据替换。

    完整代码

    <body>
      <div id="app">
        <h1>{{name}} 's is {{age}}</h1>
      </div>
    
      <script>
        //监听数据
        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 () {
                //如果当前观察者存在,去订阅主题
                if (currentObserver) {
                  currentObserver.subscribeTo(subject)
                }
                return val
              },
              set: function(newVal) {
                val = newVal
                //告诉订阅这个属性的所有观察者,都去更新
                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() //订阅过我的观察者去执行自己的 update()
            })
          }
        }
    
        //创建观察者
        class Observer {
          constructor(vm, key, cb) {
            this.subjects = {} //我订阅的主题
            this.vm = vm //mvvm 对象
            this.key = key //属性
            this.cb = cb //订阅之后去做的事情
            this.value = this.getValue() //获取到真实的值时,去订阅这个主题
          }
          update() {
            let oldVal = this.value //修改前的值
            let value = this.getValue() //修改后的值,再次调用 get ,get 中的值已是 set 后的值了
            if(value !== oldVal) {
              this.value = value //新值赋值给 this.value
              this.cb.bind(this.vm)(value,oldVal) //调用回调函数,传递 value,oldVal 绑定this为 mvvm 对象
            }
          }
          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] //调用 observe(data) 中的 get 方法
            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) { //div
              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)) { //循环两次,拿到 [{{name}}, name], [{{age}}, age]
              let raw = match[0] //{{name}} {{age}}
              let key = match[1].trim() //name age,删除字符串两端的空白字符
              node.nodeValue = node.nodeValue.replace(raw, this.$data[key]) //字符串替换成对应的数据
              //解析模板时,遇到了变量,就创建一个观察者
              new Observer(this, key, function(val, oldVal) {
                node.nodeValue = node.nodeValue.replace(oldVal, val) //cb回调函数,新值与旧值做替换
              })
            }
          }
        }
    
        let vm = new mvvm({
          el: '#app',
          data: {
            name: 'zhangsan',
            age: 3
          }
        })
    
        //修改数据
        setInterval(function() {
          vm.$data.age++
        },1000)
      </script>
    </body>
    

以上就是单向绑定的实现。

实现双向绑定

修改视图,还会反馈到数据上

场景:input 修改那些表单

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

如何把 input 的内容反馈到数据里呢?

v-model="name" 这种语法标记 name 属性 当用户修改 input 的时候,值会传递到 name 属性对应的值上 name 属性对应的值一修改,又会反馈到 h1 中

以上是数据双向传递的过程

完整代码

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

  <script>
    //监听数据
    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 () {
            if (currentObserver) {
              currentObserver.subscribeTo(subject)
            }
            return val
          },
          set: function (newVal) {
            val = newVal

            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]) {
          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) { //div
          this.compileNode(node) //解析节点上的 v-model 属性
          node.childNodes.forEach(childNode => {
            this.traverse(childNode) //递归
          })
        } else if (node.nodeType === 3) { //处理文本
          this.compileText(node)
        }
      }
      //处理指令
      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] ////把� data 中 name 属性对应的值赋值给 input 元素
            new Observer(this.vm, key, function (newVal) {
              node.value = newVal ////再创建一个观察者,监听到 data 中的数据发生变化时,把新值赋值给 node.value
            })
            //把输入内容赋值给 data 中的 name 属性对应的值,data 一更新,所有监听 data 的订阅者就会更新...
            //因为是箭头函数,所以这里的 this 是 compile 对象
            node.oninput = (e) => {
              this.vm.$data[key] = e.target.value
            }
          }
        })
      }
      //判断属性名是否是指令,比如 type 就不是指令
      isDirective(attrName) {
        return attrName === 'v-model'
      }
      compileText(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.vm.$data[key])
          new Observer(this.vm, key, function (val, oldVal) {
            node.nodeValue = node.nodeValue.replace(oldVal, val)
          })
        }
      }

    }

    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: 'zhangsan',
        age: 3
      }
    })
  </script>
</body>

增加事件指令,完善代码

增加v-on:click="sayHi"

<div id="app" >
  <input v-model="name" v-on:click="sayHi" type="text">
  <h1>{{name}} 's age is {{age}}</h1>
</div>
...
let vm = new mvvm({
  el: '#app',
  data: { 
    name: 'zhangsan',
    age: 3
  },
  methods: {
    sayHi(){
      alert(`hi ${this.name}` )
    }
  }
})

完整代码

<body>

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

<script>

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 // 'sayHi'
    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) {
      //this === vm , key === name,age 把 name,age 从 data 中拿出来了 vm = {name:xxx,age:xxx}
      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) {
      //执行 sayHi() 时,alert(`hi ${this.name}` ) 这里遇到的 this 就是 vm 
      //经过上面的设置,vm = {name:xxx,age:xxx},就可以拿到 name 的值了
      this.$methods[key] = this.$methods[key].bind(this)
    }
  }

}

let vm = new mvvm({
  el: '#app',
  data: { 
    name: 'zhangsan',
    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)


</script>
</body>

Releases

No releases published

Packages

No packages published

Languages