Vue响应式原理

385 阅读7分钟

一:首先需要了解的东西

  1. 数据驱动
  2. 响应式的核心原理
  3. 发布订阅模式和观察者模式

二:数据驱动

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

三:数据响应式核心原理Vue2

  1. Object.defineProperty
  • 单个属性

    let data = { //模拟Vue中的data选项 msg = "Hello Word" } //模拟Vue的实例 let vm = {}

    //数据劫持,当访问设置vm中的成员的时候,做一些干预操作 Object.defineProperty(vm,"msg",{ //可枚举 enumerable: true, //可配置(可以使用delete删除,可以通过defineProperty重新定义) configurable: true, //当获取值的时候执行 get(){ console.log('get:',data.msg) return data.msg }, //当设置值的时候执行 set(newValue){ console.log('set:'newValue) if(newValue === data.msg){ return } data.mag = newValue doucment.querySelector(".app").textContent = data.msg } })

    vm.msg = '哈哈哈' console.log(vm.msg)

  • 多个属性

    let data = { //模拟Vue中的data msg:'Hello Word', count: 5 }

    //模拟Vue的实例 let vm = {} proxyData(data) function proxyData(data){ //遍历data中的所有属性 Object.keys(data).forEach( key => { //把data中的属性,转换成vm的setter/getter Object.defineProperty(vm , key , { //可枚举 enumrable: true, //可配置(可以使用delete删除,可以通过defineProperty重新定义) configurable: true, get(){ console.log('get:',key,data[key]) return data[key] }, set(newValue){ console.log('set:',key,newValue) if(newValue === data[key]){ return } data[key] = newValue //数据更改,更新Dom值 document.querySelector('#app').textContent = data[key] }, }) }) }

    vm.msg = 'Hello World' console.log(vm.msg)

三:数据响应式核心原理Vue3

  1. Proxy

  2. 直接监听对象,而非属性(因此处理多个属性是不需要遍历)

  3. ES6中新增加,IE不支持,新能由浏览器优化

    let data = { //模拟Vue中的选项 msg : 'Hello Word', count : 0 }

    //模拟Vue实例 let vm = new Proxy(data,{ //执行代理行为的的函数 //当访问vm的成员会执行 get(targer, key){ console.log("get", key , target[key]) rerurn target[key] }, //当设置vm的成员会执行 set(target, key , newValue){ console.log("set", key , newValue) if(target[key] === newValue){ return } target[key] = newValue document.querySelector('#app').textContent = target[key] }, })

    vm.msg = "Hello" console.log(vm.msg)

四:发布订阅模式

  1. 发布订阅模式(订阅者,发布者,信号中心)

  2. 我们假设,存在一个‘信号中心’,某个任务执行完成,就向信号中心“发布”一个信号,其他任务可以向信号中心“订阅”这个信号,从而知道什么时候自己可以开始执行.这就叫做“发布订阅者模式”

  3. vue自定义事件

    let vm = new Vue() //click事件(注册多个,所以后面使用数组存储),change事件 //对象形式存储事件{click:[fn1,fn2],change:[fn1,fn2]}

    //注册事件 //第一个参数为事件,第二个参数为回调函数 vm.on("dataChange",()=>console.log(dataChange))vm.on("dataChange", () => { console.log('dataChange') }) vm.on("dataChange", () => { console.log("dataChange2") })

    //触发事件 vm.$emit("dataChange")

  • 兄弟组件通信

    //事件中心 let eventHub = new Vue()

    //componentA.vue文件 //发布者 addToda:function(){ //发布消息(事件) eventHub.$emit('add-toda',{text:this.newTodaText}) this.newTodaText = '' }

    //componentB.vue文件 //订阅者 create:function(){ //订阅消息(事件) //第一个参数是事件,第二个参数是回调函数 event.$on('add-toda',this.addtoda) }

五:模拟发布订阅模式

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

//事件触发器
class EventEmitter {
  //记录所有事件以及事件对应的处理函数
  constructor(){
    //为什么是对象在发布订阅模式有说
    //{"click":[fn1,fn2]}
    this.subs = {}
  }
  //注册事件
  //第一个参数是事件类型,第二个参数是事件函数
  $on(eventType,hander){
     this.sub[eventType] = this.subs[eventType] || []
     this.subs[eventType].push(hander)
  }
  $emit(eventType){
     //如果存在
     if(this.subs[eventType]){
        this.subs[eventType].forEach( (hander) => {
          //直接调用函数
          hander()
        } )
     }
  }
}



//测试
let en = new EventEmitter()
//注册事件
em.$on("click",() => {
  console.log('click1')
})
em.$on("click",() => {
  console.log('click2')
})

//触发事件
em.$emit('click')

浏览器输出 click1 click2

六:观察者模式

  1. 观察者(订阅者)-Watcher

  2. update():当事件发生时,具体要做的事情

  3. 目标(发布者)-Dep

  4. subs数组:存储所有的观察者

  5. addSub():添加观察者

  6. notify:当事件发送,调用所有观察者的update()方法

  7. 没有事件中心(与发布订阅者不同)

  8. 模拟观察者模式

    class Dep { constructor(){ //记录所有订阅者,数组的形式 this.subs = [] } //添加订阅者方法 addSub(sub){ //判断对象是否存在并且具有update方法 if(sub && sub.update){ this.subs.push(sub) } } //事件发生通知所有的订阅者,调用所有订阅者的update方法 notify(){ this.subs.forEach((sub) => { //调用update方法 sub.update() }) } }

    //发布者-目标 //订阅者-观察者 class Watcher { //改方法由发布者调用更新视图或者其他方法 update(){ console.log('update') } }

    let dep = new Dep() let watcher = new Watcher() dep.addSub(watcher) dep.notify()

七:发布订阅者和观察者的区别

  1. 观察者模式是由具体目标调度,比如事件触发,Dep就会调去调用观察者方法,所以观察者模式的订阅者发之间是存在依赖的
  2. 发布/订阅模式由统一中心调用,因此发布者和订阅者不需要知道对方的存在

八:模拟Vue响应式原理-分析

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

八:Vue类

  1. 负责接收初始化的参数(选项)

  2. 负责把data中的属性注入到vue实例,转换成getter/setter

  3. 负责调用observer监听data中所有属性的变化

  4. 负责调用compiler解指令/插值表达式

    class Vue { constructor(options){ //1.通过属性保存选项的数据(下面几个属性是用来记录options传来的数据) //如果没有传值那么就是空对象 this.options=optionsthis.options = options || {} this.data = options.data || {} //elVue实例使用的根DOM元素this.el是Vue实例使用的根 DOM 元素 this.el = typeof options.el === "string" ? document.querySelector(options.el) : options.el //2.把data中的成员转换成getter和setter,注入到vue实例中 this._proxyData(this.data)//3.调用observer对象,监听数据变化newObserver(this.data) //3.调用observer对象,监听数据变化 new Observer(this.data) //4.调用compiler对象,解析指令和差值表达式 new Compiler(this) } //代理data属性 _proxyData(data){ //遍历data中的所有属性 Object.keys(data).forEach( (key) => { //把data的属性注入到Vue实例中 //在箭头函数中,此时this指向Vue实例,如果是function函数则指向window Object.defineProperty( this , key , { enumerable: true, configurable : true, get(){ return data[key] }, set(newValue){ if(newValue === data[key]){ return } data[key] = newValue } }) }) } }

九:Observer类

  1. 负责把data选项中的属性转换成响应式数据

  2. data中的某个属性也是对象,把改属性转换成响应式数据

  3. 数据变化发送通知

  4. observer类中的defineReactive方法传递第三个属性的原因:当访问data属性中的值的时候,首先会触发vue中的object.defineProperty中的set()方法,而vue中的object.defineProperty中的set()调用了data[key](this.$data),当调用了data[key]时又会触发observer中的get()方法,如果不使用第三个参数而直接和上面一样使用data[key]的方式会出现死循环

    class Observer { constructor(data){ //调用walk方法 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]) }) } //调用Object.defineproperty把属性转换为getter和setter definerReactive(obj, key, val){ let that = this //负责收集依赖,并发送通知(为每一个属性创建dep对象) let dep = new Dep() //当val是对象的时候(内部的属性没有转换为ser()和get()),所以需要调用walk转换 this.walk(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get(){ //收集依赖(target在watcher中实现) Dep.target && dep.addSub(Dep.target) return val }, set(newValue){ if(newValue === val){ return } val = newValue //给data属性重新赋值为对象也需要将新对象中的值作为set()和get() this.walk(newValue) //发生通知 dep.notify() }, }) } }

十:Compiler类

  1. 负责编译模板,解析指令/差值表达式

  2. 负责页面首次渲染

  3. 当数据变化后重新渲染视图

    class Compiler { //vm是vue实例 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 //text v-text v-model 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'] //如果前面有值才会执行后面 //key 是watcher方法需要的 updateFn && updateFn.call(this, node, this.vm[key], key) } //处理v-text指令 textUpdater(node, value, key){ node.textContent = value 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 = value }) //双向绑定 node.addEventListener('input',() => { this.vm[key] = node.value }) } //编译文本节点,处理差值表达式 compileText(node){ //匹配差值表达式 {{mag}} 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){ //判断是否以v-开头,是的话返回true,不是的话返回false return attrName.startsWith('v-') } //判断节点是否文本节点 isTextNode(node){ //nodeType为3是文本节点 rerurn node.nodeType === 3 } //判断节点是否元素节点 isElementNode(node){ //nodeType为1是元素节点 return node.nodeType === 1 } }

十一:Dep类

  1. 负责收集依赖,添加观察者(Watcher)

  2. 通知所有观察者

    class Dep{ costructor(){ //存储所有的观察者 this.subs = [] } //添加观察者 addSub(sub){ //判断sub是否存在并且存在update方法 if(sub && sub.update){ //添加至观察者 this.subs.push(sub) } } //发生通知 notify(){ this.subs.forEach((sub) => { //调用ypdate方法 sub.update() }) } }

十二:Watcher类

class Watcher{
  //第一个是Vue实例  第二个是值的key  第三个是回调函数
  constructor(vm, key, cb){
    this.vm = vm
    //data中的属性名
    this.key = key
    //回调函数负责更新视图
    this.cb = cb
    //把watcher对象记录到Dep类的静态属性target
    Dep.target = this
    //触发get方法,在get方法中调用addSub(访问这个属性就会触发observer)
    this.oldValue = vm[key]
    //设置为空是防止后续重复添加
    Dep.target = null
  }
  //当数据发生变化的时候跟新视图
  update(){
    let newValue = this.vm[this.key]
    if(this.oldValue === newValue){
      return
    }
    //不相等就更新
    this.cb(newValue)
  }
}