实现一个简单的vue响应式(理解vue的响应式原理)

291 阅读5分钟

上一篇:实现一个简单版的vueRouter【history模式+hsah模式】

模拟实现的有vm实例对象中的elel、data、$options、把data中的成员注入到vm实例对象中

实现模拟vue响应式的整体结构图:

  • Vue
    把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
  • Observer
    能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
  • Compiler
    解析每个元素中的指令/插值表达式,并替换成相应的数据
  • Dep
    添加观察者(watcher),当数据变化通知所有观察者
  • Watcher
    数据变化更新视图

项目的目录

1、vue.js文件内实现的功能

负责接收初始化的参数(选项)
负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
负责调用 observer 监听 data 中所有属性的变化
负责调用 compiler 解析指令/插值表达式
代码如下:

class Vue {

  constructor(options) {
    // 1、保存vue实例传递过来的数据
    this.$options = options // options是vue实例传递过来的对象
    this.$data = options.data // 传递过来的data数据
    // el 是字符串还是对象
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2、把this.$data中的成员转换成getter 和setter ,并且注入到vue实例中,使vue实例中有data里面的属性
    // 但是this.$data自身内部成员并没有实现在自身内部属性的gettter和setter,需要通过observer对象来实现
    this._proxyData(this.$data)
    // 3、调用observer对象,监视data数据的变化
    new Observer(this.$data)
    // 4、调用compiler对象,解析指令和差值表达式
    new Compiler(this) // this是vue实例对象
  }

  _proxyData (data) {
    // 遍历传递过来的data对象的数据,key是data对象中的属性名
    Object.keys(data).forEach((key) => {
      // 使用js的Object.defineProperty(),把数据注入到vue实例中,this就是vue实例
      Object.defineProperty(this, key, {
        configurable: true, // 可修改
        enumerable: true, // 可遍历
        // get 是 Object.defineProperty()内置的方法
        get () {
          return data[key]
        },
        // set 是 Object.defineProperty()内置的方法
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }

      })
    })


  }

}

2、oberver.js文件实现的功能

负责把 data 选项中的属性转换成响应式数据
data 中的某个属性也是对象,把该属性转换成响应式数据
数据变化发送通知
代码如下:


/**
 * Observer类:作用是把data对象里面的所有属性转换成getter和setter
 * data 是创建vue实例的时候,传递过来的对象里面的data,data也是个对象
 */

class Observer {
  // constructor 是创建实例的时候,立刻自动执行的
  constructor(data) {
    this.walk(data)
  }

  // 遍历data对象的所有属性
  // data 是创建vue实例的时候,传递过来的对象里面的data,data也是个对象
  walk (data) {
    // 判断data是否是对象
    if (!data || typeof data !== 'object') {
      return
    }
    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key])
    })

  }
  // 把data对象里面的所有属性转换成getter和setter
  defineReactive (obj, key, val) {
    // 解决this的指向问题
    let that = this

    // 为data中的每一个属性,创建dev对象,用来收集依赖和发送通知
    // 收集依赖:就是保存观察者
    let dep = new Dep()

    // 如果val也是对象,就把val内部的属性也转换成响应式数据,
    /// 也就是调用Object.defineProperty()的getter和setter
    that.walk(val)

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        // Dep.target就是观察者对象,调用dev对象的addSub方法,把观察者保存在dev对象内
        // target是Dep类的静态属性,但是却是在Watcher类中声明的
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        // 对vue实例初始化后,传入的data数据的值进行修改,由字符串变成对象
        // 也要把新赋值的对象内部的属性,转成响应式
        that.walk(newValue)
        // data里面的数据发生了变化,调用dev对象的notify方法,通知观察者去更新视图
        dep.notify()
      }
    })
  }

}

3、compiler.js文件实现的功能

负责编译模板,解析指令/插值表达式
负责页面的首次渲染
当数据变化后重新渲染视图
代码如下:


