Vue 的数据响应式

Vue到底对 data 做了什么?


import Vue from 'vue/dist/vue.js'

Vue.config.productionTip = false

const myData = {
    n: 0
}
console.log(myData)

const vm = new Vue({
    data: myData,
    template: `
        <div>{{n}}<button @click="add">+1</button></div>
    `,
    methods: {
        add(){
            this.n +=10
        }
    }
}).$mount("#app")

setTimeout(() => {
    this.n += 10
    console.log(myData)
}, 3000)
  1. 我们有如上的代码,其中 data 中的的数据引用外面的变量 myData;
  2. 我们在两处进行打印,一处是初始化的时候打印,一处是在变化之后进行打印;
  3. 我们可以看到初始化的时候打印这个myData就只是我们定义的对象,但是在变化之后再打印这个对象就会发生变化,好像是被包裹了一层东西;


    image.png
  4. 一开始是 {n:0},传给 new Vue 之后就立马变成{n:(...)};
  5. {n:(...)}是个什么东西,为什么变现和 {n:0}一致?
  6. 我们了解这个知识点需要知道什么是 ES6 的 getter 和 setter。

getter 和 setter


  1. 我们声明一个变量,这个变量的属性如下,具有一个姓的属性,一个名的属性,还有一个age属性;
    let obj0 = {
        姓: "高",
        名: "圆圆",
        age: 18
    };
    
  2. 需要一:得到姓名,那么只要在里面添加一个方法就好了,在对象中,属性名和属性值一样的时候,是可以直接写函数的。
    let obj1 = {
        姓: "高",
        名: "圆圆",
        姓名() {
            return this.姓 + this.名;
        },
        age: 18
    };
    console.log("需求一:" + obj1.姓名())
    
    • 但是这个函数我们需要调用,也就是后面又括号,那么怎么去掉括号呢?
  3. 需求二:姓名之后不加括号也能得出值
    • 这个需要使用 ES6 的新语法,get方法
    •   let obj2 = {
            "姓": "高",
            "名": "圆圆",
            get 姓名() {
                return this.姓 + this.名;
            },
            age: 18
        }
        console.log("需求二:" + obj2.姓名)
      
    • 我们使用 get 之后发现,这个姓名是一个属性,不用加括号,只不过是以函数形式定义的,看起来跟普通的属性并没有区别
    • 这种写法是 getter,用于获取这个值
  4. 需求三:姓名可以被写,这个写法是 setter
    •   let obj3 = {
            "姓": "高",
            "名": "圆圆",
            get 姓名() {
                return this.姓 + this.名;
            },
            set 姓名(xxx){
                this.姓 = xxx[0]
                this.名 = xxx.slice(1)
            },
            age: 18
        }
      
        obj3.姓名 = '刘诗诗'
      
        console.log(`需求三:姓 ${obj3.姓}, 名 ${obj3.名}`)
        console.log(obj3)
      
    • 我们将这个obj3打印出来看看
    • image.png
    • 这个姓名其实跟我们之前的那个data中的数据是一样的形式,说明我们之前的那个数据也是getter和setter形成的
    • 这儿的姓名不是一个真实的属性,但是我们可以对这个属性进行读和写,读写通过getter和setter进行操作

Object.defineProperty


  1. 我们之前在使用getter和setter的时候是在定义的时候直接使用的,当我们在定义好一个对象之后,无法再添加get和set。
  2. 但是如果我们想在后面写怎么办?
  3. 这个时候我们就需要使用Object.defineProperty
  4. 下面是一个示例代码
    •   let obj3 = {
            "姓": "高",
            "名": "圆圆",
            get 姓名() {
                return this.姓 + this.名;
            },
            set 姓名(xxx){
                this.姓 = xxx[0]
                this.名 = xxx.slice(1)
            },
            age: 18
        }
        var _新属性 = 0
        Object.defineProperty(obj3, '新属性', {
            get(){
                return _新属性
            },
            set(value){
                _新属性 = value
            }
        })
      
    • 上述代码中,我们想要添加一个新属性,其中_新属性是为了盛放新属性的值
    • 我们定义的属性是不存在的,所以在get和set方法中不可以使用本属性,因为新属性的使用是通过get和set方法的,如果在get和set中使用就会造成死循环
    • 这个_新属性其实是一个代理

