vue2和vue3的数据绑定原理

vue2 数据劫持

核心方法: Object.defineProperty

H5方法,所以不兼容IE8以下
let obj = {},
value = 1
Object.defineProperty(obj,'a',{
    get() {
        console.log('这里监听到了数据获取')
        return value
    },
    set(newValue, value) {
        if(newValue !== value) {
            value = newValue
            console.log('这里监听到了数据更改')
        }
    }
})
console.log(obj.a) // 这里监听到了数据获取   1
obj.a = 2 // 这里监听到了数据更改

所以再初始化Vue时,对data进行了劫持,每个属性都通过Object.defineProperty变成了getter/setter,一旦数据发生改变,就会触发set,然后去更新view

let data = {
    name: 'nike',
    info: {
        age: 21
    }
}
Object.keys(data).forEach(key=>{
    defineProperty(data, key, data[key])
})
function defineProperty(target, key, value) {
    Object.defineProperty(target,key,{
        get() {
            console.log('这里监听到了数据获取')
            return value
        },
        set(newValue, value) {
            if(newValue !== value) {
                value = newValue
                console.log('这里监听到了数据更改')
            }
        }
    })
}
data.name = 'tom' // 这里监听到了数据更改
data.info.age = 22 // 这里监听到了数据获取(这里没有触发更改,get和set相对立,总要触发一个)
data.info = {age:22} // 这里监听到了数据更改

至于data.info.age = 22为什么没有触发set呢,因为上面的逻辑仅仅是对data下面的一层进行了劫持,而再往下的改变是监听不到的,所以就引出了两外一个东西

  1. Watch

    watch: {
        info: {
            handler(){},
            deep: true
        }
    }

    此处的deep表示深度监听,这样就会对该属性递归遍历并逐一劫持,类似于深拷贝

  2. vue.$set
    从字面意思看,就是手动触发set

