手把手带你实现一个自己的简易版 Vue3(六)

👉 项目 Github 地址:github.com/XC0703/VueS...

(希望各位看官给本菜鸡的项目点个 star,不胜感激。)

9、组件的渲染(一)

9-1 组件渲染的流程

在上面我们创建了 vnode,那么接下来顺其自然就应该到渲染 vnode 到我们的页面上,即实现 render 方法。

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
import { apiCreateApp } from "./apiCreateApp";

// 实现渲染Vue3组件==>vnode==>render
export function createRender(renderOptionDom) {
  // 真正实现渲染的函数(渲染vnode)
  let render = (vnode, container) => {
    // 第一次渲染(三个参数:旧的节点、当前节点、位置)
    patch(null, vnode, container);
  };

  // 返回一个具有createApp方法的对象,其中createApp负责生成一个具有mount挂载方法的app对象(包含属性、方法等),进而实现1、生成vnode;2、render渲染vnode
  return {
    createApp: apiCreateApp(render),
  };
}

render 渲染函数调用 patch 函数,patch 函数负责根据 vnode 的不同情况(组件、元素)来实现对应的渲染:

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
// patch函数负责根据vnode的不同情况(组件、元素)来实现对应的渲染
const patch = (n1, n2, container) => {
  // 针对不同的类型采取不同的渲染方式(vonode有一个shapeFlag标识来标识组件/元素)
  const { shapeFlag } = n2;
  // 等效于shapeFlag && shapeFlag === ShapeFlags.ELEMENT
  if (shapeFlag & ShapeFlags.ELEMENT) {
    // 处理元素
    // console.log("元素");
  } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    // 处理组件
    processComponent(n1, n2, container);
  }
};

其中 processComponent 方法是负责处理组件的渲染的(分为初次渲染和更新两种情况):

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
// 组件的创建方法(分为初次渲染和更新两种情况)
const processComponent = (n1, n2, container) => {
  if (n1 === null) {
    // 组件第一次加载
    mountComponent(n2, container);
  } else {
    // 更新
  }
};

其中 mountComponent 方法负责组件的真正渲染(实现由虚拟 dom 变成真实 dom),因为是第一次,要做的事情更多(比如绑定 effect),如果是更新,则触发 effect 即可:

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
// 组件渲染的真正方法(实现由虚拟dom变成真实dom),步骤(核心):
const mountComponent = (InitialVnode, container) => {
  // 1、先有一个组件的实例对象(即Vue3组件渲染函数render传入的第一个参数proxy,其实proxy参数将组件定义的所有属性合并了,等效于在setup入口函数里面返回一个函数,可以用proxy.来获取属性)
  const instanece = (InitialVnode.component =
    createComponentInstance(InitialVnode)); // 记得在weak-vue\packages\runtime-core\src\vnode.ts文件给vnode定义中加上这个属性
  // 2、解析数据到这个实例对象中
  setupComponet(instanece);
  // 3、创建一个effect让render函数执行
  setupRenderEffect();
};

9-2 组件渲染的具体实现

9-2-1 组件渲染的三个步骤方法的实现

上面说到,组件渲染分为三步,下面我们就分别实现这三个方法。

创建组件实例的 createComponentInstance 方法:

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
// 创建组件实例
export const createComponentInstance = (vnode) => {
  // instance本质是一个对象(包含组件的vnode,前面实现的组件的一些属性如参数props、自定义属性attrs,setup入口函数的状态等)
  const instance = {
    vnode,
    type: vnode.type, // 组件的所有属性都在这里面
    props: {}, // 组件的参数
    attrs: {}, // 自定义属性
    setupState: {}, // 用来存储setup入口函数的返回值
    ctx: {}, // 用来处理代理,保存实例的值,和下面的proxy一起用。没有这个会导致用类似instance.props.xxx才能获取属性,有了之后直接proxy.xxx便能直接获取了
    proxy: {}, // 和上面的ctx一起用
    render: false, // 存储组件实例的渲染函数
    isMounted: false, // 是否挂载
  };
  instance.ctx = { _: instance };
  return instance;
};

