小狐狸学mini-vue(二、初始化和渲染流程)

16、实现初始化component功能

  1. createApp的开始是先将传入的App组件转换成虚拟节点,调用createVnode将根组件转换成为vnode
  • 在一开始要搞清楚vnode的两种形态,当type是对象的时候表示要patch的是一个组件。
  • vnodetypestring的时候表示要patch的是元素,后面我们会添加ShapeFlag来区分vnode的类型。

这个时候是组件。createApp(App)

js 复制代码
const App = {
  // render 函数返回的vnode 是一个元素类型的,
  // 如果其返回的是 组件类型的虚拟节点,同样也会走到path(sunTree)里面
  render() {
    return h('div', `${this.msg}`)
  },

  setup() {
    return {
      msg: 'hello world'
    }
  }
}

export default App // 对应到代码里面就是 App 是一个组件

当调用完instance.render()的时候,返回的虚拟节点,同样也交给path(subTree)函数去处理。

17、使用rollup打包库

bash 复制代码
yarn add @rollup/plugin-typescript rollup tslib -D
json 复制代码
  "scripts": {
    "build": "rollup -c rollup.config.js"
  },

rollup.config.js

js 复制代码
import typescript from '@rollup/plugin-typescript';
import pkg from './package.json'

export default {
  input: './src/index.ts',
  output: [
    // cjs  => common.js
    // esm
    {
      format: 'cjs',
      file: pkg.main
    },
    {
      format: 'es',
      file: pkg.module
    }
  ],
  plugins: [
    typescript()
  ]
}

package.json

json 复制代码
{
  "main": "./lib/vue3-mini-vue.cjs.js",   // common.js
  "module": "./lib/vue3-mini-vue.esm.js", // esm 
}

patch 的的时候,遇到的 vnode 的两周形态,组件和元素分别是什么样子的?如下图。

18、实现初始化 element 主流程

js 复制代码
function patch(vnode, container) {
  // 需要判断当前的 vnode 是不是 element
  // 如果是一个 element 的话就应该处理 element
  // 如何区分是 组件类型还是 element 类型??
  if (typeof vnode.type === "string") {
    processElement(vnode, container);
  } else {
    // 处理组件
    processComponent(vnode, container);
  }
}


// 处理元素
function processElement(vnode: any, container: any) {
  mountElement(vnode, container);
}

// 挂载元素
function mountElement(vnode: any, container: any) {
  const el = document.createElement(vnode.type);

  // vnode.children 可能是 string 也可能是数组
  const { children } = vnode;
  if (typeof children === "string") {
    el.textContent = children;
  } else if (Array.isArray(children)) {
    // 用 el 作为 vode 孩子的父节点去创建 DOM 元素
    mountChildren(vnode, el);
  }

  // props
  const { props } = vnode;
  for (let key in props) {
    let val = props[key];
    el.setAttribute(key, val);
  }
  container.append(el);
}

// 挂载孩子节点
function mountChildren(vnode: any, container: any) {
  // 如果孩子是数组就,进行递归的调用,往刚才创建的 根节点下面添加新的元素
  vnode.children.forEach((v) => {
    patch(v, container);
  });
}

子组件渲染一个字符串

子组件渲染一个数组

js 复制代码
const App = {
  render() {
    return h('div', {
      id: 'root',
      class: ['red', 'hard']
    }, [
      h('p', {}, 'A'),
      h('p', {}, 'B'),
      h('p', {}, 'C'),
    ])
  },

  setup() {
    return {
      msg: 'hello world'
    }
  }
}

export default App

19、实现组件代理对象

现在我们要实现的是在 render函数里面可以访问到setup函数返回的state数据通过this.xxx的形式。

如下:

js 复制代码
const App = {
  render() {
    return h('div', {}, this.msg)  // 这里的 this 是从哪里来的呢?想一想
  },

  setup() {
    return {
      msg: 'hello world'
    }
  }
}

代码实现:

实现效果

实现$el

js 复制代码
window.self = null
const App = {
  render() {
    window.self = this   // 这里
    return h('div', {}, this.msg)
  },

  setup() {
    return {
      msg: 'hello world -'
    }
  }
}

