目录
- 框架设计概览
- 响应式系统
- 渲染器
- 组件化
- 编译器
- 服务端渲染
第一篇 框架设计概览
第1章 权衡的艺术
1.1.命令式和声明式
声明式(注重结果)和命令式(注重过程)。
Vue封装了命令式的过程,对外暴露出了声明式的结果。
1.2.性能和可维护性的权衡
命令式(简单js操作)的性能优于声明式,命令式可维护性差于声明式。
1.3.虚拟DOM的性能到底如何
1.4.运行时和编译时
参考:读懂vue源码必懂什么叫运行时和编译时_vue 运行时编译-CSDN博客
第2章 框架设计的核心要素
2.1.提升用户的开发体验
提供友好的错误提示(console.warn),有助于开发者快速定位问题;
自定义console打印值initCustomFormatter。
2.2.控制框架代码的体积
区分开发环境、生产环境,减轻生产环境代码体积。
2.3.框架要做到良好的Tree-Shaking
消除那些永远不会执行的代码;想要实现Tree-Shaking,模块必须是ES Module。
javascript中的esm - 知乎 (zhihu.com)
执行rollup会自动把一些无用函数删除;一些引用的函数,可能也没有什么作用的不会自动删除,通过标记/#_PURE_/让rollup识别该函数可以删除。
2.4.框架应该输出怎样的构建产物
应对不同的使用场景,输出不同的产物
- 场景一:script直接引入
方案一:需要输出一个立即执行的函数表达式(在rollup.js中配置format:iife来输出这种形式的资源)
方案二:输出ESM格式资源(使用时sctipt标签内写type=”module”,rollup.js输出格式配置为format:esm) - 场景二:通过require引入资源
const Vue=require('vue')
解决方案:在rollup.config.js中设置format:cjs
2.5.特性开关
可以选择关闭原有框架的特性,减少资源体积;也可以在框架升级做兼容时,用户通过特性开关可以取消兼容,减少兼容这部分的代码逻辑。__VUE_OPTIONS_API__
2.6.错误处理
callWithErrorHandling
统一处理错误,避免过多的try catch
用户可以自定义统一的错误处理函数
vue.js中也就可以注册统一的处理函数:
const app=createApp(app)
app.confing.errorHandler=()=>{
}
2.7.良好的TypeScript类型支持
runtime-core/arc/apiDefineComponent.ts,为类型支持服务。
第3章 Vue.js 3的设计思路
3.1.声明式地描述UI
使用与HTML标签一致的方式来描述属性,例如<div></div>
;
使用与HTML标签一致的形式描述属性:<div id="app"></div>
;
使用:或v-bind来描述动态绑定的属性,例如<div :id="dynamicId"></div>
;
使用或v-on来描述事件,例如点击事件<div @click="handler"></div>;
使用与HTML标签一致的方式来描述层级结构,例如:<div><span></span></div>
;
虚拟DOM,用js对象的方式来描述UI
3.2.初始渲染器
渲染器的作用:把虚拟DOM渲染为真实DOM
// vnode:虚拟DOM对象;container:一个真实的dom元素,作为挂载点
function renderer(vnode,container){
// 使用 vnode.tag 作为标签名称创建 DOM元素
const el = document.createElement(vnode.tag)
// 遍历 vnode.props,将属性、事件添加到 DOM元素
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以 on 开头,说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(),// 事件名称 onclick ---> click
vnode.props[key] // 事件处理函数
)
}
// 处理 children
if (typeof vnode.children === 'string') {
// 如果 children 是字符串,说明它是元素的文本子节点 el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach(child => renderer(child, el))
}
// 将元素添加到挂载点下
container.appendChild(el)
}
思路
- 创建元素: 把
vnode.tag
作为标签名称来创建DOM元素。 - 为元素添加属性和事件: 遍历
vnode.props
对象,如果key
以on
字符开头,说明它是一个事件,把字符on
截取掉后再调用toLowerCase
函数将事件名称小写化,最终得到合法的事件名称,例如onClick
会变成click
,最后调用addEventListener
绑定事件处理函数。 - 处理
children
: 如果chidlren
是一个数组,就递归地调用renderer
继续渲染,注意,此时我们要把刚刚创建的元素作为挂载点(父节点);如果children
是字符串,则使用createTextNode
函数创建一个文本节点,并将其添加到新创建的元素内。
3.3.组件的本质
组件就是一组DOM元素的封装
vue.js中有状态组件是使用对象结构来表达的。
有状态组件:带有数据和逻辑的组件,它们存储组件内部状态和方法。有状态组件通常用于处理动态数据,并使用 reactive 响应式系统处理状态更新。
无状态组件:纯粹的渲染组件,不包含内部状态,仅通过 props 获取数据。因为它们没有状态和逻辑,所以它们比有状态组件更快,因为它们可以被 Vue 进行优化。
//组件内容
const MyComponent={
reader(){
return {
tag:'div',
props:{
onClick:()=>alert('hello')
},
children:'click me'
}
}
}
//定义虚拟dom
const vNode={
'tag':MyComponent
}
//渲染
const subtree = vnode.tag.reader()
readerer(subtree,container)
3.4.模板的工作原理
编译器:将模板翻译为渲染函数
//模板
<div @click="handler">
click me
</div>
//编译器将他转化成渲染函数
render(){
return h('div',{onClick:handler},'click me')
}
3.5.Vue.js是各个模块组成的有机整体
组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟DOM的设计决定的。编译器和渲染器之间是存在信息交流,互相配合提升性能,交流的媒介是虚拟DOM对象。
第二篇 响应系统
第4章 响应系统的作用与实现
4.1.响应式数据与副作用函数
副作用函数:函数的执行会直接或间接影响其他函数的执行,即为函数产生了副作用。(例如一个函数修改了全局变量,这其实也是一个副作用。)
4.2.响应式数据的基本实现
定义一个副作用函数,通过proxy
监听属性的读写操作,
当get
时,将对应副作用函数放入桶(数据结构为set)中;
当set
时从桶中取出副作用函数并执行,实现数据响应式。
4.3.设计一个完善的响应系统
1.不依赖副作用函数的名字:定义一个全局变量activeEffect
存储被注册的副作用函数;
2.将副作用函数与被操作的目标字段之间建立明确关系,通过树型结构明确关系,用WeakMap创建桶。
WeakMap 由target(操作对象)--->Map构成;
Map由key(操作对象的key)和set(存储副作用函数)构成
4.4.分支切换与cleanup
分支切换指某个值改变时,代码执行的分支会跟着改变,比如三元表达式。分支切换可能会产生遗留的副作用函数,导致不必要的更新。
clearup是为了将构造函数从所有与之关联的依赖集合(set)中移除。
注意1:effectFun.deps存储的正是target->key所指向的set(存储副作用函数的集合),因此删除effectFun.deps中的effectFun正是删除桶中的effectFun)
注意2:在监听属性set方法时会遍历执行构造函数,前面删除又新增集合Set,会出现无限循环执行(语言规范:再调用forEach遍历Set时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时forEach遍历没有结束,那么该值会被重新访问),因此需要新创建一个Set再去遍历执行对应的副作用函数。
4.5.嵌套的effect和effect栈
场景:组件发生嵌套。
问题:内层的副作用函数会覆盖activeEffect
的值,并且不会恢复。
解决方案:定义一个副作用函数栈effectStack
,执行时压入栈顶,执行完毕后弹出,始终让activeEffect
指向栈顶。
4.6.避免无限递归循环
场景:当副作用函数既包含读取,又设置当前监听对象的值时。
问题:副作用函数在执行中,就要开始下一次执行,导致无限递归的调用自己,于是就产生了栈溢出。
解决方案:在trigger触发时判断当前触发的函数是否与activeEffect
相同,若相同则不执行。
var data = { text: '2343', exit: '12', flag: true };
// WeakMap为弱引用,不影响垃圾回收
var bucket = new WeakMap();
// 存储当前的副作用函数
let activeEffect = null;
// 将副作用函数和当前target、key绑定,放至桶中
const track = (target, key) => {
if (!activeEffect) return;
// target-->key-->effect
let bucTat = bucket.get(target);
if (!bucTat) {
bucket.set(target, (bucTat = new Map()));
}
let bucKey = bucTat.get(key);
if (!bucKey) {
bucTat.set(key, (bucKey = new Set()));
}
bucKey.add(activeEffect);
// 为了后续避免副作用函数不必要的更新
activeEffect.depts.push(bucKey);
};
// 执行副作用函数
const trigger = (target, key) => {
const bucTar = bucket.get(target);
if (!bucTar) return;
const bucKey = bucTar.get(key);
// 避免Set集合(清除副作用函数、新增副作用函数)、forEach导致的死循环
const effects = new Set(bucKey);
effects &&
effects.forEach(fn => {
// 避免副作用函数无限递归调用自身,造成死循环问题,比如 foo++
if (fn == activeEffect) return;
fn();
});
};
// 代理
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newValue) {
target[key] = newValue;
trigger(target, key);
return true;
},
});
// 清除副作用函数上一次建立的相应联系
const cleanUp = effectFn => {
for (let i = 0; i < effectFn.depts.length; i++) {
const depts = effectFn.depts[i];
depts.delete(effectFn);
}
effectFn.depts.length = 0;
};
const effect = val => {
const effectFn = () => {
cleanUp(effectFn);
activeEffect = effectFn;
val();
};
effectFn.depts = [];
effectFn();
};
onMounted(() => {
effect(() => {
document.getElementById('odiv').innerHTML = obj.flag ? obj.text : obj.exit;
});
});
setTimeout(() => {
obj.flag = false;
obj.text = '111';
}, 3000);
4.7.调度执行
可调度:当trigger触发副作用函数重新执行时,有能力决定副作用函数的执行时机、次数以及方式。
实现方法:为effect函数设计一个选项参数options,允许用户指定调度器
4.8.计算属性computed与lazy
- 副作用函数不立即执行:不需要副作用函数立即执行的情况下,在computed函数下,创建一个lazy的effect(在options里面增加:lazy:true),在effect中判断lazy,如果为true,返回副作用函数执行过的结果,否则直接执行,就实现了副作用函数不立即执行。
- 对值进行缓存:避免无效的重新计算,定义value(存储上一次计算的值),dirty(判断是否要重新计算,dirty为true表示以来的值有变化,要重新计算),当依赖的值发生变化时,在刚刚定义的effect中增加调度函数,设置dirty为true,计算之后,将dirty设置为false
当在另外一个effect中读取读取计算属性的值时,修改依赖的值不会重新渲染:当读取计算属性的值时,手动调用track函数进行追踪,当依赖的响应式数据发生变化时,手动调用trigger函数触发响应式。
function computed(getter) { let value='' let dirty = true const effectFn = effect(getter,{ lazy: true, scheduler() { if (!dirty) { dirty = true // 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应 trigger(obj,'value') } }) const obj ={ get value() { if (dirty){ value = effectFn() dirty = false } // 当读取 value 时,手动调用 track 函数进行追踪 track(obj,'value') return value } } return obj }
4.9.watch的实现原理
原理:观测响应数据,当数据发生变化时通知并执行相应的回调函数。
watch(source,cb){
let getter
//watch第一个函数为getter函数的情况
if(typeof source=='function'){
getter=source
} else {
//通过读取对象的值实现副作用函数和对象绑定,traverse函数是为了递归读取操作,用于保证绑定对象全部的值
getter=()=>traverse(source)
}
//定义旧值与新值
let oldValue,newValue
const effectFn=effect(
()=>getter(),
{
lazy:true,
scheduler(){
newValue=effectFn()
cd(newValue,oldValue)
oldValue=newValue
}
}
)
//第一次执行得到的值
oldValue=effectFn()
},
//traverse函数是为了递归读取操作
traverse(value,seen=new Set()){
//seen避免死循环
if(typeof value!=='object' || value===null || seen.has(value)) return
seen.add(value)
for(const k in value){
traverse(value[k],seen)
}
return value;
}
4.10.立即执行的watch与回调执行时机
原理:接收immediate
参数,将schedule
调度函数独立出来,判断immediate
为true
时立即执行job
,触发回调执行,立即执行oldValue
为undefined
。
const watchFunc = (source, cb, options = {}) => {
// 接收参数options,用于传immediate等参数
let getter;
if (typeof source === 'function') {
getter = source;
} else {
getter = () => traverse(source);
}
let newVal;
let oldVal;
// 将schedule调度函数独立出来
const job = () => {
newVal = effectFunc();
cb(newVal, oldVal);
oldVal = newVal;
};
const effectFunc = effect(getter, {
lazy: true,
schedule: job,
});
// immediate为true时立即执行job,触发回调执行
if (options.immediate) {
job();
} else {
oldVal = effectFunc();
}
};
4.11.过期的副作用
竞态问题:由于响应时间不同,可能先请求的后返回结果,导致最终的结果是先请求的。watch
函数的回调函数接收第三个参数onInvalidate
,在调用cb
回调函数之前,先调用过期函数。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。