[Livro recomendado] "Vue.js Design and Implementation" me leva ao código-fonte!

Vamos começar com a conclusão: este é o livro avançado mais valioso entre os livros de Vueframework no mercado. No processo de leitura deste livro, você pode sentir que está sendo liderado por membros da equipe principal do Vue para ler o código-fonte , e, ao mesmo tempo , informa como projetar a estrutura passo a passo e também informa por que ela foi projetada dessa maneira. …

Leitores recomendados deste livro:

  1. Pessoas com experiência real de desenvolvimento Vue2 ou Vue3
  2. Leia o código fonte do Vue
  3. Pessoas com experiência no desenvolvimento de frameworks MVVM
  4. Pessoas com experiência em desenvolvimento que desejam entender o framework Vue

Este livro é na verdade um livro relativamente grosso. No passado, quando um livro de espessura semelhante era encontrado, ele o corroia do começo ao fim... E você precisa de muito tempo para ler, caso contrário, muitas vezes você verá a parte de trás e esquecerá a frente. O estilo de leitura recomendado é a leitura direcionada a objetivos. A maioria de nós não está lendo máquinas, muitos "tomos" não podem ser lidos do começo ao fim, e livros muito grossos exigem muitos blocos consecutivos de tempo para serem lidos do começo ao fim. Caso contrário, quando você tiver a chance de pegar o livro, você quase esqueceu o que leu da última vez.

ps: Minha própria ordem de leitura é 1, 2, 3, 4, 6, 5, 7, 8, 12, 15, 16, 17, 9, 10, 11... (leia sob demanda depois)

A parte de abertura: Capítulos 1~3 (devem ser lidos!)

A diferença entre declarativo e imperativo é discutida abertamente, o consumo de energia e as vantagens e desvantagens dos dois paradigmas são comparados e as características do Vue3 como um framework de UI imperativo são resumidas de forma concisa. Em seguida, da perspectiva do design da estrutura, ele explica os conceitos de design aos quais o ambiente de desenvolvimento e o ambiente de produção precisam prestar atenção, incluindo um mecanismo de relatório de erros mais amigável, como fazer melhor uso do mecanismo Tree-Shaking e controle de efeitos. Finalmente, o terceiro capítulo discute a relação entre Vdom e renderização de componentes em Vue a partir do mecanismo de componentes e renderizadores.Toda a parte de abertura usa uma voz muito simples para revelar todo o conceito de design Vue ao leitor.

Parte 2: Sistema de Resposta (Parte da leitura obrigatória)

Sabemos que o Vue implementa a vinculação de dados bidirecional com base no observador e no mecanismo de assinatura de mensagens para realizar o processo de resposta da página de dados. A segunda parte também é a maior deste livro. Como realizar o mecanismo de resposta do Vue3, leitores com Vue2 a experiência do código-fonte pode ser comparada Vamos dar uma olhada no princípio reativo que o Vue3 implementa com base nos efeitos colaterais da programação funcional.

Para as sugestões de leitura nesta parte, abra o IDE e siga as alterações de código de caso no livro para pensar e digitar.

O quarto capítulo apresenta o princípio da ligação bidirecional do Vue3 e explica o princípio de implementação de computed/watch no vue3. Existem também soluções para as condições de corrida.

Aqui está um código que segue o caso no Capítulo 4 do livro


