12.组件的实现原理

将一个大的页面分为多个组件,每个组件都可以完成独立的功能并且可以被复用。

1.渲染组件

从用户角度,是一个描述对象

const myComponent1 = {
    name:'myComponent1',
    data(){
        return {name:'wjt'}
    }
}

从渲染器角度,是一个特殊的虚拟DOM

const myComponent2 = {
    // type:Fragment,   //描述片段
    type:Text,  //描述节点
    props:{

    }
}

在patch函数中定义对应策略

function patch(n1,n2,container,anchor){
    if(n1 && n1.type !== n2.type){
        unmount(n1)
        n1 = null
    }

    const {type} = n2

    if(typeof type === 'string'){

    }else if(type === Text){
       //作为文本元素处理
    }else if(type === Fragment){
       //作为片段处理
    }

    if(!n1){
        //挂载组件
        mountComponent(n2,container,anchor)
    }else{
        //更新组件
        patchComponent(n1,n2,anchor)
    }
}

模拟一个组件,创建一个用来描述组件的对象,并且使用渲染器进行渲染

//组件
const myComponent = {
    name:'myComponent',
    render(){
        return {
            type:'div',
            children:'我是文本内容'
        }
    }
}

//描述组件的vNode对象,type属性值作为组件的选项对象
const CompVNode = {
    type:myComponent
}
renderer.render(CompVNode,document.querySelector('#app'))

//渲染组件的功能函数
function mountComponent(vnode,container,anchor){
 //通过vnode获取组件的选项对象,即vnode.type
 const componentOptions = vnode.type
 //获取组件的渲染函数render
 const {render} = componentOptions
 //执行渲染函数,获取组件要渲染的内容,即render函数返回的虚拟DOM
 const subTree = render()
 //最后调用patch函数来挂载组件所描述的内容,即subTree
 patch(null,subTree,container,anchor)
}

2.组件状态与自更新

模拟组件

const myComponent = {
    name:'myComponent',
    data(){
        return{
            foo:'hello world'
        }
    },
    render(){
        return{
            type:'div',
            children:`foo的值是${this.foo}`
        }
    }
}

挂载组件函数

function mountComponent(vnode,container,anchor){
  const componentOptions = vnode.type
  const {render,data} = componentOptions

  //调用data函数得到原始数据,调用reactive函数将其包装为响应式数据
  const state = reactive(data())
  //调用render函数时,将this设置为state,所以render函数内部可以通过this访问组件自身状态数据
  const subTree = render.call(state,state)
  patch(null,subTree,container,anchor)
}

上面的代码完成了两件事:

1.执行data函数,对执行结果使用reactive函数进行包装,将组件的data数据变成响应式数据state

2.调用render函数时,将this指向state

当组件自身状态发生变化时,需要去触发组件的更新,所以需要调用effect函数

function mountComponent(vnode,container,anchor){
    const componentOptions = vnode.type
    const {render,data} = componentOptions
  
    const state = reactive(data())
  
    //调用effect函数,将函数注册响应式
    effect(()=>{
        const subTree = render.call(state,state)
        patch(null,subTree,container,anchor)
    })

  }

一旦自身的响应式数据发生变化,组件就会自动执行渲染函数,从而完成更新。但是,由于effect的执行是同步的,因此当响应式数据发生变化时,与之关联的副作用函数会同步执行。

如果多次修改响应式数据的值,将会导致渲染函数多次执行,这是影响性能的。所以需要一个机制,以使得无论对响应式数据进行多少次修改,副作用函数都只会执行一次。

实现一个调度器,当副作用函数重新执行时,不会立即执行,而是缓冲到一个微任务队列中,等到执行栈清空后,再将它从微任务队列中取出并执行。有了缓存机制,就可以对任务进行去重,从而避免了多次执行。

  //任务缓存队列,用一个Set数据结构来表示,这样就可以对任务进行去重
  const queue = new Set()
  //一个标志,代表是否正在刷新任务队列
  let isFlushing = false
  //创建一个立即resolve的Promise实例
  const p = Promise.resolve()

  //调度器的主要函数,用来将一个任务添加到缓冲队列中,并开始刷新队列
  function queueJob(job){
   //将job添加到任务队列queue中
   queue.add(obj)
   //如果还没有开始刷新队列,则刷新之
   if(!isFlushing){
    //将该标志设置为true以避免重复刷新
    isFlushing = true
    //在微任务中刷新缓冲队列
    p.then(()=>{
        try{
            //执行队列中的任务
            queue.forEach(job=>job())
        } finally{
            //重置状态
            isFlushing = false
            queue.clear = 0
        }
    })
   }
  }