下面来实现 获取$el,获取当前 组件真实的DOM

  1. 就是把根元素真实的DOM赋值给组件实例的vnodeel,然后在组件代理对象获取的那块劫持对应的key,从instance上面取出对应的数据即可实现。

  2. 我们给组件instance上添加一个proxy属性来进行代理,并作为render函数执行的this传递进去。

20、实现shapeFlags

shapeFlag是什么?他就是用描述虚拟节点的类型,

  • 可以对虚拟节点进行分类
  • 位运算的修改(|)和查找(&)

vnode添加shapeFlag属性, 用来区分虚拟节点的类型。

shared/shapeFlag.ts

vnode.ts

renderer.ts

21、实现注册事件功能

  • 在处理props的时候处理特殊的key(含有on开头的key)

22、实现props功能

  • 在组件实例上添加props属性
  • 组件代理对象的时候,如果setupState里面没有的话,就继续看props里面有没有,有的话就从props里面取值了。
  • props 是不可以被修改的

23、实现emit

  • 构造两个组件, 在子组件里面emit出去一个事件,然后在父组件里面 on
  • setup函数调用的时候需要传递一个对象。
  • 找到props里面对应的onxxx开头的函数,然后调用它。
  • 支持emit传递参数
  • 支持emit('add-foo')的形式调用props上面的函数。

测试代码

js 复制代码
import { h } from "../../lib/vue3-mini-vue.esm.js"
import Foo from "./Foo.js"

const App = {
  setup(props) {
    return {
    }
  },
  render() {
    return h('div', {}, [
      h('div', {}, 'App组件'),
      h(Foo, {
        onAdd(a, b) {
          console.log('onAdd触发了', a, b)
        },
        "onAddFoo"() {
          console.log('on-add-foo触发了')
        }
      })
    ])
  }
}

export default App

Foo 组件

js 复制代码
import { h } from "../../lib/vue3-mini-vue.esm.js"

const Foo = {
  setup(props, { emit }) {
    const handleClick = () => {
      console.log('emit add')
      emit('add', 1, 2)
      emit('add-foo')
    }
    return {
      handleClick
    }
  },
  render() {
    return h('button', {
      onClick: () => {
        this.handleClick()
      }
    }, 'emit')
  }
}

export default Foo

24、实现slot

  • 需要用this.$slots获取到组件 vnodechildren然后渲染到组件里面。
  • 指定元素渲染的位置
  • 我们就要获取到渲染的元素,和获取到要渲染的位置。既"具名插槽"将 childeren设计成一个对象。
  • 实现作用域插槽, 需要在slot插槽上面访问到组件内部的变量, 这个时候就需要给虚拟节点外面包一层函数,用来接受内部的参数来当做props使用。

代码实现

创建vnode的时候添加 slotshapeFlag标识

vnode.ts

js 复制代码
export function createVnode(type, props?, children?) {
  const vnode = {
    type,
    props,
    children,
    shapeFlag: getShapeFlag(type),
    el: null,
  };
 ......
  // 如果是有状态组件 并且 children 是 object 那么就是 slot children
  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    if (typeof children === "object") {
      vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN;
    }
  }
  return vnode;
}

然后给组件实例上添加一个slot的字段

component.ts

ts 复制代码
import { initSlots } from "./componentSlots";
// 创建组件实例
export function createComponentInstance(vnode) {
  const component = {
    ...
    slots: {},
    ...
  };

  return component;
}

// 初始化组件

export function setupComponent(instance) {
  // initProps
  initProps(instance, instance.vnode.props);
  initSlots(instance, instance.vnode.children);
  ...
}

初始化 slot

ts 复制代码
import { ShapeFlags } from "../shared/shapeFlag";

export function initSlots(instance, children) {
  // 把 slots 放到对象里面
  const slots = {};
  const { vnode } = instance;
  // 如果 vnode 的形状里面包含 slot_children,那么
  if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
    for (const key in children) {
      const value = children[key];
      slots[key] = (props) => normalizeSlotValue(value(props));
    }
    instance.slots = slots;
  }
}

function normalizeSlotValue(value) {
  return Array.isArray(value) ? value : [value];
}

componentPublicInstance.ts

js 复制代码
const publicPropertiesMap = {
  $el: (i) => i.vnode.el,
  $slots: (i) => i.slots,    // 从组件实例上获取 传递给组件的 slot 数据
};

