vue响应式原理

132 阅读7分钟

数据驱动

##数据响应式、双向绑定、数据驱动##

数据响应式

数据响应式-数据响应式中的数据指的是数据模型,数据模型仅仅是普通的js对象,当我们修改数据的时候,视图会进行更新,避免了繁琐的dom操作,提高了开发效率

双向绑定

数据、视图其中一个发生改变,另一个也随之改变 我们可以使用v-model在表单元素上创建双向绑定,包含了数据响应式

数据驱动-Vue最独特的特性之一

开发过程中只需要关注数据的本身,而不需要关心数据是如何渲染到视图上的

数据响应式的核心原理

vue2.0使用的是object.defineproperty遍历监听对象的每一个属性,这里暂时不展开,后面会详细说明 vue3.0使用的是proxy直接监听对象,而不是属性,是es6中新增的,ie不支持,性能由浏览器优化,比object.defineproperty要好的多

两者里面都有get和set方法,不过object.defineproperty中的get和set是不需要传参的get() set(newValue),因为他只处理一个属性的读取和写入,而proxy中是要处理对象中的所有属性,所以需要传入值 get(target,key) set(target,key,newValue)

发布订阅模式

发布订阅模式

订阅者
发布者
信号中心

vue中的自定义事件,以及node中的事件机制都是基于发布订阅模式的,

自定义事件的注册

  1. 首先创建一个vue实例
  2. 然后通过$on注册事件,同一个事件可以注册多个事件处理函数
通过代码模拟实现自定义事件的实现机制
 // 事件触发器
        class EventEmitter {
            constructor () {
                // {'click': [fn1, fn2]}
                this.subs = Object.create(null) // 记录所有的事件和事件对应的处理函数
            }
            // 注册事件
            $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()
                    });
                }
            }
        }

        // 测试
        let em =  new EventEmitter()
        em.$on('click', () => {
            console.log('click1');
        })
        em.$on('click', () => {
            console.log('click2');
        })

        em.$emit('click')

这段代码并没有体现出发布者和订阅者,只体现出了事件中心,也是实现了发布订阅模式,我们可以通过兄弟组件传值的方式来体会

观察者模式

vue的响应式机制中,使用了观察者模式

观察者模式和发布订阅模式的区别,没有事件中心,只有发布者和订阅者,并且发布者需要知道订阅者的存在

  • 观察者(订阅者)--Watcher
    • update(),当事件发生的时候,会调用update方法,内部就是更新视图,数据发生变化的就会触发
  • 目标(发布者)-- Dep
    • sub数组:存储所有的观察者
    • addSub():添加观察者
    • notify(): 当事件发生,调用所有观察者的update()方法
  • 没有事件中心
手写一个简单的不传参的观察者模式
// 发布者-目标
        class Dep {
            constructor() {
                // 记录所有的订阅者
                this.subs = []
            }

            addSub (sub) {  // 把订阅者添加到订阅者的数组中
                // sub 对象,存在且必须有update方法
                if(sub && sub.update){
                    this.subs.push(sub)
                }
            }

            notify () { // 当事件发生的时候,通知所有的订阅者,调用订阅者的update方法
                this.subs.forEach((sub) => {
                    sub.update()
                })
            }
        }
        // 订阅者-观察者
        class Watcher {
            update () {  // 当事件发生的时候,由发布者来调用,更新视图等操作
                console.log('update');
            }
        }
        // 测试
        let dep = new Dep() // 发布者对象
        let watcher = new Watcher() // 订阅者对象

        dep.addSub(watcher)
        // 当事件发生的时候调用notify方法
        dep.notify()

观察者模式和发布订阅模式的区别

观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的

发布订阅模式由统一调度中心调用,一次发布者和订阅者不需要知道对方存在

vue响应式原理简单实现

  • 功能
    • 负责接收初始化的参数(选项)
    • 负责把data中的属性注入到Vue实例,转换成getter/setter
    • 负责调用observer监听data中所有属性的变化
    • 负责调用compiler解析指令/差值表达式
class Vue {
    constructor(options) {
        // 通过属性保存选项中的数据
        this.$options = options || {}
        this.$data = options.data || {}     // $data中的setter是真正监视数据变化的地方
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
        // 是string,那么就是一个选择器,使用queryselector获取对应的dom对象,如果是dom对象那么就直接返回

        // 把data中的成员转换成setter和getter,注入到vue实例中
        this._proxyData(this.$data)
        // 调用observer对象,监听数据的变化
        // 调用compiler对象,解析指令和差值表达式
    }