调度器本质上是利用了微任务的异步执行机制,实现了对副作用函数的缓冲。

当响应式数据发生变化,副作用函数不会立即同步执行,而是会被queueJob函数调度,最后在一个微任务中执行。

  function mountComponent(vnode,container,anchor){
      const componentOptions = vnode.type
      const {render,data} = componentOptions

      const state = reactive(data())
      effect(()=>{
        const subTree = render.call(state,state)
        patch(null,subTree,container,anchor)
      },{
        //指定该副作用函数的调度器为queueJob即可
        scheduler:queueJob
      })
  }

不过上面diamante也有缺陷:在effect函数内调用patch函数完成渲染时,第一个参数总是null。这意味着,每次更新都会进行全新的挂载,而不会打补丁,这是不正确的。正确的做法是:每次更新时,都拿subTree与上一次组件所渲染的subTree进行打补丁。所有需要实现组件实例,用来维护整个组件的生命周期状态,这样渲染器才能在正确的实际执行合适的操作。

3.组件实例与组件的生命周期

组件的实例本质上就是一个状态集合(对象),维护着组件运行过程中的所有信息。

function mountComponent(vnode,container,anchor){
   
    const componentOptions = vnode.type
    const {render,data} = componentOptions

    const state = reactive(data())
    //定义组件实例,一个组件实例本质上就是一个对象,包含了与组件相关的所有状态信息
    const instance = {
        //组件自身的状态数据,即data
        state,
        //表示组件是否被挂载,初始值为false
        isMounted:false,
        //组件所渲染的内容
        subTree:null
    }

    //将组件实例设置到vnode上,用于后续更新
    vnode.component = instance

    effect(()=>{
        //调用组件的渲染函数,获得子树
        const subTree = render.call(state,state)
        //检查组件是否已经被挂载
        if(!instance.isMounted){
            //初次挂载,调用patch函数第一个参数传递null
            patch(null,subTree,container,anchor)
            //终重点:将组件实例的isMounted设置为true,这样当更新发生时就不会再次进行挂载操作,而是会执行更新
            instance.isMounted = true
        }else{
            //当isMounted为true时,说明组件已经被挂载,只需要完成自更新即可
            //所以在调用patch函数时,第一个参数为组件上一次渲染的子树
            //需要使用新的子节点树对上一次渲染的子节点树进行更换补丁
            patch(instance.subTree,subTree,container,anchor)
        }
        //更新组件实例的子树
        instance.subTree = subTree
    },{scheduler:queueJob})
}

组件实例的instance.isMounted可以区分组件的挂载和更新。所以可以在适当的时机调用组件对应的生命周期钩子

function mountComponent(vnode,container,anchor){
    const componentOptions = vnode.type
    //从组件选项对象中取得组件的生命周期函数
    const { render,data,beforeCreate,created,beforeMount,mounted,beforeUpdate,updated }  = componentOptions

    //调用beforeCreate钩子
    beforeCreate && beforeCreate()

    const state = reactive(data())
    const instance = {
        state,
        isMounted:false,
        subTree:null
    }
    vnode.component = instance
    //在这里调用created钩子
    created && created.call(state)

    effect(()=>{
        const subTree = render.call(state,state)
        if(!instance.isMounted){
            //在这里调用beforeMount钩子
            baforeMount && beforeMount.call(state)
            patch(null,subTree,container,anchor)
            instance.isMounted = true
            //在这里调用mounted钩子
            mounted && mounted.call(state)
        }else{
            //在这里调用beforeUpdate钩子
            beforeUpdate && beforeUpdate.call(state)
            patch(instance.subTree,subTree,container,anchor)
            //在这里调用updated钩子
            updated && updated.call(state)
        }
        instance.subTree = subTree
    },{scheduler:queueJob})
}

4.props与组件的被动更新

在虚拟DOM层面,组件的props与普通HTML标签的属性差别不不大。

假设有如下模板

<MyComponent title="A Big Title" :other="val">MyComponent>

对应的虚拟DOM是

const vnode = {
    type:myComponent,
    props:{
        title:'A big Title',
        other:this.val
    }
}

对应的组件描述

const MyComponent = {
    name:'MyComponent',
    //组件接受名为title的props,并且该props的类型为String
    props:{
        title:String
    },
    render(){
        return {
            type:div,
            children:`props data`  //访问props数据
        }
    }
}

