Vue3源码01 : 代码管理策略-monorepo
Vue3源码02: 项目构建流程和源码调试方法
“本文会先对子项目
reactivity
进行一个基本的介绍,随后会介绍Vue3
中的响应式原理,最后会编写一个极简版的响应式系统。在下一篇文章中,将会详细讲解reactivity
项目中具体源码的实现细节,敬请朋友们期待。 ”
前言
有可能朋友们会疑惑,源码分析为什么要从reactivity
讲起,而不是从其他地方开始分析?请大家先看Vue3
官方文档中的包依赖关系图:
---------------------
| |
| @vue/compiler-sfc |
| |
----- -------- ------
| |
v v
--------------------- ----------------------
| | | |
------------>| @vue/compiler-dom --->| @vue/compiler-core |
| | | | |
---- ---- --------------------- ----------------------
| |
| vue |
| |
---- ---- --------------------- ---------------------- -------------------
| | | | | | |
------------>| @vue/runtime-dom --->| @vue/runtime-core --->| @vue/reactivity |
| | | | | |
--------------------- ---------------------- -------------------
其实,我们在core/packages/
目录下可以发现一共有16个文件夹,也就是说有16个子项目。但是最核心最重要的,就是图中涉及的7个项目。其中@vue/compiler-sfc、@vue/compiler-dom、 @vue/compiler-core
跟编译相关。@vue/runtime-dom、@vue/runtime-core、@vue/reactivity
跟运行时相关。而图中的子项目vue
更像是一个家长,可以把其他子项目提供的能力聚合在一起,再统一对外提供能力。当vue
将其聚合在一起的时候,其中一些子项目的能力只是供内部其他子项目调用,并不会对外暴露所有子项目完整的能力。
在本系列文章中会先讲运行时相关的子项目,再讲编译阶段相关的子项目,因为运行时跟我们实际开发更贴近,一开始就深入编译阶段容易让很多朋友打退堂鼓。而对于运行时相关的子项目,我们从依赖图的最末端讲起,再层层回到依赖项的最顶端,这样一开始涉及的内容会尽可能的少,然后逐渐丰富,符合认知规律。但凡事无绝对,由于@vue/runtime-dom
内容相对较少且和实际开发联系比较紧密,因此会在讲解@vue/runtime-core
之前进行分析。所以,关于运行时相关的源码分析,实际分析顺序如下:
- reactivity
- @vue/runtime-dom
- @vue/runtime-core
我们先介绍下这几个子项目各自的职责:
reactivity
: 为数据提供响应式的能力,我们日常开发中出现的reactive
、ref
等函数都出自该项目中;@vue/runtime-dom
: 针对浏览器的运行时,内部会涉及到DOM API
,其依赖于@vue/runtime-core
提供的能力;@vue/runtime-core
: 平台无关的运行时核心,内部依赖reactivity
提供的数据响应式能力了。有了这个核心库,就可以针对特点平台自定义渲染器,@vue/runtime-dom
就是案例。
“对于上文的介绍,大家可能会比较疑惑,比如到底什么是运行时?平台无关又是什么意思?什么是数据响应式?
@vue/runtime-dom
和@vue/runtime-core
到底是什么关系?请大家暂时先将这些疑问放下,在后续的文章内容中逐渐会解答大家的疑惑。 ”
reactivty
现在正式步入了reactivity
的分析,下面首先会阐述Vue3
中数据响应式的概念。接着以一个案例为起点,逐步实现一个极简版本的响应式系统。
Vue2的响应式原理
在Vue2
中,所谓响应式,我们可以粗略的这样理解,就是利用Object.defineProperty
方法,为某个对象reactiveObj
属性key
设置get
、set
属性,当某个地方X
调用了reactiveObj.key
则会触发get
方法,此时在get
方法中做一条记录:X
使用了reactiveObj
对象的key
属性。当为对象reactiveObj
的key
属性赋值的时候,会触发reactiveObj
的set
方法,此时在set
方法中,通知X
将自己负责的地方执行一些更新逻辑。如果这个更新逻辑是操作DOM
显示新的内容,对于用户来讲直接的感受就是没操作DOM
的情况下,只是修改了自己定义的一个普通对象上的一个属性的值,但是DOM
上的内容却自己发生了变化。事实上,在Vue2
中,通常情况下,定义的所有数据都默认是响应式的,也就是说会默认为每个数据对象的每个属性调用Object.defineProperty
方法,让其数据默认具备响应式的能力。
Vue2和Vue3关于响应式的最重要的区别
从本质上讲Vue3
的响应式原理和Vue2
的响应式原理没有根本的不同。都可以简单的理解为,使用一个对象的属性的时候,记录下是谁在使用,当对象的属性值发生变化时再通知那些使用过该属性值的地方做相应的处理。当然,虽然本质上没有太大的不同,但在实现响应式的方案却又有很大的差别。主要有两个核心的差异:
- 利用的基础能力不同,
Vue2
利用了Object.defineProperty
,Vue3
利用了Proxy
的相关API
; Vue2
是默认会让所有的数据具备响应式的能力,Vue3
需要手动调用函数让特定数据具备响应式的能力;
当然Vue2
和Vue3
还有很多不同,比如因为采用底层能力的不同导致的兼容性不同、Object.defineProperty
有新增属性或数组的响应式丢失问题等等还有很多其他的不同。但我认为核心的不同就是上面的两点:一个代表了实现的基本原理不同,一个代表了响应式相关的应用实践的差异。
手写极简版Vue3响应式系统
“在
Vue
的世界,不管是Vue2
还是Vue3
,我们无论在template
中写了什么内容,都会在程序内部转化成虚拟DOM
,然后再将虚拟DOM
转化成真实DOM
,最后再将真实DOM
在合适的时机挂载到document
上某个具体的地方。Vue3
有两个render
函数,第一个render
函数是对模版进行编译的函数compile
执行完的返回值,执行该函数可以获得虚拟DOM
对象;另一个render
函数是将虚拟DOM
转化成真实DOM
,并将真实DOM
挂载到document
上。一定要分清这两个render
函数的不同,这两个函数可以说是整个Vue3
的灵魂。对于响应式原理来讲,我们关心的是第二个render
函数,如果对于刚才对render
函数的描述还比较模糊也没关系在这里,现在只需要将这句话刻在脑海里:执行完一个函数后,document
上的内容就发生了变化。 ”
假如有一个场景,初始代码如下,直接让浏览器页面上某个地方显示字符串yangyitao
。
// 代码片段1
let dataObj = {name:'yangyitao'};
document.getElementById('anyRealId').innerText = dataObj.name // Id为`anyRealId`的元素真实存在
dataObj.name = "杨艺韬";
但是,我们希望在改变了dataObj.name
的值后,元素anyRealId
中的内容也发生变化,也就是显示的内容由yangyitao
变为杨艺韬
。可能你会进行下面的改造:
// 代码片段2
let dataObj = {name:'yangyitao'};
const functionA = ()=>{
document.getElementById('anyRealId').innerText = dataObj.name // Id为`anyRealId`的元素真实存在
}
functionA();
dataObj.name = "杨艺韬";
functionA();
没错,这既保证了数据能够正常初始化,又保证了数据更新后,触发页面内容的变化。但是这样存在几个问题:
- 手动调用函数,看起来比较繁琐
- 目前的案例只用到了对象
dataObj
的name
属性,所以我们知道在name
的值发生了变化手动调用函数,但是如果runctionA
中使用了dataObj
的100个属性,而且这100个属性可以在任何地方发生改变。程序会显得极其臃肿,也考验我们的记忆力,可操作性极低。 - 在有些情况下,数据变化后,我们并不需要更新页面上显示的内容,怎么进行区分呢?
为了实现这些目标,我们可能对程序进一步进行优化:
代码语言:javascript复制<!--代码片段3-->
<html>
<head></head>
<body>
<div id="app"></div>
</body>
<script>
const objMap = new Map()
const reactive = (obj) => {
return new Proxy(obj, {
get: function (target, property) {
let propertyMap = objMap.get(target) || new Map()
let effectArr = propertyMap.get(property) || []
if (effectArr.indexOf(functionA) === -1 && !!functionA) {
effectArr.push(functionA)
propertyMap.set(property, effectArr)
objMap.set(target, propertyMap)
}
return target[property]
},
set: function (target, property, val) {
target[property] = val
let propertyMap = objMap.get(target) || new Map()
let effectArr = propertyMap.get(property) || []
effectArr.forEach(item => {
item()
})
}
})
}
let dataObj = {name: 'yangyitao'}
let reactiveDataObj = reactive(dataObj)
const functionA = () => {
document.getElementById('app').innerText = reactiveDataObj.name // Id为`anyRealId`的元素真实存在
}
functionA()
setTimeout(() => {
reactiveDataObj.name = '杨艺韬'
}, 3000)
</script>
</html>
在浏览器中打开页面,会发现页面中的yangyitao
会在3秒钟后自动更换为杨艺韬
。这样对于函数functionA
而言,我们完成了简单的数据变化触发页面变化的功能。为了具备一定的通用性,我们将代码改造成下面这个样子:
<!--代码片段4-->
<html>
<head></head>
<body>
<div id="app"></div>
</body>
<script>
const objMap = new Map()
let activeEffect = null
const reactive = (obj) => {
return new Proxy(obj, {
get: function (target, property) {
let propertyMap = objMap.get(target) || new Map()
let effectArr = propertyMap.get(property) || []
if (effectArr.indexOf(activeEffect) === -1 && !!activeEffect) {
effectArr.push(activeEffect)
propertyMap.set(property, effectArr)
objMap.set(target, propertyMap)
}
return target[property]
},
set: function (target, property, val) {
target[property] = val
let propertyMap = objMap.get(target) || new Map()
let effectArr = propertyMap.get(property) || []
effectArr.forEach(item => {
item()
})
}
})
}
const effect = (fn) => {
activeEffect = fn
fn()
}
let dataObj = {name: 'yangyitao'}
let reactiveDataObj = reactive(dataObj)
const functionA = () => {
document.getElementById('app').innerText = reactiveDataObj.name // Id为`anyRealId`的元素真实存在
}
effect(functionA)
setTimeout(() => {
reactiveDataObj.name = '杨艺韬'
}, 3000)
</script>
</html>
这样,程序多了一点通用性,我们可以把functionA
换成functionB
或者其他。上面的代码逻辑并不严密,很多边界条件都没处理,但这并不影响我们达到目标。在后续只要更改了reactiveDataObj
的name
属性的值,页面就会自动发生变化,而不需要上文中手动调用一个函数来触发变化。
代码片段4中有几个关键点:
- 调用
reactive
函数传入数据对象,该函数会返回一个代理对象reactiveDataObj
,在后续functionA
中使用对象reactiveDataObj
而非dataObj
- 先执行一遍传入
effect
函数中的functionA
,并用一个全局变量activeEffect
记录该函数。
其实到了这里,我们可以认为自己已经理解了Vue3
最核心的原理。在下一篇文章中,将会详细讲解reactivity
项目中具体源码的实现细节,敬请朋友们期待。