Vue之深刻响应式原理

理解Vue响应式原理,只需经过解决如下几个问题便可

  • 如何实现数据劫持?
  • 如何实现数据代理?(如何对this.xxx的访问代理到this.data.xxx上?)
  • 如何实现数据编译?
  • 如何实现发布订阅模式?
  • 如何实现更新视图?(如何监听数据的读写操做?如何实现数据修改DOM更新?)
  • 如何实现双向数据绑定?
  • 如何实现依赖缓存?
  • template改变的时候,如何清理依赖项集合?eg:v-if和组件销毁

前提

<div id="app">
  <div id="app">
    <p>{{ a.a }}</p>
    <span>{{ b }}</span>
</div>
</div>
<script>
const vm = new Mvvm({
    el: '#app',
    data: {
      a: {
        a: '我是a'
      },
      b: '我是b'
    }
  })
</script>

问题:如何实现数据劫持

答:经过Object.defineProperty()方法,对data中的属性,在访问或者修改对象的其中某个属性时,经过一段代码拦截这个行为,进行额外的操做或者修改返回结果node

// TODO:1.定义Mvvm类
function Mvvm(options={}){
    // TODO:this表明的是fade实例对象
    // TODO:将全部属性挂载到$options
    this.$options = options
    var data = this._data = this.options.data
    // TODO:调用数据劫持
    observe(data)
}

// TODO:3.观察者
function Observe(obj){
  for (let key in obj) {
    let val = obj[key]
    // TODO:深度劫持
    observe(val)
    
    Object.defineProperty(obj,key,{
      enumerable : true,
      get(){
        return val
      },
      set(newVal){
        if (newVal === val) return
        val = newVal
        // TODO:深度劫持
        observe(newVal)
      }
    })
  }
}

// TODO:2.数据劫持-使每一个对象都具备get和set方法
function observe(vmData){
  if (typeof data !== 'object') return
  return new Observe(vmData)
}

问题:如何实现数据代理-如何对this.xxx的访问代理到this.data.xxx上?

答:对于每一个data上的属性,都在app上作一个代理,实际操做的是this.data
实现的代码以下:数组

function Mvvm(options = {}) {
  // TODO:this表明的是zhufeng实例对象
  // TODO:将全部属性挂载到$options
  this.$options = options
  //this._data
  var data = this._data = this.$options.data
  observe(data) 
  // TODO:4.数据代理
  for (let key in data) {
    Object.defineProperty(this,key,{
      enumerable : true,
      get(){
        return this._data[key]
      },
      set(newVal){
        this._data[key] = newVal
      }
    })
  }
}

问题:如何实现数据编译

答:经过获取vm管理DOM的根节点,让其在内存中完成相关的正则匹配工做,替换DOM中的文本节点缓存

// TODO:5数据编译
function Compile(el,vm){
  vm.$el = document.querySelector(el)
  let fragment = document.createDocumentFragment()
  while (child = vm.$el.firstChild) {
    fragment.appendChild(child)
  }

  replace(fragment)

  // TODO:6.数据替换
  function replace(frag){
    Array.from(frag.childNodes).forEach(function (node) {
      let text = node.textContent
      let regExp = /\{\{(.*)\}\}/
      if (node.nodeType === 1 && regExp.test(text)) {
        let arr = RegExp.$1.trim().split('.')
        let val = vm
        arr.forEach(function (k) {
          val = val[k]
        })
        node.textContent = text.replace(regExp,val).trim()
      }
      if (node.childNodes && node.childNodes.length) {
        replace(node)
      }
    })
  }
  vm.$el.appendChild(fragment)
}
function Mvvm(options = {}) {
  ....
  // TODO:进行编译
  new Compile(optionns.el,this)
}

问题:如何实现发布订阅模式

答:发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行app

// TODO:8.发布订阅模式
// TODO:桥梁
function Dep(){ // 桥梁
  this.subs = [] // 订阅事件池
}
// TODO:进行订阅的方法(往里面扔函数)
Dep.property.addSub = function (sub) {  //sub就是watcher
  this.subs.push(sub)
}
// TODO:进行发布/通知的方法(让函数的每一项一次执行)
Dep.prototype.notify = function () {
  this.subs.forEach(sub => sub.update())//绑定的事件,都有一个update属性
}

// TODO:订阅者
function Watcher(fn){ //Watcher是一个类,经过这个类建立的实例都拥有update方法
  this.fn = fn
}

Watcher.prototype.update = function () { //调用fn()
  this.fn()
}

问题:如何更新视图-当数据改变须要从新刷新视图

答:如今咱们要订阅一个事件,当数据改变须要从新刷新视图,这就须要在replace替换的逻辑里来处理
经过new Watcher把数据订阅一下,数据一变就执行改变内容的操做函数

  • 监听变化
// TODO:6.数据替换
  function replace(frag) {
    ...
    node.textContent = text.replace(regExp, val).trim()
    
    // TODO:监听变化
    new Watcher(vm,RegExp.$1,function (newVal) {
        node.textContent = text.replace(regExp,newVal).trim()
    })
    
    if (node.childNodes && node.childNodes.length) {
        replace(node)
    }
    ...
  }
  • 重写Watcher构造函数
// TODO:订阅者
function Watcher(vm,exp,fn){ //Watcher是一个类,经过这个类建立的实例都拥有update方法
  this.fn = fn
  this.vm = vm
  this.exp = exp

  Dep.target = this
  let arr = exp.trim().split('.')
  let val = vm
  arr.forEach(function (key) {
    val = val[key]
  })
  Dep.target = null //  // 上面获取val[key]的时候会调用get方法, 所以使用完毕以后须要把该属性置为null
}
  • 重写数据劫持get和set方法this

    • 解:当获取值的时候就会自动调用get方法,因而咱们去找一下数据劫持那里的get方法
// TODO:3.观察者
function Observe(obj){
  // TODO:建立桥梁
  let dep = new Dep()
  for (let key in obj) {
    let val = obj[key]
    // TODO:深度劫持
    observe(val)

    Object.defineProperty(obj,key,{
      enumerable : true,
      get(){
        // TODO:将watcher添加到订阅事件中 [watcher]
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set(newVal){ // 更改值得时候
        if (newVal === val) return // 设置的值和之前的是同样的东西
        val = newVal // 若是之后在获取值的时候将刚才设置的值丢回去
        // TODO:深度劫持
        observe(newVal)
        // TODO:执行update方法
        dep.notify()
      }
    })
  }
}
  • 修改watcher的update方法spa

    • 解:当set修改值的时候执行了dep.notify方法,这个方法是执行watcher的update方法,那么咱们再对update进行修改一下
Watcher.prototype.update = function () { //调用fn()
  this.fn()
  // notify的时候值已经更改了
  // 再经过vm, exp来获取新的值
  let arr = this.exp.trim().split('.')
  let val = this.vm
  arr.forEach(function(key){
    val = val[key]
  })
  this.fn(val) // 将每次拿到的新值去替换{{}}的内容便可
}

问题:双向数据绑定

未完待续...prototype

相关文章
相关标签/搜索