对于一个组件来说,有两部分关于props的内容需要关心

为组件传递的props数据,即组件的vnode.props对象

组件选项中定义的props选项,即MyComponent.props对象

function mountComponent(vnode,container,anchor){
    const componentOptions = vnode.type
    //从组件选项对象中取出props定义,即propsOption
    const {render,data,props:propsOption,beforeCreate/*其他属性*/} = componentOptions
    beforeCreate && beforeCreate()
    const state = reactive(data())
    //调用resolveProps函数解析出最终的props数据与attrs数据
    const [props,attrs] = resolveProps(propsOption,vnode.props)

    const instance = {
        state,
        //将解析出的props数据包装为shallowReactive并定义到组件实例上
        props:shallowReactive(props), 
        isMounted:false,
        subTree:null
    }
    vnode.component = instance
    //省略部分代码

}

//resolveProps函数用于解析组件props和attrs数据
function resolveProps(options,propsData){
    const props = {}
    const attrs = {}
    for(const key in propsData){
        if(key in options){
            //如果为组件传递的props数据在组件自身的props选项中有定义,则将其视为合法的props
            props[key] = propsData[key]
        }else{
            //否则将其视为attrs
            attrs[key] = propsData[key]
        }
    }
    //最后返回props与attrs数据
    return [props,attrs]
}

没有定义在MyComponent.props选项的props数据将存储到attrs对象中

处理完props数据后,需要关注props数据变化的问题

props的值发生变化时,父组件的渲染函数会重新执行,也会产生新的虚拟DOM。

接下来,组件会进行自更新。在更新过程中,渲染器发现父组件的subTree包含组件类型的虚拟节点,所以会调用patchComponent函数完成子组件的更新

function patch(n1,n2,container,anchor){
    if(n1 && n1.type !== n2.type){
        unmount(n1)
        n1 = null
    }
    const {type} = n2
    if(typeof type === 'string'){
        //文本类型节点
    }else if(type === Fragment){
        //Fragment类型节点
    }else if(typeof type === 'object'){
        //vnode.type的值是选项对象,作为组件来处理
        if(!n1){
            mountComponent(n2,container,anchor)
        }else{
            //更新组件
            patchComponent(n1,n2,anchor)
        }
    }
}

因为父组件的更新导致子组件的更新称之为子组件的被动更新,当子组件被动更新时,需要做的是:

1.检测子组件是否真的需要更新,因为子组件的props可能是不变的。

2.如果需要更新,更新子组件的props及solts等

function patchComponent(n1,n2,anchor){
    //获取组件实例,即n1.component,同时让新的组件虚拟节点n2.component也指向组件实例
    const instance = (n2.component = n1.component)
    //获取当前的props数据
    const {props} = instance
    //调用hasPropsChanged检测为子组件传递的props是否发生变化,如果没有变化,则不需要更新
    if(hasPropsChanged(n1.props,n2.props)){
        //调用resolveProps函数重新获取props数据
        const [nextProps] = resolveProps(n2.type.props,n2.props)
        //更新props
        for(const k in nextProps){
            props[k] = nextProps[k]
        }
        //删除不存在的props
        for(const k in props){
            if(!(k in nextProps)){
                delete props[k]
            }
        }
    }
}
//长度不同或者属性键值对不上,就是发生了变化
function hasPropsChanged(prevProps,nextProps){
  const nextKeys = Object.keys(nextProps)
  //如果新旧props的数量变了,则说明有变化
  if(nextKeys.length !== Object.keys(prevProps.length)){
    return true
  }
  for(let i = 0;i<nextKeys.length;i++){
    const key = nextKeys[i]
    //有不相等的props,则说明有变化
    if(nextProps[key] !== prevProps[key]){
        return true
    }
  }
  return false
}

由于props数据和组件自身的状态数据都是需要暴露到渲染函数中,并使得渲染函数能够通过this访问,因此需要封装一个渲染上下文对象

function mountComponent(vnode,container,anchor){
    //省略部分代码
    const instance = {
        state,
        props:shallowReactive(props),
        isMounted:false,
        subTree:null
    }

    vnode.component = instance

    //创建渲染上下文对象,本质上是组件实例的代理
    const renderContext = new Proxy(instance,{
        get(t,k,r){
            //取得组件自身状态与props数据
            const {state,props} = t
            //先尝试读取自身状态数据
            if(state && k in state){
                return state[k]
            }else if(k in props){
                //如果自身没有该数据,从props中读取
                return props[k]
            }else{
                console.error('该属性不存在')
            }
        },
        set(t,k,v,r){
            const { state,props} = t
            if(state && k in state){
                state[k] = v
            }else if(k in props){
               console.warn('该属性为只读')
            }else{
                console.error('不存在')
            }
        }
    })
}