/**
 * 主要就是用来操作dom
 * 负责编译模板,解析指令/插值表达式
 * 负责页面的首次渲染
 * 当数据变化后重新渲染视图
 */

class Compiler {
  constructor(vm) {
    this.el = vm.$el // vue实例下的模板
    this.vm = vm // vm就是vue实例
    this.compile(this.el) // compiler实例对象创建后,会立即调用这个方法
  }

  // 编译模板,处理文本节点和元素节点
  compile (el) {
    let childNodes = el.childNodes // 是个伪数组
    Array.from(childNodes).forEach((node) => {
      if (this.isTextNode(node)) {
        // 编译文本节点,处理差值表达式{{}}
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 编译元素节点,处理指令
        this.compileElement(node)
      }

      // 递归调用compile,把所有的子节点都处理掉,也就是嵌套的节点都处理掉
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })

  }
  // 编译元素节点,处理指令,这里只处理v-text和v-model
  compileElement (node) {
    // console.dir(node.attributes)
    Array.from(node.attributes).forEach((attr) => {
      // console.log(attr.name)
      let attrName = attr.name // 指令属性名 v-model\v-text\type\v-count
      // 判断是否是vue指令
      if (this.isDirective(attrName)) {
        // v-text ==> text
        attrName = attrName.substr(2) // text\model\on:click\html
        let key = attr.value // 指令属性值 // msg\count\text\clickBtn()

        // 处理v-on指令
        if (attrName.startsWith('on')) {
          const event = attrName.replace('on:', ''); // 获取事件名
          // 事件更新
          this.onUpdater(node, key, event);
        } else {
          this.update(node, key, attrName);
        }
      }

    })
  }

  update (node, key, attrName) {
    let updateFn = this[attrName + 'Updater'] // textUpdater(){} 或者 modelUpdater(){}
    // this 是compiler对象
    updateFn && updateFn.call(this, node, this.vm[key], key) // updateFn的名字存在才会执行后面的函数

  }

  // 处理v-text指令
  textUpdater (node, value, key) {
    // console.log(node)
    node.textContent = value

    // 创建watcher对象,当数据改变去更新视图
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }

  // 处理v-html指令
  htmlUpdater (node, value, key) {
    // console.log(node)
    node.innerHTML = value

    // 创建watcher对象,当数据改变去更新视图
    // this.vm: vue的实例对象 key:data中的属性名称 ()=>{}: 回调函数,负责更新视图
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }

  // 处理v-model指令
  modelUpdater (node, value, key) {
    // console.log(node, value)
    node.value = value
    // console.log(node.value)
    // 创建watcher对象,当数据改变去更新视图
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })

    // 双向数据绑定
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }


  // 处理v-on指令
  onUpdater (node, key, event) {
    // console.log(node ,key, event)
    node.addEventListener(event, () => {
      // 判断函数名称是否有()
      if (key.indexOf('(') > 0 && key.indexOf(')') > 0) {
        this.vm.$options.methods[key.slice(0,-2)]()
      } else { 
        this.vm.$options.methods[key]()
      } 
    })
  }


  // 编译文本节点,处理差值表达式{{  msg }}
  compileText (node) {
    // console.dir(node)
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent // 获取文本节点内容:{{ msg }}

    if (reg.test(value)) {
      let key = RegExp.$1.trim() // 把差值表达式{{  msg }}中的msg提取出来
      // 把{{  msg }}替换成 msg对应的值,this.vm[key] 是vue实例对象内的msg
      node.textContent = value.replace(reg, this.vm[key])

      // 创建watcher对象,当数据改变去更新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }

  // 判断元素属性是否是vue指令
  isDirective (attrName) {
    return attrName.startsWith('v-')
  }

  // 判断节点是否是文本节点(元素节点1\属性节点2\文本节点3)
  isTextNode (node) {
    return node.nodeType === 3
  }

  // 判断节点是否是元素节点(元素节点1\属性节点2\文本节点3)
  isElementNode (node) {
    return node.nodeType === 1
  }
}

4、dep.js文件实现的功能 Dep(Dependency)

收集依赖,添加观察者(watcher)
通知所有观察者

代码如下:


/**
 * 收集依赖,添加所有观察者(watcher)
 * 通知观察者,数据发生改变了,去更新视图,通过watcher对象里面的update方法来实现
 */


class Dep {
  constructor() {
    this.subs = [] // 保存所有的观察者
  }

  // 把观察者添加到this.subs中去
  addSub (sub) {
    // 约定,每一个观察者对象中,必须要有一个update方法
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发送通知给观察者,告诉观察者,数据发生改变了,你要去更新视图
  notify () {
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}

5、watcher.js文件实现的功能 Dep(Dependency)

当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
自身实例化的时候往 dep 对象中添加自己

代码如下:

/**
 * 当data数据发生变化,dep对象中的notify方法内通知所有的watcher对象,去更新视图
 * Watcher类自身实例化的时候,向dep对象中addSub方法中添加自己(1、2)
 */

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm // vue的实例对象
    this.key = key // data中的属性名称
    this.cb = cb // 回调函数,负责更新视图

    // 1、把watcher对象记录到Dev这个类中的target属性中
    Dep.target = this // this 就是通过Watcher类实例化后的对象,也就是watcher对象
    // 2、触发observer对象中的get方法,在get方法内会调用dep对象中的addSub方法
    // this.oldValue = this.vm[this.key] //更新之前的页面数据
    // Dep.target = null
    this.oldValue = vm[key] //更新之前的页面数据
    Dep.target = null
  }
  // 当data中的数据发生变化的时候,去更新视图
  update () {
    const newValue = this.vm[this.key]
    if (newValue === this.oldValue) {
      return
    }
    this.cb(newValue)
  }
}

6、index.html文件内容:

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

<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>Mini Vue</title>
</head>

<body>
  <div id="app">
    <h1>标题:差值表达式</h1>
    <h3>{{ msg }}</h3>
    <h3>{{ count }}</h3>
    <h1>标题:v-text</h1>
    <div v-text="msg"></div>
    <h1>标题:v-model</h1>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
    <h1>标题:v-html 指令</h1>
    <div v-html="count"></div>
    <h1>标题:v-on 指令</h1>
    <button v-on:click="clickBtn">点击触发v-on命令方法名不带()</button>
    <button v-on:click="clickBtn2()">点击触发v-on命令方法名带()</button>
  </div>

  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>

  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello Vue1',
        count: 100,
        person: { name: 'zs' },
        dog: {},
      },
      methods: {
        clickBtn() {
          alert('改变了页面msg的值')
          vm.msg = 'clickBtn'
        },
        clickBtn2() {
          alert('改变了页面count的值')
          vm.count = '1000000000'
        }
      }
    })
    // vm.dog.name = 'dog'
    // vm.msg = { test: 'Hello' }
    // console.log(vm.msg)

  </script>
</body>

</html>

7、直接打开index.html文件就可以查看效果

总结

问题一:给属性重新赋值成对象,是否是响应式的? 是
问题二:给 Vue 实例新增一个成员是否是响应式的? 不是

通过下图回顾整体流程:

  • Vue 记录传入的选项,设置 data/data/el
    把 data 的成员注入到 Vue 实例
    负责调用 Observer 实现数据响应式处理(数据劫持)
    负责调用 Compiler 编译指令/插值表达式等

  • Observer 数据劫持
    -----负责把 data 中的成员转换成 getter/setter
    -----负责把多层属性转换成 getter/setter
    如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
    添加 Dep 和 Watcher 的依赖关系
    数据变化发送通知

  • Compiler 负责编译模板,解析指令/插值表达式
    负责页面的首次渲染过程
    当数据变化后重新渲染

  • Dep 收集依赖,添加订阅者(watcher)
    通知所有订阅者

  • Watcher 自身实例化的时候往dep对象中添加自己
    当数据变化dep通知所有的 Watcher 实例更新视图

参考

深入响应式原理
github.com/DMQ/mvvm