了解MVVM

MVVM(Model–view–viewmodel)是一种软件架构模式

Posted by Azr on March 1, 2019

MVVM是将数据模型数据双向绑定的思想作为核心,因此在View和Model之间没有联系,通过ViewModel进行交互,而且Model和ViewModel之间的交互是双向的,因此视图的数据的变化会同时修改数据源,而数据源数据的变化也会立即反应到View上。

了解

① 目的

  1. 能够掌握Vue中的双向数据绑定原理以及核心代码模块
  2. 实现一个简单的vue框架
  3. 学习ES6语法
  4. 对于Vue框架掌握的更加牢固
  5. 使用Vue开发时,遇到bug可以快速定位
  6. 了解框架底层原理,面试加分
  7. 代码摘抄自Vue的源码

② 概念

M(Model,模型层 ),

V(View,视图层),

VM(ViewModel,视图模型,V与M连接的桥梁)

MVVM框架实现了数据双向绑定

当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改

修改V层则会通知M层数据进行修改

MVVM框架实现了视图与模型层的相互解耦

③ 双向数据绑定方式

  1. 发布-订阅者模式

    Backbone一般通过pub、sub的方式来实现数据和视图的绑定,但是使用起来比较麻烦

  2. 脏值检查

    Angular 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变。

  3. 数据劫持

    Vue 则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的settergetter**,在数据变动时发布消息给订阅者,触发相应的监听回调。

④ 需求

  1. 实现一个Compiler模板解析器,能够对模版中的指令和插值表达式进行解析,并且赋予不同的操作
  2. 实现一个Observer数据监听器,能够对数据对象的所有属性进行监听
  3. 实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM
  4. 创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,既Vue

小架子

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>MVVM-dome</title>
</head>
<body>
  <div id="app">
    <p></p>
    <p v-text = 'msg'> </p>
  </div>
  <script src="./src/vue.js"></script>
  <script src="./src/compile.js"></script>
  <script>
    // const vm1 = new Vue()
    const vm = new Vue({
      el: '$app',
      data: {
        msg: 'hello'
      }
    })
    console.log(vm)
  </script>
</body>
</html>

src/vue.js

/**
 *  定义一个类 Vue的构造函数,用于创建Vue实例
 */
class Vue {
  constructor(options) {
    // 设值默认参数
    options = options || {}
    // 给Vue添加属性 
    this.$el = options.el
    this.$data = options.data
    // Compiler主要负责解析模板指令和插值表达式,将模板中的变量转换成数据. 然后渲染整个页面和视图
    if(this.$el) {
      new Compile(this.$el, this)
    }
  }
}

src/compile.js

/**
 * 负责解析模板内容
 */
 class Compile{
   constructor(el ,vm ) {  }
 }

compile

① fragment文档碎片

语法
 let fragment = document.createDocumentFragment();

fragment 是一个指向空DocumentFragment对象的引用。

描述

