开篇来一段大家都会背诵的八股文。
某面试官: 请你简要介绍一下Vue的响应式原理。
答:Vue利用发布订阅模式结合Object.defineProperty劫持对象的get和set方法来实现响应式。
某面试官追问:你知道什么是依赖吗?
答:。。。
某面试官再次追问:请你简述一下依赖收集的过程。
答:。。。
我相信大部分人对于vue的响应式的了解程度也和上面八股文篇描写的差不多。所以我做了一个违背老祖宗的决定那就是
将Vue的响应式原理公众于世!!!!
因为是介绍响应式原理,所以我们以Vue2的一个最最简单的实例来入门。
从图可知,即使我们什么原理都不知道,也应该新建一个Vue类。
class Vue {
constructor({ el, data }) {
this.el = document.querySelector(el)
this.data = data
}
}
下一步,把普通对象变成响应式对象。如何实现呢? 八股文已经给出了答案。
在Vue的构造器中增加一行 observe(data)
class Vue {
constructor({ el, data }) {
this.el = document.querySelector(el)
this.data = data
// 执行observe函数将普通对象转换成响应式对象
observe(data)
}
}
那么很显然,observe函数就是将普通对象变成响应式对象的函数。这一块并不难,所以这里直接给出代码,注意看注释
// observe 函数,observe中文是观察,观察一个普通对象 使它变成响应式的
const observe = (target) => {
// 如果不是对象数据类型,只是普通类型那么变成响应式
if (typeof target !== 'object') return
// 新建一个Observe类 实现响应式的具体逻辑
new Observe(target)
}
class Observe {
constructor(data) {
Object.entries(data).forEach(item => {
Object.defineProperty(data, item[0], {
get() {
// 劫持对象的get方法
console.log(`看来你想获取${item[0]}的值`)
return item[1]
},
set(newValue) {
// 劫持对象的set方法
// 这里要注意的是如果新值是对象类型 不要忘记响应化
if (typeof newValue === 'object')
observe(newValue)
item[1] = newValue
}
})
})
}
}
没错 将一个普通对象变成响应式对象就是这么简单,这一小节已经完成了。
由于本文主要介绍的是响应化原理,那么一些虚拟dom渲染的过程就以图带过。我们看到Vue的构造函数中的el属性
我们可以从el属性的值获取到实际dom结构。
我以图的形式来描述dom和watcher的关系
由图中的对应关系可得。每一个响应式变量都会对应一个watcher。
那么以我们本文开始的做进一步解释
<div id="app">
{{name}}
div>
新建一个watcher实例需要三个属性,
1、监听的响应式对象(很好,在上一小节中我们已经完成了)
2、监听的key (从图文关系可知,每一个key对应一个watcher,所以需要传入key)
3、回调函数,也就是监听的响应式对象的key值发生变化的时候,需要执行的回调,这个回调函数就是dom更新函数。以保证数据改变—>回调执行—>dom更新
根据目前的分析,一个初步的Wather类已经写好了
class Watcher {
constructor(target, key, callBack) {
this.target = target
this.key = key
this.callBack = callBack
}
// 传入的callback就是dom的更新函数,updateDom就是执行这个函数
upDateDom() {
this.callBack()
}
}
那么现在问题来了?如何让watcher观察的key和响应式数据中的key一一对应上呢?
八股文早已给出了答案
新建一个Dep类(很关键哦!),Dep是dependence的缩写,依赖的意思。那么他们的对应关系依然用图说话
由图可知,dom中的插值表达式和watcher是一对一的关系,但是响应式数据和watcher是一对多的关系
我们可以这样理解,不止一个dom节点使用到了name属性,所以name更新要通知全部的watcher更新它们的dom所以是一对多的关系。
新建一个发布订阅模式。
class Dep {
constructor() {
// 保存响应式数据的所有依赖,这里的依赖就是watcher实例
this.depList = []
}
// 由于是一对多的关系,所以要有一个add方法
addDep(watcher) {
this.depList.push(watcher)
}
// 通知全部的watcher更新视图
noticeWatcher() {
// 上面已经说过了,每一个依赖都是一个watcher实例
// noticeWatcher就说明watcher(订阅者)已经收到了通知
// 需要更新dom
this.depList.forEach(item => {
item.upDateDom()
})
}
}
由上图可知,响应式变量的每一个key都需要保存使用到它的dom节点(我们称为依赖)
下一小节介绍依赖收集。
如果你坚持到了这一小节,那么你已经完成了 发布者(Dep类),订阅者(Watcher类),响应式数据(Observe)类。已经是梅西进禁区----只差临门一脚了。依赖收集的过程就是将响应式数据关联的依赖(也就是使用到该响应式数据的dom节点)给收集起来。
注意看,这个男人叫小帅,他开始写最关键的代码了。改写watcher类。
class Watcher {
constructor(target, key, callBack) {
this.target = target
this.key = key
this.callBack = callBack
this.getValue()
}
// 传入的callback就是dom的更新函数,updateDom就是执行这个函数
upDateDom() {
this.callBack()
}
getValue() {
// 执行getValue函数完成依赖收集
this.target[this.key]
}
}
什么?? 执行getValue的函数就可以完成依赖收集????
以本文的html为例,新建一个watcher实例
var app = new Vue({
el: '#app',
data: {
name: 'wxs'
}
})
new Watcher(app.data, 'name', () => {
console.log('假设这是一个dom更新函数!!!')
})
当我们新建一个vue实例时,说明app.data已经变成了响应式对象,所以监听者watcher观察的就是这个对象,key为‘name’。由于在watcher的构造函数中执行了函数getValuethis.target[this.key]
那么响应式对象的get方法就被触发了。
不好理解的话看图
在watcher的getValue
函数中插一行代码,构造器中加一行代码
getValue() {
// 保存当前watcher实例
Dep.target = this
// 执行getValue函数完成依赖收集
this.target[this.key]
}
它保存的是当前正在新建的wather实例。不一定要保留在Dep.target中,Dep.aaa,window.aaa都可以。也许现在你还不知道为什么要保存它。但是我保证,你在10行之内就知道它是干什么的了。
由于刚刚触发了响应式对象的get方法,我们回到响应式类Observe中。
上一小节最后一句
那说明响应式对象的每一个key都要有一个数组来存它全部的依赖。改写Observe类。
class Observe {
constructor(data) {
Object.entries(data).forEach(item => {
// 新建依赖收集器
const dep = new Dep()
Object.defineProperty(data, item[0], {
get() {
// 劫持对象的get方法
// 新建watcher实例会执行到key的get方法 所以Dep.target就是key需要收集的依赖
if (Dep.target !== null) {
// 避免重复收集
dep.addDep(Dep.target)
}
return item[1]
},
set(newValue) {
// 劫持对象的set方法
// 这里要注意的是如果新值是对象类型 不要忘记响应化
if (typeof newValue === 'object')
observe(newValue)
item[1] = newValue
// 通知依赖更新
dep.noticeWatcher()
}
})
})
}
}
写到这里也许你开始不耐烦了。但是
已经结束啦!!!!
是的你没有看错,但你完成这一步。Vue的响应式已经全部完成啦!。当然,我们实现的是简易版本的。但是请你相信。当你认真看完并理解。你已经掌握了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>Documenttitle>
head>
<body>
<div id="app">
{{name}}
div>
<script>
class Dep {
constructor() {
// 保存响应式数据的所有依赖,这里的依赖就是watcher实例
this.depList = []
}
// 由于是一对多的关系,所以要有一个add方法
addDep(watcher) {
this.depList.push(watcher)
}
// 通知全部的watcher更新视图
noticeWatcher() {
// 上面已经说过了,每一个依赖都是一个watcher实例
// noticeWatcher就说明watcher(订阅者)已经收到了通知
// 需要更新dom
this.depList.forEach(item => {
item.upDateDom()
})
}
}
// observe 函数,observe中文是观察,观察一个普通对象 使它变成响应式的
const observe = (target) => {
// 如果不是对象数据类型,只是普通类型那么变成响应式
if (typeof target !== 'object') return
// 新建一个Observe类 实现响应式的具体逻辑
new Observe(target)
}
class Observe {
constructor(data) {
Object.entries(data).forEach(item => {
// 新建依赖收集器
const dep = new Dep()
Object.defineProperty(data, item[0], {
get() {
// 劫持对象的get方法
// 新建watcher实例会执行到key的get方法 所以Dep.target就是key需要收集的依赖
if (Dep.target !== null) {
// 避免重复收集
dep.addDep(Dep.target)
}
return item[1]
},
set(newValue) {
// 劫持对象的set方法
// 这里要注意的是如果新值是对象类型 不要忘记响应化
if (typeof newValue === 'object')
observe(newValue)
item[1] = newValue
// 通知依赖更新
dep.noticeWatcher()
}
})
})
}
}
class Vue {
constructor({ el, data }) {
this.el = document.querySelector(el)
this.data = data
// 执行observe函数将普通对象转换成响应式对象
observe(data)
}
}
class Watcher {
constructor(target, key, callBack) {
this.target = target
this.key = key
this.callBack = callBack
this.getValue()
// 依赖收集完成之后重置,避免重复收集
Dep.target = null
}
// 传入的callback就是dom的更新函数,updateDom就是执行这个函数
upDateDom() {
this.callBack()
}
getValue() {
// 保存当前watcher实例
Dep.target = this
// 执行getValue函数完成依赖收集
this.target[this.key]
}
}
var app = new Vue({
el: '#app',
data: {
name: 'wxs'
}
})
new Watcher(app.data, 'name', () => {
console.log('假设这是一个dom更新函数!!!')
})
script>
body>
html>
算上html结构以及换行才125行代码。学会它,超过隔壁莽村李青!