helpers/renderSlots.ts 从slots中读取对应 name的内容创建出虚拟节点,在子组件中渲染出来。

ts 复制代码
import { createVnode } from "../vnode";

export function renderSlots(slots, name) {
  console.log(slots);
  const slot = slots[name];
  if (slot) {
    return createVnode("div", {}, slot);
  }
}

测试代码

app.js

js 复制代码
import { h } from "../../lib/vue3-mini-vue.esm.js";
import { Foo } from "./Foo.js";

const App = {
  setup() {
    return {};
  },
  render() {
    const app = h("div", {}, "App");

    return h("div", {}, [
      app,
      // 把组件的孩子渲染在 组件里面
      h(
        Foo,
        {},
        {
          header: ({ age }) => h("p", {}, "header" + age),
          footer: () => h("p", {}, "footer"),
        }
      ),
    ]);
  },
};

export default App;

foo.js

js 复制代码
import { h, renderSlots } from "../../lib/vue3-mini-vue.esm.js";

export const Foo = {
  setup() {
    return {};
  },
  render() {
    const age = 18;
    const foo = h("p", {}, "foo");
    return h("div", {}, [
      renderSlots(this.$slots, "header", { age }),
      foo,
      renderSlots(this.$slots, "footer"),
    ]);
  },
};

渲染结果。

25、实现Fragment 和 Text类型节点

  • Fragment 只需要渲染 children里面的vnode
  • 渲染文本是不需要新创建DOM元素的。

代码实现

添加两种特殊的 type用来进一步区分 vnode的大类型

vnode.ts

javascript 复制代码
export const Fragment = Symbol("Fragment");
export const Text = Symbol("Text");

/**
 * 创建文本的虚拟节点
 * @param text
 * @returns
 */
export function createTextVNode(text: string) {
  return createVNode(Text, {}, text);
}

在patch的时候进行区分,根据不同的 type 处理不同的vnode

js 复制代码
function patch(vnode, container) {
  // 需要判断当前的 vnode 是不是 element
  // 如果是一个 element 的话就应该处理 element
  // 如何区分是 组件类型还是 element 类型??
  const { type, shapeFlag } = vnode;
  switch (type) {
    case Fragment:
      processFragment(vnode, container);
      break;
    case Text:
      processText(vnode, container);
      break;
    default:
     .......
  }
}

/**
 * 其实就是只把 children 渲染出来就行
 * @param vnode
 * @param container
 */
function processFragment(vnode, container) {
  mountChildren(vnode, container);
}

/**
 * 根据文本虚拟节点创建真实的文本节点
 * @param vnode
 * @param container
 */
function processText(vnode, container) {
  // 取出文本内容
  const { children } = vnode;
  const textNode = (vnode.el = document.createTextNode(children));
  container.append(textNode);
}

测试代码 app.js

js 复制代码
import { createTextVNode, h } from "../../lib/vue3-mini-vue.esm.js";
import { Foo } from "./Foo.js";

const App = {
  setup() {
    return {};
  },
  render() {
    const app = h("div", {}, "App");

    return h("div", {}, [
      app,
      // 把组件的孩子渲染在 组件里面
      h(
        Foo,
        {},
        {
          header: ({ age }) => [
            h("p", {}, "header" + age),
            createTextVNode("我是文本节点!!!!"),
          ],
          footer: () => h("p", {}, "footer"),
        }
      ),
    ]);
  },
};

export default App;

foo.js

js 复制代码
import { h, renderSlots } from "../../lib/vue3-mini-vue.esm.js";

export const Foo = {
  setup() {
    return {};
  },
  render() {
    const age = 18;
    const foo = h("p", {}, "foo");
    return h("div", {}, [
      renderSlots(this.$slots, "header", { age }),
      foo,
      renderSlots(this.$slots, "footer"),
    ]);
  },
};
相关推荐
余道各努力,千里自同风1 分钟前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave8 分钟前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
软件小伟10 分钟前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾32 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧40 分钟前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
chusheng18401 小时前
Java项目-基于SpringBoot+vue的租房网站设计与实现
java·vue.js·spring boot·租房·租房网站
asleep7011 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
游走于计算机中摆烂的1 小时前
启动前后端分离项目笔记
java·vue.js·笔记