Vue双向绑定:原理篇(详细)

前言

提起Vue的双向绑定数据响应式,很多人都知道是数据劫持发布者-订阅者模式,这里具体分析一下这两部分具体是怎么实现。

(最近看了相关资料,对原来不足的地方进行修改完善,还增加了更新的粒度和Vue3的Proxy内容)

什么是响应式

如果经常使用VueReact等框架开发,对响应式更新这个词并不陌生,简单来说就是视图会自动更新。

  • 原生JS实现就需要先找到DOM,再修改DOM

​ 比如

const clockDom = document.getElementById
clockDom.innerText = '修改文本内容'
  • 现在使用响应式框架,修改数据的时候,我们不需要关注DOM。在Vue中,this.xxx就可以实现页面数据更新。我们从数据劫持开始了解。

数据劫持

  • 数据劫持其实就是数据响应式基础,当获取数据或者修改数据的时候,能够被我们知道,然后触发响应操作,在Vue2中是通过Object.defineProperty()实现的。

比如,下面这个对象

let person = {
    name:'tom',
    age:15
}
  • 我们可以person.name获取到tom,但是我想在获取到tom的时候,还要进行其他操作,就要使用Object.defineProperty()
Object.defineProperty(person,'name',{
    get(){
        console.log('name属性被读取了...');
    },
    set(newVal){
        console.log('name属性被修改了...');
    }
})
  • 访问name属性的时候,会调用get方法,而修改name属性的时候,会调用set方法,可以去执行相应的操作。

  • 但是,这个时候访问被拦截了,我们获取不到name的属性值,所以需要在get方法里面return一个值,上面代码修改如下:

let person = {}
let val = 'tom'
Object.defineProperty(person,'name',{
    get(){
        console.log('name属性被读取了...');
        return val;
    },
    set(newVal){
        console.log('name属性被修改了...');
        val = newVal;
    }
})
  • 因为属性值可以由对象直接提供,不会单独声明,所以传入对象的时候,可以传入键和值。所以将val变量和defineProperty方法提取到一个函数中,就形成defineReactive函数
function defineReactive(obj, key, val) { // 这里相当于let val= val(传入的参数)
    Object.defineProperty(obj, key, {
        get() {
            console.log(`${key}属性被读取了...`);
            return val;
        },
        set(newVal) {
            console.log(`${key}属性被修改了...`);
            val = newVal;
        }
    })
}

至此,就完成了简单的数据劫持

发布者-订阅者模式

模式简介
  • 发布者和订阅者是互相不知道对方的存在的,发布者只需要把消息发送到订阅器里面,订阅者只管接受自己需要订阅的内容。

  • 主要有三个概念:发布者、订阅器、订阅者

发布者 Observer
  • Observer就是进行数据劫持,内部包含了defineReactive()函数。每次数据读或写时,我们能感知到数据被读取了或数据被改写了。要使数据变得“可观测”。
订阅器 dep
  • 收集依赖,内部维护了一个数组,用来记录该数据的所有Watcher,一旦数据发生变化就会发布通知所有Watcher
订阅者 Watcher
  • 作为依赖,会被dep收集。其实是个中介角色,数据发生变化时通知它,然后它去通知其他地方。

了解到这里,可能还会有些疑问:

  • 依赖是什么?怎么产生?这就需要知道解析器 Compile,它会对模板进行解产生Watcher。

  • 下面介绍一下双向绑定的整体流程,有一个更直观的了解。

整体流程

初始化data
  • 首先要知道每个组件都是一个Vue实例,也就是new Vue(),然后将data等数据传入进去。
new Vue({
	el: '#app', // 挂载点
	data: {  // 状态
	},
	methods: { // 方法
	},
});
  • 在Cass Vue中,可以在constructor中获取到data
 constructor(options) {
    this.$el = options.el //获取挂载点
    this.$data = options.data
 }
  • 然后对data.xx的一级属性进行劫持,方法是直接遍历data的key,使用Object.defineProperty方法对每个属性都进行劫持,返回对应的值data[key]

  • 这也是为什么可以在Vue实例中直接通过this.xxx访问到data中的数据的原因。

  • 使用过React就知道,React使用setdata()方法才能修改数据。

