vue

✍️ Tangxt ⏳ 2020-07-11 🏷️ 数据响应式

03-数据响应式

★前言

了解「数据响应式」前,我看了 vue 官网的这篇介绍:

深入响应式原理 — Vue.js

但我看得云里雾里的……

根据官网推荐的视频(纯英文的视频实在是听不懂),我大概看了一下视频下方的这篇文章:

Build a Reactivity System - Advanced Components - Vue Mastery

其中提到:

这篇文章真得写得很好呀!从最原始到最终的结果,是一步步走过来的,我看过一些讲 Vue 响应式原理的文章,直接一上来就是Object.defineProperty(),没有丝毫的铺垫过程……

再看这张图,我不确定自己的理解是否正确:

深入响应式原理


gettersetter

Vue 对 data 做了什么?

1)一张「内存图」贯穿你整个对 Vue 的学习

内存图

2)为什么方方要用一节课的时间来讲这个看似很简单的`options.data`(深入理解一下)?

我们认为 data 是很简单的,即它就是一个内部数据而已,没啥特别的

但实际上,为了方便我们之后学习 options 的进阶属性,我们必须揉碎它,剥开它

data的主要原理,可以看这个:深入响应式原理 — Vue.js -> 内容篇幅太少了,可能你看完之后并不能深刻的理解! -> 当然,你也可以看源代码,但这是非常不切实际的……

相较于官方文档,方方的讲解 -> 对 data 扩充了非常多的内容!

2)小实验

1、从一个小点开始

log的异同

结果:

result

2、解释 `log` 的异同?

前置知识:ES 6 新特性列表getter 和 setter

关于 ES6 新出来的 API,你只需要知道它们是干啥的就行了,不需要自己动手去一个个了解,也不需要去记住它们是怎么用的 -> 当你在做项目的时候,需要用到了某个 API,再去了解它的语法、用法……

CRUD 这个对象:

let obj0 = {
  : "",
  : "圆圆",
  age: 18
};

①需求一:得到姓名

let obj1 = {
  : "",
  : "圆圆",
  姓名() {
    return this. + this.;
  },
  age: 18
};

console.log("需求一:" + obj1.姓名());
// 姓名后面的括号能删掉吗?不能,因为它是函数
// 怎么去掉括号?

括号不能删

②需求二:姓名不要括号也能得出值

let obj2 = {
  : "",
  : "圆圆",
  get 姓名() {
    return this. + this.;
  },
  age: 18
};

console.log("需求二:" + obj2.姓名);

// 总结:getter 就是这样用的。不加括号的函数,仅此而已。

删掉括号

用了ES6的新语法,仅仅在姓名属性名前边加个get关键字就可以实现我们的需求…… -> 为什么可以这样? -> 嗯,没有为什么,ES6的新语法特性就是如此……总之,你可以这样理解:姓名本质是属性不是函数,但是姓名的定义是用函数姿势定义的! -> 这样的姓名属性叫「计算属性」,虽然跟普通属性长得一模一样…… -> 读法「get 一个属性 的值」,如「get 姓名 的值」

③需求三:姓名可以被写

let obj3 = {
  : "",
  : "圆圆",
  get 姓名() {
    return this. + this.;
  },
  set 姓名(xxx) {
    this. = xxx[0]
    this. = xxx.substring(1)
  },
  age: 18
};

obj3.姓名 = '高媛媛'

console.log(`需求三:姓 ${obj3.},名 ${obj3.}`)

// 总结:setter 就是这样用的。用 = xxx 触发 set 函数

setter


虽然了解了 getset 到底是什么,有啥用,但我还是不理解 {n: (...)} 是个啥子东西……

蛛丝马迹

可以看到我们并没有写一个叫「姓名」的属性(像个真实属性一样,对比 get 姓名 ,颜色更为明显,同我们定义的普通属性age简直一模一样,只是值是(...)),但是 obj3 旗下就是有个叫 姓名 的普通属性!

浏览器的看法:

开发者你确实可以对姓名读和写,但是并不存在一个叫姓名的属性 -> 读和写的操作是通过getset完成的!

所以我们可以推出:

虽然我们理解了n:(...)这种形式,但是我们并不理解为啥要把n变成是get nset n


Q:对象的方法定义有几种姿势?

最常见的姿势(引入函数):

let o = {
  property: function (parameters) {},
}  

简写姿势(不需要用关键字function提示这是一个引入了函数的属性):

// Shorthand method names (ES2015)
let o = {
  property(parameters) {},
}

不常见但很有用的姿势(引入getter or setter方法):

let o = {
  get property() {},
  set property(value) {}
}

我的认知改变:

function方法」 & 「getter方法 or setter方法」 是有区别的!

