持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情 (opens new window)
# 一、什么是响应式?
Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改数据时,会驱动视图更新。举个简单的例子:
<div id="app">
<p class="content">result1:{{result1}}</p>
<p class="content">result2:{{result2}}</p>
</div>
<script>
new Vue({
el:'#app',
data(){
return{
num1:1,
num2:2,
//设置result1和result2的初始值恰好是num1+num2
result1:3,
result2:3
}
},
created(){
//改变num1的值
this.num1=2
this.result2=this.num1+this.num2
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
当改变num1值之前,页面上应该resulr1和result2的值都是3,而当改变num1的值以后,会触发视图更新,页面上result1的值是3,result2的值是4。数据改变,视图也会随之更新。
而原生JS一个值改变,是不会驱动视图更新的,例如:
let num1 = 1
let num2 = 2
let result = num1 + num2
console.log(result) // 3
num1 = 2
console.log(result) // 3,没有改变
2
3
4
5
6
# 二、vue2响应式原理?
# 1、响应式API
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj,prop,descriptor)接收三个参数,
obj:要定义属性的目标对象;
prop:要定义或修改属性的名称或Symbol;
descriptor:要定义或修改的属性描述符;返回值是被传递给函数的对象。
2
3
4
5
6
7
属性描述符包括:
- 数据描述符:具有值的属性,该值可以是可写的( writable: true),也可以是不可写的( writable: false)
存取描述符:由getter函数和setter函数所描述的属性。一个描述符只能是数据描述符或者存取描述符,不能同时是两者。
属性描述符描述的都是对象,在使用Object.defineProperty()定义属性时候有一些可选的键值。如下:
configurable(可配置的):当设置值为true的时候,表示该属性的描述符可以被改变,同时该属性也能从对应的对象上删除。默认为false。
enumerable(可枚举的):当设置值为true时,表示该属性是可枚举属性。默认为false。
数据描述符的可选键值:
value(属性值):可以设置任何有效的Javascript值(数值,对象,函数等),默认为undefined。
writable(可写的):当设置的值为true时,代表value可以被重新赋值。默认为false。
存取描述符的可选键值:
set:属性的setter函数,若没有setter,则为undefined。当属性值被修改时,会调用该函数。写法:set(newValue){oldValue=newValue}。默认为undefined。
get:属性的getter函数,若没有getter,则为undefined。当访问属性的时候,会调用该函数。执行的时候不需要传入任何参数,但是会传入this对象。该函数的返回值会被用作属性的值。写法:get(){return oldValue}。默认为undefined。
另外,defineProperty应当直接在Object构造器对象上调用此方法,而不是在任意一个Object类型的实例身上调用。
举个例子:
const obj_origin = {}
Object.defineProperty(obj_origin, 'name', { //劫持对象obj_origin
value: '秦霄贤',
writable: false
})
obj_origin.name = '哈尼~'//非严格模式下
console.log(obj_origin.name); //秦霄贤
2
3
4
5
6
7
8
# 2、响应式原理
// vue2.0 响应式原理--数据变了 可以更新视图
function type(data) {
return Object.prototype.toString.call(data).slice(8, -1) // 'Number'
}
let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype) // 继承
// pushxxxx 函数劫持, 把函数重写
Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {
proto[method] = function() {
oldArrayPrototype[method].call(this, ...arguments)
updateView()
}
})
// observer函数专门用于劫持数据
function observer(target) {
if (typeof target !== 'object' || typeof target == null) { // 崩
return target
}
if (Array.isArray(target)) {
// target.__proto__ = proto
Object.setPrototypeOf(target, proto)
}
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 响应式
function defineReactive(target, key, value) {
observer(value)//这个递归是默认执行的
// Object.defineProperty () 可以重新定义属性 给属性安插 getter setter 方法
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
value = newVal
updateView()
}
}
})
}
function updateView() {
console.log('更新视图');
}
let data = {
name: 'hunny',
age: {
n: 19
},
hobby: ['reading']
}
observer(data)
// (1)当改变属性是基本数据类型的时,不触发更新视图
data.name = 'hunny'
// (2)但改变属性是对象类型的值时,触发更新视图
data.age.n = 101
data.age = {
n: 102
}
// (3)当属性不存在时,不触发更新视图
data.sex = 'boy'
// (4)当改变数组类型,触发更新视图
Array.prototype.push = function() {}
data.hobby.push('singing')
// (5)当改变数组原本的长度,数组长度会改变,会放两个empty,占位符,但是不触发更新视图
data.hobby.length = 5
console.log(data.hobby.length); //5,实际上是2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# 3、缺点
(1)vue2 对象中不存在的属性不能被拦截, Object.defineProperty()只能劫持对象
(2)数组改变length属性不会驱动视图更新,虽然长度被改变了。
(3)当对象是一个嵌套的对象的时候,需要使用递归进行深度监听,vue2当中 递归是默认执行的
(4)vue2中无法监听到对象的增删,所以vue2还补充了Vue.$set(),Vue.$delete()等其他方法进行弥补。
(5)对象新增的属性没有响应式 数组的部分操作没有响应式。数组的一些方法会改变数组,在vue2中被重写。
# 三、vue3响应式原理?
# 1、响应式API
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。const myProxy=new Proxy(target,handler)。target:被Proxy代理虚拟化的目标对象(整个对象),可以是任何类型的对象,包括原生数组、函数,另一个代理等;handler:一般以函数作为属性的对象,各睡醒的函数分别定义了在执行各种操作时代理的myProxy的行为。
Proxy的内置方法:
- Proxy.revocable():创建一个可撤销的Proxy对象
handler独享是一个容纳一批特定属性的占位符对象。它包含Proxy的各个捕获器。
handler对象上的方法:(以实现响应式原理用到的方法进行列举)
handler.has():针对in操作符的代理方法
handler.set():设置属性值操作的捕获器
handler.get():拦截对象的读取属性的操作
handler.deleteProperty():拦截对对象属性的删除操作
举个例子:
const obj_origin = {}
const handler = {
get(obj, name) {
return name in obj ? obj[name] : '嘻哈'
}
}
const myProxy = new Proxy(obj_origin, handler)
myProxy.name = '嗯哼~'
myProxy.age = undefined
console.log(myProxy.name, myProxy.age); //嗯哼~ undefined
console.log(myProxy.gender);//嘻哈
2
3
4
5
6
7
8
9
10
11
12
# 2、响应式原理
// vue3 响应式原理
let toProxy = new WeakMap() // 弱引用映射表 存放原对象:代理过的对象
let toRaw = new WeakMap() // 代理过的对象:原对象
// 判断对象
function isObject(val) {
return typeof val === 'object' && val !== null
}
function hasOwn(target, key) {
return target.hasOwnProperty(key)
}
// 核心方法
function reactive(target) {
return createReactiveObject(target)
}
// 创建响应式对象
function createReactiveObject(target) {
if (!isObject(target)) { // 原始类型,直接返回
return target
}
let proxy = toProxy.get(target)
if (proxy) { // 代理过
return proxy
}
if (toRaw.has(target)) { // 防止多层代理
return target
}
let baseHandler = {
get(target, key, receiver) {
// reflect对象上的方法与proxy上的方法一一对应
let res = Reflect.get(target, key, receiver) // reflect 做反射
return isObject(res) ? reactive(res) : res
},
set(target, key, value, receiver) { // [1,2,3,4]
// 判断是新增属性还是修改属性
let hadKey = hasOwn(target, key)
let oldValue = target[key]
let res = Reflect.set(target, key, value, receiver)
if (!hadKey) {
console.log('新增属性');
} else if (oldValue !== value) {
console.log('修改属性');
}
return res
},
deleteProperty(target, key) {
let res = Reflect.deleteProperty(target, key)
console.log('删除属性');
return res
}
}
let observed = new Proxy(target, baseHandler)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
// 数组
let users = ['cat', 'dog', 'mice']
let myProxy = reactive(users)
// (1)当给数组添加元素时,'新增属性'
myProxy.push('fish')
// (2)当改变数组原本长度时,'修改属性'
myProxy.length = 5
// (3)当进行多层代理时, 利用get读取时才代理
let otherProxy = reactive({
name: 'bird',
food: {
type: 'meat'
},
hobbies: ['flying', 'singing']
})
// 当修改基本类型的属性值,'修改属性'
otherProxy.name = 'duck'
// 当删除基本类型的属性时,'删除属性'
delete otherProxy.name
console.log(otherProxy.name); //undefined
// 当改变对象类型的属性时,'修改属性'
otherProxy.food.type = 'leaves'
console.log(otherProxy.food.type); //leaves
// 被代理过的对象需要记录一下,不要再次代理了
// reactive(otherProxy)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 3、对比vue2
(1)Proxy代理的对象是整个对象,而不是对象的某个特定的属性。
(2)代理之后操作的对象是实例对象,而不是原对象。
(3)vue3的响应式原理不需要递归,性能比较好。