解析数据到该组件实例的 setupComponet 方法:

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
// 解析数据到该组件实例
export const setupComponet = (instance) => {
  // 代理
  instance.proxy = new Proxy(instance.ctx, componentPublicInstance as any);

  // 拿到值(上面instance的props等)
  const { props, children } = instance.vnode;
  // 把值设置到组件实例上
  instance.props = props;
  instance.children = children; // 相当于slot插槽
  // 看一下这个组件有无状态(有状态代表有setup入口函数或者render函数)
  const shapeFlag = instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT;
  if (shapeFlag) {
    setupStateComponent(instance);
  } else {
    // 如果无状态,说明是简单组件,直接渲染即可。
  }
};

9-2-2 对 setup 进行处理

其中,处理有状态的组件 setupStateComponent 方法定义如下,从上面知道:

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
// 处理有状态的组件
function setupStateComponent(instance) {
  // setup方法的返回值是我们的render函数的参数
  // 拿到组件的setup方法
  //   其中我们可以知道:
  // 1、setup方法的参数是组件参数props、上下文对象context(包含了父组件传递下来的非 prop 属性attrs、可以用来触发父组件中绑定的事件函数emit、一个指向当前组件实例的引用root、用来获取插槽内容的函数slot等)
  // 2、setup方法的返回值可以是一个对象(包含代理的响应式属性以供渲染函数使用),也可以是直接返回渲染函数
  const Component = instance.type; // createVNode时传入给type的是rootComponent,本质是一个对象,组件的所有属性都在这里,比如setup方法,比如render方法
  const { setup } = Component;
  console.log(setup);
  setup();
}

此时 npm run build 后去执行我们的测试用例:

html 复制代码
<!-- weak-vue\packages\examples\7.createApp.html -->
<div id="app">111111111111</div>
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<script>
  let { createApp } = VueRuntimeDom;

  let App = {
    setup(props, context) {
      console.log("setup函数执行了!!!");
    },
  };
  createApp(App, { name: "张三", age: 10 }).mount("#app");
</script>

可以看到打印出预期的结果,说明我们目前的链路是正确的,此时拿到了 setup 入口函数。

拿到 setup 函数后就要对传入的参数进行处理了。因为我们知道,setup 函数定义时传入的 propscontext 实际上是两个形参,实际执行时要去处理真正的实参。

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
//  2、处理参数
const setupContext = createContext(instance); // 返回一个上下文对象
setup(instance.props, setupContext); // 实际执行的setup函数(实参)

// 处理context上下文对象(包含了父组件传递下来的非 prop 属性attrs、可以用来触发父组件中绑定的事件函数emit、一个指向当前组件实例的引用root、用来获取插槽内容的函数slot等)
function createContext(instance) {
  return {
    sttrs: instance.attrs,
    slots: instance.slots,
    emit: () => {},
    expose: () => {},
  };
}

此时去执行我们的测试用例:

html 复制代码
<!-- weak-vue\packages\examples\7.createApp.html -->
<script>
  setup(props, context) {
    console.log("setup函数执行了!!!");
    console.log(props,context);
  },
</script>

可以看到,propscontext 被打印了出来:

说明我们目前的链路是正确的,此时正确处理了 setup 入口函数的参数。


9-2-3 对 render 进行处理

在上面我们正确处理了有状态的组件,即有 setup 入口函数的情况。如果没有 setup 入口函数,则会有 render 渲染函数方法,执行即可。render 方法传入的参数是 proxy,与获取 setup 的步骤类似,可以在 setupStateComponent 方面里面直接拿到 render 方法。执行时也是将 proxy 的形参转化为实参 instance.proxy

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
// 获取
const Component = instance.type; // createVNode时传入给type的是rootComponent,本质是一个对象,组件的所有属性都在这里,比如setup方法,比如render方法
const { setup, render } = Component;

// 执行
render(instance.proxy);