Q:触发一个方法执行有三种姿势?

Object.defineProperty

在上个标题里边,我们知道:

ES6提供了一种新的语法特性,可以让我们在一个对象上设置一个getter和一个setter,然后对一个虚拟的属性进行读写……

文档:Object.defineProperty() -> 主要看 Examples

1)如何使用`Object.defineProperty()`?

我们之前在声明并初始化obj3的时候就直接写了它的getset

let obj3 = {
  : "",
  : "圆圆",
  get 姓名() {
    return this. + this.;
  },
  set 姓名(xxx) {
    this. = xxx[0]
    this. = xxx.substring(1)
  },
  age: 18
};

但是如果我们之后还有为obj3添加新的getset属性呢?如这样:obj3.get xxx() {},显然这是加不了的……

所以这个时候 Object.defineProperty() 就派上用场了……

它的参数:

  1. 第一个参数:告诉我你要在哪个对象上进行define,即你要define哪个对象
  2. 第二个参数:告诉我你要定义一个什么东西,如一个叫 xxx 的东西(虚拟属性
  3. 第三个参数:对象值,写上getset就好了,注意你不需要这样写 get xxx(){}……因为第二个参数已经明确了这是一个叫xxx的东西,所以你直接 get(){} 就行了 -> 不要多此一举哈!()

使用

通过Object.defineProperty()这种方式,我们给obj3添加了一个虚拟属性xxx,也叫属性……在定义xxxset方法时,xxx这个属性是不存在的,即 obj3 此时是咩有这个属性的,即便我们在外界可以obj3.xxx这样访问,但这样访问的本质是调用了gettersetter方法,之前我们初始化obj3之所以可以用this.姓啥的,那是因为obj3就是有个属性呀,所以我们必须得找一个东西来放置set过来的值,所以我们定义了一个叫_xxx的东西,用来存储xxx的值,而每次get的值,实际拿到的是_xxx的值,也就是说getset_xxx构成了闭包……

注意,setget里边的this指向是obj3哈!

总之,定义完一个obj3之后,你还想追加所谓的「虚拟属性」的话,那么你得通过Object.defineProperty()来哈!而且关于这新的虚拟属性(假如叫xxx),你在Object.defineProperty()的第三个参数里边的setget是不能出现this.xxx这样的代码,如果出现了,那么就会出现死循环,然后爆栈! -> 避免死循环,所以需要用到一个临时变量(全局也好,局部也罢,如叫_xxx),用来存储xxx这个属性的值,get xxx,即obj3.xxx时拿到的是_xxx的值,set xxx,即obj3.xxx = 'hi'时,update的东西是_xxx…… -> xxx给我的感觉就像是个傀儡一样!表面上看xxx真得是obj3的属性,实际上是个被settergetter操控的傀儡!


目前,我们已经知道了gettersetter以及Object.defineProperty(),但我们依旧还是不理解为啥会有{n:0} -> vm -> {n:(...)}这样的变化!

gettersetter:在定义一个对象的时候,可以直接定义一个gettersetter

Object.defineProperty():定义完对象过之后,你又突然想加一个gettersetter,那就使用Object.defineProperty()这个东西来搞吧!

★代理和监听

测试对象:

let data0 = {
  n: 0
}

1)需求一:用 `Object.defineProperty` 定义 `n`

let data1 = {}

Object.defineProperty(data1, 'n', {
  value: 0
})

console.log(`需求一:${data1.n}`)

// 总结:这煞笔语法把事情搞复杂了?非也,继续看。

分析:

需求一

一个无厘头的看法(天生的属性 vs 后来的属性):

颜色深浅

通过 Object.defineProperty 定义的属性似乎在告诉我,此属性最好不要访问,就跟 IE 禁止使用 __proto__ 一样

2)需求二:`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}`)
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)

// 抬杠:那如果对方直接使用 data2._n 呢?
// 算你狠

解析:

为啥不能 data2.n = -1 这样?这种赋值操作不是烂大街了吗?还有什么值是不能赋给变量的?JS 可不是强类型语言呀!

通过ES6的gettersetter是可以做到这一点的!

做法要点:

图示三个地方

如果我们使用了getset,那么我们可以在set里边做个判断,即不满足条件,就不set,甚至可以报一个错!

问题来了:

  1. API调用仔,居然这样做:obj2._n = -1 -> 这样一来,n就是-1的初始值了,所以我们的需求GG了!

怎么办?

  1. data2对象的这个_n属性,能不能不要暴露给 API 调用仔?甚至能否连data2这个名字也不要暴露给 API 调用仔?

方案:

使用代理!

3)需求三:使用代理