DocumentFragments` 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。

因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

compile.js

/**
 * 负责解析模板内容
 * @param el {object} new Vue 选择器
 * @param vm {object} new Vue Vue实例
 */
 class Compile{
   constructor(el ,vm ) {
     // 参数
     this.el = typeof el === 'string' ? document.querySelector(el) : el 
     this.vm = vm
     // 编模模板
     if(this.el) {

     }
   }
 }

vue.js

/**
 *  定义一个类 Vue的构造函数,用于创建Vue实例
 */
class Vue {
  constructor(options) {
    // 设值默认参数
    options = options || {}
    // 给Vue添加属性 
    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods
    // Compiler主要负责解析模板指令和插值表达式,将模板中的变量转换成数据. 然后渲染整个页面和视图
  
    if(this.$el) {
      // this == 整个Vue实例
      let c  = new Compile(this.$el, this)
      console.log(c)
    }
  }
}

compile.js

  1. 把el中所有的子节点放入内存中 fragmen
  2. 在内存中编译fragment
    1. 创建node2Fragment的核心方法
    2. 创建toArray的工具方法
    3. 打印consol.dir(fragment) 查看控制台内容把fragment一次性的添加到页面
/**
 * 负责解析模板内容
 * @param el {object} new Vue 选择器
 * @param vm {object} new Vue Vue实例
 */

 class Compile{
   constructor(el ,vm ) {
     // 参数
     this.el = typeof el === 'string' ? document.querySelector(el) : el 
     this.vm = vm
     // 编模模板
     if(this.el) {
       // 1. 把el中所有的子节点放入内存中  fragment

       // 1-1 创建node2Fragment的核心方法
       // 1-2 创建toArray的工具方法
       // 1-3 打印consol.dir(fragment) 查看控制台内容
       let fragment = this.node2Fragment(this.el)
       consol.dir(fragment)
       // 2. 在内存中编译fragment
       // 3. 把fragment一次性的添加到页面
      
     }
   }
   /**
    * 核心方法
    */
   node2Fragment(node){
    let fragment = document.createDocumentFragment()
    
    // 把el中所有的子节点添加到文档碎片中
    let childNodes = node.childNodes
    console.log(childNodes)

    this.toArray(childNodes).forEach(node => {
      // 把所有的字点添加到fragment
      fragment.appendChild(node)
    })
   }

   /**
    * 工具方法
    */
   toArray(likeArray) {
    return [].slice.call(likeArray)
   }
 }

② frament子节点

  1. 在内存中编译文档碎片
/**
* 在内存中编译文档碎片
* @param {string} fragment 
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node=>{
  // 编译子节点
  console.log(node)
  // 是元素节点 需要解析指令
  // 是文本节点 需要解析插值表达式
})
}
  1. 需要工具函数来区分元素/文本节点

nodeType 节点的类型 1. 元素节点 3. 文本节点

isElementNode(node) { return node.nodeType === 1 }
isTextNode(node) { return node.nodeType === 3 }
  1. 如果是元素节点,需要解析指令

    如果是文本节点,需要解析插值表达式

/**
* 在内存中编译文档碎片
* @param {string} fragment 
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node=>{
  if(this.isElementNode(node)) {
     // 是元素节点 需要解析指令
    this.compileElement(node)
  }
  if(this.isTextNode(node)) {
    // 是文本节点 需要解析插值表达式
    this.compileText(node)
  }
})
}
  1. 当前节点还有子节点,如<div></div>,就需要递归进行解析
/**
* 在内存中编译文档碎片
* @param {string} fragment 
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node=>{
  if(this.isElementNode(node)) {  }
  if(this.isTextNode(node)) {  }
  if(node.childNodes && node.childNodes.length > 0) {
    // 当前节点还有子节点 需要递归解析
    this.compile(node)
  }
})
}

③ 解析指令

  1. 获取当前节点下所有的属性
/**
* 解析html标签
* @param {string} node 
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
  // console.log(attr)
  // 2. 解析Vue的指令(所有以v-开头的指令)
})
}
  1. 是否是v-开头的指令
isDirective(attrName) { return attrName.startsWith('v-') }
  1. 解析Vue的指令(所有以v-开头的指令)

V-TEXT

compile.js

/**
* 解析html标签
* @param {string} node 
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
  // console.dir(attr)

  // 2. 解析Vue的指令(所有以v-开头的指令)
  let attrName = attr.name
  if(this.isDirective(attrName)) {
    let type = attrName.slice(2)

    let expr = attr.value
    // 如果是v-text指令
    if(type === 'text') {
      node.textContent = this.vm.$data[expr]
      // console.log(node)
    }
  }
})

index.html

....
<body>
  <div id="app">
    <p v-text="msg" title="1"> </p>
  </div>
  <script src="./src/vue.js"></script>
  <script src="./src/compile.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello'
      }
    })
  </script>
</body>
</html>

V-HTML

compile.js

/**
* 解析html标签
* @param {string} node 
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
  // console.dir(attr)

  // 2. 解析Vue的指令(所有以v-开头的指令)
  let attrName = attr.name
  if(this.isDirective(attrName)) {
    let type = attrName.slice(2)

    let expr = attr.value
    // 如果是v-text指令
    if(type === 'text') {
      node.textContent = this.vm.$data[expr]
      // console.log(node)
    }
    // 如果是v-html指令
    if(type === "html") {
      node.innerHTML = this.vm.$data[expr]
    }
  }
})

index.html

....
<body>
  <div id="app">
    <p v-text="msg" title="1"> </p>
    <p v-html="tag" title="2"> </p>
  </div>
  <script src="./src/vue.js"></script>
  <script src="./src/compile.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello',
        tag: '<h3>tag<h3>'
      }
    })
  </script>
</body>
</html>

V-MODEL

compile.js

/**
* 解析html标签
* @param {string} node 
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
  // console.dir(attr)

  // 2. 解析Vue的指令(所有以v-开头的指令)
  let attrName = attr.name
  if(this.isDirective(attrName)) {
    let type = attrName.slice(2)

    let expr = attr.value
    // 如果是v-text指令
    if(type === 'text') {
      node.textContent = this.vm.$data[expr]
      // console.log(node)
    }
    // 如果是v-html指令
    if(type === "html") {
      node.innerHTML = this.vm.$data[expr]
    }
    // 如果是v-model指令
    if(type === "model") {
      node.value = this.vm.$data[expr]
    }
  }
})

index.html

....
<body>
  <div id="app">
    <p v-text="msg" title="1"> </p>
    <p v-html="tag" title="2"> </p>
    <input type="text" v-model="msg">
  </div>
  <script src="./src/vue.js"></script>
  <script src="./src/compile.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello',
        tag: '<h3>tag<h3>'
      }
    })
  </script>
</body>
</html>

④ v-on指令

index.html

...
<body>
  <div id="app">
    <p v-text="msg" title="1"> </p>
    <p v-html="tag" title="2"> </p>
    <input type="text" v-model="msg">
    <button v-on:click="clickFn">Button</button>
  </div>
  <script src="./src/vue.js"></script>
  <script src="./src/compile.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello',
        tag: '<h3>tag<h3>'
      },
      methods:{
        clickFn(){
          console.log(this this.msg)
        }
      }
    })
  </script>
</body>
</html>
  1. if(type === "on") { console.log("on") }在控制台无法打印出来,需要解析以下V-ON指令,既 if(type.split(":")[0] === "on"){ console.log("on")}

  2. 为了简化代码,创建isEventDirective的工具方法

  3. 由于页面打印的是this.name,this的指向是node,需要使用bind更改一下this的指向,即node.addEventListener(eventType, this.vm.$methods[expr].bind(this.vm)

    这样打印出来的this就是vm实例,但是this.msg还是understand,但是打印this.$data.msg是可以打印出来hello的。

/**
* 解析html标签
* @param {string} node 
*/
compileElement(node){
  .... 
    // if(type === "on") { console.log("on") }
    if(this.isEventDirective(type)){ 
      // console.log("on")
      // 给当前元素注册时间
      let eventType = type.split(":")[1]
      // console.log(expr)
      node.addEventListener(eventType, fn.bind(vm))
    }
  }
})
    
isEventDirective(type) {  return type.split(":")[0] === "on" }

⑤ 通过compileUtil简化代码

  1. 创建CompileUtil
let CompileUtil = {
    text(node, vm, expr) {
      node.textContent = vm.$data[expr]
    },
    html(node, vm, expr) {
      node.innerHTML = vm.$data[expr]
    },
    model(node, vm, expr) {
      node.value = vm.$data[expr]
    }
   }
 }
  1. 简化compileElement中的v-text指令
compileElement(node){
  .... 
    if(type === 'text') {
      // node.textContent = expr
      // node.textContent = this.vm.$data[expr]

      // 简化1 
      CompileUtil['text'](node, this.vm, expr)
    }
})   
  1. 因重复性比较多,就不需要进行判断,直接在V-ON之后用else
compileElement(node){
  .... 
   if(this.isEventDirective(type)){ 
      let eventType = type.split(":")[1]
      node.addEventListener(eventType, this.vm.$methods[expr].bind(this.vm)) 
    }else {
      CompileUtil[type] && CompileUtil[type](node. this.vm, expr)
    }
}) 
  1. 再次简化, 将v-on 中的方法提取出来,并且添加判断。
compileElement(node){
  .... 
   if(this.isEventDirective(type)){ 
      CompileUtil["eventHandler"](node, this.vm, type, expr)
    }else {
      CompileUtil[type] && CompileUtil[type](node. this.vm, expr)
    }
}) 

let CompileUtil = {
    text(node, vm, expr) {
      node.textContent = vm.$data[expr]
    },
    html(node, vm, expr) {
      node.innerHTML = vm.$data[expr]
    },
    model(node, vm, expr) {
      node.value = vm.$data[expr]
    },
    eventHandler(node, vm, type, expr) {
      let eventType = type.split(":")[1]
      // node.addEventListener(eventType, vm.$methods[expr].bind(vm))
      let fn = vm.$methods && vm.$methods[expr]
      if(eventType && fn) {
        node.addEventListener(eventType, fn.bind(vm))
      }
    }
   }
 }

⑥ 插值表达式

index.html

...
<body>
  <div id="app">
    <p>hi</p>
    <p>msg的内容是</p>
    <p v-text="msg" title="1"></p>

    
    <p v-html="tag" title="2"></p>
    <input type="text" v-model="msg">
    <button v-on:click="clickFn">Button</button>
  </div>
  <script src="./src/vue.js"></script>
  <script src="./src/compile.js"></script>
  <script src="./src/observe.js"></script>
  <script src="./src/watcher.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello',
        tag: '<h3>tag<h3>'
      },
      methods:{
        clickFn(){
          console.log("this", this)
          console.log("click", this.$data.msg)
        }
      }
    })
  </script>
</body>
</html>

compile.js

/**
* 解析文本节点
* @param {string} node 
*/
compileText(node) {
  let txt = node.textContent
  let reg = /\{\{(.+)\}\}/
  if(reg.test(txt)){
    let expr = RegExp.$1
    // console.log(expr)
     node.textContent = txt.replace(reg, this.vm.$data[expr])
  }
}

⑦ 显示复杂数据

index.html

...
<body>
  <div id="app">
    <p>hi</p>
    <p>msg的内容是</p>
    <p v-text="msg" title="1"></p>

    <p></p>

    <p v-html="tag" title="2"></p>
    <input type="text" v-model="msg">
    <button v-on:click="clickFn">Button</button>
  </div>
  <script src="./src/vue.js"></script>
  <script src="./src/compile.js"></script>
  <script src="./src/observe.js"></script>
  <script src="./src/watcher.js"></script>
  <script>

    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello',
        tag: '<h3>tag<h3>',
        car: {
          brand: '宝马',
          color: 'red'
        }
      },
      methods:{
        clickFn(){
          console.log("this", this)
          console.log("click", this.$data.msg)
        }
      }
    })
    console.log(vm.$data['msg'])        // hello 
    console.log(vm.$data['car.color'])  // understand
  </script>
</body>
</html>

以上代码无法解析data中的car

compile.js


/**
* 解析文本节点
* @param {string} node 
*/
compileText(node) {
  let txt = node.textContent
  let reg = /\{\{(.+)\}\}/
  if(reg.test(txt)){
    let expr = RegExp.$1
    node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
  }
} 
let CompileUtil = {
...
    getVMValue(vm, expr) {
      let data = vm.$data
      expr.split(".").forEach( key=> {
        data = data[key]
      })
      return data
    }
}

进行简化

/**
* 解析文本节点
* @param {string} node 
*/
compileText(node) {
  CompileUtil.mustache(node, this.vm)
} 
let CompileUtil = {
...
  mustache(node, vm) {
    let txt = node.textContent
    let reg = /\{\{(.+)\}\}/
    if (reg.test(txt)) {
      let expr = RegExp.$1
      // debugger
      node.textContent = txt.replace(reg, this.getVMValue(vm, expr))

      new Watcher(vm, expr, newValue => {
        node.textContent = txt.replace(reg, newValue)
      })
    }
  },
}

observe

observe用于给data中的所有数据添加getter / settert,方便在获取或者设置data中数据的时候,实现逻辑

vue.js

class Vue {
  constructor(options) {
	....
    // 监视data中的数据
    new Observer(this.$data)
  }
}

① 数据劫持

main.html

.... 
<body>
  <script>
    let obj = { name: 'qaz'}
    let temp = obj['name']
    Object.defineProperty(obj, 'name', {
      configurable: true, // 表示属性可以配置
      enumerable: true, // 表示这个属性可以遍历
      get() {
        // 每次获取对象的这个属性的时候,就会被这个get方法给劫持到
        // getter
        console.log('get执行了')
        return temp
      },
      // 每次设置这个对象的属性的时候,就会被set方法劫持到
      // 设置的值也会劫持到
      // setter
      set(newValue) {
        console.log('set方法执行了')
        temp = newValue
      }
    })
  </script>
</body>
</html>

② 对data中的数据进行劫持

  1. walk遍历data中所有的数据,都添加上getter和setter
    1. 给data对象的key设置getter和setter
    2. 如果data[key]是一个复杂的类型,递归的walk
  2. defineReactive定义响应式的数据(数据劫持)
    1. data中的每一个数据都应该维护一个dep对象
    2. dep保存了所有的订阅了该数据的订阅者
/**
 * observe用于给data中的所有数据添加getter / settert
 */
class Observer {
  constructor(data) {
    this.data = data
    this.walk(data)
  }
  /**
   * 核心方法
   * 遍历data中的数据,都添加上getter / settert
   * @param {string} data 
   */
  walk(data) {
    if(!data || typeof data != 'object') { return }
    Object.keys(data).forEach(key => {
      console.log(key)
      // 给data对象的key设置getter和setter
      this.defineReactive(data, key, data[key])
      // 如果data[key]是一个复杂的类型,递归的walk
      this.walk(data[key])
    }) 
  }

  /**
   * 定义响应式的数据(数据劫持)
   * data中的每一个数据都应该维护一个dep对象
   * @param {object} obj 
   * @param {string} key 
   * @param {string} value 
   */
  defineReactive(obj, key, value) {
    let that = this
    let dep = new dep()
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // console. log('获取到的数据的值', value)
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set (newValue) {
        if(value === newValue ) { return }
        value = newValue
        this.walk(newValue)
      }
    })
  }
}

watcher

① watcher对象的创建

/**
 * watcher模块负责把compile模块与observe模块关联起来
 */
class Watcher{
  /**
   * 
   * @param {string} vm 当前Vue实例
   * @param {string} expr data中数据的名字
   * @param {string} cb 当数据发生改变,需要调用cb
   */
  constructor (vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    // 需要把wxpr的旧值保存起来
    this.oldValue = this.getVMValue(vm, expr)
  }

  /**
   * 对外暴漏的一个方法,这个方法用于更新页面
   */
  update() {
    // 对比expr是否发生了改变,有改变 ==> 调用cb
    let oldValue = this.oldValue
    let newValue = this.newValue

    if(oldValue != newValue) {
      this.cb(newValue, oldValue)
    }
  }

  /**
   * 用于获取vm中的数据
   * @param {string} vm 
   * @param {string} expr 
   */
  getVMValue(vm, expr) {
    // 获取到data中的数据
    let data = vm.$data
    expr.split(".").forEach(key => {
      data = data[key]
    })
    return data
  }
}

② 关联observe与compile

通过watcher对象,监听expr的数据变化,当数据发生变化,就执行回调函数

compile.js

/**
* 解析文本节点
* @param {string} node 
*/
compileText(node) {
  CompileUtil.mustache(node, this.vm)
  // 通过watcher对象,监听expr的数据变化,当数据发生变化,就执行回调函数
  new Watcher(vm, expr, (newValue) => {
    node.textContent = newValue
  })
}

还有CompileUtil中的mustache, text,hrml,model需要进行watcher

 let CompileUtil = {

    mustache(node, vm) {
      let txt = node.textContent
      let reg = /\{\{(.+)\}\}/
      if(reg.test(txt)){
        let expr = RegExp.$1
        node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
        // 添加监听watcher
        new Watcher(vm, expr, newValue => {
          node.innerHTML = txt.replace(reg, newValue)
        })
      }
    },
    text(node, vm, expr) {
      node.textContent = vm.$data[expr]
      // 添加监听watcher
      new Watcher(vm, expr, (newValue) => {
        node.textContent = newValue
      })
    },
    html(node, vm, expr) {
      node.innerHTML = vm.$data[expr]
      // 添加监听watcher
      new Watcher(vm, expr, (newValue) => {
        node.innerHTML = newValue
      })
    },
    model(node, vm, expr) {
      node.value = vm.$data[expr]
      // 添加监听watcher
      new Watcher(vm, expr, (newValue) => {
        node.value = newValue
      })
    },
    eventHandler(node, vm, type, expr) {
      let eventType = type.split(":")[1]
      let fn = vm.$methods && vm.$methods[expr]
      if(eventType && fn) {
        node.addEventListener(eventType, fn.bind(vm))
      }
    },
    getVMValue(vm, expr) {
      let data = vm.$data
      expr.split(".").forEach( key=> {
        data = data[key]
      })
      return data
    }
   }

observe.js

defineReactive(obj, key, value) {
    let that = this
    let dep = new dep()
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() { Dep.target && dep.addSub(Dep.target) return value },
      set (newValue) {
        if(value === newValue ) { return }
        value = newValue
        this.walk(newValue)
        // 调用watcher update方法
        // window.watcher.update()
      }
    })
}
  1. 调用watcher update方法。window.watcher.update()
  2. window会覆盖,选择用发布者模式

③ 订阅-发布者模式

也叫观察者模式

​ 定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。

例子:

微信公众号

  1. 订阅者:只需要要订阅微信公众号
  2. 发布者(公众号):发布新文章的时候,推送给所有订阅者

优点:

  1. 解耦合
  2. 订阅者不用每次去查看公众号是否有新的文章
  3. 发布者不用关心谁订阅了它,只要给所有订阅者推送

watcher.js

/**
 * dep对象用于管理所有的订阅者和通知这些订阅者
 */
class Dep {
  constructor() {
    // 用于管理订阅者
    this.subs = []
  }

  // 添加订阅者
  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 通知
  notify() {
    // 遍历所有的订阅者,调用watcher的update方法
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

④ 通过订阅-发布者模式更新数据

  1. this表示的就是新创建的watcher对象
  2. 需要把expr的旧值给存储起来
  3. 清空Dep.target
class Watcher{
  constructor (vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb

    // 需要把wxpr的旧值保存起来
    // this.oldValue = this.getVMValue(vm, expr)

    // this表示的就是新创建的watcher对象
    // 存储到Dep.target属性上
    Dep.target = this
    // 需要把expr的旧值给存储起来
    this.oldValue = this.getVMValue(vm, expr)
    // 清空Dep.target
    Dep.target = null
  }
}

⑤ 修复复杂类型数据的bug

model中实现双向的数据绑定, 给node注册input事件,当前元素的value值发生改变,修改对应的数据

compile.js

let CompileUtil = {
   model(node, vm, expr) {
      node.value = vm.$data[expr]
      // 实现双向的数据绑定, 给node注册input事件,当前元素的value值发生改变,修改对应的数据
      node.addEventListener("input", function() { 
          // this.$data[expr] = this.value
          self.setVMValue(vm, expr, this.value) 
      })
      // 添加监听watcher
      new Watcher(vm, expr, (newValue) => {  node.value = newValue })
   },
  setVMValue(vm, expr, value) {
    let data = vm.$data
    // car brand
    let arr = expr.split(".")
    arr.forEach((key, index) => {
      // 如果index是最后一个
      if (index < arr.length - 1) {
        data = data[key]
      } else {
        data[key] = value
      }
    })
  }
}

⑥ 代理数据到vm实例

vue.js

class Vue {
  constructor(options = {}) {

    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods

    // 监视data中的数据
    new Observer(this.$data)

    // 把data中所有的数据代理到了vm上
    this.proxy(this.$data)
    // 把methods中所有的数据代理到了vm上
    this.proxy(this.$methods)
    // 如果指定了el参数,对el进行解析
    if (this.$el) {
      // compile负责解析模板的内容  模板和数据
      let c = new Compile(this.$el, this)
    }
  }

  proxy(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key]
        },
        set(newValue) {
          if (data[key] == newValue) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}

总结

  1. 将指令/插值表达式仅次于解析
  2. 通过observe对数据进行劫持
  3. 使用watcher关联observe与compile
  4. 由于watcher监视的地方较多,使用订阅-发布者模式

本文首次发布于 Azr的博客, 作者 @azrrrrr ,转载请保留原文链接.

原文链接: http://amor9.cn/2019/03/01/mvvm