炫意html5
最早CSS3和HTML5移动技术网站之一

一步一步学习Vue2 响应式原理

1.前言

vue的核心思想便是数据驱动与组件化,我们在使用vue来开发日常工作项目的时候可以大大的减少来我们对原生dom的操作,使得写起代码也是得心应手。那么在vue中是如何实现这样的响应式系统的呢?今天来一探究竟。

2. 响应式原理

在vue2 中响应式系统是通过 发布订阅模式来实现的。 举个例子🌰,从前有一个 小s (sourcedata)梦想成为一个受大家欢迎的人,于是他就去问他的朋友 小 O(observe)该如何才能成为一个受大家欢迎的人呢?于是小O送了一部最新款iphone 小D(dep)给小s ,并告诉小s 说只要你肯乐于助人,终有一天可以成为一个受大家欢迎的人。于是小s 拿着最新款iphone 高高兴兴的回家了。花开两朵、各表一枝。小a最近手头紧, 听说小s是远近闻名的好人,于是便想这找小s借点钱来应应急,可是自己拉不下脸去找小s借钱,于是便委托小w(watcher)去帮忙,于是小w帮小a 从小s 处借了(get)1000元。热心的小s在小w临走时还不忘将小w的电话☎️记在(depend)自己的 iPhone(dep)上,并且对小w说等这个月我发工资了再通知你来拿钱,转眼间到了发工资的日子了,小s 工资刚领到手还没捂热呢,便想起来小w借钱的事,于是用 自己的 iphone (dep)通知(notify)小w自己今天发工资了 ,可以来借钱了。于是 小w便再次(update)来帮小a找小s借钱。你们猜最终小s有成为受大家欢迎的人么(🐶)

上面在小s成为一个受大家欢迎的人的故事中有几个重要的角色,小O , 小D , 小W。他们(它们)三个是小s成长道路上必不可少的三位;他们的扮演的角色大致如下:

  • 小O:妥妥的导师,负责指明 小s成长道路的方向(毕竟如果方向不对,兄der你懂的 ), 与此同时 也负责给小s提供工具(iphone)
  • 小D: 妥妥的工具,负责记录小s想帮助的人的联系方式,在必要的时候可以到联系那些需要帮助的人。
  • 小W:妥妥的中间商(还好他不赚差价😂),负责将自己的联系方式留给小s,还负责在小s通知自己的时候去联系那些通过自己去找小s寻求帮助的人

所以,接下来要做的就是将上面的故事用代码描绘出来了吧😄

那么首先第一个出场的角色就是 小s ,所以我们给他来个简单定义

const data = {
count: 1
}
复制代码

接下来就是第二个,导师的角色了

Observe 类

import Dep from './Dep'
interface IObserve {
[key: string]: any
}
export default class Observe {
value: IObserve | Array<any>;
constructor(value: IObserve | Array<any>) {
this.value = value
// 对象处理
if (isPlainObject(this.value)) {
this.walk()
} else {
//  劫持数组
}
}
walk() {
// 人生导师,开始指路(手动滑稽+🐶)
Object.keys(this.value).forEach(key => {
defineReactive(this.value, key)
})
}
}
export function defineReactive(target: IObserve, key: string) {
let val = target[key];
// 财大气粗的小O 直接送一部iphone 小D 给小s ,当然,如果小s 内有很多个
//key时,那么我们的小o就会送给小s和key数量相同的iphone,毕竟人傻钱多(🕶️)
const dep = new Dep()
Object.defineProperty(target, key, {
get() {
// 当有人来借钱了,就给他记在iphone的通讯录上
dep.depend()
return val
},
set(newVal) {
if (val != newVal) {
val = newVal
// 发工资了,掏出我的iphone,逐个联系,速来领钱 🐶
dep.notify()
}
}
})
}
复制代码

首先 在Observe 类中 会循环遍历目标对象的键名,然后通过这些键名来调用 Object.defineProperty Api来劫持目标对象上的getter、setter。与此同时这也是 Vue2 中响应式的缺点,因为这个Api调用的前提是要知道目标对象的键名,所以在对于 vue初始化之后再向 data对象中添加键值对,它们是不具备响应式。虽然vue2中增加了$set这样的api来弥补这个Api的不足,但这仍然增加了使用成本与学习成本。所以vue3采用的Proxy代理的方式来实现数据的劫持。

接着第三个角色,便是 小D 了,虽然它只是一个工具🥲

Dep 类