let data3 = proxy({ data: { n: 0 } }) // 括号里是匿名对象,无法访问

function proxy({ data }/* 解构赋值,别TM老问 */) {
  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 就是 obj
console.log(`需求三:${data3.n}`)
data3.n = -1
console.log(`需求三:${data3.n},设置为 -1 失败`)
data3.n = 1
console.log(`需求三:${data3.n},设置为 1 成功`)

// 杠精你还有话说吗?
// 杠精说有!你看下面代码

解析要点:

无人可修改

可以看到,API 调用仔是无法去修改我写的代码的!

{ data: { n: 0 } } -> 就是options -> 通过解析赋值拿到「真实的值」{ n: 0 }

一个比喻(代理有房子吗? vs 房子是房东的):

比喻

你是租房的,中介是obj,房东是data

代理的作用

当我们data3.n的时候,访问的是obj的虚拟属性n,但拿到的是data.n的值 -> 门面上你租的房子像是中介所属的,实际上是房东的!

所以,总体上看:

总之,API 调用仔 无法直接修改 data,必须得经过代理的审查,确认无误后,才能修改data

不过,这样依旧存在问题:

依旧有bug

API调用仔绕过了代理,直接操作真实数据!

4)需求四

let myData = { n: 0 }
let data4 = proxy({ data: myData }) // 括号里是匿名对象,无法访问