import { flushJob, jobQueue } from "./jobQueue";
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
const effectStack = []; // 副作用栈,防止当前副作用多个连带依赖影响执行依赖链上的副作用
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn); // 清除原有的依赖
    // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
    activeEffect = fn;
    // 在调用副作用函数之前,将当前副作用函数压入栈中
    effectStack.push(effectFn);
    // 执行副作用函数, res 承接fn() 结果并在最后副作用函数结束后返回
    const res = fn();
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    return res;
  };
  effectFn.options = options; // options 可以让用户设置调度 options.scheduler ...
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = [];
  // 计算属性原理,非 lazy 才立即执行
  if (!options.lazy) {
    // 执行副作用函数
    effectFn();
  }
  return effectFn;
}
// 储存副作用的函数桶
const bucket = new WeakMap();
// 原始数据
const data = { text: "hello world" };
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) {
  if (!activeEffect) return;
  // 根据 target 从“桶”中取得depsMap,它也是一个Map类型: key --> effects
  let depsMap = bucket.get(target);
  // 如果不存在 depsMap, 那新建一个Map 并与 target 关联(创建依赖收集)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 再根据key 从 depsMap 中取得 deps, 它是一个 Set 类型,里面存储着左右与当前 key 相关联的副作用函数: effects
  let deps = depsMap.get(key);
  // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  deps.add(activeEffect);
  // dpes 就是一个与当前副作用函数存在联联系的依赖集合, 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps);
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取得 depsMap, 它是 key --> effects
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 根据key 取得所有副作用函数 effects
  const effects = depsMap.get(key);
  //执行副作用函数
  // effects && effects.forEach(fn => fn());
  const effectsToRun = new Set(effects); // Set.prototype.forEach https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Set/forEach
  effects &&
    effects.forEach(effectFn => {
      // 为了避免无线递归调用,从而避免栈溢出 e.g. effect(() => obj.foo++)
      if (effectFn !== activeEffect) {
        // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
        effectsToRun.add(effectFn);
      }
    });
  effectsToRun.forEach(effectFn => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn);
    } else {
      // 否则执行默认行为
      effectFn();
    }
  });
}
function cleanup(effectFn) {
  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;
}
// 调度案例
effect(
  () => {
    console.log("do someting");
  },
  {
    scheduler(fn) {
      jobQueue.add(fn);
      flushJob();
    },
  }
);
// 计算属性案例
function computed(getter) {
  // value 用来缓存上一次计算的值
  let value;
  // dirty 标志,用来标识是否需要重新计算值,true 则意味着“脏”,需要计算
  let dirty = true;
  // 把 getter 作为副作用函数,创建一个 lazy 的 effect
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true; // 调度器内重置 true 防止缓存锁死返回值;
      // 当计算属性的响应式数据变化时,手动调用 trigger 函数触发响应
      trigger(obj, "value");
    },
  });
  const obj = {
    // 当读取 value 时才执行 effectFn
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      // 当读取 value 时,手动调用 track 函数进行追踪
      track(obj, "value");
      return value;
    },
  };
  return obj;
}
// watch 函数接受 sourc: 响应式数据, cb 是回调函数
function watch(source, cb, options = {}) {
  let getter;
  if (typeof source === "function") {
    getter = source;
  } else {
    getter = () => traverse(source);
  }
  // 定义旧值和新值
  let oldValue, newValue;
  // cleanup 用来储存用户注册的过期回调
  let cleanup;
  function onInvalidate(fn) {
    cleanup = fn;
  }
  const job = () => {
    // c重新执行副作用函数,得到的是新值
    newValue = effectFn();
    // 调用回调cb前, 先用过期回调
    if (cleanup) {
      cleanup();
    }
    // 当数据变化时调用回调函数
    cb(newValue, oldValue, onInvalidate);
    oldValue = newValue; // 更新旧值
  };
  // 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到 effectFn 中以便后续手动调用
  const effectFn = effect(
    // 触发递归读取操作,从而建立联系
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        // 调度函数中判断 flush 是否为 ’post', 如果是,将其放到微任务队列中执行
        if (options.flush === "post") {
          const p = Promise.resolve();
          p.then(job);
        } else {
          job();
        }
      },
    }
  );
  if (options.immediate) {
    // 当 immediate 为 true 时立即执行 job, 从而触发回调执行
    job();
  } else {
    oldValue = effectFn();
  }
}
function traverse(value, seen = new Set()) {
  // 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
  if (typeof value !== "object" || value == null || seen.has(value)) return;
  // 将数据添加到 seen 中,代表遍历读取过了,避免循环引用引起的死循环
  seen.add(value);
  // 暂时不考虑数据等其他结构
  // 假设 value 就是一个对象,使用 for...in 读取对象的没一个值,并递归调用 traverse 进行处理
  for (const k in value) {
    traverse(value[k], seen);
  }
  return value;
}
//add some
export default { effect: effect };

