理解Vue响应式原理,实现一个Mini vue

MVVM(Model-View-ViewModel)是一种程序的架构设计,相比于MVC,ViewModel充当了控制层(Control),Vuejs的核心就是实现这个ViewModel,用一张图表示,就是下面这样

在JS中,Model可以看做是Object对象,View就是HTML网页。在传统开发中,当数据发生变化时,开发者需要手动更新DOM,进而改变视图层。而在Vue中,一切视图的改变都是基于数据的变动,也就是说,开发者不需要再操作DOM。ViewModel的核心就是建立视图和数据之间的联系,做到数据驱动视图的变化。所谓双向数据绑定,不过就是数据驱动视图变化,视图驱动数据变化

Vue实现ViewModel是通过Object.defineProperty实现的,关于defineProperty的基本语法可移步至此。Vue在实例化的过程中,会遍历data中的所有属性,然后通过Object.defineProperty把这些属性全部转为 getter/setter,内部用一个Observer对象监听数据的设置和读取,这个过程也叫作数据劫持。每一个Vue实例都会有一个Watcher(订阅者)对象,在模板编译过程中,需要插入数据的地方都会用getter去访问data属性,比如v-model或者v-text等等,Watcher会把用到的data属性记为依赖,在内部用Dep对象统一管理这些依赖,这样就建立了视图和数据之间的联系。当渲染视图的数据依赖发生变化时,setter函数会被调用,Watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。

用一张图表示就是下面这样

下面实现一个简版的Vue.js

<!DOCTYPE html>
<html lang="en">

<head>
  
  <title>Mini Vue.js</title>
</head>
<div id="app"></div>
<body>
  <script>
  // vue 实例,接收一个 option(Object) 参数
  class Vue {
    constructor(options = {}) {
      this.$options = options
      // 简化了对data的处理
      let data = this._data = this.$options.data
      // 遍历data, 将所有data最外层属性代理到Vue实例上
      // this.key 就能访问到 data 对象中的数据
      Object.keys(data).forEach(key => this._proxy(key))
      // 监听数据
      observe(data)

      // 获取dom节点
      this.$el = document.querySelector(options.el)


      // 渲染DOM
      this._randerDom()
    }
    _randerDom(val) {
      // 解析字符串模版,为了简单直接用了innerHTML
      if (this.$el && this.$options && this.$options.template) {
        this.$el.innerHTML = this.$options.template(this._data)
      }
    }
    // 暴露一个在vue实例外使用订阅者的接口,在实例内部主要在指令中使用
    // 指令的实现需要编写complie函数,为了简单,不写该函数也不影响理解vue原理
    $watch(expOrFn, cb) {
      // 添加订阅者
      new Watcher(this, expOrFn, cb)
    }
    _proxy(key) {
      // 把这data属性全部转为 getter/setter。
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get: () => this._data[key],
        set: (val) => {
          this._data[key] = val
        }
      })
    }
  }

  function observe(value) {
    // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
    if (!value || typeof value !== 'object') {
      return
    }
    return new Observer(value)
  }

  /*
   * Observer.js
   */
  class Observer {
    constructor(value) {
      this.value = value
      this.walk(value)
    }
    walk(value) {
      // 遍历传入的data, 将所有data的属性添加set&get
      Object.keys(value).forEach(key => this.convert(key, value[key]))
    }
    convert(key, val) {
      // 添加set&get方法
      defineReactive(this.value, key, val)
    }
  }

  function defineReactive(obj, key, val) {
    var dep = new Dep()
    // 给传入的data内部对象递归的调用observe,来实现深度监听
    var chlidOb = observe(val)

    Object.defineProperty(obj, key, {
      enumerable: true, // 可枚举
      configurable: true, // 可修改
      get: () => {
        // Watcher实例在实例化过程中,会为Dep添加一个target属性,在读取data中的某个属性,会触发当前get方法。
        // 如果Dep类存在target属性,将订阅者添加到dep实例的subs数组中
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return val
      },
      set: (newVal) => {
        // 在设置data中的某个属性,会触发当前set方法。
        if (val === newVal) return
        val = newVal
        // 对新值进行监听
        chlidOb = observe(newVal)
        // 通知所有订阅者,数值被改变了
        dep.notify()
      }
    })
  }

  // 订阅者管理员,管理所有订阅者队列
  class Dep {
    constructor() {
      this.subs = [] // 订阅者队列
    }
    addSub(sub) {
      this.subs.push(sub) // 添加订阅者
    }
    notify() {
      // 当改变data中的属性值时,会通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
      this.subs.forEach((sub) => sub.update())
    }
  }

  /*
   * Watcher订阅者
   */
  class Watcher {
    constructor(vm, expOrFn, cb) {
      this.vm = vm // 被订阅的数据一定来自于当前Vue实例
      this.cb = cb // 当数据更新时需要执行的回调函数
      this.expOrFn = expOrFn // 被监听的数据(表达式或函数)
      this.oldVal = this.get() // 维护更新之前的数据
    }
    // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
    update() {
      this.vm._randerDom() // 检测的数据变动后,更新dom 
      this.run()
    }
    run() {
      const newVal = this.get()
      if (newVal !== this.oldVal) {
        this.cb.call(this.vm, this.oldVal, newVal)
        this.oldVal = newVal;
      }
    }
    get() {
      // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
      Dep.target = this
      const val = this.vm._data[this.expOrFn]
      // 置空,用于下一个Watcher使用
      Dep.target = null
      return val;
    }
  }


  let app = new Vue({
    el: '#app',
    template(data) {
      return `
        <p>${data.title}</p>`
    },
    data: {
      'title': 'Hello Vue'
    }
  });
  app.$watch('title', function(oldVal, newVal) {
    console.log('oldVal:' + oldVal)
    console.log('newVal:' + newVal)
  })
  </script>
</body>
</html>

不到200行代码,实现了Vue的核心功能,包括依赖收集、数据劫持、视图渲染。当然,由于是简版的Vue,只是为了便于理解Vue的原理,所以很多功能都剔除了。示例可以直接在chrome下运行,不需要babel编译。

运行效果

优秀文章首发于聚享小站,欢迎关注!
原文地址:https://www.cnblogs.com/yesyes/p/15356913.html