此时去执行我们的测试用例可以看到打印出我们的正确结果:

此时其实得使用类似 proxy.props.xxx 这种方式才能拿到属性,如果想实现 proxy.xxx 这种方式实现,则需要借助代理,即把我们的实参 instance.proxy 进行代理,直接打点获取值时去 get 到正确的值返回。

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
// 代理
instance.proxy = new Proxy(instance.ctx, componentPublicInstance as any);

其中代理配置如下:

typescript 复制代码
// weak-vue\packages\runtime-core\src\componentPublicInstance.ts
import { hasOwn } from "@vue/shared";

// 处理组件实例代理时的配置对象
export const componentPublicInstance = {
  // target即{ _: instance }
  get({ _: instance }, key) {
    // 获取值的时候返回正确的结果,如proxy.xxx==>proxy.props.xxx
    const { props, data, setupState } = instance;
    if (key[0] === "$") {
      // 表示该属性不能获取
      return;
    }
    if (hasOwn(props, key)) {
      return props[key];
    } else if (hasOwn(setupState, key)) {
      return setupState[key];
    }
  },
  set({ _: instance }, key, value) {
    const { props, data, setupState } = instance;

    if (hasOwn(props, key)) {
      props[key] = value;
    } else if (hasOwn(setupState, key)) {
      setupState[key] = value;
    }
  },
};

在上面我们知道,Vue3 组件可以有 setup 入口函数,也可以没有。如果没有 setup 入口函数,则会有 render 渲染函数方法。而 setup 入口函数可以返回一个函数(此时相当于优先级高的 render,即使下面有 render 也不会执行了),也可以返回一个对象(存放响应式数据)。那为什么 setup 入口函数返回一个函数时相当于 render 渲染函数呢?为什么没有 setup 时会去执行 render 呢?这里会详细讲解。

在上面,我们在用于处理有状态组件的方法 setupStateComponent 中,拿到了 setup,因此可以判断组件上面是不是挂载了 setup 方法,然后不同情况不同处理:

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
if (setup) {
  const setupContext = createContext(instance); // 返回一个上下文对象
  const setupResult = setup(instance.props, setupContext); // 实际执行的setup函数(实参)
  // setup返回值有两种情况:1、对象;2、函数==>根据不同情况进行处理
  handlerSetupResult(instance, setupResult); // 如果是对象,则将值放在instance.setupState;如果是函数,则就是render函数
} else {
  // 没有setup则会有instance.type.render方法的(处理无setup有render的情况)
  finishComponentSetup(instance); // 通过vnode拿到render方法
}

其中,处理 setup 函数的返回结果的函数如下:

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
// 处理setup函数的返回结果
function handlerSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    instance.render = setupResult; // 处理有setup且返回函数的情况==>没必要使用组件的render方法了
  } else if (isObject(setupResult)) {
    instance.setupState = setupResult; // 处理有setup且返回对象的情况==>要使用组件的render方法了
  }

  // 最终也会走render(把render挂载到实例上去)
  finishComponentSetup(instance);
}

可以看到,有 setup 实际上最终也可能会去走 render,只不过这个 rendersetup 自己返回的。

typescript 复制代码
// weak-vue\packages\runtime-core\src\component.ts
// 处理render(把render挂载到实例上去)
function finishComponentSetup(instance) {
  // 判断组件中有没有render方法,没有则
  const Component = instance.type; // createVNode时传入给type的是rootComponent,本质是一个对象,组件的所有属性都在这里,比如setup方法,比如render方法
  if (!instance.render) {
    // 这里的render指的是上面instance实例的render属性,在handlerSetupResult函数中会赋值(赋值的情况:组件有setup且返回函数),如果没有setup则此时会为false,则需要赋组件的render方法
    if (!Component.render && Component.template) {
      // TODO:模版编译
    }
    instance.render = Component.render;
  }
  console.log(instance.render);
}

此时去执行我们的测试用例:

html 复制代码
<!-- weak-vue\packages\examples\7.createApp.html -->
<div id="app">111111111111</div>
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<script>
  let { createApp } = VueRuntimeDom;

  let App = {
    setup(props, context) {
      return () => {
        console.log("这是setup方法返回的render函数!!!");
      };
      //return {
      //   props,
      // };
    },

    render(proxy) {
      console.log("这是组件本身自己定义的render函数!!!");
    },
  };
  createApp(App, { name: "张三", age: 10 }).mount("#app");
</script>

可以看到打印出预期的结果:

如果我们将 setup 返回值为对象,则打印结果为:

说明目前链路正确,render 渲染方法已经正确地挂载到我们的组件实例上。


9-3 总结

其实目前就干了四件事,具体请看源码和注释:

  1. 给组件创建一个 instance 实例并添加相关属性信息
  2. 处理 setup 方法中的参数(传递 props、封装 context
  3. 处理 render 方法中的参数(代理 proxy
  4. 处理 setup 方法的返回值(函数或者对象)

到这里,组件渲染的大体流程讲得差不多了,源码请看提交记录:9、组件的渲染(一)

10、组件的渲染(二)

10-1 初始化渲染 effect

在上面我们实现了组件渲染的大体流程,接下来的工作便是真正的渲染了,即怎么渲染真实节点到页面上。

针对不同的类型采取不同的渲染方式(vnode 有一个 shapeFlag 标识来标识组件/元素 ,元素直接用 h 函数渲染,组件要更复杂地处理),同时组件创建分为初次渲染和更新两种情况。

初次渲染的步骤为:1、生成组件的实例对象;2、解析数据到这个实例对象中;3、创建一个 effectrender 函数执行。

除了最后一个创建一个 effectrender 函数执行的 setupRenderEffect 方法,这些内容都在我们的 weak-vue\packages\runtime-core\src\render.ts 文件实现过了。所以下面我们来实现这个 setupRenderEffect 方法。

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
// 创建一个effect让render函数执行(响应式)
function setupRenderEffect(instance) {
  // 创建effect(原理可以回前面的内容找)
  effect(function componentEffect() {
    // 判断是否是初次渲染
    if (!instance.isMounted) {
      // 获取到render返回值
      const proxy = instance.proxy; // 已经代理了组件,可以访问到组件的所有属性和所有方法
      console.log(proxy);
      instance.render.call(proxy, proxy); // render函数执行,即调用render函数,第一个参数表示render函数的this指向组件实例proxy,第二个参数表示执行render函数的参数也是proxy
    }
  });
}

此时去执行我们的测试用例:

html 复制代码
<!-- weak-vue\packages\examples\7.createApp.html -->
<div id="app">111111111111</div>
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<script>
  let { createApp } = VueRuntimeDom;

  let App = {
    render(proxy) {
      console.log("这是组件本身自己定义的render函数!!!");
      console.log(this, proxy); // this值被指向了proxy
    },
  };
  createApp(App, { name: "张三", age: 10 }).mount("#app");
</script>

可以看到打印结果符合预期(三个都表示组件实例):

执行 render 方法是为了渲染节点==>h函数。

10-2 h 方法的实现

注意,我们前面在测试用例使用过一个 h 函数来进行组件的渲染。实际上它也进行了创建虚拟 dom 的操作,本质也是调用了 createVNode 方法:

typescript 复制代码
// h函数的使用(可能有多种情况)
h("div", { style: { color: "red" }, onClick: fn }, `hello ${proxy.state.age}`);

// h函数可能有下面几种使用情况
h("div", "hello");
h("div", {});
h("div", [h("span")]);
h("div", {}, "hello");
h("div", {}, ["lis"]);
h("div", {}, [h("span")]);
h("div", {}, "1", "2", "3");

可以看到,第一个参数不一定为根组件而是元素,第二个参数是包含一些属性的对象,第三个参数为渲染的子内容(可能是文本/元素/自内容数组),对此我们的 createVNode 方法在参数上都作出了更改,同时生成的虚拟 dom 也据此作出了区分。

typescript 复制代码
// weak-vue\packages\runtime-core\src\vnode.ts
export const createVNode = (type, props, children = null) => {};

h 函数的核心之一是根据参数不同情况采取不同的处理:

typescript 复制代码
// weak-vue\packages\runtime-core\src\h.ts
// h函数的作用==>生成vnode(createVNode原理可以回去前面的内容看),核心之一==>处理参数
export function h(type, propsOrChildren, children) {
  // 先根据参数个数来处理
  const i = arguments.length;
  if (i === 2) {
    // 情况1:元素+属性(传入一个对象)
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      if (isVnode(propsOrChildren)) {
        // 排除h("div",[h("span")])这种情况,因为h函数返回值也是一个对象,但不是属性
        return createVNode(type, null, [propsOrChildren]);
      }
      return createVNode(type, propsOrChildren); // 没有儿子
    } else {
      // 情况2:元素+children
      return createVNode(type, null, propsOrChildren);
    }
  } else {
    if (i > 3) {
      children = Array.prototype.slice.call(arguments, 2); // 第二个参数后面的所有参数,都应该放在children数组里面
    } else if (i === 3 && isVnode(children)) {
      children = [children];
    }
    return createVNode(type, propsOrChildren, children);
  }
}

此时在 weak-vue\packages\runtime-dom\src\index.ts 文件中导出,即实现在 VueRuntimeDom 模块中导出,打包后即可在 html 文件中使用了:

html 复制代码
<!-- weak-vue\packages\examples\7.createApp.html -->
<div id="app">111111111111</div>
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<script>
  let { createApp, h } = VueRuntimeDom;

  let App = {
    render(proxy) {
      return h("div", { style: { color: "red" } }, "hello");
    },
  };
  createApp(App, { name: "张三", age: 10 }).mount("#app");
</script>

此时在我们 weak-vue\packages\runtime-core\src\render.ts 文件的 setupRenderEffect 方法中打印出生成的 vnode 树:

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
const subTree = instance.render.call(proxy, proxy);
console.log("h函数生成的vnode树:", subTree);

打印正确,说明我们此时 h 函数的思路正确。


10-3 创建真实的节点

10-3-1 处理元素

前面我们实现了一个 patch 方法用来渲染 vnode

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
// 真正实现渲染的函数(渲染vnode)
let render = (vnode, container) => {
  // 第一次渲染(三个参数:旧的节点、当前节点、位置)
  patch(null, vnode, container); // 渲染vnode(此时是元素的vnode)
};

// patch函数负责根据vnode的不同情况(组件、元素)来实现对应的渲染
const patch = (n1, n2, container) => {
  // 针对不同的类型采取不同的渲染方式(vonode有一个shapeFlag标识来标识组件/元素)
  const { shapeFlag } = n2;
  // 等效于shapeFlag && shapeFlag === ShapeFlags.ELEMENT
  if (shapeFlag & ShapeFlags.ELEMENT) {
    // 处理元素(h函数)
    console.log("此时处理的是元素!!!");
  } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    // 处理组件
    processComponent(n1, n2, container);
  }
};

