《vue.js设计与实现》笔记(1~4章)
最近在看这本书,以下是做的笔记
第1章
命令式编程与声明式编程
关注结果(声明式)和关注过程(命令式)
运行时和编译时(vue、react、svelte)
坐火车时,进站检票(编译时)和上车检票(运行时)
框架性能与可维护性
第2章
框架设计的考虑因素
- 上手体验:足够的提示和引导
- 代码体积与tree-shaking:用尽量少的代码实现尽量多的功能,去除es模块死代码,tree-shaking处理副作用,需要手动告知,怎么告知,添加注释代码"/#PURE/"
- 构建输出产物和开关:灵活配置,生产环境和开发环境使用不同的包
- 错误处理:健壮、心智负担
- ts类型支持:维护性
总结一句话:体验、性能、灵活可配置、健壮、可维护
第3章
vue.js是基于编译时+运行时的架构,模版通过编译器转化成虚拟dom,虚拟dom通过渲染器渲染成真实dom
虚拟dom即对真实dom的js对象形式的描述,组件是对一组dom元素的封装,也能用虚拟dom来进行描述,这些描述即需要进行渲染的内容。
渲染器实现
function renderer(vnode, container) {
if (typeof vnode.tag === 'string') {
const el = document.createElement(vnode.tag);
for (const key in vnode.props) {
if (/^on/.test(key)) {
el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
}
}
if (typeof vnode.children === 'string') {
el.appendChild(document.createTextNode(vnode.children))
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => renderer(child, el));
}
container.appendChild(el);
} else if (
typeof vnode.tag === 'function' ||
typeof vnode.tag === 'object'
) {
const subtree = vnode.tag;
renderer(subtree, container);
}
}
编译器的作用将模版编译成渲染函数,模版的工作原理即将template标签里的模版内容编译成渲染函数,例如
一个vue组件
<template>
<div @click="handle">
click me
</div>
<template>
<script>
export default {
data(){/*...*/},
methods:{
handler:()=>{/*...*/}
}
}
</script>
编译之后的样子
export default {
data(){/*...*/}
methods:{
handler:()=>{/*...*/}
},
render(){
return h('div',{onClick:handler},'click me');
}
}
第4章
副作用函数:执行会对函数外部产生直接或间接影响的函数
最简单的响应式
const bucket = new Set();
const data = { text: 'initial' };
const obj = new Proxy(data, {
get(target, key) {
bucket.add(effect);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
bucket.forEach(fn => fn());
return true;
}
});
function effect() {
document.body.innerText = obj.text;
}
effect();
setTimeout(() => {
obj.text = 'vue3';
}, 3000);
响应式系统需要考虑的问题
- 硬编码副作用,副作用函数与目标字段之间建立明确关系:副作用函数注册机制,存储的数据结构<WeakMap:<target,Map:<key,Set>>>,分离track(追踪收集副作用)和trigger(触发副作用执行)
- 分支切换,产生遗留副作用函数:每次副作用执行时,先把它从与之关联的依赖集合中删除。
- 副作用嵌套:副作用函数栈
- 无限递归:确保trigger触发执行的副作用和当前执行的副作用不相同
- 调度执行(触发副作用之后,能够决定副作用函数执行时机、次数及方式):副作用函数第二个参数options,传入scheduler方法控制调度。
完善之后的响应式
let activeEffect;
const bucket = new WeakMap();
const data = {
ok: true,
text: 'hello world',
Comp1: true,
Comp2: false,
foo: 1
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
fn();
}
effectFn.deps = [];
effectFn();
}
const track = function (target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, deps = new Set());
}
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
const trigger = function (target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set();
effects && effects.forEach(fn => {
if (fn !== activeEffect) {
effectsToRun.add(fn);
}
})
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
});
/**=========================测试用例============================== */
// 1. 响应式实现
/**
*
function effect() {
document.body.innerText = obj.text;
}
effect();
setTimeout(() => {
obj.text = 'vue3';
}, 3000);
*
*/
// 2. 分支切换,产生遗留副作用函数
/**
effect(function effectFn() {
console.log('reRender')
document.body.innerText = obj.ok ? obj.text : 'not';
});
setTimeout(() => {
obj.ok = false;
}, 1000);
*/
// 3. 副作用嵌套
/**
let temp1, temp2;
effect(function effectFn1() {
console.log('effectFn1 run');
effect(function effectFn2() {
console.log('effectFn2 run');
temp2 = obj.Comp2;
})
temp1 = obj.Comp1;
})
*/
// 4. 无限递归
/**
effect(() => obj.foo++);
*/
// 5. 调度执行(触发副作用之后,能够决定副作用函数执行时机、次数及方式)
/**
*
const jobQueue = new Set();
const p = Promise.resolve();
let isFlushing = false;
function flushJob() {
if (isFlushing) return;
isFlushing = true;
p.then(() => {
jobQueue.forEach(job => job());
}).finally(() => {
isFlushing = false;
})
}
effect(() => {
console.log(obj.foo);
}, {
scheduler(fn) {
jobQueue.add(fn);
flushJob();
}
})
obj.foo++;
obj.foo++;
*
*/
computed和lazy
懒执行即手动执行副作用函数,通过lazy参数控制副作用函数的执行,当调用副作用函数时,通过其返回值能拿到对应的副作用函数,这样就可以手动执行了。
computed函数即副作用函数的懒执行并缓存返回值,computed通过两个变量来实现缓存,一个变量缓存值,另一个变量标识是否需要重新计算
computed实现
function computed(getter) {
let value;
let dirty = true;
const effectFn = effect(getter, {
lazy: true,
schedule() {
if (!dirty) {
dirty = true;
trigger(obj, 'value');
}
}
})
const computeValue = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
track(obj, 'value');
return value;
}
}
return computeValue;
}
watch
watch函数即副作用函数的二次封装,主要依赖于副作用的调度方法,数据变化时通知并更新相应的回调函数。
watch实现
function watch(value, cb, options = {}) {
let getter;
if (typeof value === 'function') {
getter = value;
} else {
getter = () => traverse(value);
}
let oldValue, newValue;
let cleanup;
const onInvalidate = (fn) => {
cleanup = fn;
}
const job = () => {
newValue = effectFn();
if (cleanup) cleanup();
cb && cb(newValue, oldValue, onInvalidate);
oldValue = newValue;
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: () => {
if (options?.flush === 'post') {
const p = Promise.resolve();
p.then(job);
} else {
job();
}
}
})
if (options.immediate) {
job();
} else {
oldValue = effectFn();
}
}
function traverse(value, ans = new Set()) {
if (typeof value !== 'object' || value === null || ans.has(value)) return;
ans.add(value);
for (const key in value) {
traverse(value[key], ans);
}
return value;
}