5.setup函数的作用与实现

setup主要是配合组合式api,用于创建各种组合逻辑,响应式数据,函数,注册生命周期等。只会在挂载的时候执行一次,返回值有两种:

1.返回一个函数,该函数将作为组件的render函数

//这种方式通常以模板来表达其渲染内容的情况。如果组件以模板来表达渲染内容,那么setup函数不可以再返回函数,否则会与模板编译生成的渲染函数产生冲突
const Comp = {
    setup(){
        //setup函数可以返回一个函数,该函数将作为组件的渲染函数
        return (()=>{
            return {type:'div',children:'hello'}
        })
    }
}

2.返回一个对象,该对象包含的的数据将暴露给模板使用

const Comp = {
    setup(){
      const count = ref(0)
      return {
        count
      }
    },
    render(){
        return {type:'div',children:`count is ${this.count}`}
    }
}

可以看到,setup函数暴露的数据可以在渲染函数中通过this来访问

另外,setup函数接两个参数。第一个参数是props数据对象,第二个参数也是一个对象

const Comp = {
    props:{
        foo:String
    },
    setup(props,setupContext){
        props.foo  //访问传入props数据
        //setupContext中包含与组件接口有关的重要数据
        const {solts,emit,attrs,expose} = setupContext
        //...
    }
}

实现setup组件选项

function mountComponent(vnode,container,anchor){
    //从组件的vnode获取对应的配置
    const componentOptions = vnode.type
    //从组件选项中取出setup函数
    let {render,data,setup,/**其他选项*/} = componentOptions
    //执行组件创建前生命周期钩子
    beforeCreate && beforeCreate()
    //state为经过响应式注册的响应式数据
    const state = data?reactive(data()):null
    //获取组件中的props和attrs等属性
    const [props,attrs] = resolveProps(propsOption,vnode.props)

    //一个组件的综合描述对象,包含组件所有的信息和状态
    const instance = {
        state,
        props:shallowReactive(props),
        isMounted:false,
        subTree:null
    }

    //setupContext,因为目前还没涉及到emit和slots,暂时只把attrs放进去
    const setupContext = {attrs}
    //调用setup函数,将只读版本的props作为第一个参数传递,避免用户意外地修改props值
    //将setupContext作为第二个参数传递
    const setupResult = setup(shallowReadonly(instance.props),setupContext)
    //setupState用来存储由setup返回的数据
    let setupState = null
    //如果setup函数的返回值是函数,则将其作为渲染函数
    if(typeof setupResult === 'function'){
        //存在歧义,报告冲突
        if(render){
            console.error('setup返回的是渲染函数,render选项会被忽略')
        }
        render = setupResult
    }else{
        //如果setup的返回值不是函数,则作为数据状态赋值给setupState
        setupState = setupResult
    }
    //将组件的描述信息对象赋值给vnode中component属性
    vnode.component = instance
    //使用proxy使组件各种状态的读取和修改都可以监测到
    //renderContext也就是我们完成的渲染上下文
    const renderContext = new Proxy(instance,{
        get(target,key,result){
            const {state,props} = target
            if(state && key in state){
                return state[key]
            }else if(key in props){
                return props[key]
            }else if(setupState && key in setupState){
                //渲染上下文需要增加对setupState的支持
                return setupState[key]
            }else{
                console.error('不存在')
            }
        },
        set(target,key,value,result){
            const {state,props} = target
            if(state && key in state){
                state[key] = value
            }else if(key in props){
                console.warn('props的属性是只读')
            }else if(setupState && key in setupState){
                //渲染上下文需要增加对setupState的支持
                setupState[key] = value
            }else{
                console.error('不存在')
            }
        }
    })
}

上面是setup函数的最小实现,需要注意

1.setupState是一个对象,目前只存在attrs

2.根据setup函数的返回值类型决定如何处理。如果返回值为函数,直接作为组件的渲染函数。这里为了避免歧义,需要先检查组件选项中是否已经存在render选项,如果存在,需要打印警告信息

3.渲染上下文renderContext应该正确的处理setupState,因为setup函数返回的数据状态也应该暴露到渲染环境

6.组件事件与emit的实现