复制代码

Os Capítulos 5 e 6 apresentam os esquemas de resposta para tipos de referência e tipos primitivos, respectivamente. Estudantes com experiência no uso do Vue2 recomendam fortemente estudar o Capítulo 6, porque o Proxy só pode fazer referência a tipos de proxy.O Capítulo 6 informa como o Vue3 faz proxy do tipo original de Js através do Proxy.

O Capítulo 5 recomenda a leitura de Como Objeto e Matriz são proxies por Proxy. Esta parte procura um slot integrado do tipo de referência semelhante à [[call]]API Proxy, fazendo referência a um grande número de produções de especificação ES. Esta parte pertence aos alunos que estão interessados ​​em linguagem e encapsulamento subjacente podem optar por ler.

Parte 3: Renderização

seção de renderização (deve ser lida)

A renderização é a parte mais importante da resposta de dados à camada de visualização.

  • seção de montagem
  • Seção de desinstalação
  • Manipulação de eventos: como descrever eventos em nós virtuais e como montar e atualizar eventos
seção diff (opcional)

Pode-se dizer que esta parte do diff foi quebrada e contada para você, e muitas legendas foram adicionadas para facilitar o entendimento. Esta parte analisa principalmente as vantagens e desvantagens de cada versão do algoritmo diff para três iterações e usa três capítulos para falar sobre a implementação do algoritmo Diff.

  1. Diferença Simples
  2. Diferença de terminação dupla
  3. Diferença rápida

Parte 4: Componentização (leitura obrigatória)

Lógica do componente (deve ser lida)

Esta parte fala principalmente sobre o método de processamento de renderização e atualização de componentes e como a API de configuração é implementada em mountComponent. Pode-se dizer que envolve muitos problemas ocultos que são ignorados no desenvolvimento real:

  • Você quer misturá-lo com o Vue2?
  • Como implementar o ciclo de vida na configuração?
  • Como usar a configuração para implementar o encapsulamento de componentes?
  • Como funções como emissão de slot são encapsuladas em componentes?
// 组件逻辑部分demo
import { effect } from "./bookDemo.js";
import queueJob from "./jobQueue.js";

const vnode = {
  type: MyComponent,
  props: {
    title: "A big title",
    other: this.val,
  },
};

function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type; // type 是个对象....
  const {
    render,
    data,
    props: propsOptions,
    beforeCreate,
    created,
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
  } = componentOptions;

  beforeCreate && beforeCreate();
  const state = reactive(data()); // data 数据响应化
  const [props, attrs] = resolveProps(propsOptions, vnode.props);
  // 定义组件实例,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息
  const instance = {
    // 组件自身的状态数据,即 data
    state,
    props: shallowReactive(props),
    // 一个布尔值,用来表示组件是否被挂载,初始值 false
    isMounted: false,
    // 组件所渲染的内容,即子树(subTree)
    subTree: null,
  };

  // 将组件实例设置到 vnode 上,用于后续更细
  vnode.component = instance;

  // 创建渲染上下文对象,本质上是组件实例的代理
  const renderContext = new Proxy(instance, {
    get(t, k, r) {
      // 取得组件自身状态与 props 数据
      const { state, props } = t;
      if (state && k in state) {
        return state[k]; // 尝试先读取自身属性
      } else if (k in props) {
        return props[k];
      } else {
        console.log("不存在");
      }
    },
    set(t, k, v, r) {
      const { state, props } = t;
      if (state && k in state) {
        state[k] = v;
      } else if (k in props) {
        props[k] = v;
      } else {
        console.log("不存在");
      }
    },
  });

  created && created.call(renderContext); // 在这里调用 created 将 renderContex 的 this 调整
  effect(
    () => {
      // 调用组件渲染函数,获得子树
      const subTree = render.call(state, state);
      // 检查组件是否已经被挂载
      if (!instance.isMounted) {
        beforeMount && beforeMount.call(state);
        // 初次挂载,调用 patch 函数第一个参数传递 null
        patch(null, subTree, container, anchor);
        // 重点:将组件实例的 isMounted 设置为 true, 这样当更新发生时就不会再次进行挂载操作,而是会执行更新
        instance.isMounted = true;
        mounted && mounted.call(state);
      } else {
        beforeUpdate && beforeUpdate.call(state);
        // 当 isMounted 为 true ,说明组件已经挂载,只需要完成自更新即可,所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
        patch(instance.subTree, subTree, container, anchor);
        updated && updated.call(state);
      }
      // 更新组件实例的子树
      instance.subTree = subTree;
    },
    { scheduler: queueJob } //这里的 queueJob 调度封装
  );
}

