Vue 响应式原理模拟

253

目标

  • 模拟一个最小版本的vue
  • 响应式原理在面试中常问的问题
  • 实际项目中问题的原理层面解决
    • vue 新增成员是否是响应式的?
    • 给属性重新复制成对象是否是响应式的?
  • 为学习源码做铺垫

数据驱动

  1. 数据响应式:数据模型,是普通的javaScript对象,当我们修改数据时,视图也会跟着更新,避免了繁琐的DOM操作,提高开发效率。
  2. 双向绑定:
    • 数据改变,视图改变,数据也会跟着改变
    • 使用v-model在表单元数上创建数据双向绑定
  • 数据驱动是vue最独特的特性之一。开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图上的。

数据响应式核心原理

Vue 2.x
    // 模拟vue 的data
    const obj = {
         msg:"hello",
         count: 10
     }
     // 模拟vue 的实例
     const vm = {}
     proxyData(obj)
     function proxyData(data){
        Object.keys(data).forEach((key)=>{
            Object.defineProperty(vm,key,{
                // 可枚举(可遍历)
                enumerable: true,
                // 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义)
                configurable: true,
                get(){
                    console.log('get',data[key])
                    return data[key]
                },
                set(newValue){
                    console.log('set',newValue)
                    if(newValue === data[key]){
                        return 
                    }
                    data[key] = newValue
                }
            })
        })
        
    }
    vm.msg = "word"
Vue 3.x
  • 直接监听对象,而不是属性
  • ES6新增,IE 不支持,性能由浏览器优化
    // 模拟 Vue 中的data
     const obj = {
         msg:"hello",
         count: 10
     }
     // 模拟 Vue实例
     let vm = new Proxy(obj,{
        get(target,key){
           console.log(target, key)
           return target[key]
        },
        set(target,key,newValue){
            console.log(target,key,newValue)
            if(target[key] === newValue){
                return 
            }
            target[key] = newValue
            document.querySelector("#app").textContent = target[key]
        }
    })
    vm.msg = "word"
    console.log(vm.msg)

发布订阅模式和观察者模式

发布/订阅模式
  • 订阅者
  • 发布者
  • 信号中心
  • 假如有一个班级,孩子考完试之后,家长就可以像老师订阅孩子的学习成绩,并且家长也可以订阅多个孩子的成绩,老师发布学生的成绩。孩子就是这个信号中心,家长就是订阅者,老师就是发布者。这就是发布订阅者模式。
vue 的自定义事件
let vm = new Vue()
vm.$on('dataChange', () => {
console.log('dataChange')
})
vm.$on('dataChange', () => {
console.log('dataChange1')
})
vm.$emit('dataChange')
兄弟组件通信过程
// eventBus.js
// 事件中心
let eventHub = new Vue()
// ComponentA.vue
// 发布者
addTodo: function () {
// 发布消息(事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
}
// ComponentB.vue
// 订阅者
created: function () {
// 订阅消息(事件)
eventHub.$on('add-todo', this.addTodo)
}
模拟 vue自定义事件
        // 事件触发器
        class EventEmiter{
            constructor(){
                // { eventType: [ handler1, handler2 ] }
                this.subs = Object.create(null)
                console.log(this.subs)
            }
            //注册事件 (订阅消息)
            $on(eventType,handler){
              this.subs[eventType] = this.subs[eventType] || []
              this.subs[eventType].push(handler)
            }
            //触发事件 (发布消息)
            $emit(eventType){
               if(this.subs[eventType]){
                this.subs[eventType].forEach(handler => {
                    handler()
                });
               }
            }
        }
        // 测试
        const em = new EventEmiter()
        // 注册事件
        em.$on("click",()=>{
            console.log("click1")
        })
        em.$on("click",()=>{
            console.log("click2")
        })
        // 触发事件
        em.$emit("click")
观察者模式
  • 观察者模式与发布者的区别,观察者没有事件中心只有发布者和订阅者,并且发布者需要知道订阅者的存在。
  • 观察者(订阅者)
    • updata() 当事件发生时,具体要做的事情(在vue的响应式机制中,当数据发生变化的时候,调用观察者的update方法,update方法内部更新视图)
  • 目标(发布者)
    • subs数组:存储所有的观察者
    • addSub():添加观察者
    • notify():当事件发生,调用所有的观察者update()方法
  • 没有事件中心
        // 目标(发布者)
        class Dep {
          constructor(){
          //   存储所有的观察者
            this.subs = []
          }
          //   添加观察者
          addSub(sub){
            if(sub && sub.update){
                this.subs.push(sub)
            }
          }
        //   通知所有的观察者
          notify(){
              this.subs.forEach((sub)=>{
                sub.update()
              })
          }
        }
        // 观察者(订阅者)
        class Watcher{
            update(){
                console.log("update")
            }
        }
        // 测试
        let dep = new Dep()
        let watcher = new Watcher()
        dep.addSub(watcher)
        dep.notify()