    _proxyData(data) {  // 代理数据
        // 遍历data中的所有属性,这里没有考虑属性的递归
        Object.keys(data).forEach(key => {
            // 因为通过this来调用的_proxyData,所以函数内部的this就是构造函数的this,就是vue实例

            // 把data的属性注入到vue的实例中,所以第一个参数就是要定义属性的对象,就是this,就是vue实例
            Object.defineProperty(this,key,{
                enumerable: true,
                configurable: true,
                get () {
                    return data[key]
                },

                set (newValue) {
                    if(data[key] === newValue) {
                        return 
                    }
                    data[key] = newValue
                }
            })
        })
    }
}
Observer
  • 功能
    • 负责把data选项中的属性转换成响应式数据
    • data中的某个属性也是对象,把该属性转换成响应式数据
    • 数据变化发送通知
  • 结构 observer
    • walk(data)--遍历data中的所有属性
    • defineReactive(data, key, value)-定义响应式数据
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) {  // 核心作用就是调用Object.defineporperty把属性转换成getter和setter
        // 如果对象的key所对应的值(也就是val)是对象,就把其内部的属性转换成响应式的
        this.walk(val)
        let that = this
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get () {    // 这里会形成一个闭包,this.$data会引用到这里的get方法,get中又使用了val,所以val不会被回收
                return val  
            // 为什么不是obj[key],因为当我们实时访问obj[key]的时候就会触发defineReactive的get方法,get方法里面又调用了obj[key],这样的话形成了一个死循环
            },
            set (newValue) {
                if(newValue === val) {
                    return 
                }
                // 这里this指向data
                val = newValue
                that.walk(newValue) // 处理当前一个属性赋值成一个新的对象的时候,让这个对象内部的属性是响应式的
                // 发送通知
            }
        })
    }
}

同时在vue.js中调用observer对象,监听数据的变化

Compiler
  • 功能--一句话就是操作dom
    • 负责编译模板,解析指令和插值表达式
    • 负责页面的首次渲染
    • 当前数据变化后重新渲染视图
  • 结构
    • el-- vue构造函数传过来的options.el,已经转换成了dom对象,模板
    • vm-- vue实例
    • compile(el)--遍历dom对象的所有节点,文本节点解析插值表达式,元素节点解析指令
    • compileElement(node) 解析指令
    • compileText(node) 解析插值表达式
    • isDirective(attrName) compileElement中调用,判断当前属性是否是指令
    • isTextNode(node) 判断是不是文本节点
    • isElementNode(node) 判断是不是元素节点
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) {
        // console.log(node.attributes);
        // 遍历属性节点
        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    // v-text=msg 的msg
                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) {
        console.log(node);
        // 正则匹配
        let reg = /\{\{(.+?)\}\}/ // 可以匹配到插值表达式
        let value = node.textContent // {{ text }}
        if(reg.test(value)) {   // 如果是一个插值表达式的话
            let key = RegExp.$1.trim() // 获取插值表达式的变量名
            node.textContent = value.replace(reg, this.vm[key])
        }
    }
    // 判断元素属性是否是指令
    isDirective (attrName) {
        return attrName.startsWith('v-')
    }
    // 判断节点是否是文本节点
    isTextNode (node) {
        return node.nodeType === 3
    }
    // 判断节点是否是元素节点
    isElementNode (node) {
        return node.nodeType === 1
    }
}

同时在vue.js中调用compiler对象,解析指令和差值表达式

Dep
  • 功能-每一个响应式属性都会创建一个dep对象,负责收集所有依赖该属性的地方,所有依赖该属性的位置都会创建一个watcher对象,所以dep收集的就是依赖于该属性的watcher对象,当属性发生变化的时候,通过dep的notify发送通知,调用watcher的update方法
    • 收集依赖添加观察者
    • 通知所有的观察者
  • 结构
    • subs-数组,存储dep中的所有的watcher
    • addSub(sub)-添加watcher
    • notify-通知
class Dep {
    constructor() {
        this.subs = []    // 存储所有的观察者
    }

    // 添加观察者
    addSub (sub) {
        if(sub && sub.update) {
            this.subs.push(sub)
        }
    }
    // 发送通知
    notify () {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}
Watcher

  • 功能
    • 当数据发生变化触发依赖,dep通知所有的Watcher实例更新视图
    • 当去自身实例化创建一个watcher对象的时候,内部需要把自己添加到watcher对象的subs数组中(往dep对象中添加自己)
  • 结构
    • update()--更新视图
    • cb--callback指明如何更新视图
    • key--data中的属性名称,结合vm获取属性的值
    • oldValue--更新前的视图
    • vm--vue实例
// 作用,创建watcher对象的时候,需要把watcher对象放到
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]  // vm[key]这么访问的时候,是可以直接触发get方法的
        Dep.target = null // 防止重复添加
    }

    // 当数据发生变化的时候,更新视图
    update () {
        let newValue = this.vm[this.key]
        if(this.oldValue === newValue) { // 没有变化就什么都不做
            return 
        }
        this.cb(newValue)   // 不等的话,更新视图,需要新值,所以需要参数newValue
    }

然后同时需要在compiler中,所有视图中依赖数据的位置,创建一个watcher对象,当数据发生改变时,更新视图