因为我们上面实现的 subTree 也是一棵 vnode,所以我们此时也可以去用 patch 方法来渲染 subTree

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
console.log("h函数生成的vnode树:", subTree);
patch(null, subTree, container); // 渲染vnode

此时执行测试用例可以看到打印结果:

说明此时处理元素的链路是正确的。我们上面实现了组件的处理方法 processComponent,我们类似地要实现一个元素处理方法 processElement(同样分为第一次挂载和更新两种情况)。

剩下的步骤是关于元素的具体处理(关于元素的处理方面前面我们已经实现过了,即 renderOptionDom):

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
/** ---------------处理元素--------------- */
const processElement = (n1, n2, container) => {
  if (n1 === null) {
    // 元素第一次挂载
    mountElement(n2, container);
  } else {
    // 更新
  }
};

const mountElement = (vnode, container) => {
  // 递归渲染子节点==>dom操作==》挂载到container/页面上
  const { shapeFlag, props, type, children } = vnode;
  // 1、创建元素--记得把真实dom挂载到vnode上,方便后面更新时使用
  let el = (vnode.el = hostCreateElement(type));
  // 2、创建元素的属性
  if (props) {
    for (const key in props) {
      hostPatchProp(el, key, null, props[key]);
    }
  }
  // 3、放入到对应的容器中
  hostInsert(el, container);
};

