第一章:权衡的艺术

运行时和编译时

  • 运行时:用户直接提供数据对象构造成 DOM ```typescript const obj = { tag: ‘div’, children: [ {tag: ‘span’, children: ‘hello word’} ] }

function Render (obj, root) { const el = document.createElement(obj.tag); if (typeof obj.children === ‘string’) { const text = document.createTextNode(obj.childten); el.appendChild(text); } else if (obj.children) { obj.children.forEach(child => Render(child, el)); }

root.appendChild(el); }

Render(obj, document.body);

  1. - 编译时:用户可以提供 HTML 字符串,将其编译为数组对象后再交给运行时处理
  2. ```typescript
  3. <div>
  4. <span>hello world</span>
  5. </div>
  6. const obj = {
  7. tag: 'div',
  8. children: [
  9. {tag: 'span', children: 'hello word'}
  10. ]
  11. }

vue.js3 是一个运行时 + 编译时的架构

  1. // 调用 Compiler 编译得到树型结构的数据对象
  2. const obj = Compiler(html);
  3. // 再调用 Render 进行渲染
  4. Render(obj, document.body);

第二章:框架设计的核心要素

特性开关

可使用预定义变量来控制是否引入相应代码进而控制相应的特性是否生效
rollup.js 配置

  1. {
  2. __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
  3. }

webpack.DefinePlugin 插件实现

  1. new webpack.DefinePlugin({
  2. __VUE_OPTIONS_API__: JSON.stringify(true) // 开启特性
  3. })

错误处理

使用统一的错误处理接口提高程序的简洁性和健壮性

  1. let handlerError = null;
  2. export default {
  3. foo(fn) {
  4. callWithErrorHandling(fn);
  5. },
  6. // 用户可以调用该函数注册统一的错误处理函数
  7. registerErrorHandler(fn) {
  8. handlerError = fn;
  9. }
  10. }
  11. function callWithErrorHandling(fn) {
  12. try {
  13. fn && fn();
  14. } catch (e) {
  15. // 将捕获到的错误传递给用户的错误处理程序
  16. handlerError(e);
  17. }
  18. }

第三章:Vue.js3 的设计思路

初始渲染器

渲染器就是把虚拟 DOM 渲染为真实 DOM

  • 创建元素
  • 为元素添加属性和事件
  • 处理 children

    1. const vnode = {
    2. tag: 'div',
    3. props: {
    4. onClick: () => alert('hello')
    5. },
    6. children: 'click me'
    7. }
    1. function renderer(vnode, container) {
    2. // 使用 vnode.tag 作为标签名称创建 DOM 元素
    3. const el = document.createElement(vnode.tag);
    4. // 遍历 vnode.props,将属性、事件添加到 DOM 元素
    5. for (const key in vnode.props) {
    6. if (/^on/.test(key)) {
    7. // 如果 key 以 on 开头,说明是事件
    8. el.addEventListener(
    9. key.substr(2).toLowerCase(), // onClick ---> click
    10. vnode.props[key] // 事件处理函数
    11. )
    12. }
    13. }
    14. // 处理 children
    15. if (typeof vnode.children === 'string') {
    16. // 文本节点
    17. const text = document.createTextNode(vnode.children);
    18. el.appendChild(text);
    19. } else if (Array.isArray(vnode.children)) {
    20. // 递归遍历子节点
    21. vnode.children.forEach(child => renderer(child, el));
    22. }
    23. // 将元素添加到挂载点下
    24. container.appendChild(el);
    25. }

    第四章:响应系统的作用与实现

    副作用函数

    函数的执行会直接或间接的影响其他函数的执行,这就是产生了副作用。

