一、 Object.defineProperty
在学习vue响应式原理之前,必须搞懂 Object.defineProperty
。
Object.defineProperty(obj, prop, descriptor)
看到一篇写的十分不错的博客:理解Object.defineProperty方法。
二、vue响应式更新
2.1响应式
所谓响应式,简单说就是用户更改数据(Data)时,视图可以自动刷新
,页面UI能够响应数据变化。
Vue 最独特的特性之一,是其非侵入性的响应式系统。
—— 尤雨溪
看一个没有响应式的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<button onclick="flushMsg()">更新msg</button>
<script>
let app = document.getElementById('app');
let msg = "123";
app.innerHTML = `<p>msg: ${ msg }</p>`;
function flushMsg(){
console.log('点击了按钮,msg更新为12345');
msg = "12345";
}
</script>
</body>
</html>
效果:
可以看到,我通过按钮点击事件改变了msg的值,但是页面是不会自动改变的,即没有实现响应式。那么怎么实现呢,很简单,将点击事件改为:
function flushMsg(){
console.log('点击了按钮,msg更新为12345');
msg = "12345";
app.innerHTML = `<p>msg: ${ msg }</p>`
}
即我们改变了数据后,需要重新操作dom元素,更新数据,还是很简单的。可是当我们的项目变得复杂的时候,一个页面的数据会变得非常多,那么我们要给每一个变量都进行相应的处理,即数据改变,则调用dom改变视图。还好,vue帮我们完成了这步操作,我们只需要在data
中声明数据(m
),在视图中展示
数据(v
),其他的交给vue吧(vm
)。所以我们也称vue为mvvm框架。
2.2 vue响应式
首先直接列出几个核心:
数据劫持 / 数据代理
侦测数据的变化依赖收集
收集视图依赖了哪些数据发布订阅模式
数据变化时,自动“通知”需要更新的视图部分,并进行更新
2.2.1 数据劫持
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function render() {
console.log('模拟视图渲染, 页面应该改变')
}
let data = {
name: 'yancy',
language: ['c', 'java', 'javascript']
}
observe(data)
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive(obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function() {
console.log('触发get:', value) // 监听
return value
},
set: function(newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal !== value) { //数据变化
console.log('触发set:', newVal) // 监听
value = newVal
render(); //数据改变 重新渲染视图
}
}
})
}
}
</script>
</body>
</html>
我们将储存着数据的对象data
的所有属性使用Object.defineProperty
进行监听,获取属性的值,触发get函数。更改属性的值,触发set函数,就实现了数据劫持,所以我们页面的数据都要在vue实例中的data属性中声明,因为vue只会劫持data对象中的数据。
2.2.2 依赖收集
上面我们已经能够劫持观察到数据的变化了,那么下一步便是将数据的变化渲染到页面上,2.2.1我们使用render()函数一笔带过了。
想要把数据的改变渲染到页面上,我们应该要知道页面中什么地方使用到了数据,然后重新渲染。
我们只有通过收集依赖才能知道哪些地方依赖
我的数据,以及数据更新时派发更新。那依赖收集是如何实现的?其中的核心思想就是“事件发布订阅模式
”。
2.2.3 发布订阅
当数据变化触发依赖,dep(发布者)通知所有的Watcher(观察者)实例更新视图。
至于vue数据更新后的具体渲染过程,那么就涉及了虚拟DOM和diff算法,得再写一篇博客学习一下。
三、双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="app">
<input type="text" name="" id="input">
<p id="p"></p>
</div>
<script>
let input = document.getElementById('input');
let p = document.getElementById('p');
var obj = {}
Object.defineProperty(obj,'msg',{
set: function(newVal){
input.value = newVal;
p.innerHTML = newVal;
}
})
input.addEventListener('keyup',function(e){
obj.msg = e.target.value;
})
</script>
</body>
</html>
可以看到,关键是代码:
Object.defineProperty(obj,'msg',{
set: function(newVal){
input.value = newVal;
p.innerHTML = newVal;
}
})
作用设置obj上的msg属性被写入时触发的set函数。