此时可以重新打包后去执行我们的测试用例:

可以看到我们的页面中出现了该元素,并且 style 属性也成功赋上去了。

10-3-2 处理 children

上面我们实现了元素和属性的挂载,接下来的工作是处理 children。由于 h 函数的 children 可以传多种情况,因此要具体情况具体处理。

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
// 3、处理children
if (children) {
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    console.log("这是文本字符串形式子节点:", children);
    hostSetElementText(el, children); // 文本形式子节点,比如这种情况:h('div',{},'张三'),将children直接插入到el中
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 递归渲染子节点
    console.log("这是数组形式子节点:", children);
    mountChildren(el, children); // 数组形式子节点,比如这种情况:h('div',{},['张三',h('p',{},'李四')]),将children递归渲染插入到el中
  }
}

此时去执行测试用例:

可以看到此时渲染正确。下面是递归渲染子节点的方法:

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
// 递归渲染子节点
const mountChildren = (container, children) => {
  for (let i = 0; i < children.length; i++) {
    // children[i]两种情况:1、['张三']这种元素,字符串的形式;2、h('div',{},'张三')这种元素,对象的形式(vnode)
    // 但两种情况都需要转换成vnode来处理,方便借助patch函数来渲染
    const child = (children[i] = CVnode(children[i])); // 第一种情况转换成vnode,记得将children[i]重新赋值
    // 递归渲染子节点(vnode包含了元素、组件、文本三种情况)
    patch(null, child, container);
  }
};

我们知道,我们前面实现过的 patch 函数是负责根据 vnode 的不同情况(组件、元素)来实现对应的渲染,但此时我们的 child 还可能是文本的 vnode,因此 patch 方法要增加对这种类型的处理:

typescript 复制代码
// weak-vue\packages\runtime-core\src\render.ts
/** ---------------处理文本--------------- */
const processText = (n1, n2, container) => {
  if (n1 === null) {
    // 创建文本==>直接渲染到页面中(变成真实dom==>插入)
    hostInsert((n2.el = hostCreateText(n2.children)), container);
  } else {
    // 更新文本
    if (n2.children !== n1.children) {
      const el = (n2.el = n1.el!); // el是上面初次创建的真实文本节点
      hostSetText(el, n2.children as string);
    }
  }
};

此时去执行我们的测试用例:

html 复制代码
<!-- weak-vue\packages\examples\7.createApp.html -->
<div id="app">111111111111</div>
<script src="../runtime-dom/dist/runtime-dom.global.js"></script>
<script>
  let { createApp, h } = VueRuntimeDom;

  let App = {
    render(proxy) {
      return h(
        "div",
        { style: { color: "red" } },
        h("div", {}, ["张三", h("p", {}, "李四")])
      );
    },
  };
  createApp(App, { name: "张三", age: 10 }).mount("#app");
</script>

可以看到打印及渲染正确:

10-4 总结

这个章节中,我们主要实现了 h 函数,用于创建 vnode(即 render 的返回值),然后将 vnode 根据不同情况进行处理,即进行 dom 的操作来渲染到页面上。


到这里,组件渲染的具体措施讲得差不多了,源码请看提交记录:10、组件的渲染(二)

相关推荐
neter.asia16 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫16 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
十一吖i34 分钟前
前端将后端返回的文件下载到本地
vue.js·elementplus
光影少年36 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_37 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891139 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾40 分钟前
前端基础-html-注册界面
前端·算法·html
Dragon Wu42 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym1 小时前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫1 小时前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js