Object.defineProperty有一个bug,就是无法监听数组(因为数组没key

let data = {
    name: [],
}
Object.keys(data).forEach(key=>{
    defineProperty(data, key, data[key])
})
function defineProperty(target, key, value) {
    Object.defineProperty(target,key,{
        get() {
            console.log('这里监听到了数据获取')
            return value
        },
        set(newValue, value) {
            if(newValue !== value) {
                value = newValue
                console.log('这里监听到了数据更改')
            }
        }
    })
}
data.name.push('nike') // 这里监听到了数据获取

为了解决这个问题,Vue对数组的方法进行了重写

// 重写push
let oldPush = Array.prototype.push
Array.prototype.push = function() {
    console.log('这里触发view更新')
    oldPush.call(this,...arguments)
}

vue3 数据劫持

很明显,Object.defineProperty有一些缺陷,不仅要遍历data逐个劫持,还不能监听到数组的改变,所以VUE3使用了ES6Proxy
Proxy字面理解代理,就跟经纪人一样,一旦与某个明星data绑定,那么这个明星想干嘛就得先通过代理

let data = {
    msg: {
        a: 10
    },
    arr: [1, 2, 3]
}
let handler = {
    get(target, key) {
        // 懒监听,去获取的时候才监听对象里面的对象,而不是直接递归循环监听
        console.log('获取key: ' + key)
        if (typeof target[key] === 'object' && target[key] !== null) {
            return new Proxy(target[key], handler)
        }
        return Reflect.get(target, key)
    },
    set(target, key, value) {
        let oldValue = target[key]
        console.log('更新key: ' + key)
        if (oldValue !== value) {
            // 通知view更新
        }
        return Reflect.set(target, key, value)
    }
}
let proxy = new Proxy(data, handler)
proxy.arr.push(4)

输出结果
image.png
为什么每次都有length,其实Proxy的监听数组实现是把数组变成了一个类数组对象而已

let arr = {
    '0': 1,
    '1': 2,
    length: 2
}

Proxy除了get,set还有deleteProperty/apply/getOwnPropertyDescriptor等等12个方法,恰好与Reflect对应,所以在这些方法里面可以实现拦截器

set(target, key, value) {
    if(key[0] === '_') {
        throw new Error('这是私有变量,不能更改')
    }
    return Reflect.set(target, key, value)
}

双向绑定原理

使用订阅者-发布模式,见https://segmentfault.com/a/11...
其中核心组成部分:

  1. 监听器Observer: 上面的数据劫持
  2. 订阅者容器: 监听器监听到数据变动时,遍历订阅者容器发布消息
  3. Compile:解析模板指令,将模板中的变量替换成数据,比如{{title}}
  4. Watcher: 连接ObserveCompile的桥梁

订阅者容器

function Dep() {
    this.subs = []
}
Dep.prototype = {
    addSub(sub) {
        this.subs.push(sub)
    },
    notify() {
        // 每个订阅者都有一个update方法
        this.subs.forEach(sub=>sub.update())
    }
}

Compile

核心思想:

  • 解析特殊指令,比如{{}},@,bind,v-for
  • dom节点转换为文档碎片,提高性能
function Compile(el) {
    this.$el = document.querySelector(el)
    this.$fragment = this.node2Fragment(this.$el) // 将根节点下所有Dom转换为文档碎片
    this.init() // 解析指令
    this.$el.appendChild(this.$fragment) // 将文档碎片插入根节点
}
Compile.prototype = {
    node2Fragment(el) {
        let fragment = document.createDocumentFragment(),
            child
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
}

解析指令比较复杂,跳过

Watcher

function Watch(vm, exp) {
    this.vm = vm  // 数据集合
    this.exp = exp // 需要监听的属性
    this.value = this.get() // 初始化时触发自己的get
}
Watch.prototype = {
    update() {
        // 执行Compile的方法,触发view更新
    },
    get() {
        Dep.target = this // Dep.target表示当前订阅者
        let value = this.vm[this.exp] // 这里会触发Observer的getter,因为数据集合已经被劫持
        Dep.target = null  // 重置
        return value
    }
}
Object.defineProperty(data,key,{
    get() {
        Dep.target && dep.addDep(Dep.target) // 向订阅者容器中添加当前订阅者
        return val
    },
    set() {
        dep.notify() // 如果发生变化,通知所有订阅者
    }
})

总结

Vue双向绑定原理是采用发布订阅者模式,在初始化时劫持数据的各个属性的setter/getter,在数据变动时发布消息给订阅者,触发响应的监听回调。
而每个组件都对应一个Watcher实例,它会在组件渲染的过程中把接触过的数据记录为依赖,当依赖的setter出发时,会通知Watcher,从而使组件重新渲染
它的框架:MVVM
image.png

MVC缺点:viewmodel可以直接通信互相影响,而与之比较:

  1. 数据与视图分离
  2. 数据驱动视图,开发者只需要关心数据,DOM操作被封装

英目频道
名乎利乎,道路奔波休碌碌;来者往者,溪山清净且停停

路漫漫其修远兮,吾将上下而求索

67 声望
4 粉丝
0 条评论
推荐阅读
vue点击空白处
{代码...} 使用 {代码...}

yingmhd1阅读 1k

「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs40阅读 4.7k评论 5

封面图
【已结束】SegmentFault 思否写作挑战赛!
SegmentFault 思否写作挑战赛 是思否社区新上线的系列社区活动在 2 月 8 日 正式面向社区所有用户开启;挑战赛中包含多个可供作者选择的热门技术方向,根据挑战难度分为多个等级,快来参与挑战,向更好的自己前进!

SegmentFault思否20阅读 5.6k评论 10

封面图
Vue2 导出excel
2020-07-15更新 excel导出安装 {代码...} src文件夹下新建一个libs文件夹,新建一个excel.js {代码...} vue页面中使用 {代码...} ===========================以下为早期的文章今天在开发的过程中需要做一个Vue的...

原谅我一生不羁放歌搞文艺14阅读 19.9k评论 9

用了那么久的 SVG,你还没有入门吗?
其实在大部分的项目中都有 直接 或 间接 使用到 SVG 和 Canvas,但是在大多数时候我们只是选择 简单了解 或 直接跳过,这有问题吗?没有问题,毕竟砖还是要搬的!

熊的猫17阅读 1.5k评论 2

封面图
嘿,vue中keep-alive有个「大坑」你可能还不知道
背景是这样的,我们使用vue2开发一个在线客服使用的IM应用,基本布局是左边是访客列表,右边是访客对话,为了让对话加载更友好,我们将对话的路由使用<keep-alive>缓存起来。但是如果将所有对话都缓存,未...

wuwhs12阅读 2.5k

封面图
你可能需要的多文档页面交互方案
在日常工作中,面对不同的需求场景,你可能会遇到需要进行多文档页面间交互的实现,例如在 A 页面跳转到 B 页面进行某些操作后,A 页面需要针对该操作做出一定的反馈等等,这个看似简单的功能,却也需要根据不同...

熊的猫8阅读 1.2k

封面图

路漫漫其修远兮,吾将上下而求索

67 声望
4 粉丝
宣传栏