// data3 就是 obj
console.log(`杠精:${data4.n}`)
myData.n = -1
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`)

// 我现在改 myData,是不是还能改?!你奈我何
// 艹,算你狠

5)需求五:就算用户擅自修改 `myData`,也要拦截他

let myData5 = { n: 0 }
let data5 = proxy2({ data: myData5 }) // 括号里是匿名对象,无法访问

function proxy2({ data }/* 解构赋值,别TM老问 */) {
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
  // 因为我怕你们看不懂
  let value = data.n
  // delete data.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 // obj 就是代理
}

// data3 就是 obj
console.log(`需求五:${data5.n}`)
myData5.n = -1
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)

简单解析:

proxy2的逻辑

可以看到,不管你是通过myData5.n来读写,还是通过data5.n来读写,都会被gettersetter监听到!

6)这代码看着眼熟吗?

let data5 = proxy2({ data:myData5 }) 
let vm = new Vue({data: myData})

可以看到,我们之前不懂的这个:{n:0} -> vm -> {n:(...)},现在轻而易举地就能知道是为什么了!

以上讲的原理,就是 Vue 内部的源代码!

7)现在我们可以说说 `new Vue` 做了什么了

vm = new Vue({data: myData})

这行代码,Vue 做两件事情:

  1. vm 成为 myData 的代理 -> 你 vm.m 就是在取 myData.m,你 vm.v 就是在取 myData.v,同样,vm.v = 'hi'这样赋值,也是在对myData.v赋值…… -> 通过this来访问vm属性也是如此,因为this就是代理,就是vm
  2. myData的所有属性进行监控 -> 虽然我们在使用 Vue 的时候,基本上都是 new Vue({data: {n:0}}) 这样做的,但是奈何总有人会new Vue({data: myData})这样写 -> 必须要让代理vm知道所有的事情,这样一来就可以调用render(data),然后更新 UI 了!不然,myData变化了,UI 还是没有作出相应的变化!

总之,Vue 做了那么多功夫,总而言之就一句话:

你对 data 的任何修改,我 Vue 必须要知道!不然,我就无法去render了! -> 无法render,UI 就不会变化了!那么 我这个 Vue 的立身之本也就没有了!

8)小结

研究方法:

方方对data研究方法要比知识本身更为重要,那这是什么研究方法呢?

打log

通过这两个log,我们发现了myDataVue 处理之前,和被Vue处理之后,的结果是不一样的!

显然,通过方方的讲解之后,我们知道了一旦传入了myData给 Vue,那么 Vue 就会对这个 myData 进行串改,而这样的结果是,之前的n不见了,取而代之的是get n() {}set n() {}

总之,你得学会这种打log姿势,就是处理前这东西是咋样的?处理后这东西是咋样的? -> 思考「为啥会有这样的变化?Vue 到底对 myData 做了什么?」

我们在使用其它类库的时候,根本就不会这样传参数:{data:myData},即不会用个变量来传!而是直接给个匿名的数据,如{data:{n:0}} -> 不然,你永远都想不到 {n:0} 是否已经有了变化

这些研究方法的意义:

话说,方方是如何查看源代码,然后来验证自己是对的呢? -> 方方说看源码是很难的,其难度比上述探究「为什么」的过程,还要难 100 倍以上

★现场飙车看 Vue.js 源代码

如何查看 Vue 源码? -> vuejs/vue at 2.6 -> vue/state.js

★小结

代理和监听

示意图:

示意图

如果data有多个属性,如nmk,那么就会有get n / get m / get k 等 -> 做到这一点很简单 -> 循环加闭包

注意,传给data选项的{n:0},Vue 并咩有创建一个新的实例,而是直接把 {n:0}里边的 n 改成是 get nset n罢了!不然,我们之前log出来的myData就不应该有 getter/setter

理解了 data 才能继续学习 computedwatch

★Vue 的 data 的 bug

1)什么是数据响应式?

数据响应式

响应式是一个模糊的概念,只要你能对我的操作进行一些反应,那就是「响应式」的!

响应式网页:Smashing Magazine — For Web Designers And Developers

目前,我们已经理解了数据响应式,但是面试一般都不会问,面试的精髓在于「它会问变态的情况,而不是常态的情况」!

2)你好像说 Vue 有 bug

1、 `Object.defineProperty` 的问题

监听 & 代理 obj.n ,我们知道 obj 旗下必须得有一个 'n' 属性才行,即 Object.defineProperty(obj,'n',{...}) 可如果前端开发者比较水,没有给 n 怎么办呢?

如这样:

不写 n

绕过警告:

只检查第一层

Vue 没法监听一开始就不存在的 obj.b 当然,如果你非得「先上车后补票」,即先在 template 插入 data 里边没有的属性,如 xxx ,之后,再通过事件交互啥的,为 data 追加属性 xxx ,而且该 xxx 属性是被 Vue 监听的!

那么你可以这样做:

  1. key 都声明好,后面不再加属性 -> 这种做法你不想要
  2. 使用 Vue.set 或者 this.$set -> 你想要的做法

代码:

// 这种做法你不想要,因为自己一开始就不想写 b 属性
{
  obj: {
    a: 0,
    b: undefined
  }
}

// 第二种姿势:

{
  setB() {
    // this.obj.b = 1; //请问,页面中会显示 1 吗?

    // 这 set === $set -> true
    Vue.set(this.obj, 'b', 1)

    // or

    // 为啥加个「$」? -> 因为万一 data 旗下有个 set 属性呢?所以为了避嫌就只好加个 $ 了
    this.$set(this.obj, 'b', 1)
  }
}

2、`Vue.set` 和 `this.$set`?

作用:

★数组的变异方法

1)`data`中有数组 怎么办?

数组值要特殊对待

解决这个问题(更新了data.array就会让页面响应式数据渲染)有两种姿势:

  1. this.$set() or Vue.set()
  2. 变异的数组方法 this.array.push()

注意,我们这是在对数组中的元素进行 CURD,可不是直接把整个数组this.array给重新赋值给一个新数组了,如果重新赋值为一个新数组,那肯定是响应式数据更新的!

数组值


Q:如果data旗下的某个属性是数组类型的值,那么其template里边的更新时机是?

数组是很奇葩的


2)变异方法的实现

文档使用介绍:列表渲染 — Vue.js

1、ES6 写法

ES6 写法

2、ES5 写法

ES5 写法

★总结

总结

★练习题

测试数据响应式

1)题一

let obj = {
    : '', : '圆圆',
    ___A____ {
        return this. + this.
    }
}

请问 A 处的 getter 应该怎么写?

  1. 姓名()
  2. get 姓名
  3. get 姓名()
  4. getter 姓名()

2)题二

let obj = {}
Object.defineProperty(obj, __A___, _____B____)

请问 A、B 两处应该怎么写,才能让 obj.n 的值为 5

  1. A 写 n,B 写 5
  2. A 写 ‘n’,B 写 5
  3. A 写 n,B 写 {value: 5}
  4. A 写 ‘n’,B 写 {value: 5}

3)题三

Vue 中,用于设置 data 中的新增的 key 的 API 是:

  1. Vue.set
  2. Vue.$set
  3. vm.set
  4. vm.$set

4)题四

Vue 中,数组的变异方法有哪 7 个?

  1. push()
  2. filter()
  3. map()
  4. reduce()
  5. pop()
  6. shift()
  7. unshift()
  8. splice()
  9. sort()
  10. reverse()

5)题五

面试中,面试官经常会问「说说你对 Vue 数据响应式的理解」,请试着回答这个面试题。

要求:回答 20 字以上,能写篇博客更好。

这一题只是想让你知道,平时不总结,面试就结巴。

6)题六

题目链接

请回答2、3、4小题

答案解析:《Vue 自测题》中答错率最高的题的解释 - 知乎

★了解更多

➹:Vue 数据响应式的理解 - 上帝之兵的文章 - 知乎

➹:Vue全解 - 上帝之兵的文章 - 知乎

★总结