由于自己工作主要的技术栈是vue,感觉最近自己每天都是代码搬运工,很无聊,就想着去系统学习vue3源码,提升自己对vue的理解能力。但是每次遇到自己不是很懂得知识后,就放弃思考,也就没了继续学习的想法了。
这次我TM一定要把这些内容啃下来,不然直接回家种地。(真的需要逼自己一把,找机会回杭州。)
vue3源码分析
基础知识
命令式
详细描述做事的过程。关注过程的一种编程范式,他描述了完成一个功能的详细逻辑与步骤。
声明式
不关注过程,只关注结果的范式。 并不关心完成一个功能的详细逻辑与步骤。
- 命令式的性能 > 声明式的性能
- 命令式的可维护性 < 声明式的可维护性 (对于用户来说)
所以框架的设计过程其实是一个不断在可维护性和性能之间进行取舍的过程。在保证可维护性的基础上,尽可能的减少性能消耗。
Vue封装了命令式的逻辑,而对外暴露出了声明式的接口。
编译时 compiler
把template中的html编译成render函数。
<div id="app"></div>
<script>
const {compile, createApp} = Vue
const html = `
<p class="pp">编译时</p>
`
const render = compile(html)
createApp({
render
}).mount("#app")
</script>
运行时 runtime
运行时可以利用render把vnode渲染成真实的dom节点。
render函数就是挂载h函数生成的虚拟dom。提供的render属性函数是用来生成虚拟dom树的。(vue2的render option, vue3 setup返回一个函数, comple(template)编译器生成的)
<div id="app"></div>
<script>
// 运行时可以利用render把vnode渲染成真实的dom节点。
const {render, h} = Vue
// 生成vnode
const vnode = h("p", {class: "pp"}, "运行时")
// 挂载容器
const container = document.getElementById("app")
// 挂在虚拟dom
render(vnode, container)
</script>
const vnode = {
type: "p",
props: {
class: "pp"
},
children: "运行时"
}
function render(vnode) {
const node = document.createElement(vnode.type)
node.className = vnode.props.class
node.innerHTML = vnode.children
document.body.appendChild(node)
}
render(vnode)
对于dom渲染而言,可以分成两个部分
- 初次渲染,即挂载。
- 更新渲染,即打补丁。
可以查看vue官网的渲染机制
Vue为啥要使用运行时加编译时
1.针对于纯运行时 而言:因为不存在编译器,所以我们只能够提供一个复杂的 JS对象(难以编写)。即Vnode对象。
2.针对于 纯编译时 而言:因为缺少运行时,所以它只能把分析差异的操作,放到编译时进行,同样因为省略了运行时,所以速度可能会更快。但是这种方式这将损失灵活性。比如 svelte,它就是一个纯编译时的框架,但是它的实际运行速度可能达不到理论上的速度。
3.运行时+编译时:比如 vue 或 react 都是通过这种方式来进行构建的,使其可以在保持灵活性的基础上,尽量的进行性能的优化,从而达到一种平衡。
副作用
副作用指的是:当我们对数据进行 setter或 getter 操作时,所产生的一系列后果。
vue对ts支持友好
为了vue拥有良好的ts类型支持,vue内部其实做了很多事情。定义了很多类型,所以我们编写ts才会很容易。并不是因为vue是ts写的。
写测试用例,调试,看源码
- 摒弃边缘情况。
- 跟随一条主线。
在我们测试打断点时,有很多边缘判断,我们只跟着条件为true的逻辑走。
fork vue3仓库到自己的github仓库(可以将自己阅读源码的过程提交到自己的仓库下,方便以后查看),并克隆到本地。然后做一下操作
- 安装依赖
- 开启sourcemap。
"build": "node scripts/build.js -s"
- 打包构建
- 在
packages/vue/example/...
中编写测试用例,断点调试看源码。
ts配置项
// https://www.typescriptlang.org/tsconfig
{
// 编辑器配置
"compilerOptions": {
// 根目录
"rootDir": ".",
// 严格模式标志
"strict": true,
// 指定类型脚本如何从给定的模块说明符查找文件。
"moduleResolution": "node",
// https://www.typescriptlang.org/tsconfig#esModuleInterop
"esModuleInterop": true,
// JS 语言版本
"target": "es5",
// 允许未读取局部变量
"noUnusedLocals": false,
// 允许未读取的参数
"noUnusedParameters": false,
// 允许解析 json
"resolveJsonModule": true,
// 支持语法迭代:https://www.typescriptlang.org/tsconfig#downlevelIteration
"downlevelIteration": true,
// 允许使用隐式的 any 类型(这样有助于我们简化 ts 的复杂度,从而更加专注于逻辑本身)
"noImplicitAny": false,
// 模块化
"module": "esnext",
// 转换为 JavaScript 时从 TypeScript 文件中删除所有注释。
"removeComments": false,
// 禁用 sourceMap
"sourceMap": false,
// https://www.typescriptlang.org/tsconfig#lib
"lib": ["esnext", "dom"],
// 设置快捷导入
"baseUrl": ".",
// 路径别名
"paths": {
"@vue/*": ["packages/*/src"]
}
},
// 入口
"include": [
"packages/*/src"
]
}
vue3与vue2的优势
- 性能更好
- 体积更小
- 更好的ts支持,vue提供了很多的接口
- 更好的代码组织,更好的逻辑抽离。(hooks)
响应式
vue2 Object.defineProperty实现响应式
Object.defineProperty缺陷
- vue2中对data对象做深度监听一次性递归,性能较差。
- 由于js限制,无法监听新增、删除属性。需要使用
Vue.set, Vue.delete
- 无法原生监听数组,需要特殊处理。重写了数组的一些方法,让其可以做到响应式。(
push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
) 具体看这里
vue3 Proxy实现响应式
Proxy
handler操作中的方法的receiver参数表示当前代理对象本身。
只有使用当前代理对象操作才会触发handler中对应的拦截方法。
Reflect
Reflect提供的方法(get, set
)传入的receiver参数可以替换掉操作对象中的this值。
Reflect.set(target, propertyKey, value[, receiver]) // receiver将作为target setter方法的this。防止target对象使用getter,setter获取和设置对象属性时,未使用代理对象,从而失去响应式。
receiver
这个参数对于vue中的响应式实现具有非常重要的意义。因为只要是读取属性,我们就需要走代理对象,而不是原始对象。
WeakMap
WeakMap引用的对象,是弱引用,并不阻止js的垃圾回收机制。
-
弱引用:不会影响垃圾回收机制。即:WeakMap的key不再存在任何引用时,会被直接回收。
-
强引用:会影响垃圾回收机制。存在强引用的对象永远不会 被回收。
例如:在vue源码中使用在保存代理对象,如果当前对象以前被代理过,直接返回代理对象。
响应式依赖函数和对象以及对象属性映射的数据结构
响应式实现
reactive
setter 执行依赖函数, getter 收集依赖函数。
reactive.ts
reactive函数,返回createReactiveObj(target, mutableHandlers, reactiveMap)。
createReactiveObj返回一个Proxy实例。放在WeakMap中判断创建。
effect.ts
内部调用ReactiveEffect类的run方法首次触发,依赖收集。
ReactiveEffect类接收fn作为参数。run方法中将activeEffect
全局变量赋值为this。并执行fn。如果有获取属性操作,就会触发Proxy的getter方法。
并触发track, 定义WeakMap数据结构保存依赖函数。(每个对象每个属性都保存依赖收集函数), trigger, 触发收集的依赖函数。建立了targetMap和activeEffect之间的联系。
baseHandlers.ts
定义Proxy操作方法,get(track), set(trigger)等。
dep.ts
createDep 创建一个set对象。用于存储依赖函数。
依赖收集和依赖触发数据结构
下面这个是比较直观的响应式基本代码。就是基于上面的数据结构构建的一套响应式流程。getter收集依赖,setter触发依赖。effect帮助getter收集依赖。
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap();
function getDep(target, key) {
// 1.根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 2.取出具体的dep对象
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
// vue2对raw进行数据劫持
function reactive(raw) {
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key);
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
// 将依赖函数传入到set中
dep.depend();
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
// 调用该依赖相关的所有函数
dep.notify();
}
}
})
})
return raw;
}
// vue3对raw进行数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target, key, newValue) {
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
}
})
}
断点跟踪vue源码
reactive
createReactiveObject effect
ReactiveEffect
run createGetter
track
createSetter
trigger
当我们修改代理对象中的属性,我们间接的在代理对象的set拦截器中修改了被代理对象的属性值。所以代理和被代理对象是同步的。
reactive, effect实现思路
调用reactive(返回proxy代理对象) > 在effect中创建ReactiveEffect实例 > 调用run方法(触发effect传入的回调,有代理对象的getter操作) > 触发代理对象的get方法(track函数收集依赖) > 收集对象对应的属性对应的activeEffect函数 > 触发代理对象的set方法(有代理对象的setter操作) > 触发对象对应的属性对应的activeEffect函数。
reactive局限性
- 不能处理基本数据类型。因为Proxy代理的是一个对象。
- 不能进行解构,结构后将失去响应性。因为响应性是通过代理对象进行处理的。结构后就不存在代理对象了,因此就不具备响应式了。
ref
测试用例
const { ref, effect } = Vue
const obj = ref({
name: "zh"
})
effect(() => {
// 先触发ref的 getter 行为 (触发trackValue触发ref的依赖收集,放在ref实例的dep中,当ref对象直接修改时,直接触发get value进行依赖函数执行)
// value.name 又触发代理对象的 getter 行为 (这个是将effect回调和代理对象的 key 进行绑定的)
document.getElementById("app").innerHTML = obj.value.name
})
setTimeout(() => {
// 先触发ref的 getter 行为
// value.name 又触发代理对象的 setter 行为
obj.value.name = "oop"
}, 1000)
断点跟踪vue源码
- RefImpl类创建一个ref实例。
- RefImpl中判断当传入的是否是一个对象,是则直接调用reactive做响应式。将其代理对象赋值给ref对象的
_value
属性保存。 - RefImpl中提供
get value
,set value
方法。在我们处理(读取value属性和为value属性赋值)ref对象时,就会调用对应的方法进行依赖收集和依赖触发。
然后obj.value.name
又会触发代理对象name属性的依赖收集。
总结
obj.value就是一个reactive返回的代理对象 ,这里并没有触发set value。不管是对复杂数据类型赋值还是读值,他都值触发refImpl实例的get value。
但是对于简单数据类型就不一样了。 构建简单数据类型时,他并不是通过代理对象去触发依赖收集和依赖触发的。而是通过refImpl中的get value set value主动去收集依赖和触发依赖的,这就是为啥get value 中的trackValue将依赖收集到ref实例的dep中的原因。
ref复杂数据类型
-
对于 ref 函数,会返回 RefImpl 类型的实例
-
在该实例中,会根据传入的数据类型进行分开处理
-
复杂数据类型:转化为 reactive 返回的 proxy 实例。在获取
ref.value
时返回的就是proxy实例。 -
简单数据类型:不做处理
-
-
无论我们执行 obj.value.name, 还是 obj.value.name=xxx, 本质上都是触发了 get value。
-
响应性 是因为 obj.value 是一个reactive 函数生成的 proxy
ref简单数据类型
我们需要知道的最重要的一点是:简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。
只是因为 vue 通过了 set value()的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个“类似于”响应性的结果。
我们就知道了网上所说的,ref的响应性就是将其参数包裹到value中传入reactive实现的,了解了这些,我们就可以大胆的说扯淡了。
xdm,一起学习vue核心思想吧,为了能突破现状,加油啊。天天对着表单表格看,人都傻掉了。
往期文章
专栏文章
最近也在学习nestjs,有一起小伙伴的@我哦。