代理和监听


  1. 需求一:用 Object.defineProperty 定义 n
    •   let data1 = {}
        Object.defineProperty(data1, 'n', {
            value: 0
        })
        console.log(`需求一:${data1.n}`)
      
    • 我们定义了一个data1,想要添加一个属性,并设置成0
  2. 需求二:n不能小于0
    •   let data2 = {}
        data._n = 0
        Object.defineProperty(data2, 'n', {
            get(){
                return this._n  // _n用来存储n的值
            },
            set(value){
                if(value < 0) return
                this._n = value
            }
        })
        console.log(`需求二:${data2.n}`)
        data2.n = -1
      
    • 我们使用了一个临时的变量_n去存储
    • 如果对方直接使用这个data._n呢?
    • 我们怎么将这个data._n将其变得访问不到呢?
    • 可以使用代理
  3. 需求三:使用代理
    •   let data3 = proxy({ data: {n:0} })  // 括号里面的匿名对象无法访问,因为没有名字
        function proxy({data}){
            const obj = {}
            // 这里的 n 写死了,理论上应该是遍历属性
            Object.defineProperty(obj, 'n' {
                get(){
                    return data.n
                },
                set(value){
                    if(value<0)return
                    data.n = value
                }
            })
            return obj  // obj就是代理
        }
        console.log(`需求三:${data3.n}`)
        data3.n = -1
      
    • 函数传进去的匿名对象是无法访问的
    • 函数里面使用的obj是一个代理
    • 我们将data3中的所有属性值通过get和set的方式转给代理obj
    • data3理论上跟obj是等价的,我们通过在obj里面去设定一些机制,防止外界随意篡改,如不可以小于0
    • 代理的作用就是做一些限制保护,只要原始传进去的对象不暴露在外面就行。
    • 但是如果对于一些个变量已经暴露在外面怎么办?
  4. 需求四:就算用户擅自修改myData,也要拦截它
    •   let myData = {n:0}
        let data4 = proxy({ data: myData })
        myData.n = -1
      
    • 上面这个代码就直接绕过了代理,修改了最原始的myData
    • 我们通过下面的代码实现了怎样能在用户修改的时候做一个监听呢?
    •   let myData5 = {n:0}
        let data5 = proxy2({ data: myData5 })
        // 这个改进的代码可以100%的防止超出规则的数据更改
        function proxy2({data}){
            // 使用这个value存储原始的data中的n,先将其记下来,因为待会要删除这个n
            let value = data.n
            // 我们创建一个新的n就是覆盖原先的n,覆盖就相当于删除
            // 这个部分是监听逻辑,只要n发生更改,就出触发这个,但是返回值还是需要通过判断才能更改的
            // 我们将原先的属性删掉,通过get和set获取以及重置,这样就可以在其中设置规则,即监听
            Object.defineProperty(data, 'n', {
                get(){
                    return value
                },
                set(newValue){
                    if(newValue<0)return
                    value = newValue
                }
            })
            // 这部分是代理逻辑
            const obj = {}
            Object.defineProperty(obj, 'n', {
                get(){
                    return data.n
                },
                set(value){
                    if(value<0)return
                    data.n = value
                }
            })
        }
      
    • 上面这串代码改写的代理函数分为两部分,一部分是负责监听,另一个部分是用于代理;
    • 负责监听的那部分通过使用闭包,拿到数据就在内部存储一下数据,并通过get和set的方式重新定义同名数据,这样外界在访问的时候就必须通过这个get和set,在这个里面做一些限制,就可以实现监听效果
    • 代理逻辑还是跟原先的一样
  5. let data5 = proxy2({ data: myData5 })这段代码跟const vm = new Vue({data: {n: 0}})很是类似