设计一个完善的响应系统

  • 当读取操作发生时,将副作用函数收集到“桶”中
  • 当设置操作发生时,从桶中取出副作用函数并执行 ```typescript // 存储副作用函数的桶 const bucket = new WeakMap();

// 用一个全局变量存储被注册的副作用函数 let activeEffect; // effect 栈 const effectStack = [];

function effect(fn) { const effectFn = () => { // 调用 cleanup 函数完成清除工作 cleanup(effectFn); // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = effectFn; // 在调用副作用函数之前将当前副作用函数压入栈中 effectStack.push(effectFn); fn(); // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并将 activeEffect 还原为之前的值 effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; }

  1. // 用来存储所有与该副作用函数相关联的依赖集合
  2. effectFn.deps = [];
  3. // 执行副作用函数
  4. effectFn();

}

function cleanup(effectFn) { // 遍历 effectFn.deps 数组 for (let i = 0; i < effectFn.deps.length; i++) { // deps 是依赖集合 const deps = effectFn.deps[i]; // 将 effectFn 从依赖集合中移除 deps.delete(effectFn); } // 最后需要重置 effectFn.deps 数组 effectFn.deps.length = 0; }

const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 activeEffect 添加到存储副作用函数的桶中 track(target, key); // 返回属性值 return target[key]; }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal; // 把副作用函数从桶里取出并执行 trigger(target, key); } })

// 在 get 拦截函数内调用 track 函数追踪变化 function track(target, key) { // 没有 activeEffect,直接 return if (!activeEffect) return;

  1. let depsMap = bucket.get(target);
  2. if (!depsMap) {
  3. bucket.set(target, (depsMap = new Map()))
  4. }
  5. let deps = depsMap.get(key);
  6. if (!deps) {
  7. depsMap.set(key, (deps = new Set()));
  8. }
  9. // 把当前激活的副作用函数添加到依赖集合 deps 中
  10. deps.add(activeEffect);
  11. // deps 就是一个与当前副作用函数存在联系的依赖集合
  12. // 将其添加到 activeEffect.deps 数组中
  13. activeEffect.deps.push(deps);

}

// 在 set 拦截函数内调用 trigger 函数触发变化 function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return;

  1. const effects = depsMap.get(key);
  2. const effectsToRun = new Set(effects);
  3. effects && effects.forEach(effectFn => {
  4. // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
  5. if (effectFn !== activeEffect) {
  6. effectsToRun.add(effectFn);
  7. }
  8. })
  9. effectsToRun.forEach(fn => fn()); // 避免无限执行

}

  1. Map WeakMap 的区别
  2. ```typescript
  3. const map = new Map();
  4. const weakMap = new weakMap();
  5. (function() {
  6. const foo = { foo: 1 };
  7. const bar = { bar: 2 };
  8. map.set(foo, 1);
  9. weakMap.set(bar, 2);
  10. })();

WeakMap 的 key 是弱引用,不会影响垃圾回收器的工作,所以当 bar 给清除后无法中 weakMap 中获取到对象 bar。
所以 WeakMap 经常用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息。

调度执行

可调度,就是当触发副作用函数重新执行时,能够控制副作用函数执行时机、次数以及方式

  1. function effect(fn, options = {}) {
  2. const effectFn = () => {
  3. // ...
  4. }
  5. // 将 options 挂载到 effectFn 上
  6. effectFn.options = options;
  7. // ...
  8. }
  9. function trigger(target, key) {
  10. // ...
  11. effectsToRun.forEach(effectFn => {
  12. // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
  13. if (effectFn.options.scheduler) {
  14. effectFn.options.scheduler(effectFn);
  15. } else {
  16. // 否则直接执行副作用函数
  17. effectFn();
  18. }
  19. });
  20. }

不包含过渡状态的调度器

  1. // 定义一个任务队列
  2. const jobQueue = new Set();
  3. // 使用 Promise.resolve() 创建一个 promise 实例,用他将一个任务添加到微任务队列中
  4. const p = Promise.resolve();
  5. // 一个标志代表是否正在刷新队列
  6. let isFlushing = false;
  7. function flushJob () {
  8. // 如果队列正在刷新,则什么都不做
  9. if (isFlushing) return;
  10. // 设置为 true,代表正在刷新
  11. isFlushing = true;
  12. //在微任务队列中刷新 jobQueue 队列
  13. p.then(() => {
  14. jobQueue.forEach(job => job());
  15. }).finally(() => {
  16. // 结束后重置 isFlushing
  17. isFlushing = false;
  18. })
  19. }