emit 用来触发组件的自定义事件

 const MyComponent = {
   name: 'MyComponent',
   setup(props, { emit }) {
     // 发射 change 事件,并传递给事件处理函数两个参数
     emit('change', 1, 2)

     return () => {
       return // ...
     }
   }
 }

使用该组件时,可以监听由emit函数发射的自定义事件

<MyComponent @change="handler" />

上面的模板对应的虚拟DOM为

 const CompVNode = {
   type: MyComponent,
   props: {
      onChange: handler
   }
 }

自定义事件change被编译名字为onChange的属性,并且存储在props数据对象中。这实际上是一种约定。

在具体的实现上,触发自定义事件的本质就是根据事件名称去props数据对象中寻找对应的事件处理函数并执行,如下

 function mountComponent(vnode, container, anchor) {
   // 省略部分代码

   //组件信息状态集合
   const instance = {
     state,
     props: shallowReactive(props),
     isMounted: false,
     subTree: null
   }

   // 定义 emit 函数,它接收两个参数
   // event: 事件名称
   // payload: 传递给事件处理函数的参数
   function emit(event, ...payload) {
     // 根据约定对事件名称进行处理,例如 change --> onChange
     const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
     // 根据处理后的事件名称去 props 中寻找对应的事件处理函数
     const handler = instance.props[eventName]
     if (handler) {
       // 调用事件处理函数并传递参数
       handler(...payload)
     } else {
       console.error('事件不存在')
     }
   }

   // 将 emit 函数添加到 setupContext 中,用户可以通过 setupContext 取得 emit 函数
   const setupContext = { attrs, emit }

   // 省略部分代码
 }

实现一个emit函数,并且将其添加到setupContext对象中,这样用户就可以通过setupContext取得emit函数了。会根据预订对事件名称进行转换,以便能够在props数据对象中找到对应的事件处理函数。

最后,调用事件处理函数并透传参数即可。

另外需要注意的是,没有任何显示的声明为props的属性都会存储到attrs中,换句话说,任何事件类型的props,即onXXX属性,都不会出现在props中。这导致我们无法根据事件名称在instance.props中找到对应的事件处理函数。

为了解决这个问题,需要在解析props数据的时候对事件类型的props做特殊处理,如下

 function resolveProps(options, propsData) {
   const props = {}
   const attrs = {}
   for (const key in propsData) {
     // 以字符串 on 开头的 props,无论是否显式地声明,都将其添加到 props 数据中,而不是添加到 attrs 中
     if (key in options || key.startsWith('on')) {
       props[key] = propsData[key]
     } else {
       attrs[key] = propsData[key]
     }
   }

   return [ props, attrs ]
 }

7.插槽的工作原理和实现

模板中使用插槽

MyComponent组件:

  <template>
   <header><slot name="header" /></header>
   <div>
     <slot name="body" />
   </div>
   <footer><slot name="footer" /></footer>
 </template>

父组件使用MyComponent组件:

   <MyComponent>
   <template #header>
     <h1>我是标题</h1>
   </template>
   <template #body>
     <section>我是内容</section>
   </template>
   <template #footer>
     <p>我是注脚</p>
   </template>
 </MyComponent>

父组件的模板会被编译成如下的渲染函数

 // 父组件的渲染函数
 function render() {
   return {
     type: MyComponent,
     // 组件的 children 会被编译成一个对象
     children: {
       header() {
         return { type: 'h1', children: '我是标题' }
       },
       body() {
         return { type: 'section', children: '我是内容' }
       },
       footer() {
         return { type: 'p', children: '我是注脚' }
       }
     }
   }
 }

组件模板中的插槽内容会被编译成为插槽函数,而插槽函数的返回值就是具体的插槽内容,组件MyComponent的模板会被编译成如下渲染函数

 // MyComponent 组件模板的编译结果
 function render() {
   return [
     {
       type: 'header',
       children: [this.$slots.header()]
     },
     {
       type: 'body',
       children: [this.$slots.body()]
     },
     {
       type: 'footer',
       children: [this.$slots.footer()]
     }
   ]
 }

渲染插槽内容的过程,就是调用插槽函数并渲染由其返回的内容的过程

在运行时的实现上,插槽则依赖于setupContext中的slots对象

 function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  // 直接使用编译好的 vnode.children 对象作为 slots 对象即可
  const slots = vnode.children || {}
  // 将 slots 对象添加到 setupContext 中
  const setupContext = { attrs, emit, slots }

 }