那么 Vue 到底对数据做了什么?


  1. 啥是代理?(设计模式)
    • 对 myData 对象的属性读写,全权由另一个对象 vm 负责(通过将myData中的属性用get和set的方式给vm)
    • 那么 vm 就是 myData 的代理
    • 比如 myData.n 不用,偏要用 vm.n 来操作 myData.n
  2. 有关 vm = new Vue({data: myData})
    1. 会让 vm 成为 myData 的代理(proxy)
    2. 会对 myData 的所有属性进行监控
    3. 为什么要监控?为了防止 myData 的属性变了,vm不知道
    4. vm 知道了又如何?知道属性变了就可以调用 render(data)
    5. UI=render(data)
  3. image.png
  4. 全程最开始的对象是不变的,我们操作和展示的时候,只动vm中的对象

什么是数据响应式


  • 什么是响应式?
    • 如果一个物体对于外界的刺激能做出反应,他就是响应式的
  • Vue 的 data 是响应式
    • const vm = new Vue({data: {n: 0}})
    • 我如果修改 vm.n,那么 UI 中的 n 就会响应我
    • Vue 2 通过 Object.defineProperty 来实现数据响应式

Vue 的 data 的 bug


  • Object.defineProperty 的问题
    • Object.defineProperty(obj, 'n', {...})
    • 必须要有一个 'n',才能监听&代理obj.n
    • 如果前端工程师没有给出n怎么办?
    • 情况一:第一层属性没有n,Vue会给出一个警告
      • image.png
      • 会警告并没有这个属性
    • 情况二:属性是一个对象,对象里面有属性,这种情况下,Vue只会检查第一层属性
      • 属性里面的属性如果不存在,就无法进行后续操作的
      • image.png
      • image.png
      • 点击之后,并不会显示b的值
      • 为什么:因为Vue无法监听一开始就不存在的obj.b
  • 解决方法:
    • 一开始就将需要用到的key监听好,如b: undefined
    • 使用 Vue.set 或者 this.$set
      • this.$set(this.obj, 'b', 1)
      • Vue.set(this.obj, 'b', 1)

Vue.set 和 this.$set


  • 作用
    • 新增 key
    • 自动创建代理和监听(如果没有创建过)
    • 触发UI更新(但并不会立刻更新)
  • 举例
    • this.$set(this.object, 'm', 100)

数组的变异方法


  1. 我们知道数组最终是一个对象;
  2. 如a = ['a', 'b', 'c'],实际上会是a = {0: 'a', 1: 'b', 2: 'c'}
  3. 这样在 vue 中,当我们需要给数组添加元素的时候,不可以直接使用原先的this.a[3] = "d"这样的方法,因为vue对原先不存在的数据是无法进行监听的,需要使用this.$set()
  4. 这样子,我们对于数组的所有操作,都得使用$.set?
  5. 使用 this.a.push('d'),这个方法经过Vue改编了,同名,可以支持在vm中使用,就是在原先的方法那边添加一个set


    image.png
  6. 篡改的方式是添加一层原型,在原型上面篡改
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • 写在前面 我相信很多同学对Vue的数据响应式是通过Vue.js文档[https://link.zhihu.com/...
    luci_dity阅读 298评论 0 0
  • getter和setter getter 示例代码 ```let obj1 = { //以前写js函数调用姓:卢,...
    卢卢2020阅读 223评论 0 0
  • 这方面的文章很多,但是我感觉很多写的比较抽象,本文会通过举例更详细的解释。(此文面向的Vue新手们,如果你是个大牛...
    Ivy_2016阅读 15,357评论 8 64
  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    NARUTO_86阅读 37,343评论 8 86
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,543评论 0 11