前言
学习前端到了一定的程度,尤其看了部分 jQuery
源码后,我想尝试着自己去动手造轮子。
但是我的方向或者参考貌似只能是 Vue
。毕竟它的 MVVM
(Model-View-ViewModel)架构实在是好用,我们只需要关注自己的数据,去改变数据后视图会自动的进行更新。所以我决定自己动手实现一遍 Vue
的响应式原理。进而更深刻的理解 MVVM
这种模式及其好处。
所以在本文开始之前你必须已经对 Vue 的基本使用有过了解
若您在观看图中对代码的高亮要求较高,也可直接去 语雀 平台查看
首先附上本文所要讲解的内容范围,我会对图中所有知识点进行讲解并逐步分析实现。
基本使用
下面是我们使用 Vue
的时候的一段代码:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 响应式原理</title>
</head>
<body>
<div id="app">
<input type="text" v-model="sentence">{
{ sentence }}
<p>{
{ name }} --- {
{ age }}</p>
</div>
<script type="module">
import Vue from './Vue.js'
const vm = new Vue({
el: '#app',
data() {
return {
name: 'Jerry',
age: 20,
sentence: 'Hello MVVM',
info: {
height: 180
}
}
}
})
console.log(vm)
window.vm = vm // 便于浏览器中使用 vm 变量测试
</script>
</body>
</html>
我们在使用 Vue
的时候向其传入了一个对象。并且在 data
中的数据可以实现响应式。
接下来我们先来实现数据的代理,即当我们使用 vm.name
的时候也可以拿到 Jerry
。
数据代理
数据代理之前我们先创建 Vue
这个类:
// Vue.js
export default class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
if (typeof options.data === 'function') {
this.$data = options.data()
} else if (typeof options.data === 'object' && options.data !== null) {
this.$data = options.data
} else {
console.error('data is must be a function or object')
}
this.$options = options
// 数据代理
}
}
接下来实现通过 vm
也可以拿到 vm.$data
上的属性,并且可以同步修改,原理其实就是 Object.defineProperty()
。
// Vue.js
export default class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
if (typeof options.data === 'function') {
this.$data = options.data()
} else if (typeof options.data === 'object' && options.data !== null) {
this.$data = options.data
} else {
console.error('data is must be a function or object')
}
this.$options = options
// 数据代理
this.proxyData()
}
proxyData() {
for (const key in this.$data) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: false,
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
}
通过代理,我们就可以在 Vue
的示例对象上访问到 data
中的数据了,并且实现了同步,测试如下:
到这里我们的数据代理已经实现好了,接下来我们实现响应式系统。
Object 响应式
Observer
首先,我们来使 data
中的数据变成响应式的,要变成响应式的就必须被观测,所以我们创建一个 Observer
类对数据进行观测:
// Observer.js
export default class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
}
}
并且我们要在 new Vue
的时候就要对数据进行观测,所以在 Vue
的构造方法中我们需要实例化 Observer
:
// Vue.js
import Observer from "./Observer.js"
export default class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
if (typeof options.data === 'function') {
this.$data = options.data()
} else if (typeof options.data === 'object' && options.data !== null) {
this.$data = options.data
} else {
console.error('data is must be a function or object')
}
this.$options = options
this.proxyData()
// 数据观测/劫持
new Observer(this.$data) // 新增
}
proxyData() {
for (const key in this.$data) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: false,
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
}
接着我们就来看一下 Observer
这个类的具体实现,我们都知道 Vue
的响应式的核心是使用了 Object.defineProperty
来将对象的属性变成 getter
和 setter
形式来实现的,所以我们一起来实现一下:
// Observer.js
export default class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
for (const key in data) {
const value = data[key]
defineReactive(data, key, value)
}
}
}
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newVal) {
value = newVal
}
})
}
我们这样设置后会有两个问题:
- 我们现在只对
vm
的第一层属性添加了响应式并没有对其子属性添加响应式 - 试想如果改变了
vm.info = {color: 'red'}
那么新添加的对象{color: 'red'}
并不具备响应式
对于子属性我们需要递归添加响应式,对于新赋的值我们一应该让其具备响应式:
// Observer.js
export default class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
for (const key in data) {
const value = data[key]
defineReactive(data, key, value)
}
}
}
function defineReactive(obj, key, value) {
// 新增 递归子属性添加响应式
if (typeof value === 'object' && value !== null) {
new Observer(value)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newVal) {
if (value !== newVal) {
value = newVal
// 为赋的新值添加响应式
new Observer(value)
}
}
})
}
不过我们如果只是向这样简单的将对象的属性变成 getter
和 setter
的形式那么也没有任何意义。
试想我们的目的是为了收集页面中使用了我们的 data
中数据的地方,即收集依赖。而页面中只要有对数据的依赖,那么必定会走 get
方法获取数据,所以我们应该在 get
方法中收集依赖。
当我们收集到这些依赖后,要在数据发生变化时,要通知这些依赖进行更新视图。而数据的变化又必定会触发 set
方法,所以我们要在这里去通知依赖。
所以分析到这里我们发现,我们需要有一个类来帮助我们收集依赖,并且具有通知依赖去更新视图的功能,假设这个类是 Dep
,那么我们还需要在下面新增的两个位置进行操作:
// Observer.js
export default class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
for (const key in data) {
const value = data[key]
defineReactive(data, key, value)
}
}
}
function defineReactive(obj, key, value) {
if (typeof value === 'object' && value !== null) {
new Observer(value)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖 新增
return value
},
set(newVal) {
if (value !== newVal) {