最基本的slots的实现非常简单。只需要将vnode.children作为slots对象,然后将slots对象添加到setupContext对象中。为了在render函数内和生命周期钩子函数内能够通过this.$slots来访问插槽内容,还需要在renderContext中特殊对待slots属性

 function mountComponent(vnode, container, anchor) {
   // 省略部分代码

   const slots = vnode.children || {}

   const instance = {
     state,
     props: shallowReactive(props),
     isMounted: false,
     subTree: null,
     // 将插槽添加到组件实例上
     slots
   }

   // 省略部分代码

   //渲染上下文
   const renderContext = new Proxy(instance, {
     get(target, key, result) {
       const { state, props, slots } = target
       // 当 key 的值为 $slots 时,直接返回组件实例上的 slots
       if (key === '$slots') return slots

       // 省略部分代码
     },
     set (target, key, value, result) {
       // 省略部分代码
     }
   })

   // 省略部分代码
 }

//对渲染上下文renderContext代理对象的get拦截函数做了特殊处理,当读取的键是 $slots时,直接返回组件实例上的slots对象,这样就可以通过this.$slots来访问插槽内容了

8.注册生命周期

在vue中,部分组合API是用来注册生命周期钩子函数的,例如

 import { onMounted } from 'vue'

 const MyComponent = {
   setup() {
     onMounted(() => {
       console.log('mounted 1')
     })
     // 可以注册多个
     onMounted(() => {
       console.log('mounted 2')
     })

     // ...
   }
 }

在setup函数中,调用onMounted函数即可注册mounted钩子函数,并且可以多次调用onMounted函数来注册多个钩子函数,这些函数会在组件被挂载之后再执行。

问题在于,在A组件的setup函数中调用onMounted函数会将该钩子函数注册到A组件上;在B组件调用就注册到B组件上

方法:实现一个变量currentInstance,用来存储当前的组件实例,每次当初始化组件并执行组件的setup函数之前,先将currentInstance设置为当前组件实例,再执行组件的setup函数,这样就可以通过currentInstance来获取当前正在被初始化的组件实例,从而将那些通过onMounted函数注册的钩子函数与组件实例进行关联。

全局变量,存储当前正在被初始化的组件实例

 let currentInstance = null
 // 该方法接收组件实例作为参数,并将该实例设置为 currentInstance
 function setCurrentInstance(instance) {
   currentInstance = instance
 }

有了变量和修改变量的方法,就可以修改mountComponent函数了

function mountComponent(vnode, container, anchor) {
   // 省略部分代码

   const instance = {
     state,
     props: shallowReactive(props),
     isMounted: false,
     subTree: null,
     slots,
     // 在组件实例中添加 mounted 数组,用来存储通过 onMounted 函数注册的生命周期钩子函数
     mounted: []
   }

   // 省略部分代码

   // setup
   const setupContext = { attrs, emit, slots }

   // 在调用 setup 函数之前,设置当前组件实例
   setCurrentInstance(instance)
   // 执行 setup 函数
   const setupResult = setup(shallowReadonly(instance.props), setupContext)
   // 在 setup 函数执行完毕之后,重置当前组件实例
   setCurrentInstance(null)

   // 省略部分代码
 }

以onMounted为例,为了存储由onMounted函数注册的生命周期钩子,需要在组件实例对象上添加instance.mounted数组,因为可以多次调用onMounted函数用来注册不同的生命周期函数,这些生命周期函数都会存储在instance.mounted数组中

 //实现onMounted函数
 function onMounted(fn) {
   if (currentInstance) {
     // 将生命周期函数添加到 instance.mounted 数组中
     currentInstance.mounted.push(fn)
   } else {
     console.error('onMounted 函数只能在 setup 中调用')
   }
 }

通过currentInstance获取当前组件实例,并将生命周期钩子函数添加到当前实例对象的instance.mounted数组中即可。如果当前实例不存在,提示报错。

最后,在合适的时机调用这些注册到instance.mounted数组中的生命周期钩子函数

  function mountComponent(vnode, container, anchor) {
     // 省略部分代码

     effect(() => {
       const subTree = render.call(renderContext, renderContext)
       if (!instance.isMounted) {
         // 省略部分代码

         // 遍历 instance.mounted 数组并逐个执行即可
         instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))
       } else {
         // 省略部分代码
       }
       instance.subTree = subTree
     }, {
       scheduler: queueJob
     })
   }


其余生命周期函数如onUpdate等同理

总结

12.组件的实现原理_第1张图片

12.组件的实现原理_第2张图片

你可能感兴趣的:(vue3,前端,javascript,vue.js)