组件
模板编译 最终是把模板 -> render返回的结果 -> 虚拟dom(h方法返回的是虚拟dom)
-> 渲染成真实的dom
const MyComponent = {
props: {
a: Number,
b: Number,
},
data() {
return { name: 'jw', age: 30 }
},
render(proxy) {
return h('div', {}, [
h('span', proxy.a),
h('span', proxy.b),
h('span', proxy.$attrs.c)
])
}
}
// attrs(组件标签上的属性) , props(我给你传递的自定义属性不在attrs中的)
render(h(MyComponent, { a: 1, b: 2, c: 1 }), app);
那么 h 有可能接收一个 对象作为参数,这个对象包含多个属性:
- props 对象
- data 函数
- render 函数
由于 h 底层使用createVNode
,所以要对createVNode
进行改造
function createVNode(type, props, children = null){
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT // 这个是元素
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT // 这个是组件
: 0;
const vnode = {
shapeFlag,
__v_isVNode: true,
type, // type 现在是一个对象
props,
key: props && props.key,
el: null, // 每个虚拟节点都对应一个真实节点,用来存放真实节点
children,
component:null // 组件instance
}
}
在 render
方法中的 patch
中,要考虑组件的情况
render(vnode, container) {
if (vnode == null) {
// ....
} else {
//...
patch(prevVnode, nextVnode, container);
container._vnode = nextVnode; // 第一次的渲染的虚拟节点
}
},
const patch = (n1, n2, container, anchor = null) =>{
const { shapeFlag } = n2;
if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container);
}
}
处理组件 - processComponent
分为 挂载/卸载 两个步骤
function processComponent(n1, n2, container) {
if (n1 == null) {
mountComponent(n2, container);
} else {
patchComponent(n1, n2, container);
}
}
挂载组件 - mountComponent
function mountComponent(n2, container) {
// 1) 给组件生成一个实例 instance
const instance = (n2.component = createInstance(n2));
// 2) 初始化实例属性 props attrs slots
setupComponent(instance);
// 3) 创建渲染effect
setupRenderEffect(instance, container);
}
createInstance
createInstance
是创建一个空 instance 实例,有props
,slots
,render
,vnode
等属性
createInstance
function createInstance(n2) {
const instance = {
// 组件的实例,用它来记录组件中的属性
setupState: {},
state: {},
isMounted: false, // 是否挂载成功
vnode: n2, // 组件的虚拟节点
subTree: null, // 组件渲染的虚拟节点
update: null, // 用于组件更新的方法
propsOptions: n2.type.props, // 用户传递的props
props: {},
attrs: {},
slots: {},
render: null,
proxy: null, // 帮我们做代理 -> proxyRefs
};
return instance;
}
setupComponent
setupComponent
是执行 setup
方法,对 instance
的props
attrs
slots
进行赋值
同时判断 setup
的返回值,如果返回值是对象,则把对象值赋值给 instance.setupState
如果是函数,则把函数赋值给 instance.render
,自己变为 render
方法
同时要把data
里面的值做一个响应式处理 reactive(data())
setupComponent
export let currentInstance = null;
const setCurrentInstance = (instance) => currentInstance = instance
const getCurrentInstance = () => currentInstance
function setupComponent(instance) {
let { type, props } = instance.vnode;
const publicProperties = {
$attrs: (instance) => instance.attrs,
$props: (instance) => instance.props,
};
// 把所有的访问器属性都挂载到组件实例上
instance.proxy = new Proxy(instance, {
get(target, key) {
const { state, props, setupState } = target;
if (key in state) {
return state[key];
} else if (key in setupState) {
return setupState[key];
} else if (key in props) {
return props[key];
}
const getter = publicProperties[key];
if (getter) {
return getter(instance); // 将instance传递进去
}
},
set(target, key, value) {
const { state, props, setupState } = target;
if (key in state) {
state[key] = value;
return true;
} else if (key in setupState) {
setupState[key] = value;
return true;
} else if (key in props) {
console.warn(
// 组件不能修改属性了
`mutate prop ${key as string} not allowed, props are readonly`
);
return false;
}
return true;
},
});
initProps(instance, props);
initSlots(instance,children)
// 🔥 用户编写的setup方法
const setup = type.setup;
if (setup) {
setCurrentInstance(instance)
const setupResult = setup(instance.props, {
attrs: instance.attrs, // 完成
emit: (eventName, ...args) => {
// onMyEvent onMyEvent
let handler =
props[`on${eventName[0].toUpperCase()}${eventName.slice(1)}`];
handler && handler(...args);
},
slots: instance.slots,
expose: () => {},
});
setCurrentInstance(null)
// 🔥 判断setup的返回值是否是
if (isObject(setupResult)) {
// 返回的是setup提供的数据源头
instance.setupState = proxyRefs(setupResult);
} else if (isFunction(setupResult)) {
// 如果是函数的话,则变为 render 方法
instance.render = setupResult;
}
}
const data = type.data;
if (data) {
instance.state = reactive(data());
}
!instance.render && (instance.render = type.render);
}
如果用户传入了 {a,b,c}
,但是组件使用 props
接收了 a,b
属性,那么 c
会被放在 attrs
属性上
function initProps(instance, rawProps) {
const props = {};
const attrs = {};
const options = instance.propsOptions || {};
if (rawProps) {
for (let key in rawProps) {
if (key in options) {
props[key] = rawProps[key];
} else {
attrs[key] = rawProps[key];
}
}
}
// 属性是响应式的,属性变化了 会造成页面更新
instance.props = reactive(props); // 属性会被变成响应式 props也可以不是响应式的
instance.attrs = attrs;
}
function initSlots(instance, children) {
instance.slots = children
}
拆包,如果是 ref
则直接返回 .value
,如果是设置值,如果是ref
,则设置 .value
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, recevier) {
let r = Reflect.get(target, key, recevier);
return r.__v_isRef ? r.value : r;
},
set(target, key, value, recevier) {
const oldValue = target[key];
if (oldValue.__v_isRef) {
oldValue.value = value;
return true;
} else {
return Reflect.set(target, key, value, recevier);
}
},
});
}
🚀setupRenderEffect
// 3) 创建渲染effect
setupRenderEffect(instance, container);
由于更新状态要重新渲染,所以需要创建一个渲染 effect
把 effect.run
绑定给 instance.update
方法上,以便后续更新使用
第一次渲染,即 instance.isMounted == false
,
把
instance.render
的 this 指向instance.proxy
,并且传入instance.proxy
作为render
参数,这样不论是 this 还是参数都是可以指向instance.proxy
对于
instance.proxy
的返回值 虚拟 dom 即subTree
执行patch
挂载
当组件更新时,instance.isMounted == true
,需要对把上一次的 vnode 和 这一次的 vnode 做一个比对,执行 patch(旧vnode,新vnode)
data(){
return {name:'zs',age:18}
}
render(proxy){
// 可以使用 this
return h('div', {}, [h('span', proxy.a), h('span', proxy.b), h('span', proxy.$attrs.c)])
}
function setupRenderEffect(instance, container) {
const componentUpdateFn = () => {
// 初次渲染
if (!instance.isMounted) {
const subTree = instance.render.call(instance.proxy, instance.proxy);
instance.subTree = subTree;
patch(null, subTree, container);
instance.isMounted = true;
} else {
// 组件更新 ,自身的状态变了要更新子树
let next = instance.next;
if (next) {
// 渲染前的更新
updateComponentPreRender(instance, next);
}
const subTree = instance.render.call(instance.proxy, instance.proxy);
patch(instance.subTree, subTree, container);
instance.subTree = subTree;
}
};
const effect = new ReactiveEffect(
componentUpdateFn,
// 多个更新合并成一个统一执行
() => queueJob(instance.update)
);
const update = (instance.update = effect.run.bind(effect));
update();
}
queueJob 异步更新
const queue = [];
let isFlushing = false;
const resolvePromise = Promise.resolve();
// 调度函数 实现异步渲染
export function queueJob(job) {
if (!queue.includes(job)) {
// 将任务放到队列中
queue.push(job);
}
if (!isFlushing) {
isFlushing = true;
resolvePromise.then(() => {
isFlushing = false;
let arr = queue.slice(0);
queue.length = 0; // 在执行的时候可以继续像queue中添加任务
for (let i = 0; i < arr.length; i++) {
const job = arr[i];
job();
}
arr.length = 0;
});
}
}
组件更新 - patchComponent
function processComponent(n1, n2, container) {
if (n1 == null) {
mountComponent(n2, container);
} else {
patchComponent(n1, n2, container);
}
}
组件的prop属性发生变化,插槽更新就要走到 patchComponent
function patchComponent(n1, n2, container) {
const instance = (n2.component = n1.component);
// 判断是否需要更新,主要判断 prop 是否相同
if (shouldComponentUpdate(n1, n2)) {
// 挂载最新的虚拟节点
instance.next = n2;
instance.update();
}
// updateProps(instance, prevProps, nextProps);
}
shouldComponentUpdate
function shouldComponentUpdate(n1, n2) {
const prevProps = n1.props;
const nextProps = n2.props;
return hasChangedProps(prevProps, nextProps);
}
function hasChangedProps(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps);
const prevKeys = Object.keys(prevProps);
if (nextKeys.length != prevKeys.length) {
// 如果传递的属性个数不一致说明有变化
return true;
}
for (let i = 0; i < nextKeys.length; i++) {
// 如果值不一样说明变化
const key = nextKeys[i];
if (nextProps[key] !== prevProps[key]) {
return true;
}
}
return false;
}
在 vnode
上 挂载 instance.next
,执行 instance.update
更新组件,就会重新走到 componentUpdateFn
,然后执行:
let next = instance.next;
if (next) {
// 更新新老虚拟节点的 props / slots 等
updateComponentPreRender(instance, next);
}
updateComponentPreRender
function updateComponentPreRender(instance, next) {
instance.next = null;
instance.vnode = next; // 这里为了保证vnode是最新
updateProps(instance.props, next.props);
// instance.slots = next.children; 万一用户在setup中解构了slots 一直使用解构的
// 会导致render方法重新调用 获取的是老的slots
Object.assign(instance.slots, next.children); // props 比较
}
更新 props
updateProps
function updateProps(prevProps, nextProps) {
// 组件自己不能更新属性,但是在父组件中可以更新属性(原本的props就是proxy)
// 除了属性之外还有attrs要考虑
for (const key in nextProps) {
// instance.props = nextProps 丧失响应式
prevProps[key] = nextProps[key]; // instance.props.count = 新的值
}
for (const key in prevProps) {
if (!(key in nextProps)) {
delete prevProps[key];
}
}
}