h函数
vue提供了一个h()函数用于创建vnodes;一个更准确的名称应该是 createVnode()
// 完整参数签名
function h(
type: string | Component,
props?: object | null,
children?: Children | Slot | Slots
): VNode
// 省略 props
function h(type: string | Component, children?: Children | Slot): VNode
type Children = string | number | boolean | VNode | null | Children[]
type Slot = () => Children
type Slots = { [name: string]: Slot }
h函数:第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。第二个参数是要传递的 prop,第三个参数是子节点。 vue官方文档
位运算符的应用
export const enum ShapeFlags {
ELEMENT = 1, // 虚拟节点是一个元素
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 普通组件
TEXT_CHILDREN = 1 << 3, // 儿子是文本的
ARRAY_CHILDREN = 1 << 4, // 儿子是数组
SLOTS_CHILDREN = 1 << 5, // 插槽
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT|ShapeFlags.FUNCTIONAL_COMPONENT,
}
- 1<<1,是1左移1位是2的1次方为2(左移乘以,右移除以),
- &与运算符,8位2进制,都是1才是1,所以2&4=0;
- |或运算符,只要有一个是1就是1,2|4=6;
- 用大的数和小的数做与&运算大于0就说明涵盖这个类型
创建的虚拟节点,使用位运算描述自己的类型和children的关系
export function isVNode(vnode) {
return vnode.__v_isVnode == true;
}
export function isSameVNode(n1, n2) {
return n1.type === n2.type && n1.key === n2.key;
}
export function createVNode(type, props = null, children = null) {
// 用标识来区分 对应的虚拟节点类型 , 这个表示采用的是位运算的方式 可以方便组合
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: isTeleport(type) // 因为teleport 也是对象 为了区分 增加标识
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.COMPONENT
: 0;
// 虚拟节点要对应真实节点********(虚拟节点上保存着真实的节点)
const vnode = {
__v_isVnode: true, // 添加标识是不是vnode
type,
props,
children,
shapeFlag,
key: props?.key,
el: null, // 对应的真实节点
};
if (children) {
let type = 0;
if (Array.isArray(children)) {
// [vnode,'文本']
type = ShapeFlags.ARRAY_CHILDREN;
} else if (isObject(children)) {
type = ShapeFlags.SLOTS_CHILDREN; // 孩子是插槽
} else {
// 文本
type = ShapeFlags.TEXT_CHILDREN;
}
vnode.shapeFlag |= type; // a+b a|b = c c&b > 0 有b c&b == 0 没有b
}
return vnode; // 根据 * vnode.shapeFlag 来判断自己的类型和孩子的类型*
}
h函数通过调用createVnode,生成虚拟节点
export function h(type, propsOrChildren?, children?) {
const l = arguments.length;
// h(type,{}) h(type,h('span')) => h(type,[h('span')]) / h(type,[]) h(type,'文本')
// h函数参数有2个的时候 h(type,{}),h(type,vnode),h(type,[])
// h函数参数有3个的时候 h(type,{},文本),h(type,{},vnode),h(type,{},{default:()=>{}})(插槽函数)
// h函数参数大于3个的时候,h(type,{},'',''),第二个参数必须是描述节点的属性
if (l == 2) {
//是对象不是数组:h(type,{})或者是h(type,h('span'))
if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) {
if (isVNode(propsOrChildren)) {
//第二个是虚拟对象,直接包装成children
return createVNode(type, null, [propsOrChildren]);
}
return createVNode(type, propsOrChildren);
} else {
// 数组 或者文本 h('div','aaa'),h('div',[])
return createVNode(type, null, propsOrChildren);
}
} else {
if (l > 3) {
children = Array.from(arguments).slice(2);
// h('div',{},'a','b','c') 这样操作第二个参数必须是属性 h('div','e','a','b','c')
// h('div',{},h('span')) => h('div',{},[h('span')])
} else if (l === 3 && isVNode(children)) {
children = [children];
}
return createVNode(type, propsOrChildren, children);
// l == 3
}
}
createRenderer
创建一个自定义渲染器。
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement
// ...
})
// `render` 是底层 API
// `createApp` 返回一个应用实例
export { render, createApp }
// 重新导出 Vue 的核心 API
export * from '@vue/runtime-core'
该函数的返回值包含render函数,render函数用于把虚拟dom(h函数生成的虚拟dom)生成真实的dom,也可以直接使用从runtime-dom导出的render函数直接生成浏览器的dom
export const render = (vnode, container) => {
return createRenderer(renderOptions).render(vnode, container);
};
// renderOptions是浏览器的dom生成api(不用自己定义了),这个函数就是对createRenderer的render的包装
createRenderer函数的简单实现
export function createRenderer(options) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
querySelector: hostQuerySelector,
} = options;
//处理孩子,这里是递归处理
const mountChildren = (children, el, anchor = null, parent = null) => {
for (let i = 0; i < children.length; i++) {
//初次渲染,第一个参数是null
patch(null, children[i], el, anchor, parent);
}
};
const mountElement = (vnode, container, anchor, parent) => {
const { type, props, children, shapeFlag } = vnode;
// 创建元素,虚拟节点上要保存真实的节点**********
const el = (vnode.el = hostCreateElement(type));
// 增添属性
if (props) {
for (let key in props) {
hostPatchProp(el, key, null, props[key]);
}
}
// 根据ShapeFlags的类型,处理子节点,位运算符计算
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
//递归处理子节点
mountChildren(children, el, anchor, parent);
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, children);
}
hostInsert(el, container, anchor);
};
const patch = (n1,n2,container)=>{
if(n1 = n2){
return; //无需更新
}
if(n1==null){
//初次渲染
mountElement(n2,container)
} else {
//diff算法
}
}
// 虚拟节点上的真实节点移除
const unmount = (vnode, parent) => {
hostRemove(vnode.el);
};
const render = (vnode, container, parent = null) => {
if (vnode == null) {
// 卸载:删除节点
if (container._vnode) {
// 说明渲染过了,我才需要进行卸载操作
unmount(container._vnode, parent);
}
} else {
// 初次渲染 更新
patch(container._vnode || null, vnode, container);
}
container._vnode = vnode; // 第一次渲染保存虚拟节点*********
};
return {
// createRenderer 可以用户自定义渲染方式
// createRenderer 返回的render方法 接受参数是虚拟节点和容器
render,
};
}
defineAsyncComponent()
定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
function defineAsyncComponent(
source: AsyncComponentLoader | AsyncComponentOptions
): Component
//异步函数
type AsyncComponentLoader = () => Promise<Component>
//选项对象
interface AsyncComponentOptions {
loader: AsyncComponentLoader //将要加载的组件
loadingComponent?: Component //loading组件
errorComponent?: Component //错误显示组件
// 展示加载组件前的延迟时间,默认为 200ms在加载组件显示之前有一个默认的 200ms 延迟——
//这是因为在网络状况较好时,加载完成得很快,
//加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
delay?: number
timeout?: number // 超时时间
suspensible?: boolean
onError?: ( //发生错误时回调函数
error: Error,
retry: () => void, //错误时继续请求组件,这里是一个promise链
fail: () => void,
attempts: number //尝试错误的次数
) => any
}
源码学习
import { Fragment, Text } from "./vnode";
import { h } from "./h";
import { ref } from "@vue/reactivity";
export function defineAsyncComponent(options) {
//判断是函数还是对象
if (typeof options === "function") {
options = { loader: options };
}
let Component = null;
// 超时时间定时器和loading加载定时器
let timer = null;
let loadingTimer = null;
return {
setup() {
let { loader } = options;
const loaded = ref(false); //控制组件的显示
const error = ref(false); //控制错误组件的显示
const loading = ref(false);
//封装的函数
function load() {
return loader().catch((err) => {
//如果配置项,有onError选项,返回一个promise,形成一个promise链条
if (options.onError) {
return new Promise((resolve, reject) => {
// resolve 一个promise
// 一个promise会等待另一个promise执行完毕
const retry = () => resolve(load()); //这里是循环请求load函数,产生链
const fail = () => reject(err);
options.onError(err, retry, fail);
});
} else {
throw err;
}
});
}
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true;
}, options.delay);
}
load()
.then((c) => {
Component = c;
loaded.value = true;
clearTimeout(timer); //清除定时器
})
.catch((err) => (error.value = err)) //加载组件失败时
.finally(() => {
// 无论成功或者失败都要把loading层清除掉
loading.value = false;
clearTimeout(loadingTimer);
});
//上面是异步的,超过了timeout就显示错误组件,没有的话上面成功时会清除这个定时器
if (options.timeout) {
//超过定时时间,就显示错误的组件
timer = setTimeout(() => {
error.value = true;
}, options.timeout);
}
return () => {
if (loaded.value) {
return h(Component); // 成功组件
} else if (error.value && options.errorComponent) {
return h(options.errorComponent); // 错误组件,渲染错误组件
} else if (loading.value && options.loadingComponent) {
//loading层的加载
return h(options.loadingComponent);
}
return h(Fragment, []);
};
},
};
}
KeepAlive
创建一个div用于缓存keepalive中的插槽的dom元素,切换的时候从缓存中获取,设计到max的时候,会有一个lru的算法,就是把最新的放到末尾,超出长度时,删除最老的那一个
import { ShapeFlags } from "@vue/shared";
import { getCurrentInstance } from "./component";
import { onMounted, onUpdated } from "./apiLifecycle";
import { isVNode } from "./vnode";
//******************keepAlive中放的是插槽
export const KeepAliveImpl = {
__isKeepAlive: true,
props: {
include: {}, // ['缓存名字','缓存字'] ’my1,my2‘ 正则
exclude: {},
max: {}, //这里涉及到了lru算法了
},
setup(props, { slots }) {
// 缓存的组件有哪些,方便查找 key 来描述
// key 对应的组件的定义
const keys = new Set();
const cache = new Map(); // key -> 组件
const instance = getCurrentInstance();
//只有keepalive组件才有ctx属性
let { createElement, move, unmount } = instance.ctx.renderer;
//创建盒子,用于缓存dom
let storageContainer = createElement("div");
//缓存的组件激活时,没有触发组件的生成流程,而是从缓存中拿的
instance.ctx.activate = function (vnode, container) {
//组件激活的时候 需要从缓存中拿出来就可以了
move(vnode, container); // 将刚才缓存的dom,拿到容器中
};
//组件卸载的时候,直接把dom移动到盒子中去了
instance.ctx.deactivate = function (vnode) {
// 给这个虚拟节点对应的dom 移动到隐藏的盒子中就可以了
move(vnode, storageContainer);
};
let pendingCacheKey = null; //保存将要缓存组件的key
const _unmount = (vnode) => {
let shapeFlag = vnode.shapeFlag;
if (shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
shapeFlag -= ShapeFlags.COMPONENT_KEPT_ALIVE;
}
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
shapeFlag -= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
}
vnode.shapeFlag = shapeFlag;
// 卸载真实dom
unmount(vnode, null);
// 这里要移除
};
const pruneCaheEntry = (key) => {
// 这里只是拿出第一个 [1,2,3,4] . -> 2 . [1,3,2]
let cached = cache.get(key);
cache.delete(key);
keys.delete(key);
//还原节点的标识
_unmount(cached);
};
const cacheSubTree = () => {
if (pendingCacheKey) {
cache.set(pendingCacheKey, instance.subTree);
}
};
// 加载完毕后要将组件进行缓存,缓存的是dom元素
onMounted(cacheSubTree);
onUpdated(cacheSubTree);
return () => {
//1
let vnode = slots.default();
let name = vnode.type.name;
const { include, exclude, max } = props;
// keepAlive中缓存的是vnode,还必须是状态组件 2222
if (
!isVNode(vnode) ||
!(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)
) {
return vnode;
}
const comp = vnode.type;
const key = vnode.props?.key == null ? comp : vnode.props.key;
// 3
let cacheVNode = cache.get(key);
pendingCacheKey = key; // 在组件加载完毕后缓存的key的名字
if (
(name && include && !include.split(",").includes(name)) ||
(exclude && exclude.split(",").includes(name))
) {
return vnode;
}
//4
if (cacheVNode) {
// 复用组件的实例,告诉这个组件不要在渲染了,这里就走到组件的加载了,这里是从缓存中拿的组件
vnode.component = cacheVNode.component;
// 修改组件的标识位
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE;
} else {
// lru 算法,删除key,再添加上key [1,2]添加3,[1,2,3]把1删除,访问2的时候把2先删除,再添加2,[3,2],永远让访问的保持最新
keys.delete(key); // ****将原有的移除到尾部, 让其保持最新
keys.add(key);
// 超过缓存限制了, 删除第一个
if (max && keys.size > max) {
// it.next()
pruneCaheEntry(keys.values().next().value);
}
}
// **********稍后组件卸载的时候 不要卸载。 后续可以复用这个组件的dom元素
// ***********添加标识位,说明这是一个要缓存的组件
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
return vnode;
};
},
};
export const isKeepAlive = (vnode) => vnode.type.__isKeepAlive;
provide和inject
vue2中的provide可以直接一层层的向上查找,vue3的话直接从父级上查找就行了,就近原则(子会找父亲的provide,如果自身和父亲的provide属性相同,就会覆盖,再添加自己的属性)
// vue2 中provide 是 一层层向上找 component.parent.parent
// vue3 中是只找父级,就近原则(子会找父亲的provide,如果自身和父亲的provide属性相同,就会覆盖,再添加自己的属性)
// provide 和 inject 适合编写插件,不适合用在业务代码中 (数据来源不明确)
import { currentInstance } from "./component";
// 父 -》 name provides ********
// 子1 name属性 provides
// 子2 使用name属性? provides
export function provide(key, value) {
// 如何判断 在不在组件中?
if (!currentInstance) return;
// 第一次我的provides 是来自于父亲的
// 所以一样就拷贝一个 作为自己新的provides
// 下一次调用provides 用的是自己的provides肯定和父亲的不是一个
// 我就不创建provides了;
let provides = currentInstance.provides;
// 我要知道在当前组件中我是第一次调用的provide 还是不是第一次
const parentProvides =
currentInstance.parent && currentInstance.parent.provides;
// ********第一次是来自父亲的provides,2者相同,然后create了一个新对象,第二次更新后就是另一个对象了,就不走这一步了,直接往对象中赋值了
//这里只有第一次能走进来
if (provides === parentProvides) {
// 每个组件都有自己的provides, 这样实现每次调用provide 都会产生一个新的
// Object.create创建了一个新的对象,下次就不能走进来了
provides = currentInstance.provides = Object.create(provides);
// xxx.__proto__ = provides
}
provides[key] = value;
}
export function inject(key, value) {
if (!currentInstance) return;
const provides = currentInstance.parent?.provides;
// 上级有提供过这个属性
if (provides && key in provides) {
return provides[key];
} else if (value) {
return value;
}
}
// 父 provides ={} . 儿子 = provides 孙子 =provides
// . 儿子 = provides
// 儿子提供数据了 儿子 拷贝一份增添属性作为
teleport
就是一个dom在生成的时候会产生移动的过程
export const TeleportImpl = {
__isTeleport: true, // 此组件是一个特殊的组件类型
process(n1, n2, container, anchor, operators) {
// 等会组件初始化会调用此方法
let { mountChildren, patchChildren, move, query } = operators;
if (!n1) {
const target = (n2.target = query(n2.props.to));
if (target) {
mountChildren(n2.children, target, anchor);
}
} else {
//更新后,n2就变成了n1,所以从n1上取target
patchChildren(n1, n2, n1.target); // 只是比较了儿子的差异
n2.target = n1.target;
//属性to发生了变化,重新赋值
if (n2.props.to !== n1.props.to) {
const nextTarget = (n2.target = query(n2.props.to));
n2.children.forEach((child) => move(child, nextTarget, anchor));
}
}
},
};
export const isTeleport = (type) => !!type.__isTeleport;
生命周期函数
只能在setup中使用vue3新的生命周期钩子,onMounted等,并且setup执行是在beforCreate之前执行的,就在vnode的实例上依赖收集的一个过程
import { currentInstance, setCurrentInstance } from "./component";
export const enum LifecycleHoos {
BEFORE_MOUNT = "bm",
MOUNTED = "m",
BEFORE_UPDATE = "bu",
UPDATED = "u",
}
// bm = []
// bu = []
// m = []
// u = []
function createHook(type) {
// type 是绑定到哪里 , hook 就是用户传递的钩子 获取当前的实例
// instance[type] = [hook,hook]
return (hook, target = currentInstance) => {
//这里的hook就是要执行的生命周期内的函数
if (target) {
// 生命周期 必须在setup中使用
const wrapperHook = () => {
// 为了能在函数中使用instance,使用闭包函数获得instance
setCurrentInstance(target);
hook();
setCurrentInstance(null);
};
const hooks = target[type] || (target[type] = []); // 在这里挂载了生命周期函数
hooks.push(wrapperHook);
}
};
}
export const onBeforeMount = createHook(LifecycleHoos.BEFORE_MOUNT);
export const onMounted = createHook(LifecycleHoos.MOUNTED);
export const onBeforeUpdate = createHook(LifecycleHoos.BEFORE_UPDATE);
export const onUpdated = createHook(LifecycleHoos.UPDATED);