总结
  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

vue 响应式原理模拟

整体分析

  • vue 的基本结构

  • 整体结构

  • Vue

    • 把data中的成员注入到vue实例,并把data中的成员转换成getter/setter
  • Observer

  • 能够对数据对象的所有属性进行监听,如果有变动可拿到最新值并通知Dep
  • Compiler
    • 解析每个元素中的指令、差值表达式,并替换成相应的数据
  • Dep
    • 添加观察者(Watcher),当数据变化通知所有观察者
  • Watcher
    • 数据变化更新视图

Vue

  • 功能
    • 负责接收初始化的参数(选项)
    • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
    • 负责调用 observer 监听 data 中所有属性的变化
    • 负责调用 compiler 解析指令/插值表达式
  • 结构
  • 代码
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(){
          return data[key]
        },
        set(newValue){
          if(newValue===data[key]){
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}

Observer

  • 功能
    • 负责把data选项中的属性转化成响应式数据
    • data 中的某个属性也是对象,把该属性转换成响应式数据
    • 数据变化发送通知
  • 结构
// 负责数据劫持
// 把$data中的成员转换成getter/setter
class Observer {
    constructor (data){
      this.walk(data)
    }
    // 判断数据是否是对象,如果不是就返回
    // 如果是对象,遍历对象的所有属性,转换成getter/setter
    walk(data){
       if(!data || typeof data !== 'object'){
           return 
       }
       //遍历data所有成员
       Object.keys(data).forEach((key)=>{
          this.defineReactive(data,key,data[key])
       })
    }
    // 定义响应式成员
    defineReactive(data,key,val){
       let that = this
        // 如果val是对象继续设置下面的属性为响应式
       that.walk(val)
       Object.defineProperty(data,key,{
           enumerable:true,
           configurable:true,
           get(){
               return val
           },
           set(newValue){
               if(val === newValue){
                   return 
               }
            //    如果newValue是对象继续设置newValue的成员为响应式
               val = newValue
               that.walk(val)
           }
       })
    }
}

Compiler

  • 功能
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图
  • 结构
  • 代码
class Compiler {
   constructor(vm){
        this.el = vm.$el
        this.vm = vm
        this.complie(this.el)
   }
    complie (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.complie(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(node,this.vm[key])
   }
   // 处理v-text 指令
   textUpdater(node,value){
     node.textContent = value
   }
   // 处理v-model 指令
   modelUpdater(node,value){
      node.value = value
    }
//    编译文本节点,处理插值表达式
   compileText(node){
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if(reg.test(value)){
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg,this.vm[key])
      //创建watcher对象,当数据改变更新视图
    //   new Watcher(this.vm,key,(newValue)=>{
    //      node.textContent = newValue
    //   })
    }
   }

//    判断元素属性是否是指令
   isDirective(attrName){
      return attrName.startsWith("v-")
   }
//    判断节点是否是文本节点
   isTextNode(node){
       return node.nodeType === 3
   }
//    判断节点是否是元素节点
   isElementNode(node){
       return node.nodeType === 1
   }
}

dep

  • 功能
    • 收集依赖,添加观察者(watcher)
    • 通知所有观察者
  • 结构
  • 代码
class Dep {
    constructor(){
        this.subs = []
    }
    // 添加观察者
    addSub (sub){
       if(sub && sub.update){
           this.subs.push(sub)
       }
    }
    // 发送通知
    notify(){
        this.subs.forEach(sub=>{
            sub.update()
        })
    }
}
  • 在 compiler.js 中收集依赖,发送通知
// defineReactive 中 
// 创建 dep 对象收集依赖 
const dep = new Dep() 
// getter 中 
// get 的过程中收集依赖 
Dep.target && dep.addSub(Dep.target) 
// setter 中 
// 当数据变化之后,发送通知 
dep.notify()

Watcher

  • 功能
    • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
    • 自身实例化的时候往 dep 对象中添加自己
  • 结构
  • 代码
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.oldValue = vm[key]
        Dep.target = null
    }
    update(){
        let newValue = this.vm[this.key]
        if(this.oldValue === newValue){
            return 
        }
        this.cb(newValue)
    }
}
  • 在 compiler.js 中为每一个指令/插值表达式创建 watcher 对象,监视数据的变化
 updateFn && updateFn.call(this,node,this.vm[key],key)
 // 处理v-text 指令
   textUpdater(node,value,key){
     node.textContent = value
     // 每一个指令中创建一个 watcher,观察数据的变化
     new Watcher(this.vm,key,(newValue)=>{
       node.textContent = newValue
     })
   }

视图更新数据

 // 处理v-model 指令
   modelUpdater(node,value,key){
      node.value = value
      new Watcher(this.vm,key,(newValue)=>{
         node.value = newValue
      })
      // 监听视图的变化 
      node.addEventListener('input', () => { this.vm[key] = node.value })
    }

总结

  • 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 实例更新视图