Text-Fragment
除了元素虚拟节点之外,Vue3中还有很多其他类型的虚拟节点,这里我们先来说下Text和Fragment的实现
export const Text = Symbol('Text')
export const Fragment = Symbol('Fragment')
文本类型
renderer.render(h(Text,'jw handsome'),document.getElementById('app'))
const patch = (n1,n2,container,anchor?) => {
// 初始化和diff算法都在这里喲
if(n1 == n2){return }
if(n1 && !isSameVNodeType(n1,n2)){ // 有n1 是n1和n2不是同一个节点
unmount(n1)
n1 = null
}
const {type,shapeFlag} = n2;
switch(type){
case Text:
processText(n1,n2,container); // 处理文本
break;
case Fragment:
processFragment(n1,n2,container); // 处理fragment
break;
default:
if(shapeFlag & ShapeFlags.ELEMENT){
processElement(n1,n2,container,anchor); // 之前处理元素的逻辑
}
}
}
const processText = (n1,n2,container)=>{
if(n1 == null){
hostInsert((n2.el = hostCreateText(n2.children)),container)
}else{
const el = n2.el = n1.el;
if(n2.children !== n1.children){
hostSetText(el,n2.children)
}
}
}
Fragment类型
renderer.render(h(Fragment,[h(Text,'hello'),h(Text,'jw')]),document.getElementById('app'))
const processFragment = (n1,n2,container)=>{
if(n1 == null){
mountChildren(n2.children,container);
}else{
patchChildren(n1,n2,container);
}
}
为了让Vue3支持多根节点模板,Vue.js 提供Fragment来实现,核心就是一个无意义的标签包裹多个节点。
同时这里要处理下卸载的逻辑,如果是fragment则删除子元素
const unmount = (vnode) =>{
if(vnode.type === Fragment){
return unmountChildren(vnode.children)
}
hostRemove(vnode.el)
}
组件渲染挂载流程
组件需要提供一个render函数,渲染函数需要返回虚拟DOM
const VueComponent = {
data(){
return {age:13}
},
render(){
return h('p',[h(Text,"I'm Jiang sir"),h('span',this.age+'')])
}
}
createRenderer(renderOptions).render(h(VueComponent),document.getElementById('app'))
添加组件类型
h方法中传入一个对象说明要渲染的是一个组件。(后续还有其他可能)
export const createVNode = (type,props,children = null)=>{
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT:0;
// ... 稍后可以根据类型来进行组件的挂载
}
组件的渲染
const patch = (n1,n2,container,anchor?) => {
// 初始化和diff算法都在这里喲
if(n1 == n2){return }
if(n1 && !isSameVNodeType(n1,n2)){ // 有n1 是n1和n2不是同一个节点
unmount(n1)
n1 = null
}
const {type,shapeFlag} = n2;
switch(type){
// ...
default:
if(shapeFlag & ShapeFlags.ELEMENT){
processElement(n1,n2,container,anchor)
}else if(shapeFlag & ShapeFlags.COMPONENT){
processComponent(n1,n2,container,anchor)
}
}
}
const mountComponent = (n2,container,anchor)=>{
const {render,data=()=>({})} = n2.type;
const state = reactive(data())
const instance = {
state, // 组件的状态
isMounted:false, // 组件是否挂载
subTree:null, // 子树
update:null,
vnode:n2
}
const componentUpdateFn = ()=>{
if(!instance.isMounted){
const subTree = render.call(state,state);
patch(null,subTree,container,anchor);
instance.subTree = subTree
instance.isMounted = true;
}else{
const subTree = render.call(state,state);
patch(instance.subTree,subTree,container,anchor)
instance.subTree = subTree
}
}
const effect = new ReactiveEffect(componentUpdateFn)
const update = instance.update = effect.run.bind(effect);
update();
}
const processComponent = (n1,n2,container,anchor)=>{
if(n1 == null){
mountComponent(n2,container,anchor);
}else{
// 组件更新逻辑
}
}
组件异步渲染
修改调度方法,将更新方法压入到队列中
const effect = new ReactiveEffect(
componentUpdateFn,
()=>queueJob(instance.update)
);
const update = instance.update = effect.run.bind(effect);
1
2
3
4
5
批处理操作scheduler.js
const queue = [];
let isFlushing = false;
const resolvedPromise = Promise.resolve()
export function queueJob(job){
if(!queue.includes(job)){
queue.push(job);
}
if(!isFlushing){
isFlushing = true;
resolvedPromise.then(()=>{
isFlushing = false;
let copy = queue.slice(0)
queue.length = 0; // 这里要先清空,防止在执行过程中在加入新的job
for(let i = 0; i < queue.length;i++){
let job = queue[i];
job();
}
copy.length = 0;
})
}
}
组件Props、Attrs实现
Props和Attrs关系是:没有定义在component.props中的属性将存储到attrs对象中
let {createRenderer,h,render,Text,Fragment} = VueRuntimeDOM
const VueComponent = {
data(){
return {name:'zf',age:13}
},
props:{
address:String,
},
render(){
return h('p',[`${this.name}今年${this.age}岁了`,`${this.address}`,`${this.$attrs.a}、${this.$attrs.b}`]);
}
}
render(h(VueComponent,{address:'霍营',a:1,b:2}),app);
initProps
const mountComponent = (vnode,container,anchor) =>{
let {data=()=>({}),render,props:propsOptions = {}} = vnode.type; // 这个就是用户写的内容
const state = reactive(data()); // pinia 源码就是 reactive({}) 作为组件的状态
const instance = { // 组件的实例
state,
vnode, // vue2的源码中组件的虚拟节点叫$vnode 渲染的内容叫_vnode
subTree:null, // vnode组件的虚拟节点 subTree渲染的组件内容
isMounted:false,
update:null,
propsOptions,
attrs:{},
props:{}
}
vnode.component = instance
initProps(instance,vnode.props);
}
componentProps.ts
export function initProps(instance,rawProps){
const props = {};
const attrs = {};
const options = instance.propsOptions || {}; // 获取组件用户的配置
if(rawProps){
for(let key in rawProps){
const value = rawProps[key];
if( key in options){
props[key] = value;
}else{
attrs[key] = value
}
}
}
instance.props = reactive(props); // 这里应该用shallowReactive,遵循单向数据流原则
instance.attrs = attrs;
}
属性代理
const publicPropertiesMap = {
$attrs:i=> i.attrs
}
const mountComponent = (vnode,container,anchor) =>{
// ...
const instance = { // 组件的实例
// ...
proxy:null
}
vnode.component = instance
initProps(instance,vnode.props);
instance.proxy = new Proxy(instance,{
get(target,key){
const {state,props} = target;
if(state && hasOwn(state,key)){
return state[key];
}else if(hasOwn(props,key)){
return props[key];
}
const publicGetter = publicPropertiesMap[key];
if(publicGetter){
return publicGetter(instance)
}
},
set(target,key,value){
const {state,props} = target;
if(state && hasOwn(state,key)){
state[key] = value;
return true;
}else if(hasOwn(props,key)){
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false;
}
return true;
}
});
}
组件流程整合
const mountComponent = (vnode,container,anchor) =>{
// 1) 创建实例
const instance = vnode.component = createComponentInstance(vnode);
// 2) 给实例赋值
setupComponent(instance)
// 3) 创建渲染effect及更新
setupRenderEffect(instance,container,anchor);
}
1)创建组件实例
component.ts
export function createComponentInstance(vnode){
const instance = { // 组件的实例
data:null,
vnode, // vue2的源码中组件的虚拟节点叫$vnode 渲染的内容叫_vnode
subTree:null, // vnode组件的虚拟节点 subTree渲染的组件内容
isMounted:false,
update:null,
attrs:{},
props:{},
proxy:null,
propsOptions:vnode.type.props
}
return instance
}
2)设置组件属性
const publicPropertiesMap = {
$attrs:i=> i.attrs
}
const PublicInstanceProxyHandlers = {
get(target,key){
const {data,props} = target;
if(data && hasOwn(data,key)){
return data[key];
}else if(hasOwn(props,key)){
return props[key];
}
const publicGetter = publicPropertiesMap[key];
if(publicGetter){
return publicGetter(target)
}
},
set(target,key,value){
const {data,props} = target;
if(data && hasOwn(data,key)){
data[key] = value;
return true;
}else if(hasOwn(props,key)){
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false;
}
return true;
}
}
export function setupComponent(instance){
const {props,type} = instance.vnode;
initProps(instance,props);
instance.proxy = new Proxy(instance,PublicInstanceProxyHandlers);
const data = type.data;
if(data){
if(!isFunction(data)) return console.warn('The data option must be a function.')
instance.data = reactive(data.call(instance.proxy))
}
instance.render = type.render
}
3)渲染effect
const setupRenderEffect = (instance,container,anchor) =>{
const {render} = instance
const componentUpdateFn = () =>{ // 区分是初始化 还是要更新
if(!instance.isMounted){ // 初始化
const subTree = render.call(instance.proxy,instance.proxy); // 作为this,后续this会改
patch(null,subTree,container,anchor); // 创造了subTree的真实节点并且插入了
instance.subTree = subTree;
instance.isMounted = true
}else{ // 组件内部更新
const subTree = render.call(instance.proxy,instance.proxy);
patch(instance.subTree,subTree,container,anchor);
instance.subTree = subTree;
}
}
// 组件的异步更新
const effect = new ReactiveEffect(componentUpdateFn,()=> queueJob(instance.update))
// 我们将组件强制更新的逻辑保存到了组件的实例上,后续可以使用
let update = instance.update = effect.run.bind(effect); // 调用effect.run可以让组件强制重新渲染
update();
}
属性更新
const My = {
props:{address:String},
render(){return h('div',this.address)}
}
const VueComponent = {
data(){
return {name:'zf',age:13,flag:false}
},
render(){
return h(Fragment,[
h('button',{onClick:() => this.flag = !this.flag},'切换渲染'),
h(My,{address:this.flag? '天龙苑':'回龙观'}),
])
}
}
render(h(VueComponent),app);
const updateComponent = (n1,n2)=>{
const instance = (n2.component = n1.component);
const {props:prevProps} = n1;
const {props:nextProps} = n2;
updateProps(instance,prevProps,nextProps)
}
const processComponent = (n1,n2,container,anchor)=>{
if(n1 == null){
mountComponent(n2,container,anchor);
}else{
// 组件更新逻辑
updateComponent(n1,n2)
}
}
props.ts
const hasPropsChanged = (prevProps={},nextProps={}) =>{
const nextKeys = Object.keys(nextProps);
if(nextKeys.length !== Object.keys(prevProps).length){
return true;
}
for(let i = 0; i < nextKeys.length ; i++){
const key = nextKeys[i];
if(nextProps[key] !== prevProps[key]){
return true;
}
}
return false
}
export function updateProps(instance,prevProps,nextProps){
if(hasPropsChanged(prevProps,nextProps)){ // 比较前后属性是否一致
for(const key in nextProps){ // 循环props
instance.props[key] = nextProps[key]; // 响应式属性更新后会重新渲染
}
for(const key in instance.props){ // 循环props
if(!(key in nextProps)){
delete instance.props[key]
}
}
}
}
这里我们将更新逻辑放到
componentFn中,因为除了属性更新之外,插槽也会导致页面更新
const shouldUpdateComponent = (n1,n2) =>{
const { props: prevProps, children: prevChildren } = n1
const { props: nextProps, children: nextChildren } = n2;
if(prevChildren || nextChildren) return true
if(prevProps === nextProps) return false;
return hasPropsChanged(prevProps,nextProps)
}
const updateComponent = (n1,n2)=>{
const instance = (n2.component = n1.component);
if(shouldUpdateComponent(n1,n2)){
instance.next = n2 // 将新的虚拟节点放到next属性上
instance.update(); // 属性变化手动调用更新方法
}
}
export function updateProps(prevProps,nextProps){
for(const key in nextProps){ // 循环props
prevProps[key] = nextProps[key]; // 响应式属性更新后会重新渲染
}
for(const key in prevProps){ // 循环props
if(!(key in nextProps)){
delete prevProps[key]
}
}
}
function updateComponentPreRender(instance,next){
instance.next = null;
instance.vnode = next;
updateProps(instance.props,next.props)
}
const componentUpdateFn = ()=>{
if(!instance.isMounted){
// ...
}else{
let {next} = instance;
if(next){
updateComponentPreRender(instance,next)
}
const subTree = render.call(instance.proxy,instance.proxy);
patch(instance.subTree,subTree,container,anchor);
instance.subTree = subTree;
}
}
setup函数作用
组件的render函数每次更新时都会重新执行,但是setup函数只会在组件挂载时执行一次。
- setup函数是compositionAPI的入口
- 可以在函数内部编写逻辑,解决vue2中反复横跳问题
- setup返回函数时为组件的render函数,返回对象时对象中的数据将暴露给模板使用
- setup中函数的参数为props、context({slots,emit,attrs,expose})
const My = {
props:{address:String},
render(){return h('div',this.address)}
}
const VueComponent = {
props:{
address:String
},
setup(props){
const name = ref('jw');
return {
name,
address:props.address
}
},
render (){
return h(Text,`${this.address},${this.name}`)
}
}
render(h(VueComponent,{address:'回龙观'}),app);
对
setup函数进行解析
export function setupComponent(instance){
const {props,type} = instance.vnode;
initProps(instance,props);
let {setup} = type
if(setup){ // 对setup做相应处理
const setupContext = {};
const setupResult = setup(instance.props,setupContext);
console.log(setupResult)
if(isFunction(setupResult)){
instance.render = setupResult;
}else if(isObject(setupResult)){
instance.setupState = proxyRefs(setupResult); // 这里对返回值进行结构
}
}
instance.proxy = new Proxy(instance,PublicInstanceProxyHandlers);
const data = type.data;
if(data){
if(!isFunction(data)) return console.warn('The data option must be a function.')
instance.data = reactive(data.call(instance.proxy))
}
if(!instance.render){
instance.render = type.render
}
}
新增取值范围
const PublicInstanceProxyHandlers = {
get(target,key){
const {data,props,setupState} = target;
if(data && hasOwn(data,key)){
return data[key];
}else if(hasOwn(props,key)){
return props[key];
}else if(setupState && hasOwn(setupState,key)){ // setup返回值做代理
return setupState[key];
}
const publicGetter = publicPropertiesMap[key];
if(publicGetter){
return publicGetter(target)
}
},
set(target,key,value){
const {data,props,setupState} = target;
if(data && hasOwn(data,key)){
data[key] = value;
return true;
}else if(hasOwn(props,key)){
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`)
return false;
} else if(setupState && hasOwn(setupState,key)){ // setup返回值做代理
setupState[key] = value
}
return true;
}
}
实现emit方法
const VueComponent = {
setup(props,ctx){
const handleClick = ()=>{
ctx.emit('myEvent');
}
return ()=>h('button',{onClick:handleClick},'点我啊')
}
}
render(h(VueComponent,{onMyEvent:()=>{alert(1000)}}),document.getElementById('app'))
const setupContext = {
attrs:instance.attrs,
emit:(event,...args)=>{
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`;
const handler = instance.vnode.props[eventName]; // 找到绑定的方法
// 触发方法执行
handler && handler(...args);
}
};
slot实现
const MyComponent = {
render(){
return h(Fragment,[
h('div',[this.$slots.header()]), // 获取插槽渲染
h('div',[this.$slots.body()]),
h('div',[this.$slots.footer()]),
])
}
}
const VueComponent = {
setup(){
return ()=>h(MyComponent,null,{ // 渲染组件时传递对应的插槽属性
header:() => h('p','头'),
body:() => h('p','体'),
footer:() => h('p','尾')
})
}
}
render(h(VueComponent),app)
export const createVNode = (type,props,children = null)=>{
// ....
if(children){
let type = 0;
if(Array.isArray(children)){
type = ShapeFlags.ARRAY_CHILDREN;
}else if(isObject(children)){ // 类型是插槽
type = ShapeFlags.SLOTS_CHILDREN
}else{
children = String(children);
type = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag |= type
}
return vnode;
}
const publicPropertiesMap = {
$attrs:i=> i.attrs,
$slots:i=>i.slots
}
function initSlots(instance,children){
if(instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN){
instance.slots = children;
}else{
instance.slots = {};
}
}
export function createComponentInstance(vnode){
const instance = { // 组件的实例
slots:null // 初始化插槽属性
}
return instance
}
export function setupComponent(instance){
const {props,type,children} = instance.vnode;
initProps(instance,props);
initSlots(instance,children) // 初始化插槽
}
生命周期实现原理
生命周期需要让当前实例关联对应的生命周期,这样在组件构建过程中就可以调用对应的钩子
component.ts
export const setCurrentInstance = (instance) =>currentInstance = instance
export const getCurrentInstance= () => currentInstance
export const unsetCurrentInstance= () => currentInstance = null
setCurrentInstance(instance); // 在调用setup的时候保存当前实例
const setupResult = setup(instance.props,setupContext);
unsetCurrentInstance(null);
创建生命周期钩子
apiLifecycle.ts
export const enum LifecycleHooks {
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u'
}
function createHook(type){
return (hook,target = currentInstance) =>{ // 调用的时候保存当前实例
if(target){
const hooks = target[type] || (target[type] = []);
const wrappedHook = () =>{
setCurrentInstance(target); // 当生命周期调用时 保证currentInstance是正确的
hook.call(target);
setCurrentInstance(null);
}
hooks.push(wrappedHook);
}
}
}
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT);
export const onMounted = createHook(LifecycleHooks.MOUNTED);
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE);
export const onUpdated = createHook(LifecycleHooks.UPDATED);
钩子调用
const componentUpdateFn = ()=>{
if(!instance.isMounted){
const {bm,m} = instance
if(bm){ // beforeMount
invokeArrayFns(bm)
}
const subTree = render.call(renderContext,renderContext);
patch(null,subTree,container,anchor);
if(m){ // mounted
invokeArrayFns(m)
}
instance.subTree = subTree
instance.isMounted = true;
}else{
let {next,bu,u} = instance;
if(next){
updateComponentPreRender(instance,next)
}
if(bu){ // beforeUpdate
invokeArrayFns(bu)
}
const subTree = render.call(renderContext,renderContext);
patch(instance.subTree,subTree,container,anchor)
if(u){ // updated
invokeArrayFns(u)
}
instance.subTree = subTree
}
}
shared.ts
export const invokeArrayFns = (fns) => {
for (let i = 0; i < fns.length; i++) {
fns[i](); // 调用钩子方法
}
}