目录
参数:rootComponent对象,是一个JavaScript对象(可能包含name、setup、render等属性)
返回值:返回一个带有mounted方法的对象
作用:
export function createAppAPI(render) {
return function createApp(rootComponent) {
const app = {
_component: rootComponent,
mount(rootContainer) {
console.log("基于根组件创建 vnode");
const vnode = createVNode(rootComponent);
console.log("调用 render,基于 vnode 进行开箱");
render(vnode, rootContainer);
},
};
return app;
};
}
参数:
返回值:vnode虚拟节点对象
作用:
export const createVNode = function (
type: any,
props?: any,
children?: string | Array<any>
) {
// 注意 type 有可能是 string 也有可能是对象
// 如果是对象的话,那么就是用户设置的 options
// type 为 string 的时候
// createVNode("div")
// type 为组件对象的时候
// createVNode(App)
const vnode = {
el: null,
component: null,
key: props?.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
// 基于 children 再次设置 shapeFlag
if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
} else if (typeof children === "string") {
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
}
normalizeChildren(vnode, children);
return vnode;
};
参数:text字符串
返回值:返回createVNode(Text, {}, text)一个文本vnode
作用:调用createVNode(Text, {}, text)返回一个文本vnode
export const Text = Symbol("Text");
export function createTextVNode(text: string = " ") {
return createVNode(Text, {}, text);
}
参数:type
返回值:element标识或者component标识
作用:根据type判断虚拟节点是element还是component并返回对应标识
// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {
return typeof type === "string"
? ShapeFlags.ELEMENT
: ShapeFlags.STATEFUL_COMPONENT;
}
参数:child,子节点
返回值:虚拟节点
作用:
若child为字符串或数字,则创建Text文本节点
若是其他节点则直接返回
参数:
返回值:调用createVNode返回虚拟节点
作用:调用createVNode返回虚拟节点
export const h = (type: any , props: any = null, children: string | Array<any> = []) => {
return createVNode(type, props, children);
};
参数:
返回值:无
作用:调用patch函数
const render = (vnode, container) => {
console.log("调用 path")
patch(null, vnode, container);
};
参数:
返回值:无
作用:
function patch(
n1,
n2,
container = null,
anchor = null,
parentComponent = null
) {
// 基于 n2 的类型来判断
// 因为 n2 是新的 vnode
const { type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
// 其中还有几个类型比如: static fragment comment
case Fragment:
processFragment(n1, n2, container);
break;
default:
// 这里就基于 shapeFlag 来处理
if (shapeFlag & ShapeFlags.ELEMENT) {
console.log("处理 element");
processElement(n1, n2, container, anchor, parentComponent);
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
console.log("处理 component");
processComponent(n1, n2, container, parentComponent);
}
}
}
参数:
返回值:无
作用:处理文本节点
function processText(n1, n2, container) {
console.log("处理 Text 节点");
if (n1 === null) {
// n1 是 null 说明是 init 的阶段
// 基于 createText 创建出 text 节点,然后使用 insert 添加到 el 内
console.log("初始化 Text 类型的节点");
hostInsert((n2.el = hostCreateText(n2.children as string)), container);
} else {
// update
// 先对比一下 updated 之后的内容是否和之前的不一样
// 在不一样的时候才需要 update text
// 这里抽离出来的接口是 setText
// 注意,这里一定要记得把 n1.el 赋值给 n2.el, 不然后续是找不到值的
const el = (n2.el = n1.el!);
if (n2.children !== n1.children) {
console.log("更新 Text 类型的节点");
hostSetText(el, n2.children as string);
}
}
}
参数:
返回值:无
作用:处理Fragment节点,即vue的template类似效果和React.Fragment差不多的效果
调用mountChildren挂载n2的子节点
function processFragment(n1: any, n2: any, container: any) {
// 只需要渲染 children ,然后给添加到 container 内
if (!n1) {
// 初始化 Fragment 逻辑点
console.log("初始化 Fragment 类型的节点");
mountChildren(n2.children, container);
}
}
参数:
返回值:无
作用:处理element节点
function processElement(n1, n2, container, anchor, parentComponent) {
if (!n1) {
mountElement(n2, container, anchor);
} else {
// todo
updateElement(n1, n2, container, anchor, parentComponent);
}
}
参数:
返回值:无
作用:挂载element节点
function mountElement(vnode, container, anchor) {
const { shapeFlag, props } = vnode;
// 1. 先创建 element
// 基于可扩展的渲染 api
const el = (vnode.el = hostCreateElement(vnode.type));
// 支持单子组件和多子组件的创建
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 举个栗子
// render(){
// return h("div",{},"test")
// }
// 这里 children 就是 test ,只需要渲染一下就完事了
console.log(`处理文本:${vnode.children}`);
hostSetElementText(el, vnode.children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 举个栗子
// render(){
// Hello 是个 component
// return h("div",{},[h("p"),h(Hello)])
// }
// 这里 children 就是个数组了,就需要依次调用 patch 递归来处理
mountChildren(vnode.children, el);
}
// 处理 props
if (props) {
for (const key in props) {
// todo
// 需要过滤掉vue自身用的key
// 比如生命周期相关的 key: beforeMount、mounted
const nextVal = props[key];
hostPatchProp(el, key, null, nextVal);
}
}
// todo
// 触发 beforeMount() 钩子
console.log("vnodeHook -> onVnodeBeforeMount");
console.log("DirectiveHook -> beforeMount");
console.log("transition -> beforeEnter");
// 插入
hostInsert(el, container, anchor);
// todo
// 触发 mounted() 钩子
console.log("vnodeHook -> onVnodeMounted");
console.log("DirectiveHook -> mounted");
console.log("transition -> enter");
}
参数:
返回值:无
作用:遍历vnode.children对每一个child调用patch挂载到container中
function mountChildren(children, container) {
children.forEach((VNodeChild) => {
// todo
// 这里应该需要处理一下 vnodeChild
// 因为有可能不是 vnode 类型
console.log("mountChildren:", VNodeChild);
patch(null, VNodeChild, container);
});
}
参数:
返回值:无
作用:
如果n1为空,调用mountComponent挂载组件
如果n1有值,则调用updateComponent更新组件
function processComponent(n1, n2, container, parentComponent) {
// 如果 n1 没有值的话,那么就是 mount
if (!n1) {
// 初始化 component
mountComponent(n2, container, parentComponent);
} else {
updateComponent(n1, n2, container);
}
}
参数:
返回值:无
作用:
function mountComponent(initialVNode, container, parentComponent) {
// 1. 先创建一个 component instance
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent
));
console.log(`创建组件实例:${instance.type.name}`);
// 2. 给 instance 加工加工
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container);
}
参数:
返回值:无
作用:
// 组件的更新
function updateComponent(n1, n2, container) {
console.log("更新组件", n1, n2);
// 更新组件实例引用
const instance = (n2.component = n1.component);
// 先看看这个组件是否应该更新
if (shouldUpdateComponent(n1, n2)) {
console.log(`组件需要更新: ${instance}`);
// 那么 next 就是新的 vnode 了(也就是 n2)
instance.next = n2;
// 这里的 update 是在 setupRenderEffect 里面初始化的,update 函数除了当内部的响应式对象发生改变的时候会调用
// 还可以直接主动的调用(这是属于 effect 的特性)
// 调用 update 再次更新调用 patch 逻辑
// 在update 中调用的 next 就变成了 n2了
// ps:可以详细的看看 update 中 next 的应用
// TODO 需要在 update 中处理支持 next 的逻辑
instance.update();
} else {
console.log(`组件不需要更新: ${instance}`);
// 不需要更新的话,那么只需要覆盖下面的属性即可
n2.component = n1.component;
n2.el = n1.el;
instance.vnode = n2;
}
}
参数:
返回值:无
作用:
根据新老虚拟节点的shapeFlag来判断该如何进行处理
function patchChildren(n1, n2, container, anchor, parentComponent) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1;
const { shapeFlag, children: c2 } = n2;
// 如果 n2 的 children 是 text 类型的话
// 就看看和之前的 n1 的 children 是不是一样的
// 如果不一样的话直接重新设置一下 text 即可
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (c2 !== c1) {
console.log("类型为 text_children, 当前需要更新");
hostSetElementText(container, c2 as string);
}
} else {
// 如果之前是 array_children
// 现在还是 array_children 的话
// 那么我们就需要对比两个 children 啦
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
patchKeyedChildren(c1, c2, container, anchor, parentComponent);
}
}
}
}
详见【mini-vue】DIFF算法学习笔记_名字太长不好不好的博客-CSDN博客
参数:
返回值:无
作用:
备注:effect函数的理解可以详见【mini-vue】Reactive模块学习笔记_名字太长不好不好的博客-CSDN博客
function setupRenderEffect(instance, initialVNode, container) {
// 调用 render
// 应该传入 ctx 也就是 proxy
// ctx 可以选择暴露给用户的 api
// 源代码里面是调用的 renderComponentRoot 函数
// 这里为了简化直接调用 render
// obj.name = "111"
// obj.name = "2222"
// 从哪里做一些事
// 收集数据改变之后要做的事 (函数)
// 依赖收集 effect 函数
// 触发依赖
function componentUpdateFn() {
if (!instance.isMounted) {
// 组件初始化的时候会执行这里
// 为什么要在这里调用 render 函数呢
// 是因为在 effect 内调用 render 才能触发依赖收集
// 等到后面响应式的值变更后会再次触发这个函数
console.log(`${instance.type.name}:调用 render,获取 subTree`);
const proxyToUse = instance.proxy;
// 可在 render 函数中通过 this 来使用 proxy
const subTree = (instance.subTree = normalizeVNode(
instance.render.call(proxyToUse, proxyToUse)
));
console.log("subTree", subTree);
// todo
console.log(`${instance.type.name}:触发 beforeMount hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeMount hook`);
// 这里基于 subTree 再次调用 patch
// 基于 render 返回的 vnode ,再次进行渲染
// 这里我把这个行为隐喻成开箱
// 一个组件就是一个箱子
// 里面有可能是 element (也就是可以直接渲染的)
// 也有可能还是 component
// 这里就是递归的开箱
// 而 subTree 就是当前的这个箱子(组件)装的东西
// 箱子(组件)只是个概念,它实际是不需要渲染的
// 要渲染的是箱子里面的 subTree
patch(null, subTree, container, null, instance);
// 把 root element 赋值给 组件的vnode.el ,为后续调用 $el 的时候获取值
initialVNode.el = subTree.el;
console.log(`${instance.type.name}:触发 mounted hook`);
instance.isMounted = true;
} else {
// 响应式的值变更后会从这里执行逻辑
// 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
console.log(`${instance.type.name}:调用更新逻辑`);
// 拿到最新的 subTree
const { next, vnode } = instance;
// 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
// 先更新组件的数据,然后更新完成后,在继续对比当前组件的子元素
if (next) {
// 问题是 next 和 vnode 的区别是什么
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const proxyToUse = instance.proxy;
const nextTree = normalizeVNode(
instance.render.call(proxyToUse, proxyToUse)
);
// 替换之前的 subTree
const prevTree = instance.subTree;
instance.subTree = nextTree;
// 触发 beforeUpdated hook
console.log(`${instance.type.name}:触发 beforeUpdated hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeUpdate hook`);
// 用旧的 vnode 和新的 vnode 交给 patch 来处理
patch(prevTree, nextTree, prevTree.el, null, instance);
// 触发 updated hook
console.log(`${instance.type.name}:触发 updated hook`);
console.log(`${instance.type.name}:触发 onVnodeUpdated hook`);
}
}
// 在 vue3.2 版本里面是使用的 new ReactiveEffect
// 至于为什么不直接用 effect ,是因为需要一个 scope 参数来收集所有的 effect
// 而 effect 这个函数是对外的 api ,是不可以轻易改变参数的,所以会使用 new ReactiveEffect
// 因为 ReactiveEffect 是内部对象,加一个参数是无所谓的
// 后面如果要实现 scope 的逻辑的时候 需要改过来
// 现在就先算了
instance.update = effect(componentUpdateFn, {
scheduler: () => {
// 把 effect 推到微任务的时候在执行
// queueJob(effect);
queueJob(instance.update);
},
});
}
参数:
instance,组件实例
nextVNode,虚拟节点
返回值:无
作用:
将新的虚拟节点赋值给instance.vnode
设置instance.next为null
把新的虚拟节点的props赋值给Instance.props
function updateComponentPreRender(instance, nextVNode) {
// 更新 nextVNode 的组件实例
// 现在 instance.vnode 是组件实例更新前的
// 所以之前的 props 就是基于 instance.vnode.props 来获取
// 接着需要更新 vnode ,方便下一次更新的时候获取到正确的值
nextVNode.component = instance;
// TODO 后面更新 props 的时候需要对比
// const prevProps = instance.vnode.props;
instance.vnode = nextVNode;
instance.next = null;
const { props } = nextVNode;
console.log("更新组件的 props", props);
instance.props = props;
console.log("更新组件的 slots");
// TODO 更新组件的 slots
// 需要重置 vnode
}
参数:
返回值:虚拟组件节点(vnode、type、setupState、props、slots、provides、parent、isMounted、subTree、emit、proxy)
作用:
export function createComponentInstance(vnode, parent) {
const instance = {
type: vnode.type,
vnode,
next: null, // 需要更新的 vnode,用于更新 component 类型的组件
props: {},
parent,
provides: parent ? parent.provides : {}, // 获取 parent 的 provides 作为当前组件的初始化值 这样就可以继承 parent.provides 的属性了
proxy: null,
isMounted: false,
attrs: {}, // 存放 attrs 的数据
slots: {}, // 存放插槽的数据
ctx: {}, // context 对象
setupState: {}, // 存储 setup 的返回值
emit: () => {},
};
// 在 prod 坏境下的 ctx 只是下面简单的结构
// 在 dev 环境下会更复杂
instance.ctx = {
_: instance,
};
// 赋值 emit
// 这里使用 bind 把 instance 进行绑定
// 后面用户使用的时候只需要给 event 和参数即可
instance.emit = emit.bind(null, instance) as any;
return instance;
}
参数:instance实例
返回值:无
作用:初始化设置组件
export function setupComponent(instance) {
// 1. 处理 props
// 取出存在 vnode 里面的 props
const { props, children } = instance.vnode;
initProps(instance, props);
// 2. 处理 slots
initSlots(instance, children);
// 源码里面有两种类型的 component
// 一种是基于 options 创建的
// 还有一种是 function 的
// 这里处理的是 options 创建的
// 叫做 stateful 类型
setupStatefulComponent(instance);
}
参数:instance实例
返回值:无
作用:初始化有状态的组件
function setupStatefulComponent(instance) {
// todo
// 1. 先创建代理 proxy
console.log("创建 proxy");
// proxy 对象其实是代理了 instance.ctx 对象
// 我们在使用的时候需要使用 instance.proxy 对象
// 因为 instance.ctx 在 prod 和 dev 坏境下是不同的
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
// 用户声明的对象就是 instance.type
// const Component = {setup(),render()} ....
const Component = instance.type;
// 2. 调用 setup
// 调用 setup 的时候传入 props
const { setup } = Component;
if (setup) {
// 设置当前 currentInstance 的值
// 必须要在调用 setup 之前
setCurrentInstance(instance);
const setupContext = createSetupContext(instance);
// 真实的处理场景里面应该是只在 dev 环境才会把 props 设置为只读的
const setupResult =
setup && setup(shallowReadonly(instance.props), setupContext);
setCurrentInstance(null);
// 3. 处理 setupResult
handleSetupResult(instance, setupResult);
} else {
finishComponentSetup(instance);
}
}
参数:
返回值:无
作用
function handleSetupResult(instance, setupResult) {
// setup 返回值不一样的话,会有不同的处理
// 1. 看看 setupResult 是个什么
if (typeof setupResult === "function") {
// 如果返回的是 function 的话,那么绑定到 render 上
// 认为是 render 逻辑
// setup(){ return ()=>(h("div")) }
instance.render = setupResult;
} else if (typeof setupResult === "object") {
// 返回的是一个对象的话
// 先存到 setupState 上
// 先使用 @vue/reactivity 里面的 proxyRefs
// 后面我们自己构建
// proxyRefs 的作用就是把 setupResult 对象做一层代理
// 方便用户直接访问 ref 类型的值
// 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value
// 这里也就是官网里面说到的自动结构 Ref 类型
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
参数:instance实例
返回值:无
作用:
function finishComponentSetup(instance) {
// 给 instance 设置 render
// 先取到用户设置的 component options
const Component = instance.type;
if (!instance.render) {
// 如果 compile 有值 并且当组件没有 render 函数,那么就需要把 template 编译成 render 函数
if (compile && !Component.render) {
if (Component.template) {
// 这里就是 runtime 模块和 compile 模块结合点
const template = Component.template;
Component.render = compile(template);
}
}
instance.render = Component.render;
}
// applyOptions()
}
作用:保存当前的instance
主要服务于apiInject模块,使得子组件在setup中调用inject时能够直接获取到当前组件实例instance,通过instance找到parent,获取parent中的provide的值,从而实现provide-inject功能
let currentInstance = {};
参数:无
返回值:currentInstance当前组件实例对象(只能在setup中使用)
作用:返回当前实例
// 这个接口暴露给用户,用户可以在 setup 中获取组件实例 instance
export function getCurrentInstance(): any {
return currentInstance;
}
参数:instance实例
返回值:无
作用:currentInstance = instance
export function setCurrentInstance(instance) {
currentInstance = instance;
}
作用:
// todo 需要让用户可以直接在 render 函数内直接使用 this 来触发 proxy
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
// 用户访问 proxy[key]
// 这里就匹配一下看看是否有对应的 function
// 有的话就直接调用这个 function
const { setupState, props } = instance;
console.log(`触发 proxy hook , key -> : ${key}`);
if (key[0] !== "$") {
// 说明不是访问 public api
// 先检测访问的 key 是否存在于 setupState 中, 是的话直接返回
if (hasOwn(setupState, key)) {
return setupState[key];
} else if (hasOwn(props, key)) {
// 看看 key 是不是在 props 中
// 代理是可以访问到 props 中的 key 的
return props[key];
}
}
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
return publicGetter(instance);
}
},
set({ _: instance }, key, value) {
const { setupState } = instance;
if (setupState !== {} && hasOwn(setupState, key)) {
// 有的话 那么就直接赋值
setupState[key] = value;
}
return true
},
};
作用:返回$el $slots $props
const publicPropertiesMap = {
// 当用户调用 instance.proxy.$emit 时就会触发这个函数
// i 就是 instance 的缩写 也就是组件实例对象
$el: (i) => i.vnode.el,
$emit: (i) => i.emit,
$slots: (i) => i.slots,
$props: (i) => i.props,
};
参数:
返回值:无
作用:instance.props = rawProps || {},初始化props
export function initProps(instance, rawProps) {
console.log("initProps");
// TODO
// 应该还有 attrs 的概念
// attrs
// 如果组件声明了 props 的话,那么才可以进入 props 属性内
// 不然的话是需要存储在 attrs 内
// 这里暂时直接赋值给 instance.props 即可
instance.props = rawProps;
}
参数:
返回值:无
作用:
export function emit(instance, event: string, ...rawArgs) {
// 1. emit 是基于 props 里面的 onXXX 的函数来进行匹配的
// 所以我们先从 props 中看看是否有对应的 event handler
const props = instance.props;
// ex: event -> click 那么这里取的就是 onClick
// 让事情变的复杂一点如果是烤肉串命名的话,需要转换成 change-page -> changePage
// 需要得到事件名称
let handler = props[toHandlerKey(camelize(event))];
// 如果上面没有匹配的话 那么在检测一下 event 是不是 kebab-case 类型
if (!handler) {
handler = props[(toHandlerKey(hyphenate(event)))]
}
if (handler) {
handler(...rawArgs);
}
}
参数:
返回值:无
作用:
export function initSlots(instance, children) {
const { vnode } = instance;
console.log("初始化 slots");
if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
normalizeObjectSlots(children, (instance.slots = {}));
}
}
参数:
返回值:无
作用:
遍历children获取到插槽的名称key(对具名插槽的支持)
通过children[key]获取value(value是一个函数)
创建一个新的函数,形参props是一个对象或者为空,代表value函数的形参,用于插槽传参(对作用域插槽的支持),函数体的内容调用了normalizeSlotValue函数,主要把函数value的返回结果变为一个数组
备注:配合实例代码比较好理解
const foo = h(
Foo,
{},
{
header: ({ age }) => [
h("p", {}, "header" + age),
createTextVNode("你好呀"),
],
footer: () => h("p", {}, "footer"),
}
);
// 调用传参{age}
return h("div", {}, [
renderSlots(this.$slots, "header", {
age,
}),
foo,
renderSlots(this.$slots, "footer"),
]);
const normalizeObjectSlots = (rawSlots, slots) => {
for (const key in rawSlots) {
const value = rawSlots[key];
if (typeof value === "function") {
// 把这个函数给到slots 对象上存起来
// 后续在 renderSlots 中调用
// TODO 这里没有对 value 做 normalize,
// 默认 slots 返回的就是一个 vnode 对象
slots[key] = (props) => normalizeSlotValue(value(props));
}
}
};
参数:
返回值:数组
作用:value若不为数组,则返回一个包裹着value的数组
const normalizeSlotValue = (value) => {
// 把 function 返回的值转换成 array ,这样 slot 就可以支持多个元素了
return Array.isArray(value) ? value : [value];
};
参数:
slots,插槽对象,存储所有插槽
name,插槽名称(具名插槽)
props,插槽传参(作用域插槽)
返回值:如果插槽存在则渲染对应的真实dom节点
作用:
根据name从slots中取出对应插槽slot
若slot有值并且slot为一个函数(mini-vue这里暂时只支持对函数形式,理解了函数,其他形式都很好理解了),调用createVNode创建真实dom节点
export function renderSlot(slots, name: string, props = {}) {
const slot = slots[name];
console.log(`渲染插槽 slot -> ${name}`);
if (slot) {
// 因为 slot 是一个返回 vnode 的函数,我们只需要把这个结果返回出去即可
// slot 就是一个函数,所以就可以把当前组件的一些数据给传出去,这个就是作用域插槽
// 参数就是 props
const slotContent = slot(props);
return createVNode(Fragment, {}, slotContent);
}
}
参数:
prevVNode,旧的虚拟节点
nextVNode,新的虚拟节点
返回值:布尔值,代表是否要更新组件
作用:
从prevVNode和nextVNode中取出对应props
如果新旧节点相同则不需要更新
如果没有老的虚拟节点,若新虚拟节点存在则更新,不存在则不更新
如果老的虚拟节点有值,但是新的虚拟节点没值,则要更新
调用hasPropsChanged函数根据props是否改变来判断是否需要更新
export function shouldUpdateComponent(prevVNode, nextVNode) {
const { props: prevProps } = prevVNode;
const { props: nextProps } = nextVNode;
// const emits = component!.emitsOptions;
// 这里主要是检测组件的 props
// 核心:只要是 props 发生改变了,那么这个 component 就需要更新
// 1. props 没有变化,那么不需要更新
if (prevProps === nextProps) {
return false;
}
// 如果之前没有 props,那么就需要看看现在有没有 props 了
// 所以这里基于 nextProps 的值来决定是否更新
if (!prevProps) {
return !!nextProps;
}
// 之前有值,现在没值,那么肯定需要更新
if (!nextProps) {
return true;
}
// 以上都是比较明显的可以知道 props 是否是变化的
// 在 hasPropsChanged 会做更细致的对比检测
return hasPropsChanged(prevProps, nextProps);
}
参数:
prevProps,老节点的props
nextProps,新节点的props
返回值:布尔值,代表props是否改变
作用:
比较prevProps和nextProps的长度,如果不同则肯定不同返回true
遍历新的props的key,如果nextProps[key] !== prevProps[key]不相等则返回true
返回false
function hasPropsChanged(prevProps, nextProps): boolean {
// 依次对比每一个 props.key
// 提前对比一下 length ,length 不一致肯定是需要更新的
const nextKeys = Object.keys(nextProps);
if (nextKeys.length !== Object.keys(prevProps).length) {
return true;
}
// 只要现在的 prop 和之前的 prop 不一样那么就需要更新
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i];
if (nextProps[key] !== prevProps[key]) {
return true;
}
}
return false;
}
参数:
key,父组件提供给子组件的key
value,父组件提供给子组件对应key的value
返回值:无
作用:提供变量
调用getCurrentInstance获取当前的正在执行setup的组件实例currentInstance
若currentInstance有值,则从组件实例中取出provides和parentProvides父组件的provides
若parentProvides和provides相等,则把parent.provides作为currentInstance.provides的原型重新赋值(因为provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的,详见崔老师源码中的注释)
把value赋值给provides[key]
export function provide(key, value) {
const currentInstance = getCurrentInstance();
if (currentInstance) {
let { provides } = currentInstance;
const parentProvides = currentInstance.parent?.provides;
// 这里要解决一个问题
// 当父级 key 和 爷爷级别的 key 重复的时候,对于子组件来讲,需要取最近的父级别组件的值
// 那这里的解决方案就是利用原型链来解决
// provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的
// 所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就说明是第一次做 provide(对于当前组件来讲)
// 我们就可以把 parent.provides 作为 currentInstance.provides 的原型重新赋值
// 至于为什么不在 createComponent 的时候做这个处理,可能的好处是在这里初始化的话,是有个懒执行的效果(优化点,只有需要的时候在初始化)
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
provides[key] = value;
}
}
参数:
key
defaultValue,默认值(若父组件没有这个值,则使用默认值)
返回值:返回对应父组件提供的key
作用:注入变量
调用getCurrentInstance获取当前的正在执行setup的组件实例currentInstance
若currentInstance有值,则从组件实例中取出父组件的provides
如果父组件提供了对应变量则使用
如果没有则使用defaultValue
export function inject(key, defaultValue) {
const currentInstance = getCurrentInstance();
if (currentInstance) {
const provides = currentInstance.parent?.provides;
if (key in provides) {
return provides[key];
} else if (defaultValue) {
if (typeof defaultValue === "function") {
return defaultValue();
}
return defaultValue;
}
}
}