function resolveProps(options, propsData) {
  const props = {};
  const attrs = {};
  // 遍历为组件传递的 props 数据
  for (const key in propsData) {
    if (key in options) {
      // 如果 props 数据在组件自身的 props 选项中有定义,则将其视为合法 props
      props[key] = propsData[key];
    } else {
      attrs[key] = propsData[key];
    }
  }

  return [props, attrs];
}
export default mountComponent;

// 下面是 patch 如何引用 mountComponent
function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }

  const { type } = n2;

  if (typeof type === "string") {
    // ...
  } else if (type === "Text") {
    // ...
  } else if (type === Fragment) {
    // ...
  } else if (typeof type === "object") {
    if (!n1) {
      mountComponent(n2, container, anchor);
    } else {
      // 更新组件
      patchComponent(n1, n2, anchor);
    }
  }
}

function patchComponent(n1, n2, anchor) {
  // 获取组件实例,即 n1.component, 同时让新的组件虚拟节点 n2.component 也指向组件实例
  const instance = (n2.component = n1.component);
  const { props } = instance;

  // 调用 hasPropsChanged 监测为子组件传递的 props 是否发生变化,如果没有变化,则不需要更新

  if (hasPropsChanged(n1.props, n2.props)) {
    // 调用 resolveProps 函数重新获取 props 数据
    const [nextProps] = resolveProps(n2.type.props, n2.props);
    // 更新 props
    for (const k in nextProps) {
      props[k] = nextProps[k];
    }
    // 删除不存在的 props
    for (const k in props) {
      if (!(k in nextProps)) delete props[k];
    }
  }
}

function hasPropsChanged(prevProps, nextProps) {
  const nextKeys = Object.keys(nextProps);
  // 如果新旧 props 的数量变了,则说明有变化
  if (nextKeys.length !== Object.keys(prevProps).length) {
    return true;
  }
  for (let i = 0; i < nextKeys.length; i++) {
    const key = nextKeys[i];
    // 有不相等 props, 则说明有变化
    if (nextProps[key] !== prevProps[key]) {
      return true;
    }
  }
  return false;
}

复制代码
Componentes assíncronos (opcional)
Componentes Funcionais (opcional)

Parte 5: Compiladores (leitura opcional)

这部分涉及到编译原理,主要是Vue Compiler 核心的实现,同时介绍了模板编译器DSL是如何去做词法分析 -> 语法分析-> 语义分析这套流程的。我对这一部分的看法是,读者根据个人兴趣阅读即可。毕竟编译原理属于开发绕不过去的内功,如果是非科班出身的同学建议通过这一篇入门编译原理。 不过这一篇有个必读点: 17章,主要讲了Vue3 在编译时所做的优化点:

  • 动态节点
  • block
  • 静态提升等

第六篇:服务端渲染(选读)

讲了 SSR 的特点和注意事项。这部分我建议先看总结部分,如果感兴趣可以看看,结合一下 SSR 的框架实践一下。

参考资料

《Vue.js 设计与实现》


企业微信截图_20220411195011.png

以上是我断断续续读了一个月后得出的一些阅读总结和建议,希望这篇文章对你起到一定的帮助~

おすすめ

転載: juejin.im/post/7085565765293703176