[TOC]
写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。本博客的Github地址。
响应式理解
当js对象中的数据发生改变的时候,与js对象中数据相关联的DOM视图也会进行更新(即所谓的数据驱动视图)。
数据驱动视图
Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。
响应式实现思路
想要实现响应式,需要做如下事情:
监听对象数据的变化。
收集视图依赖了哪些数据(依赖收集)。
数据变化时,自动通知和数据相关联的视图页面,并对视图进行更新。
利用Proxy或Object.defineProperty生成的Observer针对对象/对象的属性进行"劫持",在属性发生变化后通知订阅者;
解析器Compiler解析模板中的Directive(指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染;
Watcher是Observer和Compile桥梁,它将接收到的Observer产生的数据变化,并根据Compiler提供的指令进行视图渲染,使得数据变化促使视图变化。
如何监听对象数据的变化?
对象数据监听也叫做数据劫持,Vue采用数据劫持加发布者-订阅者模式,通过Object.defineProperty来劫持各个属性的setter,getter。在数据变化时发送消息给订阅者,触发相应的监听回调。当然也可以使用ES6中的Proxy来对各个属性进行代理(推荐)。
当把一个普通的JavaScript对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。 深入响应式原理
响应式代码实现
在ES5中,新增了Object.defineProperty这个API,允许我们为对象的属性设定getter和setter。我们可以使用该API对该对象的属性值获取或设置进行劫持。
// Vue2.0如何实现响应式原理
// 数据变化了,可以更新视图
function observer(target) {
// 判断target是否为对象
if (typeof target !== 'object' || target == null) {
return target;
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
// 定义响应式
defineReactive(target, key, target[key]);
}
}
}
function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
if (newValue !== value) {
updateView();
value = newValue;
}
}
});
}
function updateView() {
console.log('数据更新了');
}
// 只是一层
const person = {name: 'kobe'};
observer(person);
person.name = 'james';
console.log(person.name);
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
注意点1:针对key的value也是对象的情况(需要递归进行绑定)。
// Vue2.0如何实现响应式原理
// 数据变化了,可以更新视图
function observer(target) {
if (typeof target !== 'object' || target == null) {
return target;
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
defineReactive(target, key, target[key]);
}
}
}
function defineReactive(target, key, value) {
// 注意点1:针对key的value是对象的情况,递归遍历子对象
observer(value);
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
if (newValue !== value) {
updateView();
value = newValue;
}
}
});
}
function updateView() {
console.log('数据更新了');
}
const person = {name: 'kobe', age: {value: 12}};
observer(person);
// person.name = 'james';
person.age.value = 14;
console.log(person.age.value);
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
注意点2:针对给key重新赋值的value是对象的情况。
// Vue2.0如何实现响应式原理
// 数据变化了,可以更新视图
function observer(target) {
if (typeof target !== 'object' || target == null) {
return target;
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
defineReactive(target, key, target[key]);
}
}
}
function defineReactive(target, key, value) {
// 注意点1:针对key的value是对象的情况,递归
observer(value);
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
// 注意点2:针对给key重新赋值的value是对象的情况,如果新值是对象的话,递归该对象进行监听
observer(newValue);
if (newValue !== value) {
updateView();
value = newValue;
}
}
});
}
function updateView() {
console.log('数据更新了');
}
const person = {name: 'kobe', age: {value: 12}};
observer(person);
person.age = {value: 13};
person.age.value = 16;
// 应该输出两次'数据更新了',因为age和value都是响应式的
console.log(person.age.value);
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
Object.defineProperty的缺陷
问题:
- 对于对象新增的属性将不会是响应式的
- 不支持属性值是数组的情况
实现数组劫持
// Vue2.0如何实现响应式原理
// 数据变化了,可以更新视图
const oldArrayPrototype = Array.prototype;
const proto = Object.create(oldArrayPrototype); // 继承数组原型的方法
['push', 'shift', 'unshift'].forEach(method => {
// 函数劫持,把函数进行重写,内部继续调用老的数组方法
proto[method] = function() {
updateView(); // 面向切片编程
oldArrayPrototype[method].call(this, ...arguments);
}
});
function observer(target) {
if (typeof target !== 'object' || target == null) {
return target;
}
if (Array.isArray(target)) { // 拦截数组,将数组的方法进行重写
Object.setPrototypeOf(target, proto);
// target.__proto__ = proto;
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
defineReactive(target, key, target[key]);
}
}
}
function defineReactive(target, key, value) {
// 注意点1:针对key的value是对象的情况,递归
observer(value);
Object.defineProperty(target, key, {
get() { // get中进行依赖收集
return value;
},
set(newValue) {
// 注意点2:针对给key重新赋值的value是对象的情况
observer(newValue);
if (newValue !== value) {
updateView();
value = newValue;
}
}
});
}
function updateView() {
console.log('数据更新了');
}
const person = {name: 'kobe', hobbies: ['🏀', '⚽️']};
observer(person);
person.hobbies.push('🏉'); // 需要对数组的方法进行重写
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
Vue中数组的this.$set实现
<body>
<div id="app-4">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
<li v-for="i in nums">
{{i}}
</li>
</ol>
<p>{{obj}}</p>
</div>
<script>
var app4 = new Vue({
el: '#app-4',
data: {
todos: [
{ text: '学习 JavaScript' },
{ text: '学习 Vue' },
{ text: '整个牛项目' }
],
nums: [1, 2, 3],
obj: {
name: 'lisi'
}
},
mounted() {
this.add();
// this.setObj();
},
methods: {
add() {
console.log(this.nums);
// this.nums[0] = 6; // 这样改的话,数组中的值是发生变化了,但是页面并不会重新渲染
this.$set(this.nums, 0, 6); // 需要调用this.$set方法
console.log(this.nums);
},
setObj() {
// this.obj.age = 12; // 这样改的话,页面不会重新渲染
console.log(this.obj);
this.$set(this.obj, 'age', 18);
console.log(this.obj);
this.obj.age = 12;
}
}
});
</script>
</body>
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
需要注意:在Vue中对数组的下标赋值处理时,是不会触发视图的更新,于是Vue提供了一个静态方法:set。
this.$set方法本质上是通过改写后的splice方法来实现的:Vue中只重写了7个数组方法,分别是push,shift,unshift,pop,reverse,sort,splice。
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val) // 利用扩展的splice方法进行响应式
return val
}
2
3
4
5
// /util/lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
2
3
4
5
6
7
8
9
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
// def方法是对Object.defineProperty的封装
import {def} from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
// 遍历要重写的方法
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method] // 缓存之前的原型方法
// arrayMethods是数组原型的副本
// 将数组方法进行重写,返回结果不变,只不过在返回结果前做了一些事情
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args) // 还是调用原生数组方法进行结果获取
const ob = this.__ob__ // 是否响应式的标志,保存了依赖收集对象deps
let inserted // 保存要插入的参数值,是一个数组
switch (method) {
case 'push':
case 'unshift': // 插入操作
inserted = args // 将要插入的参数赋值给inserted
break
case 'splice':
inserted = args.slice(2) // 获取到要插入的参数
break
}
if (inserted) ob.observeArray(inserted) // 进行数据响应式
// notify change
ob.dep.notify() // 手动更新变化
return result
})
})
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
// observer/index.js
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
2
3
4
5
6
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
所以,在Vue中调用数组的push、pop等方法时其实不是直接调用的数组原型给我们提供的push、pop等方法,而是调用的重写后的push、pop等方法。
Vue.set数组实现的原理:其实Vue.set()对于数组的处理其实就是调用了splice方法(而splice是被重写过的,会触发视图更新)。
this.$set
)
实现对象新增属性劫持(Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:
var vm = new Vue({
data: {
a: 1
}
});
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
2
3
4
5
6
7
8
9
10
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。例如,对于:
Vue.set(vm.someObject, 'b', 2)
您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:
this.$set(this.someObject, 'b', 2)
Vue.set和this.$set实现原理
先来看Vue.set的源码:
import {set} from '../observer/index'
...
Vue.set = set // 静态属性
...
2
3
4
5
再来看this.$set的源码:
import { set } from '../observer/index'
...
Vue.prototype.$set = set // 原型属性
...
2
3
4
5
发现Vue.set和this.$set这两个api的实现原理是一致的,都是使用了set函数。set函数是从../observer/index
文件中导出的,区别在于Vue.set()是将set函数绑定在Vue构造函数上,this.$set()是将set函数绑定在Vue原型上。
接下来看下../observer/index
中导出的set函数:
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 先判断如果key本来就是对象中的一个属性,并且key不是Object原型上的属性。说明这个key本来就在对象上面已经定义过了的,直接修改值就可以了,可以自动触发页面刷新
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
// 给新的属性定义响应式,以后修改新的属性值也会触发视图更新
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
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
使用Proxy来实现数据劫持
Object.defineProperty方法存在如下缺点:
- 监听数组的方法不能触发Object.defineProperty方法中set操作(如果我们需要监听的话,我们需要重写数组的方法)。
- 必须遍历每个对象的每个属性,如果对象嵌套比较深的话,我们需要递归调用。