data变为响应式数据
  • data对象实例化一个Observer实例,绑定在data的ob属性上面,防止重复绑定

  • Observer实例中创建一个dep实例,用于收集依赖

  • Observer内部有Object.definedpropty,对属性进行劫持,修改成 gettersetter方法,用于依赖收集和派发更新

  • 如果data中包含数组,Vue重写了数组的7种原生方法,实现响应式

  • 如果data为多级对象,需要深度监听,递归data对象进行监听,data值更新的时候也需要进行判断深度监听

解析模板
  • 对节点和Vue指令进行编译

  • 编译过程中如果遇到{{}}v-bindv-model等指令使用到的时候,实例化Wacther

    (模板解析也另一大块内容,后面有机会再详细分析)

收集依赖
  • 编译过程中当data中的某个属性被读时(模板中使用了data数据),get 方法会被调用, 该属性的dep实例会收集该属性的Watcher,放置到dep维护的数组中。

至此,修改data中的数据,就能够影响模板中的数据

数据变化—视图更新
  • 修改data数据,Observer实例就会触发set方法,然后调用Dep 的 notify 方法,notify方法中又去调用所有依赖该属性的 Watcher 的 updater 方法,进行视图更新
视图更新—数据变化
  • 上面的流程主要是数据变化更新视图,要实现双向绑定,还需要进行事件监听,也就是注册监听用户对视图的修改事件,触发修改data数据的方法

  • 这也是v-model的实现双向绑定的原理

更新的粒度

更细的粒度更新
  • 假如有一个状态绑定着好多依赖,每个依赖表示一个具体的DOM节点,那么当这个状态发生变化时,向这个状态的所有依赖发送通知,让它们进行DOM更新操作。

  • 但是这样的代价是:粒度越细,每个状态所绑定的依赖就越多,依赖追踪的开销就越大。

中等粒度更新
  • Vue2.0开始,它引入了虚拟DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。
  • 这样状态变化后,会通知到组件,组件内部再使用虚拟DOM进行对比后进行重新渲染。
  • 这可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。

Vue3的Proxy数据劫持

  • 从前面的内容可以看出Vue2defineProperty方法和重写数组方法的形式存在很多不足,针对这些问题,Vue3使用Proxy来代替defineProperty进行数据劫持。
理解Proxy
  • Proxy是ES6新增的类,这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
  • Proxy 可以理解为:在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
特点
  • 能监听对象和数组

  • 新增的属性也能够被拦截,多层对象需要递归处理,对每一层对象进行代理

  • 除了能拦截访问修改操作之外,还能拦截 in操作符delete 操作符

// 不需要关心是哪个属性
new Proxy(data, {
  get(key) { },
  set(key, value) { },
})

// Vue2
Object.defineProperty(data, 'count', {
  get() {},
  set() {},
})

defineProperty的相同点就是对操作进行拦截,不同点是需要关心是哪个属性。

兼容性问题
  • 但是Proxy 是不能通过babel 转译的,因为在ES5中完全没有一种语法可以模拟出Proxy 的特性。因此Vue3.x 版本没有办法兼任一些低版本浏览器。

总结

  • 双向绑定:就是数据变化更新视图,视图变化更新数据

  • 数据响应式:通过对数据的访问和修改进行劫持,然后进行相应的操作,是实现双向绑定的基础。

  • Vue2数据劫持的缺点

    监听对象Object.defineProperty

    1. 只能监听对象,这个对象不是指引用类型,所以不包括数组
    2. 不能够对新增属性进行监听
    3. 不能监听数组内部的元素

    监听数组:Vue2重写了部分数组方法去实现,这部分和Object.defineProperty 就没有关系了。

    1. 直接通过索引修改数组无法触发更新

    2. 最好用**splice**方法对数组进行增删操作,因为splice在vue中重写了

    新增属性$set方法为对象的新增属性并进行拦截,如果是数组,$set内部调用splice方法

    删除属性$delete删除数据中的某个属性,并且能够侦测到数据的变化。

  • Vue3数据劫持使用Proxy

    对比Vue2的拦截方式更加全面,避免了需要考虑使用$set之类等情况

  • 发布者-订阅者模式

    Observer:发布者,内部对数据访问和修改进行拦截,发送通知给Dep

    Dep:订阅器,收集Watcher和通知Watcher

    Watcher:订阅者,通知视图更新

这一篇文章主要从原理方面进行说明,在这个基础上可以看我下一篇实现双向绑定的文章。
Vue双向绑定:实现篇

参考

Proxy - ECMAScript 6入门 (ruanyifeng.com)

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值