响应式基础

1、什么是响应式?

当用户在页面进行一些交互时,比如点击一个按钮进行查询,这样一个简单的动作,实际上需要先捕获到用户操作的DOM元素上对应的事件,在这个事件的具体实现中,会发起一个异步请求来获取后端返回的数据,然后将这个数据处理后再渲染到页面上。这一整套的流程,其实涉及两个数据流向,一是从HTML流向JS,另一个是从JS流向HTML。如果使用纯原生JS开发,则需要开发者自行实现对DOM元素的事件监听(HTML流向JS),以及将数据传递给DOM(JS流向HTML)。

纯原生开发的缺点是,开发者需要自行处理的事件监听太多,还要懂得什么时候将数据的变化更新到DOM上,并且还可能出现数据的频繁变化导致DOM渲染过于频繁而出现页面卡顿。

早期的JQuery框架,将开发者对DOM元素的捕获以及事件监听封装起来,提供了很多便捷的API,而现在流行的React、Vue、Angular等框架,则进一步弱化了开发者对DOM的处理,开发者可以专注于逻辑处理(JS的部分)。Vue这样的框架使得数据在更新后能适时地渲染到页面上,同时也简化了事件监听的处理,开发者无需再写一大堆addEventListener了。

响应式是Vue的主要特性之一,其作用概括来说,就是对数据进行劫持并收集相关依赖,在数据更新时能触发视图更新。

2、如何声明响应式数据

2.1 Vue2.6

声明响应式property
  • 声明方式:在组件的data函数返回的对象中,声明的property就是响应式数据,不推荐在data返回的对象中观察行为。
// 在组件的data函数中声明需要转换成响应式的属性
Vue.component('test-responsive', {
data: function() {
return {
count: 0,
message: '' // 即使是一个空字符串也必须提前声明
srcObj: {}
}
},
mounted() {
const newObj = {}
this.srcObj = newObj
console.log(this.srcObj === newObj) // true
}
})
  • 必须在data中声明根级响应式property:从原理上讲,vue2.6的响应式系统中,只会转换一次data返回的对象,如果没有在data中声明响应式property,而是在this.data上添加的话,是不会转换成响应式property的。
  • 深层转换:
  • 浅层响应:
  • 对数组的响应式:
  • 对普通对象的响应式:

// 留个问题:data返回的对象中的属性是如何绑定到vue组件实例上的(即为什么可以直接使用this.count)

声明计算属性computed
  • 适用场景:一个数据的变化规则比较复杂,或者同时依赖于多个其他数据。
Vue.component('test-computed', {
data: function() {
return {
firstName: '',
lastName: '',
}
},
computed: {
fullName: function() {
// 这一段如果直接写到模板表达式中,会比较长
return this.firstName + ' ' + this.lastName
}
}
})
  • getter和setter:计算属性默认只有getter,但实际上还支持setter。
Vue.component('test-computed', {
data: function() {
return {
firstName: '',
lastName: '',
}
},
computed: {
fullName: {
get: function() {
return this.firstName + ' ' + this.lastName
},
set: function(nFirstName, nLastName) {
// 注意:如果在这里尝试直接修改计算属性的值,会导致一个警告并且忽略setter中的修改,因为setter只用于修改计算属性的原始依赖
this.firstName = nFirstName
this.lastName = nLastName
}
}
}
})
  • 与methods的区别:computed计算属性是基于Vue的响应式依赖进行了缓存的,只有在相关的依赖发生变化时,计算属性才会重新求值;methods方法是每调用一次就会重新执行。
声明侦听属性watch
  • 适用场景:适用于在数据变化时执行异步或者开销较大的操作。
Vue.component('test-watch', {
data: function() {
return {
question: '',
answer: 'This is an empty question'
}
},
watch: {
question: function() {
this.answer = 'Waiting for your input'
this.getAnswer()
}
},
methods: {
getAnswer: function() {
axios.get('https://test-watch/getanswer')
.then((res) => {
this.answer = res.answer
})
}
}
})
  • computed与watch的区别:
    • 计算属性是基于依赖的数据进行计算得出一个新值并且具有缓存机制,侦听属性会在每次数据变化时执行指定的回调。
    • 计算属性适用于需要根据其他数据来计算得出结果的情况,侦听属性适用于一些异步的或者开销较大的操作。

