Vue底层学习2——手撸数据响应化
全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/14982040.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)
作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上一篇聊了聊大家熟知的理论部分,本篇就来手撸数据响应化代码,即数据遍历并重写
setter
及getter
~
Object.defineProperty(obj, prop, descriptor)
它是实现数据响应式的核心,该方法可以直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
obj
:要定义或修改的属性对象;prop
:要定义或修改的属性名称;descriptor
:要定义或修改的属性描述,详细说明可参见我之前写的《Javascript基础巩固系列——标准库Object对象》中属性描述对象章节;
下面是一个自定义setter和getter的简单例子:
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get: function() {
console.log('get!');
return temperature;
},
set: function(value) {
temperature = value;
archive.push({ val: temperature });
}
});
this.getArchive = function() { return archive; };
}
var arc = new Archiver();
arc.temperature; // 访问temperature属性时会调用get方法,控制台打印:'get!'
arc.temperature = 11 // 修改temperature属性时会调用set方法,为archive数组添加日志条目:{val: 11};
arc.temperature = 13; // 修改temperature属性时会调用set方法,为archive数组添加日志条目:{val: 13};
arc.getArchive(); //调用getArchive方法返回archive数组:[{ val: 11 }, { val: 13 }]
通过Object.defineProperty读取和设置DOM节点内容
在上一篇做原理解析的时候有提到Vue是通过Object.defineProperty
重写data
对象中各个属性的setter
及getter
,用于实现【响应式】和【依赖收集】。那么我们先做一件事,通过Object.defineProperty
读取和设置DOM节点内容,以此体会一下数据驱动视图的概念。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>defineProperty</title>
</head>
<body>
<div id="app">
<p id="name"></p>
</div>
<script>
var obj = {};
Object.defineProperty(obj, 'name', {
get: function() {
// 访问obj对象的name属性时,获取id为name的节点内容
return document.querySelector('#name').innerHTML;
},
set: function(value) {
// 修改obj对象的name属性时,设置id为name的节点内容为修改后的值
document.querySelector('#name').innerHTML = value;
}
})
// 数据驱动视图变更
obj.name = 'dreamsyang';
</script>
</body>
</html>
运行结果如下,可以看到obj
对象的name
属性值被修改后DOM节点内容也同步更新:
自建数据响应式框架
有了上面的例子做铺垫应该对响应式有些许感觉,接下来我们自己搭建一个Vue响应式框架,需要达到的效果就是在new
一个Vue实例之后实现数据初始化,达到响应式效果~为了区别于Vue,我重新命名为MVue
。
新建MVue.js
文件用于MVue
类封装,参数接收配置对象
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {
// 数据缓存
this.$options = options;
this.$data = options.data;
// 数据遍历
this.observe(this.$data);
}
}
通过observe
实现data
数据遍历
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {...}
observe(data) {
// 确定data存在并且为对象
if (!data || typeof data !== 'object') {
return;
}
// 遍历data对象
Object.keys(data).forEach(key => {
// 重写对象属性的getter和setter,实现数据的响应化
this.defineReactive(data, key, data[key]);
})
}
}
通过defineReactive
重写getter
和setter
实现数据响应化
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {...}
observe(data) {...}
defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get: function() {
return val;
},
set: function(newVal) {
// 判断属性值是否发生变化
if (newVal === val) {
return;
}
val = newVal;
// 预留视图更新
console.log(`${key}属性更新了:${val}`);
}
})
}
}
自建框架测试demo1
完成上述步骤后先看看目前的效果,写个小demo测试一下:
<!-- demo1.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo1</title>
</head>
<body>
<script src="MVue.js"></script>
<script>
const app = new MVue({
data: {
name: 'dreamsyang',
infoObj: {
location: 'chongqing',
}
}
})
app.$data.name = 'hello, dreamsyang!';
app.$data.infoObj.location = 'oh, chongqing!';
</script>
</body>
</html>
运行结果如下,可以看到app.$data.infoObj.location = 'oh, chongqing!'
并未触发setter
中的打印,其主要原因是我们在遍历data
时不是深度遍历:
通过递归实现深度遍历
我们只需要在defineReactive
执行的开始再次调用observe
即可,如果val
不为对象,就会结束执行,如果为对象就会深度遍历。
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {
// 数据缓存
this.$options = options;
this.$data = options.data;
// 数据遍历
this.observe(this.$data);
}
observe(data) {
// 确定data存在并且为对象
if (!data || typeof data !== 'object') {
return;
}
// 遍历data对象
Object.keys(data).forEach(key => {
// 重写对象属性的getter和setter,实现数据的响应化
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, val) {
// 解决数据嵌套,递归实现深度遍历
this.observe(val);
Object.defineProperty(obj, key, {
get: function() {
return val;
},
set: function(newVal) {
// 判断属性值是否发生变化
if (newVal === val) {
return;
}
val = newVal;
// 预留视图更新
console.log(`${key}属性更新了:${val}`);
}
})
}
}
再次执行demo1
结果如下,可以看到正常打印了:
参考资料
1、Object.defineProperty
:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty;
2、Vue源码:https://github.com/vuejs/vue;