github: https://github.com/OUDUIDUI/vue-source-code-study/tree/vue
Vue的设计思想
Vue
设计思想参考了MVVM
模型,即将视图View
和行为Model
抽象化,即将视图UI和业务逻辑分开来,然后通过ViewModel
层来实现双向数据绑定。
MVVM
与 MVC
最大的不同就是MVVM
实现了 View
和 Model
的自动同步,也就是当Model
的属性改变时,我们不用再自己手动操作 Dom
元素,来改变 View
的显示,而是改变属性后该属性对应 View 层显示会自动改变。
MVVM
框架的三个要素:数据响应式、模板引擎及其渲染。
- 数据响应式
- 监听数据变化并在视图中更新
- 在
Vue2.x
中,是根据Object.defineProperty()
来实现数据响应式的
- 模板引擎
- 提供描述视图的模板语法
Vue
的插槽{ {}}
和指令v-bind
、v-on
、v-model
等
- 渲染
- 将模板渲染成
HTML
进行显示
- 将模板渲染成
数据响应式原理
在JavaScript
的对象Object
中有一个属性叫访问器属性,其中有[[Get]]
和[[Set]]
特性,它们分别是获取函数或设置函数,即在获取对象特定属性的时候回调用到。
而访问器属性是不能直接定义的,必须使用Object.defineProperty()
进行定义。
const obj = {
_name: 'Matt'
};
Object.defineProperty(obj, 'name', {
get() {
return this._name;
},
set(newVal) {
console.log('set name')
this._name = newVal;
}
})
console.log(obj.name); // 'Matt'
obj.name = 'OUDUIDUI'; // 'set name'
console.log(obj.name); // 'Henry'
而Vue2.x
就是在set
函数中进行监听,当数据发生变化了,就会进行响应操作。
因此,我们可以简单实现一个Vue
中的defineReactive
函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>reactive app</title>
</head>
<body>
<div id="app"></div>
<script>
/**
* defineReactive : 将对象中某一个属性设置为响应式数据
* @param obj<Object>: 对象
* @param key<any>: key名
* @param val<any>: 初始值
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${
key}`)
return val; // 此时val存在obj的闭包里面
},
set(newVal) {
console.log(`set ${
key}`)
if (newVal !== val) {
val = newVal;
update(); // 更新函数
}
}
})
}
/**
* update : 更新函数,重新渲染app DOM
*/
function update() {
const app = document.getElementById('app');
app.innerHTML = `obj.time = ${
obj.time}`
}
const obj = {
};
defineReactive(obj, 'time', new Date().toLacaleTimeString()); // 将obj进行响应式处理
setInterval(() => obj.time = new Date().toLacaleTimeString(), 1000); // 定时更新obj.time的值
</script>
在代码中,我们在set
中,调用了update
更新函数,因此我们定时器每更新obj.time
一次,update
函数就会被调用一次,因此页面数据也会更新一次。这时候,我们就简单的实现了数据响应式。
但defineReactive
函数有个问题,就是一次只能对一个属性值进行响应式处理,而且如果这个属性是个对象的话,我们更改对象里面的值的时候,是实现不了响应式的。
const obj = {
};
defineReactive(obj, 'info', {
name: 'OUDUIDUI', age: 18}); // 将obj进行响应式处理
setTimeout(() => obj.info.age++, 1000); // 这时候不会触发set函数
因此,我们需要一个新的方法去实现对整个对象进行响应式处理,在Vue
中这个方法叫observe
。
在这个函数中,我们先需要对传入的obj
进行类型判断,然后对对象进行遍历,对每一个属性进行响应式处理。这个地方需要对数组做处理,这个放到后面再说。
/**
* observe: 将整个对象设置为响应式数据
* @param obj<Object>: 对象
*/
function observe(obj) {
// 如果obj不是对象的话,跳出函数
if (typeof obj !== "object" || obj === null) {
return;
}
// 判断传入obj的类型
if(Array.isArray(obj)){
// TODO
}else {
// 遍历obj所有所有key,做响应式处理
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
})
}
}
同时,我们需要实现对这个对象一个递归处理,因此我们需要修改一下defineReactive
函数。我们只需要在最开始的地方,调用一次observe
函数,如果传入的val
是对象,就会进行递归响应式处理,如果不是就返回。
function defineReactive(obj, key, val) {
observe(val); // 递归处理:如果val是对象,继续做响应式处理
Object.defineProperty(obj, key, {
...
})
}
我们来测试一下:
const obj = {
time: new Date().toLocaleTimeString(),
info: {
name: 'OUDUIDUI',
age: 18
}
};
observe(obj);
setInterval(() => {
obj.time = new Date().toLocaleTimeString();
}, 1000)
setTimeout(() => {
obj.info.age++;
}, 2000)
这里还有一个小问题,就是如果obj
原本有一个属性是常规类型,即字符串、数值等等,然后再将其改为引用类型时,如对象、数值等,该引用类型内部的属性,是没有响应式的。比如下来这种情况:
const obj = {
text: 'Hello World',
};
observe(obj); // 响应式处理
obj.text = {
en: 'Hello World' }; // 将obj.text由字符串改成一个对象
setTimeout(() => {
obj.text.en = 'Hi World'; // 此时修改text对象属性页面是不会更新的,因为obj.text.en不是响应式数据
}, 2000)
对于这种情况,我们只需要在defineReactive
函数中,set
的时候调用一下observe
函数,将newVal
传入,如果是对象就进行响应式处理,否则就直接返回。
function defineReactive(obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
get() {
console.log(`get ${
key}`)
return val;
},
set(newVal) {
console.log(`set ${
key}`)
if (newVal !== val) {
observe(newVal); // 如果newVal是对象,再次做响应式处理
val = newVal;
update();
}
}
})
}
我们测试一下。
function update() {
const app = document.getElementById('app');
app.innerHTML = `obj.text = ${
JSON.stringify(obj.text)}`
}
const obj = {
text: 'Hello World'
};
// 响应式处理
observe(obj);
setTimeout(() => {
obj.text = {
// 将obj.text由字符串改成一个对象
en: 'Hello World'
}
}, 2000)
setTimeout(() => {
obj.text.en = 'Hi World';
}, 4000)
最后我们来完成前面楼下的一个问题,就是数组的响应式处理。
之所以数组需要特殊处理,因为数组有七个自带方法可以去处理数组的内容,分别是