// 留个问题:vue2.6是如何实现computed和watch的

2.2 Vue3

声明响应式状态

在Vue3中允许使用选项式API或者组合式API两种编码风格来书写组件。

选项式API

与Vue2.6的声明方式一致。

  • 使用data选项来声明
  • data选项是一个函数,函数中必须返回一个对象
  • Vue在创建组件实例时会调用此函数,并且将该函数返回的对象用响应式系统进行包装
  • 这个对象的所有顶层属性都会被代理到组件实例上(在组件中用this可以访问到这些顶层属性)
  • 由于Vue3与Vue2.6在转换响应式数据时的实现原理不同,导致虽然选项式API声明方式看起来与Vue2.6无异,但是实际操作的响应式对象是不同的(这里增加一个跳转)
export default {
data() {
return {
count: 0,
message: '',
srcObj: {}
}
},
mounted() {
const newObj = {}
this.srcObj = newObj
console.log(this.srcObj === newObj) // false,这里与Vue2.6是有本质区别的
}
}

组合式API

组合式API的方式提供了两个API来声明响应式数据,一个是ref(),一个是reactive()。

// 留个问题,构建工具是如何实现替代setup钩子函数的

reactive()

  • 仅仅用于声明引用类型的声明(响应式对象、数组、set、map等)
  • 响应式对象本质是一个Proxy
  • 为了在模板中可以使用响应式状态,需要在setup函数中定义并返回,或者使用
  • 有2个局限性
    • 只对对象类型有效,对原始类型无效
    • 只能保持对初始引用对象的响应追踪(如果对响应式对象进行重新赋值,会导致原始的响应式对象失去追踪)
import { reactive } from "vue";
export default {
setup() {
const state = reactive({ count: 0 });
return {
state // 暴露state到模板
}
}
}
// 或者
<script setup>
import { reactive } from "vue";
const state = reactive({ count: 0 });
</script>

ref()

  • 可用于创建任意类型的响应式数据
  • ref()函数将传入的任意值包装为一个带.value属性的ref对象
  • ref创建的响应式对象不会丢失响应性(被解构,被传递时)
import { ref } from 'vue'
const count = ref(0) // 等价于 ref({ value: 0 })

或者

import { ref } from 'vue'
const count = ref({ count: 0})

ref()与reactive()的区别总结

对比和总结
选项式API组合式API
实现原理getter/setterProxygetter/setter
声明响应式对象data()函数reactive()函数ref()函数
解构响应式对象
(将对象属性结构为局部变量,对局部变量是否仍具有响应性)
局部变量失去响应性
(因为对局部变量的访问不会触发getter/setter)

3、DOM更新

响应式原理

从实现原理上来深入理解Vue响应式系统的。

// 在深层、浅层对象上的响应式表现,在数组和对象上的响应式表现

1、vue2中的响应式原理

建立依赖-追踪变化-响应变化

总结

2、vue3中的实现原理

响应式代理 vs. 原始对象

如何实现依赖跟踪

flowchart TB
id1((JS对象obj))
id2((JS对象obj))
id2-1(getter)
id2-2(setter)
id3(data函数)
id4{{"遍历obj的所有属性,利用Object.defineProperty函数将每个属性转换成getter/setter"}}
id5((watcher实例))
id6(组件的render函数)
id7((虚拟DOM树))
subgraph ide1 [data选项]
ide2-->|输入|id3
id3-->|输出|ide3
id3==>|"做了什么?"|id4
end
subgraph ide2 [封装前]
id1
end
subgraph ide3 [封装后]
id2---id2-1
id2---id2-2
end
id2-1-.->|进行依赖收集|id5
id2-2-.->|数据有变化时通知|id5
id5-.->|重新渲染|id6
id6-.->|渲染|id7
id7-.->|将组件渲染时用到的数据记录为依赖|id2-1

3、对比

  • 2.6使用的是Object.defineProperty()将data对象中的属性转换成响应式property,在响应式系统中操作的是原始的data对象;3使用Proxy将原始的data对象包裹为一个代理对象,此后在响应式系统中操作的是这个代理对象,修改原始data对象,并不会触发响应式更新。

总结和对比