深入理解Vue数据响应式

Vue数据响应式主要研究的是 Vue 构造选项中 data 属性的特性

深入响应式 官方文档 网址: https://cn.vuejs.org/v2/guide/reactivity.html

1. getter 与 setter

首先我们需要理解 ES6 的 getter 与 setter 语法。

// 创建一个对象,得到姓名
let obj1 = {
  姓: "高",
  名: "丽丽",
  姓名() {
    return this.姓 + this.名;
  },
  age: 18
};
console.log(obj1.姓名());    // 高丽丽
// 姓名后面的括号不能删掉,因为它是函数

// 姓名不要括号也能得出值
let obj2 = {
  姓: "高",
  名: "丽丽",
  get 姓名() {
    return this.姓 + this.名;
  },
  age: 18
};
console.log(obj2.姓名);
// 总结:getter 就是这样用的。不加括号的函数。

// 姓名可以被写
let obj3 = {
  姓: "高",
  名: "丽丽",
  get 姓名() {
    return this.姓 + this.名;
  },
  set 姓名(name){
    this.姓 = name[0]
    this.名 = name.slice(1)
  },
  age: 18
};
obj3.姓名 = '张宇'
console.log(`姓 ${obj3.姓},名 ${obj3.名}`)
// 将obj3打印出来后发现 姓名:(...) 是个伪属性。
// 总结:setter 就是这样用的。用 obj.x = xxx 触发 set 函数

get语法将对象属性绑定到查询该属性时将被调用的函数。
使用get语法时应注意以下问题:

  • 可以使用数值或字符串作为标识;
  • 必须不带参数;
  • 它不能与另一个 get或具有相同属性的数据条目同时出现在一个对象字面量中

当尝试设置属性时,set语法将对象属性绑定到要调用的函数。
使用 set 语法时请注意:

  • 它的标识符可以是数字或字符串;
  • 它必须有一个明确的参数;
  • 在对象字面量中,不能为一个已有真实值的变量使用 set ,也不能为一个属性设置多个 set。

2. Object.defineProperty() 方法

该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

let _hair = null

Object.defineProperty(obj3, 'hair', {
  get() {
    return _hair
  },
  set(color) {
    _hair = color
  }
})
ojb3.hair = 'black'
console.log(ojb3.hair)

小结
关于 Object.defineProperty() 方法

  • 可以给对象添加属性value
  • 可以给对象添加getter / setter
  • getter / setter用于对属性的读写进行监控

其他属性
描述符默认值汇总
拥有布尔值的键 configurableenumerablewritable 的默认值都是 false
属性值和函数的键 value、get 和 set 字段的默认值为 undefined
在 Vue 实例创建的时候,Vue会将 data 数据变为添加了getter setter 的数据属性。

3. 模拟 vue 的数据代理原理

需求一:用 Object.defineProperty 定义 n

let data1 = {}
Object.defineProperty(data1, 'n', {
  value: 0
})
console.log(`需求一:${data1.n}`)    // 需求一:0 

需求二:n 不能小于 0 ,比如 data2.n = -1 应该无效,但 data2.n = 1 有效

let data2 = {}

data2._n = 0 // _n 用来存储 n 的值

Object.defineProperty(data2, 'n', {
  get(){
    return this._n
  },
  set(value){
    if(value < 0) return
    this._n = value
  }
})

console.log(`需求二:${data2.n}`)    // 
需求二:0
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)    // 需求二:0 设置为 -1 失败 
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)    // 需求二:1 设置为 1 成功 

但是需求二中 如果直接修改 data2._n 属性也是无法拦截的,因此还要改进。
需求三:使用代理模式

let data3 = proxy({data:{n:0}})  // 括号里是 匿名对象,无法访问
function proxy({data} /* 解构赋值 */){
    const obj = {}
    //   这里的n理论上应该遍历data的所有key,这里简化了
    Object.defineProperty(obj,'n',{
        get(){
            return data.n
        },
        set(value){
            if(value < 0) return
            data.n = value
        }
    })
    return obj    // obj就是代理对象
}
data3.n = -1     // 这里触发了data3 的 setter 函数 不会赋值为负数
console.log(data3.n)    //   0 

在上面的代理中,若 proxy 函数的参数是有名称的对象(可访问),那么它的值还是会被修改。
需求四:使用代理的加强版——用户修改原始对象也能拦截

let myData = {n:0}
let data4 = proxy({data:myData})

