响应式原理实现
昨天讲完响应式的基本原理,今天就动手做个简单的,然后顺便把Vue中的实现讲讲
手动造一个简单的试试水
我们先自己实现一个响应式的思路
- _init进行依赖收集
- defineReactive进行数据劫持函数定义
- 触发监听事件
- 派发更新
HTML结构
下面先写一个简单的结构用于后面的说明:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>响应式实例</title>
</head>
<body>
<button class="test-btn">Click</button>
<p>You have clicked <span class='sum'>0</span> </p>
<script src="./main.js"></script>
</body>
</html>
main.js
js部分首先是_init函数
function _init () {
// 定义数据劫持
observeObj(reactiveObj);
// 添加监听事件
let button = document.querySelector('.test-btn')
button.addEventListener('click', () => {
reactiveObj.clickSum += 1
})
// 模拟Dep.target,用于标明函数执行环境,介绍见Vue实现解析
currentTarget = 'window'
// 模拟Vue的更新
fakeUpdateComponentFunc()
// 恢复Dep.target
currentTarget = ''
}
数据劫持实现
对对象的属性进行遍历,通过Object.defineProperty进行劫持函数的编写
function observeObj (reactiveObj) {
for (let key in reactiveObj) {
defineReactive(reactiveObj, key)
}
}
function defineReactive (obj, key) {
let val = obj[key]
let dep = ''
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { },
set: function reactiveSetter (newValue) { }
})
}
重点介绍下getter和setter函数
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
get: function reactiveGetter () {
console.log('getter has been called')
// 这里进行依赖收集
// 判断当前的执行环境,将dep保存给setter更新时用
if (currentTarget) {
// dep.depend()
console.log('===> dep collection done')
}
return val
},
set: function reactiveSetter (newValue) {
console.log('setter has been called')
if (newValue === val) {
return
}
console.log('===> value has been set')
val = newValue
// 这里进行派发更新,其实notify就会执行updateComponent,这里做了省略
// dep.notify()
fakeUpdateComponentFunc()
}
})
我们在来看看updateComponent函数,这里模拟了vue.$mount过程中执行的两个关键函数
function fakeUpdateComponentFunc () {
(function _render() {
// do nothing...
})(); // template/render func => vnode
(function _update() {
applyToDom('.sum', reactiveObj.clickSum)
})() // vnode => dom
}
然后就完啦,一个简单的响应式系统就实现了233,可以运行下看看效果哈
Vue中的响应式实现
看完上面的解释是不是感觉条理清晰很多呀,基本上Vue的实现思路即是如此,那么我们来看看源代码中的执行思路
我们先来看看几个基本概念
Dep
源码中的注释是这么写的
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
// dep是一个能被多个指令订阅的观察者...
Dep即Dependences,在Vue中是用一个类来实现的,拥有上面依赖收集和派发更新的两个核心方法 depend 和 notify
dep.depend
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
Dep.target是一个Dep类的静态属性,是全局唯一的,代表了当前执行的Watcher
这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组
dep.notify
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
subs就是各个Watcher,这里调用他们的update方法,就是通知各个watcher进行刷新,类似demo中的updateComponent
Watcher
Watcher也是一个class,构造函数中定义了一些与Dep相关的属性
class Watcher {
constructor() {
// ...
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
}
// Evaluate the getter, and re-collect dependencies
get() {}
/**
* Add a dependency to this directive.
*/
addDep() {}
/**
* Clean up for dependency collection.
*/
cleanupDeps() {}
}
其功能就是做一些判断,然后将自己在依赖收集时加入dep
Dep和Watcher
Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的
我们可以发现,我在demo中是没有用到Dep和Watcher的这个概念的,那为啥Vue使用到了呢?
TIP
主要原因还是在demo中,reactiveObj只有一个被监听的数据项
Vue是数据驱动的,每当数据变化都会重新render,那么vm._render()方法又会再次执行,并触发getters
为了避免重复订阅watcher与特定场景性能优化(如v-else的部分不调用render),Vue在Watcher内对dep做了特定的处理,如dep id的去重,newDepIds的刷新管理
总结
吼猴,讲完了响应式原理,我们再来看看Vue的另外一大亮点,组件化 是怎么实现的