这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
大家好,欢迎来到【最简系列】
该系列将会秉承用最简单的代码描述复杂原理理念,力求做到不贴源码,只求易懂,帮助大家理解一些热门框架/技术栈中的重点功能,希望大家会喜欢。
前言
从Vue面世以来,其响应式的特点就被人津津乐道,相应的源码解析文章层出不穷,但还是有些童鞋读过这些文章后依然困惑不已,究其原因大多是因为文章中倾向于整段/完整的复制源码,虽然这样可以将vue/vuex的响应式内容讲得更加完整,但对基础较差的童鞋却是一种煎熬,因此就有了【最简系列】
思路速读
- 发布-订阅模式
- 通过劫持对象上的属性,实现发布订阅
- vue2和vue3分别通过defineProperty和proxy实现劫持
几行代码
let data = {
name: ''
}
data = new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
console.log('执行发布', target);
}
})
// 这时候的data, 已经是被劫持后的data了(人家不纯洁了啦)
data.name = 'ssss'
新建一个js文件,打上这几行代码,然后运行这个js文件,这时候你会发现,你的控制台出现了执行发布 { name: 'ssss' }
也就是说,我们只要修改data的属性,就会触发console.log('执行发布', target);,那如果我把这行代码换成别的呢? 例如修改div中的内容? 这是不是就实现了响应式?
PS: 因为proxy写起来更简洁,所以我就用了proxy。
揪斗麻袋哟,教练这也太简单了,我还能往下学!
进阶写法
哎呀,高难度的来啦。。。并不,就算你想看,我都懒得写代码,所以还是随便写几行,凑合看吧
来,首先创建个新的js文件,就叫它vue.js吧
class Vue {
// 这里接收的option 其实就是在new Vue时传进去的对象
constructor(option) {
this.$el = document.querySelector(option.el);
this.$data = _proxy(option.data);
}
_proxy(data) {
return new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
console.log('执行发布', target);
}
})
}
}
是的,我把它改成了类的写法(什么,你不知道什么是类? 停!别往下看了,出门左转js的es6特性大全)
接着,再创建个html文件。名字嘛,叫page.html ?
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="app"></div>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
name: '',
}
})
app.$data.name = 'sss';// 模拟修改data里的数据,假装我是用methond写的好不好
</script>
</body>
</html>
把这个page.html在浏览器里打开,你会在控制台里看到执行发布 {name: "sss"},欸?难道我实现了个vue?
就当作是吧
我们再回到刚才的vue.js文件里,接下来再写几个类
class Vue {
// 这里接收的option 其实就是在new Vue时传进去的对象
constructor(option) {
this.$el = document.querySelector(option.el);
this.$data = _proxy(option.data);
}
_proxy(data) {
return new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
console.log('执行发布', target);
}
})
}
}
// 负责渲染模板(就是把你的data里的内容对应到div中)
class Compiler{
compile() {
//初始化渲染模板,并把每一个绑定数据的元素节点,例如v-model,添加watcher
}
update() {
// 重新渲染有变动的地方
}
}
// 管理所有的watcher
class Dep {
notify() {
//通知每个一个watcher
Watcher.update()
}
}
class Watcher {
update() {
// 进行新值、旧值比对(万一你往data属性里赋得值是一样呢?那肯定不用重新渲染呀)
// 通知Compiler这个类,重新渲染模板
Compiler.update()
}
}
没想到吧,每个类都这么空? 因为说好的不贴源码啊🤣
再列个图,清楚点
先是第一遍初始化
graph TD
初始化 --> 劫持data里的数据并添加进Watcher也就是收集依赖 --> 交由dep来管理 --> 通过Compiler编译template的内容并渲染,尤其是解析了各种v指令
然后后续数据有变动
graph TD
改动了data里的某个数据 --> dep通知各个watcher --> watcher会先进行比对看看自己管的这个数据有没有变动 --> 通知Compiler去把更新后的数据再次渲染出来
关于
发布-订阅模式,有的人也会称它为观察者模式, 当然,两种其实也有一些细微的差别,不过,设计模式本身并不是僵硬的模板,尤其是在光怪陆离的各种场景中,层出不穷的变形写法往往难以准确界定两者的分界线
结语
以上就是实现响应式的核心思路啦,不过不要问我是vue2还是vue3哦,因为我可能混在一起啦🤣
另外其实学习vue的响应式,其目的并不是要求你徒手写个vue出来(太吓人了好吗?)或者复述响应式源码出来(说,你是不是在准备提桶跑路了?)
而是希望起到两个作用:
- 帮助你更好的进行vue的开发工作:例如在实际的vue项目开发中,你往一个对象中添加了新的属性,却发现并没有响应,不要着急休息一会儿啊不,脑子转一会儿,原来是vue2用defineProperty劫持对象上的属性,而你添加的新属性没被劫持到,当然不会有反应啦
- 学习设计思路,或许哪天你要编写一些东西时,会突然想到: 对哦,为什么不问问神奇的观察者呢?
对了,其实我在⚡qiankun微前端中的应用通信-不受框架限制的响应式数据管理 (juejin.cn)就利用了发布-订阅设计模式编写了一个工作中需要的响应式数据管理模块哦,要不要看看呢?(顺便给个点赞、收藏、关注?)