通过简单小示例彻底搞明白vue双向数据绑定核心原理

vue 很大的一个优势就是双向数据绑定,而在 react 或小程序中是需要我们自己手动 setState、setData 去修改视图数据。

vue2 中利用的 Object.defineProperty 去劫持对象属性的 getter 和 setter,所以 data 函数里需要返回一个对象,如果没有在 data 里定义的属性是不会双向绑定的,因为没有被劫持。

双向数据绑定还用到了设计模式中的发布/订阅模式,当触发 getter 的时候去做依赖收集,触发 setter 时去通知执行收集的对应依赖回调。

Object.defineProperty

使用语法:Object.defineProperty(obj, prop, descriptor),具体使用参考下方 demo。

注意里面单独用到了一个 value 变量来存 age 的值,如果不这样直接在 get 函数里写 person.age 来取值会又触发 get 死循环了。而 set 里直接通过修改 value 的值就能改变 person 的 age 属性值,是因为我们用到了一个外部的 value 变量,set 里直接修改 value 的值,当要去值时 get 里其实就是返回的这个 value。

javascript 复制代码
let person = {
    name: '周小黑',
    age: 18
}

let value = person.age

Object.defineProperty(person, 'age', {
    get() {
        console.log('获取age:' + value)
        return value
    },
    set(e) {
        console.log('修改age:' + e)
        value = e
    }
})

console.log(person.age) // 18
person.age = 20
console.log(person.age) // 20

依赖收集和执行

当数据变动时要做的所有操作,我们需要提前收集起来,当真的发生变动时,才有东西拿出来执行。

双向数据绑定简单点理解也就是当一个属性值变动时,我们需要程序自动去做一些依赖当前值的操作,具体参考下方 demo:

javascript 复制代码
let person = {
    name: '周小黑',
    age: 18
}

let value = person.age

Object.defineProperty(person, 'age', {
    get() {
        console.log('获取age:' + value)
        return value
    },
    set(e) {
        console.log('修改age:' + e)
        value = e
        action()
    }
})

function action() {
    console.log('我是数据变动要执行的操作')
    const val = person.age *  1000
    person.money = val
    console.log(person)
}

person.age = 20
// 修改age:20
// 我是数据变动要执行的操作
// 获取age:20
// { name: '周小黑', age: [Getter/Setter], money: 20000 }

为了简单模拟,当 person 的 age 发生变动时,我们往 person 里新增一个 money 属性。

这里的代码执行逻辑:我们提前定义了一个要执行操作的 action 函数,当我们修改 age 属性的时候会触发 set,触发 set 时就说明数据发生了变动,直接在 set 里执行一下 action 函数就行了。

不过上面的代码还有一个明显的问题,就是 action 函数并不是自动去收集的,总不能每一个属性我们都自已额外定义一个 action1、action2...操作函数吧。

自动依赖收集

为了实现自动收集依赖我们在上面代码的基础上改造一下,通过封装一个 onChange 公共函数来专门收集依赖,它的参数就是一个要执行操作的 function:

javascript 复制代码
let person = {
    name: '周小黑',
    age: 18
}

let value = person.age

Object.defineProperty(person, 'age', {
    get() {
        console.log('获取age:' + value)
        return value
    },
    set(e) {
        console.log('修改age:' + e)
        value = e
        action()
    }
})

let action = null
const onChange = (callback) => {
    action = callback
    callback() // 这里先执行一次触发 get 依赖收集
}
onChange(() => {
    console.log('我是数据变动要执行的操作')
    const val = person.age *  1000
    person.money = val
    console.log(person)
})

person.age = 20
// 我是数据变动要执行的操作
// 获取age:18
// { name: '周小黑', age: [Getter/Setter], money: 18000 }
// 修改age:20
// 我是数据变动要执行的操作
// 获取age:20
// { name: '周小黑', age: [Getter/Setter], money: 20000 }

当调用依赖收集函数 onChange 时我们先将依赖收集到外部的 action 里,当修改 age 触发 set 时,我们直接执行下 action 就行了,这样就可以实现多个依赖回调的收集。

不过上面的代码还是有问题:需要自己手动调用 onChange 函数,只会执行最后一次调用 onChange 收集的回调,而且不管是不是当前的依赖属性发生变化都会执行。下面继续改造:

javascript 复制代码
let person = {
    name: '周小黑',
    age: 18
}

let value = person.age

Object.defineProperty(person, 'age', {
    get() {
        onCollect('age')
        console.log('获取age:' + value)
        return value
    },
    set(e) {
        console.log('修改age:' + e)
        value = e
        onExecute('age')
    }
})

let action = null
const onChange = (callback) => {
    action = callback
    callback() // 这里先执行一次触发 get 依赖收集
}

// 收集所有依赖的盒子
const eventBox = {}
// 收集依赖
function onCollect(key) {
    let arr = eventBox[key] || []
    arr.push(action)
    eventBox[key] = arr
}
// 执行
function onExecute(key) {
    let arr = eventBox[key] || []
    arr.map(fn => fn())
}

onChange(() => {
    console.log('我是数据变动要执行的操作')
    const val = person.age *  1000
    person.money = val
    console.log(person)
})

