Vue源码学习(一): 响应式和依赖收集

233 阅读2分钟

前言

众所周知,vue是一个数据驱动型的框架,从数据改变到页面内容的变化的一个过程,视图是展现给用户看的,而数据是动态变化,这边的变化可能是后台接口返回数据的变化,也可能是因为页面操作的变化,总之,在数据的变化下,页面的元素会产生一定的响应,那么问题就来了,页面是如果知道数据发生变化了的,进行重新的渲染的

学习目标

  1. 阅读源码了解在vue中是如何实现变化追踪的
  2. 实现一个简单的响应式和依赖收集的简单vue
  3. 绘制vue中数据追踪到依赖收集的流程图

如何收集数据的变化

JS为我们提供了方法,监测对象的数据变化,使用Object.defineProperty(),该API可以定义数据的getter和setter,来获取数据何时被获取,何时被修改,所以在实现基础的Vue的数据追踪时候,我们需要对传入给vue实例的data里的每一个数据进行响应式的转化

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    // 将options中传入的data转为响应式数据
    let list = Object.keys(this._data)
    for(var i = 0; i < list.length; i++) {
      this.observe(list[i], this._data, this._data[list[i]], this)
    }

  }
  observe(key, data, val, vm) {
    // 转化每一个属性为响应式
    this.defineReactive(key, data, val,vm)
  }
  defineReactive(key, data, val, vm) {
    Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get: function() {
            console.log(this, 'get')
            return val
        },
        set: function(newVal) {
            console.log(this, 'set')
            // vm._data[key] = newVal
            val = newVal
            dep.notify()
        }
    })
  }
}

如何收集依赖

为每一个被转为响应式的数据定义一个依赖管理器或者说依赖收集器,管理每一个数据在被使用的地方,等到数据变化的时候就去更新依赖管理器中的每一个地方,这样就可以做到在数据变化页面变化

class Dep {
  constructor() {
    this.uid = new Date().getTime()
    this.subs = []
  }
  add(watcher) {
    this.subs.push(watcher)
  }
  notify() {
    this.subs.map(watcher => {
      watcher.update()
    })
  }
}

并且在对数据转化响应式的时候去初始化一个依赖管理器

什么是依赖

依赖在vue中理解为一个监听器,一个wathcer,wathcer具备了更新页面的能力,当一个属性被多处时候的时候,每一处都会有一个对应的依赖

class Watcher {
  constructor(vm) {
    this.vm = vm
    this.get()
  }
  update() {
    this.vm.render()
  }
  get() {
    window.target = this
  }
}

在哪里收集依赖

在Vue的源码中,实例被挂载的的时候就会去初始化watcher,初始化watcher是时候会传入当前使用符串路径所表示的值,通过该值触发数据的getter方法,这个时候去收集依赖,将依赖放入依赖管理器deps

如上:我们就做到了收集依赖,在数据发生变化的时候去更新依赖

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div v-text="text"></div>
</body>
<script>
    class Vue {
      constructor(options) {
        this.$options = options
        this._data = options.data
        // 将options中传入的data转为响应式数据
        let list = Object.keys(this._data)
        for(var i = 0; i < list.length; i++) {
          this.observe(list[i], this._data, this._data[list[i]], this)
        }
        this.watch(this, this.render.bind(this))
      }
      render() {
        var query = document.querySelectorAll('[v-text]')
        var key = query[0].getAttribute('v-text')
        let data = this._data[key]
        query[0].innerHTML = this._data[key]
      }
      watch(vm, render) {
        window.target = new Watcher(this)
        return render()
      }
      observe(key, data, val, vm) {
        this.defineReactive(key, data, val,vm)
      }
      defineReactive(key, data, val, vm) {
        let dep = new Dep()
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get: function() {
                console.log(this, 'get')
                if (window.target) {
                  dep.subs.push(window.target)
                  window.target = null
                }
                return val
            },
            set: function(newVal) {
                console.log(this, 'set')
                // vm._data[key] = newVal
                val = newVal
                dep.notify()
            }
        })
      }
    }
    
    class Dep {
      constructor() {
        this.uid = new Date().getTime()
        this.subs = []
      }
      add(watcher) {
        this.subs.push(watcher)
      }
      notify() {
        this.subs.map(watcher => {
          watcher.update()
        })
      }
    }

    class Watcher {
      constructor(vm) {
        this.vm = vm
        this.get()
      }
      update() {
        this.vm.render()
      }
      get() {
        window.target = this
      }
    }
    var demo = new Vue({
      el: '#demo',
      data: {
        text: '123123'
      }
    })
</script>
</html>

总结

实现响应式数据的重点核心是去掌握vue2.0使用的核心API,Object.defineProperty(),我们在实现了对一个对象的监听之后,在vue中对数据的监听做了另外的处理,主要是劫持修改了数组的原生方法,'pop','shift','unshift','splice','sort','reverse',所以在操作数据的时候我们不会直接修改某个下标值的具体值,而是通过原生方法或者是$set(使用了split)来操作,而对于依赖收集,我们主要要去了解,什么时候去收集依赖,收集了依赖之后放在哪里,什么时候去更新依赖这个流程,也可以学习到什么是发布者订阅者模式+数据劫持

依赖收集的主要流程

1.通过defineProperty来重新定义数据的getter,和setter

2.当页面被挂载或者数据被读取的时候去实例化一个watcher,在通过wathcer中的get方法触发数据的getter收集了依赖并存放在deps中

3.当数据发生变化的时候,触发了数据的setter,并去触发deps来更新依赖,是页面发生变化