import Watcher from './Watcher'
export default class Dep {
static target: Watcher | null | undefined = null;
subscribes: Set<Watcher>;
constructor() {
// 初始化依赖数组,用Set可以自动去重
this.subscribes = new Set()
}
depend() {
//  收集依赖于当前数据的更新函数
// 当有人来借钱,就将它记在这个小本本上
if (Dep.target) {
this.subscribes.add(Dep.target)
}
}
notify() {
//  通知依赖更新数据,重新计算值
// 什么?! 小s打电话来说发工资了,小a、小b、小c、小d还不快去领钱💰 ,
//领完钱他们的资产该从新计算了
this.subscribes.forEach(watcher => watcher.updated())
}
}
const targetStack: Array<Watcher> = [];
export function pushTarget(target: Watcher) {
targetStack.push(target)
// 便于找到当前正在执行的更新函数
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码

Dep 类作为收集 watcher 依赖的类而存在,它具有以下特点:

  • 它负责在访问被劫持的数据的时候将当前正在执行的 更新函数Watcher(即那些依赖于当前数据的更新函数)收集起来;
    • 这些更新函数在进入JavaScript 函数执行栈之前会被压入vue创建的一个类似于执行栈的更新函数的数组中,同时也会保存在Dep的一个静态属性target上,因此可以通过访问全局变量 Dep.target来获取到此时正在执行的更新函数。
  • 在数据被修改的时候Dep调用notify方法去通知被它收集起来的watcher去执行update方法更新数据。

class Dep {
// 在更新函数执行时,会被保存在 Dep的target属性上,可以通过 Dep.target 
//来访问到此时正在执行的更新函数(⚠️注意这个Dep.target 其本质就是一个全局变
//量而已,不要多想😓,它仅仅是挂在Dep这个类上)
static target: null | undefined | Watcher = null
subs: Set<Watcher>;
constructor() {
// 使用set 创建,避免收集重复的watcher
this.subs = new Set()
}
depend() {
// 如果此时的更新函数有值,那么便可以将它收集起来
if (Dep.target) {
this.subs.add(Dep.target)
}
}
notify() {
// 当前dep所在的数据更新了,通知依赖当前这个数据的 watcher去执行更新数据
this.subs.forEach(watcher => watcher.update())
}
}
// definedReactive方法也要改写以下,要加上依赖收集的逻辑
function defineReactive(target: IReactive, key: string) {
let value = target[key]
// 为每一个 key 创建一个 dep的实例,这个dep中 会存放着依赖于这个key值的更新函数
const dep = new Dep()
Reflect.defineProperty(target, key, {
get() {
// 收集依赖 ,如果 有更新函数访问了 key所对应的值,那么就代表这它
//依赖于当前的key,所以要将它收集起来,当key改变之后,通过这个更
//新函数,让他重新计算值
dep.depend()
return value
},
set(newVal) {
if (value != newVal) {
value = newVal
// 当前 key 对应的值发生了改变 , 通知依赖于这个key 值的
//Watcher 去进行更新重新计算值
dep.notify()
}
}
})
}
const targetStack: Array<Watcher | undefined> = []
function pushTarget(target: Watcher | undefined) {
targetStack.push(target);
Dep.target = target
}
function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码

接着便是最后一个角色的出场了,不赚差价的中间商来也!😭

Watcher 类


import Dep, { pushTarget, popTarget } from './Dep'
type updateFC = () => any
export default class Watcher {
updateFn: updateFC;
value: any;// update 更新函数的结果值
constructor(updateFn: updateFC) {
// 保存更新函数
// 小a上次说让我从小s借多少来着?还是给它记在我的小本本上好嘞,方便
//我下次去找小s帮小a借钱
this.updateFn = updateFn
this.get()
}
get() {
// 把我的联系方式写在全局,方便小s记录我的联系方式
pushTarget(this)
// 借钱、再次借钱、又去借钱。。。
const value = this.updateFn()
popTarget()
return value
}
updated() {
this.get()
}
}
复制代码

Watcher 中最重要的作用就是要存储执行数据更新的函数,以及在适当的时机再去执行数据更新函数。

图解

上面的故事有些生硬😭,但确实是我所理解的vue的响应式(如有理解错误的地方,请务必指出,感谢大家。😘),下面再给大家表演个看图说话,梳理一下一个响应式数据创建以及工作的大致流程:
reactive.png

总结

如今vue3也出了好久了,前端这个行业发展的真是迅速,一天不学就会落后,希望这个时候才开始看vue2的源码不会太迟吧😼,一起加油呀⛽️!

炫意HTML5 » 一步一步学习Vue2 响应式原理

CSS3教程HTML5教程