onChange(() => {
    console.log('我是数据变动要执行的操作2')
    const val = person.age *  2000
    person.money = val
    console.log(person)
})

onChange(() => {
    console.log('我是数据变动要执行的操作,但是我没有任何依赖')
})

person.age = 20
// 我是数据变动要执行的操作
// 获取age:18
// { name: '周小黑', age: [Getter/Setter], money: 18000 }
// 我是数据变动要执行的操作2
// 获取age:18
// { name: '周小黑', age: [Getter/Setter], money: 36000 }
// 我是数据变动要执行的操作,但是我没有任何依赖
// 修改age:20
// 我是数据变动要执行的操作
// 获取age:20
// { name: '周小黑', age: [Getter/Setter], money: 20000 }
// 我是数据变动要执行的操作2
// 获取age:20
// { name: '周小黑', age: [Getter/Setter], money: 40000 }

定义了一个 eventBox 的对象来存所有属性的依赖回调,当触发 get 时调用 onCollect 收集依赖到盒子里,当修改数据触发 set 时,再从 eventBox 盒子里拿出对应属性的依赖回调来执行。

上面的代码其实并不难,可能最难理解的是在 get 里到底是怎么完成自动依赖收集的,当我们调用 onChange 的时候,此时外部的 action 里存的就是当前要收集的依赖回调(记住这里很关键),接着直接执行一下回调函数触发 get 依赖收集,如果回调内部有触发 get(比如上面代码里通过 person.age 获取年龄),那就会走到内部的 get 函数里,我们只用在 get 里调用一下 onCollect 把 action 收集到 eventBox 盒子对应的 key 值里就行了,如果还是不能理解可以打断点运行一下代码就明白了。

其实到这里你也就基本能明白 vue 的双向数据绑定实现原理和步骤了:getter 里自动收集依赖到一个盒子里,setter 里再拿出收集的对应依赖遍历执行,核心不就是发布/订阅模式。

上面的代码其实还是有问题:在 set 里执行回调又会触发 get,然后又会往盒子里添加重复的回调,这一点可以通过将之前的 array 数组改成 Set 数据结构来存储 key 对应的回调来解决;除此之外上面的代码最有一个没有依赖的回调也被添加到了 age 对应的回调里,这里需要每次执行了 action 后要将 action 重置为 null,然后 get 里也需要判断一下 action 不为 null 时才去收集依赖。为了理解简单数据储存前面的版本直接用的最简单的 Object 和 Array,实际中是需要结合使用 WeakMap、Map、WeakSet、Set 这些来储存的,修改后的完整代码请参考下方的 proxy 版本。

vue3 里的 proxy

vue2 中是用的 Object.defineProperty 来劫持对象的 getter、setter,vue3 中换成了 proxy,其实核心原理还是上面那些,只不过收集和执行依赖换到 proxy 里去劫持 getter、setter 了而已。

上面的 demo 换成 proxy 来实现:

javascript 复制代码
let person = {
    name: '周小黑',
    age: 18
}

let action = null
const onChange = (callback) => {
    action = callback
    callback() // 这里先执行一次触发 get 依赖收集
    action = null
}

// 收集所有依赖的盒子
const eventBox = {}
// 收集依赖
function onCollect(key) {
    let arr = eventBox[key] || new Set()
    arr.add(action)
    eventBox[key] = arr
}
// 执行
function onExecute(key) {
    let arr = eventBox[key] || []
    arr.forEach(fn => fn())
}

let value = person.age

const proxyPerson = new Proxy(person, {
    get(target, key) {
        action && onCollect(key)
        console.log('获取' + key)
        return target[key]
    },
    set(target, key, newValue) {
        target[key] = newValue
        console.log('修改' + key + ':' + newValue)
        onExecute(key)
    }
})

onChange(() => {
    console.log('我是数据变动要执行的操作')
    const val = proxyPerson.age *  1000
    proxyPerson.money = val
    console.log(proxyPerson)
})

onChange(() => {
    console.log('我是数据变动要执行的操作2')
    const val = proxyPerson.age *  2000
    proxyPerson.money = val
    console.log(proxyPerson)
})

onChange(() => {
    console.log('我是数据变动要执行的操作,但是我没有任何依赖')
})

proxyPerson.age = 20
相关推荐
超哥--14 小时前
B站视频内容智能分析系统(九):React 前端与管理面板
前端·react.js·前端框架
dsyyyyy110117 小时前
JavaScript变量
开发语言·javascript·ecmascript
kyriewen18 小时前
手写 Promise.all、race、any:不到 30 行代码,解决并发异步的所有姿势
前端·javascript·面试
胡志辉的博客19 小时前
深入浅出理解浏览器事件循环:从一道输出题讲到 Chrome 源码
前端·javascript·chrome·chromium·event loop
代码不加糖19 小时前
js中不会冒泡的事件有哪些?
前端·javascript·vue.js
懂懂tty19 小时前
Vue2与Vue3之间API差异
前端·javascript·vue.js
老毛肚20 小时前
软件测试期末考试
vue.js
小二·20 小时前
Next.js 15 全栈开发实战
开发语言·javascript·ecmascript
杨若瑜21 小时前
本地开发环境慢?localhost的锅!
vue.js
Rain50921 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js