Vue响应式原理

80 阅读2分钟

1. index.html文件

<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">
</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 world',
      count:10,
      person:{
        name:'zhaofeng'
      }
    }
  })
  // 如果data中的某个属性是对象, 我们会把这个对象内部的属性转化为响应式。
  // console.log(vm.person)
  // 当给data中的属性重新复制一个新对象的时候, 也会被转化为响应式的。
  // vm.msg = {test:'1111'}
</script>

2. vue.js文件

2.1 功能

  • 负责接收初始化的参数()选项
  • 负责把data中的属性注入到vue实例,转换成getter/setter
  • 调用Observer监听data中所有属性的变化
  • 用Compiler解析差值表达式和指令

2.2 类图

屏幕快照 2021-03-30 下午7.49.43.png

2.3 代码

class Vue{
  constructor(options){
    // 1. 通过属性保存选项的数据
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string'? document.querySelector(options.el) : options.el
    // 2. 把data中的成员转换成getter/setter, 注入到vue实例中
    this._proxyData(this.$data)
    // 3. 调用observer对象, 监听数据的变化
    new Observer(this.$data)
    // 4. 调用compiler对象,解析指令和差值表达式
    new Compiler(this)
  }
  _proxyData (data) {
    // 遍历data中的所有属性
    Object.keys(data).forEach(key => {
      // 将data的属性注入到vue实例中
      Object.defineProperty(this, key, {
        enumerable:true,
        configurable:true,
        get(){
          console.log(data[key])
          return data[key]
        },
        set(newVal){
          if(data[key] === newVal){
            return
          }
          data[key] = newVal
        }
      })

    })
  }
}

3. observer.js文件

3.1 功能

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

3.2 类图

屏幕快照 2021-03-30 下午7.52.51.png

3.3 代码

class Observer{
  constructor(data){
    this.walk(data)
  }
  walk(data){
    // 1. 判断data是否是对象
    if(!data || typeof data !== 'object') return
    // 2. 遍历data的所有属性
    Object.keys(data).forEach(key=>{
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive(obj, key, val){
    let that = this
    // 负责收集依赖并发送通知
    let dep = new Dep()
    // 如果val是对象, 把val内部的属性转换成响应式数据
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable:true,
      configurable:true,
      get(){
        // 搜集依赖
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set(newVal){
        if(newVal === val) return;
        val = newVal
        // 
        that.walk(newVal)
        // 发送通知
        dep.notify()
      }
    })
  }
}

4. compile.js文件

4.1 功能

  • 负责编译模版,解析指令/差值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图

4.2 类图

屏幕快照 2021-03-30 下午8.09.42.png

4.3 代码

class Compiler {
  constructor(vm){
    this.el = vm.$el,
    this.vm = vm
    this.compile(this.el)
  }
  // 编译模版, 处理文本节点和元素节点
  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)
      }

      // 判断node节点,是否有自节点,如果有自节点,递归调用compile
      if(node.childNodes && node.childNodes.length){
        this.compile(node)
      }
    })
  }
  // 编译元素节点, 处理指令
  compileElement(node){
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
    // 判断是否是指令
      let attrName = attr.name
      if(this.isDirective(attrName)){
        // v-text --> text
        attrName = attrName.substr(2)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  update(node, key, attrName){
    let updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }

  // 处理v-text指令
  textUpdater(node, value, key){
    node.textContent = value
    // 创建watcher对象
    new Watcher(this.vm, key, (newVal)=>{
      node.textContent = newVal
    })
  }
  // 处理v-model
  modelUpdater(node, value, key){
    node.value = value
    // 创建watcher对象
    new Watcher(this.vm, key, (newVal)=>{
      node.value = newVal
    })

    // 双向绑定
    node.addEventListener('input', (data) => {
      console.log(222, data.target.value)
      this.vm[key] = node.value
    })
  }

  // 编译文本节点, 处理差值表达式
  compileText(node){
    // 差值表达式{{ msg }} 
    let reg = /\{\{(.+?)\}\}/
    let val = node.textContent
    if(reg.test(val)){
      let key = RegExp.$1.trim()
      node.textContent = val.replace(reg, this.vm[key])

      // 创建watcher对象
      new Watcher(this.vm, key, (newVal)=>{
        node.textContent = newVal
      })
    }
  }
  // 判断元素属性是否是指令
  isDirective (attrName){
    return attrName.startsWith('v-')
  }
  // 判断节点是否是文本节点
  isTextNode(node){
    return node.nodeType === 3
  }
  // 判断节点是否是元素节点
  isElementNode(node){
    return node.nodeType === 1
  }
}

5. dep.js文件

WechatIMG16.jpeg

5.1 功能

  • 收集依赖, 添加观察者
  • 通知所有观察者

5.2 类图

屏幕快照 2021-03-30 下午8.11.20.png

5.3 代码

class Dep {
  constructor(){
    // subs数据存储所有观察者
    this.subs = []
  }
  // 添加观察者
  addSub (sub){
    if(sub && sub.update){
      this.subs.push(sub)
    }
  }
  // 通知观察者
  notify(){
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

6. watcher.js文件

WechatIMG17.jpeg

6.1 功能

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

6.2 类图

屏幕快照 2021-03-30 下午8.12.20.png

6.3 代码

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

    // 把watcher对象记录到dep类的静态属性target
    Dep.target = this
    // 触发get方法, 在get方法中调用addSub
    this.oldVal = vm[key]
    Dep.target = null

  }
  // 当数据发生变化时更新视图
  update(){
    let newVal = this.vm[this.key]
    if(this.oldVal === newVal){
      return
    }
    this.cb(newVal)
  }
}