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

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

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。

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:

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:

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 收集的回调,而且不管是不是当前的依赖属性发生变化都会执行。下面继续改造:

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 来实现:

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
目录
相关文章
|
1天前
|
JavaScript 网络架构
|
1天前
|
人工智能 JavaScript 索引
Duplicate keys detected: This may cause an update error.【Vue遍历渲染报错的解决】
这篇文章讨论了在Vue中进行列表渲染时遇到的“Duplicate keys detected”错误。这个错误通常发生在使用 `v-for` 指令渲染列表时,如果没有为每个循环项指定一个唯一的 `key` 属性,或者指定的 `key` 属性值重复了。文章提供了导致错误的原始代码示例,并给出了修正后的代码,通过在 `key` 绑定中加入索引确保 `key` 的唯一性。此外,文章还解释了为什么需要唯一 `key` 以及如何解决这个问题。
Duplicate keys detected: This may cause an update error.【Vue遍历渲染报错的解决】
|
1天前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
|
1天前
|
缓存 移动开发 JavaScript
查漏补缺方为上策!!两万六字总结vue的基本使用和高级特性,周边插件vuex和vue-router任你挑选
该文章全面总结了Vue.js的基本使用方法与高级特性,并介绍了Vue周边的重要插件Vuex和Vue-Router的使用技巧。
查漏补缺方为上策!!两万六字总结vue的基本使用和高级特性,周边插件vuex和vue-router任你挑选
|
1天前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
4月前
|
JavaScript 前端开发 开发者
vue 的双向数据绑定的原理
vue 的双向数据绑定的原理
58 0
|
JavaScript 前端开发
vue相关面试题2:1.封装vue组件作用;2双向数据绑定原理;3.Router 是什么;4.导航钩子有哪些
它们有以下参数。 ●to::即将要进入的目标路由对象。 ●from:当前导航正要离开的路由。 ●next: 一定要用这个函数才能到达下一个路由,如果不用就会遭到拦截。
274 0
|
1天前
|
缓存 JavaScript 前端开发
vue-day02计算属性,v-bind,v-if,v-for
文章介绍了Vue.js中的计算属性、class与style的绑定、条件渲染和列表渲染的使用。通过示例代码展示了如何使用计算属性简化模板逻辑、如何通过v-bind动态绑定class和style、如何使用v-if进行条件渲染以及如何使用v-for进行列表渲染。这些特性使得Vue.js在构建动态用户界面时更加灵活和强大。
vue-day02计算属性,v-bind,v-if,v-for
|
1天前
|
JavaScript
vue项目中使用vue-router进行路由配置及嵌套多级路由
该文章详细说明了如何在Vue项目中配置和使用vue-router进行单页面应用的路由管理,包括设置嵌套路由和实现多级路由导航的示例。
vue项目中使用vue-router进行路由配置及嵌套多级路由