Vue源码解读双向数据绑定小结

MVVM框架介绍

  • M (Model,数据模型层)

  • V (View,视图层,数据展示,html页面)

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

  • MVVM框架实现了数据的双向绑定
    - 当M层数据进行修改时,VM层会检测到变化,并且通知V层进行相应得修改
    - 修改V层则会通知M层数据进行修改
    - MVVM框架实现了视图与模型层得相互解耦

几种双向数据绑定的方式

  • 发布-订阅者模式,也叫观察者模式(backbone.js)
    - 它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。
    - 一般通过pub、sub的方式来实现数据和视图的绑定,但是用起来比较麻烦

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

    1. 解耦合
    2. 订阅者不用每次去查看公众号是否有新的文章
    3. 发布者不用关心谁订阅了它,只要给所有订阅者推送即可
  • 脏值检查(angular.js)
    - angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变
  • 数据劫持
    - vue.js 则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
    - Object.defineProperty不兼容IE8及以下的版本,所以vuejs不兼容ie8
    在这里插入图片描述

实现双向绑定需要三个模块

首先建立vue实例,此页代码仅演示文本节点的双向绑定

class Vue {
  constructor(option = {}) {
    this.$el = option.el
    this.$data = option.data

    new Observer(this.$data)  //数据劫持用,单纯渲染页面不用此行代码
    this.proxy(this.$data) 	// 最后第4步看此行代码
    if (this.$el) new Compile(this.$el, this)
  }

  proxy(data) {  // 最后第4步看此方法
    Object.keys(data).forEach(key => {
      console.log(this)
      
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key]
        },
        set(newValue) {
          if (data[key] == newValue) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}
  1. Compile

编译模块,实现data数据渲染至视图上,methods方法可以触发
解读:
(1)创建Vue构造函数,添加el,data,methods方法;
(2)获取el DOM对象下的全部子节点,放入内存中, 放入内存的dom节点为fragment碎片,但不存在与dom树中,由document.createDocumentFragment()创建,遍历fragment存放的节点,利用正则等表达式匹配并替换data数据,重新更新fragment的dom元素,并一起放进真实的dom树。这样做仅一次 重绘回流 即可渲染页面,大大减少了浏览器的负载,优化性能。代码如下

//编译模块
class Compile {
  constructor(el, vm) {
    this.el = typeof el === 'string'? document.getElementById('app'): el
    this.vm = vm

    if (this.el) {
      let fragment = this.nodeTfragment(this.el) //把所有节点放进fragment

      this.compile(fragment)
      this.el.appendChild(fragment)
    }
  }

  nodeTfragment(node) {
    let fragment = document.createDocumentFragment() //建立文档碎片
    let childNodes = [].slice.call(node.childNodes)  //[].slice.call 类数组用这一行代码转换成数组

    childNodes.forEach(element => {
      fragment.appendChild(element)
    });
    return fragment
  }

  compile(fragment) {
    let childNodes = [].slice.call(fragment.childNodes)
    childNodes.forEach( node => {
      if (node.nodeType === 3) {   //如果是文本节点
        let txt = node.textContent
        let reg = /\{\{(.+)\}\}/

        if (reg.test(txt)) {
          let expr = RegExp.$1  //取正则的第一组,也就是第一个出现的一对括号
          node.textContent = txt.replace(reg, this.vm.$data[expr])

          new Watch (this.vm, expr, newValue => {  //对每一个数据订阅 添加观察者
            node.textContent = txt.replace(reg, newValue)
          })
        }
      }
    })
  }
}
  1. Observer

劫持数据模块劫持数据模块
解读:
遍历数据添加劫持方法

//数据劫持
class Observer {
  constructor(data) {
    this.data = data
    this.walk(data)  //核心方法
  }

  walk(data) {
    if (!data || typeof data != 'object') return
    Object.keys(data).forEach(key => {

      let dep = new Dep() //此行代码请参考第3步
      let value = data[key]
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
          Dep.target && dep.addSub(Dep.target) //此行代码请参考第3步
          return value
        },
        set(newValue) {  //每当数据改变都要执行此方法
          if (value === newValue) return
          value = newValue
          dep.notify()  //此行代码请参考第3步,通知订阅者要改变
        }
      })
    })
  }
}
  1. Watcher 用于连接 1与2 的模块

在Compile模块为每个数据添加watcher构造函数(为订阅者模式),把各个watcher实例添加进一个数组集合,该数组称为Dep;至此,数据劫持与发布-订阅者模式结合,发布的意思为更改数据;
即数据改变则拿到了所有的watcher,通过回调函数改变数据,数据改变后则重新渲染页面。

//观察者(订阅者)
class Watch {
 constructor(vm, expr, callback) {
   this.vm = vm
   this.expr = expr
   this.callback = callback

   Dep.target = this
   this.olderValue = vm.$data[expr]  //每当获取data数据时,都要执行Observer 模块Object.defineProperty的get方法

   Dep.target = null
 }

 updata() {
   let oldValue = this.olderValue
   let newValue = vm.$data[this.expr]
   if (oldValue != newValue) {
     this.callback(newValue) 
   }
 }
}

//订阅者的操作
class Dep {
 constructor() {
   this.sub = []
 }

 addSub(watcher) {
   this.sub.push(watcher)
 }

 notify() {
   this.sub.forEach ((sub) => {
     sub.updata()
   })
 }
}
  1. 将所有data的方法直接挂在至vm实例

最后,通过proxy方法,利用Object.defineProperty方法依次添加至vm实例上请看第一段代码

  1. 结合下图总结
    在这里插入图片描述

(1) 绿色线 :实例化Vue后,通过Compile去解析所有的指令,能看到页面渲染的内容
(2)红色线:每解析一个指令,实例一个watcher,订阅数据的变化,逐个添加进Dep 构造函数的数组中
(3)蓝色线: 再通过Observer 劫持到了数据变化,通过Dep拿到了所有的订阅者,即Watcher,通知Watcher数据发生了改变,进而触发每个Watcher的update方法,更新视图。

此文档有些得不明白的地方请加v: zaq1312135000

猜你喜欢

转载自blog.csdn.net/weixin_41643133/article/details/87935594
今日推荐