vue3与vue2的区别之数据响应——手写vue3的reacti
1、 数据响应式
首先请大家认真的思考一个问题:什么是数据响应式?
答:数据变化是可侦测的,并且和数据相关的内容可以更新。
️这里一定要明确一个概念,数据响应式和视图更新是没有关系的!数据响应式是一种机制,一种数据变化的侦测机制。而实现数据响应式这种机制的方法不唯一。
那么,在vue
是如何实现数据响应式的?vue2和vue3的数据响应式有什么区别?
2、vue如何实现数据响应式?
要知道,vue3.x
实现数据响应的方案跟vue2.x
是不一样的,所以在这里我将vue2.x
和vue3.x
分别说说。这也是理解vue2.x
和vue3.x
区别的时候,可以指出来的一个巨大的区别。
2.1 vue2.x的实现方案
我贴上一个vue2.x源码-Object的变化侦测解读的链接,方便大家理解和后续关于vue2.x的学习需要。
(特别是还没阅读过vue源码的同学,可以独自过一遍这个文档,能对vue有一个更深的认识)
在下面vue2
的源码中可以看到,Observer
类会通过递归的方式把一个对象的所有属性都转化成可观测对象,所以我们可以知道vue2
需要遍历对象的所有的key
。其实现数据响应式的核心思想就是通过defineProperty
,去定义get
、set
等方法。从而能够拦截到对象属性的访问和变更。
/**
* Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
*/
export class Observer {
constructor (value) {
this.value = value
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
// 如果只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
在日常开发中,产品经理总是会跟我们说,我们做了xxxx就是为了解决客户的xxxx痛点。
那么,在继续往下阅读的时候,可以先思考一下vue2
这样的实现方案的痛点有什么?或者说缺点有什么?
因为作为客户(使用vue
开发的前端同学)的我们需要知道,vue3
是否解决了我们的痛点?
vue2的缺点:(仅仅是关于数据响应造成的缺点哦!)
- 1、影响初始化速度、数据过大时的资源问题
(在源码的Observer
方法上,对象的每一个属性都要被拦截。所有的key都要有一次循环和递归) - 2、数组的特殊处理,导致其修改数据不能使用索引
(原因在于defineProperty
不支持数组,参考vue源码-Array的变化侦测) - 3、动态添加或删除对象属性无法被侦测
(defineProperty
哭着对我说:臣妾的的setter
函数办不到呀)
对于没阅读过vue源码的前端开发来说,应该也遇到过修改了数组,或者修改对象后发现,啥变化也没有,一头雾水,拍桌子直呼:vue真垃圾,有bug。
其实这些雾水大都是上面的2、3两点引发的,vue也都提供了解决方案:$set
和$delete
,我都整理好了,需要理解的直接移步深入响应式原理。
但是,这就体验极差
🤣小故事一则:去年还没阅读源码的时候,公司一个大版本的发布后,出现了一个不是很严重,却影响使用范围很广的一个bug,我们从凌晨2点修到4点,最后还是一个大牛搞了几轮实验发现了问题,说vue有bug,某某地方赋值需要用$set
。没错,就是上面痛点里的第3点。原因还是我们太菜呀,没有阅读相关源码。
2.2 vue3.x的实现方案
文章开头我就强调了:数据响应式是一种机制,一种数据变化的侦测机制。而实现数据响应式这种机制的方法不唯一。于是乎,vue3.x
来了,他带着vue2.x
痛点的解决方案来了!
解决方案其实一点也不神秘,在ES6
之后,出现了一个新的特性:Proxy
。Vue3.x
在使用了Proxy
之后,痛点们一下子就全都解决了。Proxy
是怎么解决的呢?请听下回...请继续往下看哈看完手写reactive
之后,就全都明白啦。
顺便给个Proxy
的MDN地址: Proxy MDN传松门
3、手写reactive
在vue3.x中,定义响应式对象的方法如下:
const obj = reactive({
name: 'chenjing',
age: 18
})
3.1 测试Proxy是否生效
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log('target, key', target, key, target[key])
return target[key]
})
}
proxy-get.png
ok,生效。在简易版的
reactive
,我们要添加基本的属性get
、set
和deleteProperty
。同时,在上面代码的get
里直接return target[key]
,一来不太优雅、二来可能报错。我们先来看看vue3是怎么处理的:vue3源码图1.png
再来一个传送门:Reflect - MDN
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。
Reflect
不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同Reflect
并非一个构造函数,所以不能通过new运算符对其进行调用,或者将Reflect
对象作为一个函数来调用。Reflect
的所有属性和方法都是静态的(就像Math
对象)。
Reflect
对象提供了以下静态方法,这些方法与proxy handler methods的命名相同.
其中的一些方法与Object
相同, 尽管二者之间存在 某些细微上的差别 .
3.2 reactive基本形态
让我们来学习一下vue3
的写法后,加上了Reflect
后,于是我们最基本的reactive
就是下面这样的:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key) // 可以直接return target[key],避免报错和代码的优雅性,模仿源码采用Reflect
console.log('get', key)
return (typeof res === 'object') ? reactive(res) : res // 子属性若是对象 需要再次代理
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
console.log('set', key)
return res
},
deleteProperty() {
const res = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return res
}
})
}
reactive基本形态.png
通过跑脚本后的控制台,可以看到访问属性成功的触发了
get
。同时新增属性也触发了set
。到这里为止,
vue2
中的数据响应式在vue3
里其实已经完全实现了。回过头来想想,是不是没那么难理解了吧。没有vue2
的循环遍历递归,只是上了Proxy
的车当然了在
Vue3
内真正的实现,肯定不是这么几行代码就搞定的。只是响应式的原理就是利用了Proxy
!
既然要手写实现一个简易的reactive
函数,让我们继续往下阅读。
目前只是想简单理解vue3
数据响应式原理,了解vue3
数据响应和vue2
数据响应的区别的同学可以直接点赞了哈哈,鼓励一下互相学习进步😁
3.3 依赖的收集、触发
既然要手写实现一个简易的reactive
函数,我们就继续。
要实现reactive
函数,我们就要在get
内进行依赖收集,在set
中进行触发。即便是vue2
也是通过类似的发布订阅模式体现。在这里,我们也是通过发布订阅模式去完成。
首先是依赖收集:在get内,我们需要对依赖进行收集。在依赖收集的时候,将其按照依赖关系放入map中映射。
然后就是依赖触发:在set中,需要触发响应式函数。即完成了发布订阅。
下面代码 有需要的可以直接复制粘贴,直接跑。可以自行断点看看,有疑问的欢迎交流。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key)
console.log('get', key)
// 依赖收集
track(target, key)
return (typeof res === 'object') ? reactive(res) : res
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
console.log('set', key)
// 触发
trigger(target, key)
return res
},
deleteProperty() {
const res = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return res
}
})
}
// 保存副作用函数
const effectStack = []
// 添加副作用函数
function effect (fn) {
const e = createReactiveEffect(fn)
// 立即执行
e()
return e
}
function createReactiveEffect(fn) {
// 封装fn,处理其错误,执行之,存放到stack
const effect = () => {
try {
// 0入栈
effectStack.push(effect)
// 1 执行fn
return fn()
} finally {
// 2 出栈
effectStack.pop
}
}
return effect
}
// 保存映射关系的数据结构
const targetMap = new WeakMap()
// 当副作用函数触发响应式数据之后,执行track,进项依赖收集工作
// 目标是将target, key和前面effectStack中的副作用函数之间建立映射关系
function track (target, key) {
// 1.先拿出响应函数
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 获取target对应的map
let depMap = targetMap.get(target)
if (!depMap) {
// 初始化的时候 depMap不存在 初始化一次
depMap = new Map()
targetMap.set(target, depMap)
}
// 从depMap中 获取对应的set
let deps = depMap.get(key)
if (!deps) {
// 初始化需要创建一个Set
deps = new Set()
depMap.set(key, deps)
}
// 将副作用函数放到集合中
deps.add(effect)
}
}
// 触发响应式函数
function trigger (target, key) {
// 从targetMap中获取对应副作用函数集合
// 1. 获取target对应的map
const depMap = targetMap.get(target)
if (!depMap) return
// 根据key获取对应的deps
const deps = depMap.get(key)
if (deps) {
// 遍历执行他们
deps.forEach(dep => dep())
}
}
const obj = reactive({
name: 'chenjing',
age: 18,
look: {
height: '180cm'
}
})
effect(() => {
console.log('effect1', obj.name)
})
effect(() => {
console.log('effect2', obj.name, obj.look.height)
})
setTimeout(() => {
console.log('---- 分割线 -----')
obj.name = 'jay'
obj.look.height = '178cm'
}, 1000)
执行结果.png
4. 结尾
好了,到此手写简易版vue3的reactive函数完成,希望可以帮助到打击爱理解vue3数据响应原理。
单纯的理解数据响应原理可以理解到Proxy就差不多了
后面依赖收集触发就是具体到响应后要做的事。