function proxy({data}){
    //   这里的n理论上应该遍历data的所有key,这里简化了
    let value = data.n
    delete data.n     // 这行可以不写  因为下面创建的n属性会被覆盖
    Object.defineProperty(data,'n',{
        get(){
            return value
        },
        set(newValue){
            if(newValue < 0)return
            value = newValue
        }
    })

    // 上面这几句会监听 data 对象数据的变化

    const obj = {}
    Object.defineProperty(obj,'n',{
        get(){
            return data.n
        },
        set(value){
            if(value < 0)return
            data.n = value
        }
    })

    return obj
}
myData.n = -5
console.log(myData.n);    //   0  被监听拦截
data4.n = -6
console.log(data4.n);    //   0  被代理拦截

综上所述,Vue创建实例时对 data 的更改就包含了这个原理。

let data5 = proxy({data:myData})   // 类似于
const vm = new Vue(data:{n:0})   // Vue对于数据的更改原理正是上面解释的那样

小结
vm = new Vue({data: myData}) 会让 vm 成为 myData 的代理,并且对 myData 的所有属性监控,当 myData 里的属性变化就可以调用 render(data) 来更新页面。

4. Vue data属性存在的问题

4.1 Object.defineProperty的问题

Object.defineProperty(obj, 'n', {...}) 

必须要有一个'n',才能监听、代理 obj.n 。如果没有写'n'的话 Vue会给出警告,无法监听后面添加的属性。

new Vue({
  data: {
    obj: {
      a: 0 // obj.a 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      this.obj.b = 1;
    }
  }
}).$mount("#app");

Vue监听不了一开始就不存在的 obj.b (undefine/null),所以页面不会显示。

4.2 解决方法——Vue.set()

可以直接在 obj 里添加 {b:undefined} ,但是实际上页面中有许多元素不可能一一添加,并且会使代码很难看,那么就要用到 Vue.set (或 this.$set)
使用 Vue.set() 方法的作用

  1. 新增 key
  2. 自动创建代理和监听(如果没有创建过)
  3. 触发视图更新(但并不会立刻更新)
new Vue({
  data: {
    obj: {
      a: 0 // obj.a 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
      <button @click="addB">add b</button>
    </div>
  `,
  methods: {
    setB() {
      // this.obj.b = 1; //再次刷新后,页面中会显示 1 
      Vue.set(this.obj, "b", 1);  // 也可以 this.$set(this.obj, "b", 1);
    },
    addB() {
      this.obj.b++;
    }
  }
}).$mount("#app");

4.3 数组的变更方法

new Vue({
  data: {
    array: ["a", "b", "c"]
  },
  template: `
    <div>
      {{array}}
      <button @click="setD">set d</button>
    </div>
  `,
  methods: {
    setD() {
      //this.array[3] = "d";  //页面中不会显示 'd' 
      // 这个数组可以理解为array: [0:"a", 1:"b", 2:"c"]
      // 等下,你为什么不用 this.array.push('d')
    }
  }
}).$mount("#app");

Vue 也不能检测新增到新增了下表,我们也不会每次修改的时候都要使用 Vue.set 或者 this.$set 。

数组的篡改方法

所以当数组传给Vue时,数组的这七个方法会被篡改覆盖,文档中叫做变更方法,这些方法会自动对数组新增项添加对应的监听,并且会更新视图。
变更方法的实现
变更方法实际上就是在Vue实例上加了一层原型链,同名的放大会被最底层的原型覆盖掉,这就实现了篡改。
ES6的写法:以 push 方法为例(模拟实现,并非源码)

class VueArray extends Array{
    push(...arsgs){
        const oldLength = this.length    //  this就是当前数组
        super.push(...arsgs)
        console.log('我被篡改了')
        for(let i = oldLength; i < this.length; i++){
            Vue.set(this,i,this[i])  // 将每个新增的 key 都告诉 Vue 实例
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容

  • 数据响应式 主要原理:深入响应式原理[https://cn.vuejs.org/v2/guide/reactivi...
    珍惜时间小李阅读 338评论 0 0
  • Vue数据响应式主要研究的是 Vue 构造选项中 data 属性的特性 深入响应式 官方文档 网址: http...
    雨溪滩阅读 517评论 0 0
  • 深入理解 options.data,响应式原理 ES6中的 getter 和 setter 通过计算属性 改变原始...
    siyuetian阅读 284评论 0 0
  • Vue到底对 data 做了什么? 我们有如上的代码,其中 data 中的的数据引用外面的变量 myData; 我...
    MrTon_1965阅读 234评论 0 0
  • 一个物体能对外界的刺激作出反应,那他就是响应式的 const vm = new Vue({data:{n:0}})...
    Shigure_Rain阅读 249评论 0 0