Vue.js设计与实现读书笔记十二 第十二章 组件的实现原理

本文深入探讨Vue组件的实现原理,包括组件化的重要性、组件的渲染与更新、组件状态管理、生命周期、props与事件处理、插槽工作模式。通过分析组件的Vnode结构、状态管理、事件调度器、生命周期函数以及插槽的虚拟DOM表示,揭示了Vue组件如何高效地组织和更新页面内容。同时,阐述了setup函数的作用,以及如何在组件中处理props和响应式数据。
摘要由CSDN通过智能技术生成

当页面越来越大,内容越来越多,那么对应的Vnode也会非常多,这时候就需要组件化,组件的好处是可以复用,然后可以将页面拆为多个部分,分别来构建。

12.1 渲染组件

这一节主要说的是:之前path函数中有处理的虚拟节点的类型中没有组件类,有string普通标签,Text文本标签,Fragment多组标签,Comment注释标签,type为Object是组件标签
我们改如何用设计组件的结构呢?组件对象有哪些描述,和拥有什么能力?

组件的本身是对页面内容的封装,它用来描述页面内容的一部分,因此一个组件必须包含一渲染函数,即render函数,有了render函数,渲染器就可以完成组件渲染了

.....
else if (typeof type === 'object' || typeof type === 'function') { // 如果是组件
      // component
      if (!n1) { // 当旧的Vnode不存在的时候
        mountComponent(n2, container, anchor)
      } else {
        patchComponent(n1, n2, anchor)
      }

.....

function MyFuncComp= {
 name:"MyFuncComp",
 data()
 {},
render()
{
  return { type: 'h1', children: props.title }
  }
}
MyFuncComp.props = { // 组件的属性
  title: String
}

const CompVNode = {
  type: MyFuncComp,
  props: {
    title: 'A Big Title'
  }
}
renderer.render(CompVNode, document.querySelector('#app'))

注意区分renderer.render和组件的render,组件的render是返回组件的虚拟DOM

那么这里我们总结一下Vnode中所有节点和子节点类型:

// ①组件类型
{
  type: MyFuncComp,
    props: {
  title: 'A Small Title'
}
}
// ②多个子节点
{
  type: Fragment,
    children:[]
    
}
// ③普通标签类型
{
  type: "xxxx",
    props: {
  title: 'A Small Title'
}
}
// ④文本类型
{
  type: Text,
   
}

// ⑤注释类型
{
  type: Comment,
   
}

// 子节点的类型,
{
  children:null; //没有子节点
  children:"xxx"; // 子节点为文本
  children:[] // 子节点为一组
  
}

由以上的总结我们发现,
①节点类型有5种,而节点的子节点类型有三种,
②其中“多个子节点“” 文本类型,注释类型,都是没有props的,
③多个子节点类型的子节点一定是一个数组,
④子节只有一个节点的时候也是一个数组,而不是一个对象
组件类型没有children子组件,他的Vnode是放在类型的对象中,
还要特别注意:区分Fragment和组件类型
*

12.1 组件状态与自更新

这一章实现组件状态(组件的数)和组件渲染之间的关系
组件对象有一个data函数返回组件所以状态数据对象,而组件的render方法中应该可以用用this访问data中的数据,而data返回的对象也应该是响应式的,因为这是要是实现,当data中的数据改变,组件将重新渲染。但是不应该每次修改data都同步渲染,应该等所有操作结束再渲染一次,所以使用调度器,使用异步队列

// **任务缓存队列,用一个 Set 数据结构来表示,这样就可以自动对任务进行去重**
  const queue = new Set()
  // 一个标志,代表是否正在刷新任务队列
  let isFlushing = false
  //创建一个立即resolve的Promise 实例
  const p= Promise.resolve()
  // 调度器的主要函数,用来将一个任务添加到缓冲队列中,并开始刷新队列
  function queueJob(job){
//将job添加到任务队列queue 中
    queue.add(job)
    //如果还没有开始刷新队列,则刷新之
    if(!isFlushing)
    {
//将该标志设置为true 以避免重复刷新
      isFlushing= true
//在微任务中刷新缓冲队列
      p.then(()=>{
        
        try {
// 执行任务队列中的任务
          queue.forEach(job => job())
        }
        finally {
// 重置状态
          isFlushing = false
          queue.length = 0
        }
        
        })
      }
  }

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

注意:上面使用Set存储副作用函数,会自动去重,就不会收集单一代理对象多个相同副作用函数,不用多次执行,如果不去重,依然还是做不到节省多次渲染的开销

上面这个patch一直未null,我们应该让一个新旧组件对比,使用Diff复用新旧组件的DOM节点

12.3 组件实例与组件的声明周期

这一节讲得是组件实例,组件实例本质上是一个对象,包含与组件有关的状态信息,以及组件的声明周期,数据的响应式等等

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.comoponent = instance //将组件的实例挂到虚拟DOM上

             //在这里调用created 钩子
    created && created.call(state)

    // 组件渲染,放到effect函数中进行追踪,当state数据改执行副作用函数
    effect(() => {
      const subTree = render.call(state, state)
      if (!instance.isMounted) { // 如果是第一次挂载
               //在这里调用beforeMount钩子
        beforeMount && 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
      })
    }

那么组件被挂载玩后 对应的虚拟DOM的结构是:

 const Vnode={
     type:MyComponent,
     props:{}, //
    comoponent:
      {
        state:ractive((MyComponent.data)),// 组件实例的状态响应式数据
        isMounted:true ,// 是否已经被挂载过
        subTree:MyComponent.render(),// 组件旧的虚拟dom
      }

  }

组件的选项对象结构是

 const MyComponent= {

    name:"MyComponent",// 组件的名字
    props:{}, // 选项对象的props
    data() // 组件的状态数据
    {
      return { foo:"xxx"}
    },
    render() //组件的render函数,返回组件的Vnode
    {
      return { type: 'div', children: "文本xxx" }
    },
    created() // 组件的生命周期函数
    {
    }
    //....
  }

注意区分组件的实例,和组件的选项对象,组件的选项对象,存储着组件的声明周期,而组件的实例,存储组件渲染以及组件响应式状态数据,以及被渲染的旧的组件虚拟DOM,
在实际的Vue中组件的生命周期函数选项对象是一个数组,因为有来着mixins的生命周期函数,

这里有一个问题,就是mountComponent是走没有旧Vnode这一个分支的,那么为什么组件会有已近被挂载呢?

else if (typeof type === 'object' || typeof type === 'function') {

      if (!n1) { // 没有旧Vnode的情况下
        mountComponent(n2, container, anchor);// 直接渲染组件
      } else {
        patchComponent(n1, n2, anchor)
      }
    }

12.4 props与组件的被动更新

这一节讲的是如何处理组件的props,对于一个组件来说他有两个Props,
(1)为组件传递props数据,即组件的Vnode.props对象 // 传入的属性
(2)组件选项对象中定义的props选项,即MyComput.props对象 // 组件需要接收的属性
这两个props处理原则是,只有当是需要接收的属性,和传入的属性一样,存储在props中,否则,传入的属性就是普通属性,存储在attrs中 ,也就是说props中是传入的属性,并且组件内也接收的属性,attrs是组件出入的属性,但是组件内部为接收的属性,
props会被使用shallowReactive给变成浅响应式数据,添加到组件实例上,props是父组件的数据,props改变会触发父组件重新渲染
在Vue3中,没有定义在MyComponent.props选项中的props数据将存储到attrs对象中

当父组件的中修改props,父组件的渲染函数会重新执行,渲染器发现父组件的subTree包含组件类型的虚拟节点,会调用patchCompnent函数完成子组件的更新,
由父组件更新引起子组件更新叫做子组件的被动更新
当子组件更新的时候,需要做的是:
(1)检测子组件是否正的需要更新,因为子组件的props可能是不变的
(2)如果需要更新,则更新子组件的props、slots等内容

 function patchComponent(n1, n2, anchor) {
    const instance = (n2.component = n1.component)
    const { props } = instance
    if (hasPropsChanged(n1.props, n2.props)) {
      const [ nextProps, nex9tAttrs ] = resolveProps(n2.type.props, n2.props)
      for (const k in nextProps) {
        props[k] = nextProps[k]
      }
      for (const k in props) {
        if (!(k in nextProps)) delete props[k]
      }
    }
  }

  function 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]
      return nextProps[key] !== prevProps[key]
    }
    return false
  }

这里可以出一面试题:声明周期函数中this读取数据是先读取data和props中的数据?props中的变量可以和data中的变量重名吗?
由上面的代码可以知道,先读取data中的数据没有再读取props中的数据

12.5 setup函数的作用与实现

setup是vue3配合组合式aip,为用户提供一个地方,用于建立组合逻辑,创建响应式数据,创建通用函数,注册生命周期钩子等能力,组件的生命周期中setup只会在组件挂载的时候执行一次

这一章主要将了setup函数的两个不同类型的返回值和参数构成。
①setup返回值是 函数类型,返回的是一个Vnode,他会代替render中的Vnode
②如果是返回的是一个对象,这个对象中的数据也是响应式的,和data,props中的数据一样,都会在声明周期函数的this中被访问的到,他们顺序是data=>props=>stataResult

setup函数有两个参数,第一个是props(浅代理只读),第二个是setupContext,setupContext使用由slots,emit,attrs,expose等构成的对象

12.6 组件事件与emit的实现

emit在setupContext中,也是setup()函数的参数,当我们在setup中执行emit的时候,自定义函数会被编译为on开头的属性,存放在组件的Vnode的Props中,emit实际就是在Props中找这个函数执行他,
注意之前实现的组件的Vnode的props只有,在Vnode.type中的Props也有声明的情况下才会被保留为最终的Props,也就是只有子组件想接收的Props属性才会在最终的Props中,其他的都会在attrs中,但是自定义事件这里,默认让他在最终的Props,也就是说事件属性,不会被Props过滤

12.7插槽的工作原理与实现

先观察一下Vue的插槽是怎么用的

<FancyButton>
  Click me! <!-- 插槽内容 -->
  <template #header>

</template>

<span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

模板

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

插槽分为默认插槽,有名插槽
上面没有使用template 的都会收集到默认插槽,插槽中的内容可以是普通的html标签,也可以是组件
那么这样的组件的的Vnode是怎样的呢?

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

12.1中说组件的Vnode没有children是错的,组件的children,就是插槽的Vnode,是对象
插槽就Vnode.children 他会被添加到组件对象实例中,并且也会放到声明周期函数created中,当声明周期函数created中this.$slots可以就会访问到组件children上的所有插槽的Vnode,
注意solts是不能在created函数中设置,而props和data,setupResult的值都可以修改

12.8 注册生命周期

这一节讲的是在setup()中写声明周期函数,将这些声明周期函数注册的组件上中,每个组件都有自己的setup和自己的生命周期函数,如何让每个函数能正确的注册到自己的组件上?
注意onMounted是全局函数,当在setup中执行的时候,其实是将函数传递到了组件的实例对象上

let currentInstance = null// 存储当前需要挂载的组件的实例对象

function onMounted(fn) {
  if (currentInstance) {
    currentInstance.mounted.push(fn)
  }
}




  // 将出入的Props分类
  function resolveProps(options, propsData) {
    const props = {}
    const attrs = {}
    for (const key in propsData) {
      if ((options && key in options) || key.startsWith('on')) {
        props[key] = propsData[key]
      } else {
        attrs[key] = propsData[key]
      }
    }

    return [ props, attrs ]
  }

  function mountComponent(vnode, container, anchor) {
    const isFunctional = typeof vnode.type === 'function' //虚拟组件的类型是不是函数
    let componentOptions = vnode.type
    if (isFunctional) { //如果是函数,返回的就是Vnode,依然变成对象类型
      componentOptions = {
        render: vnode.type, // 返回虚拟DOM
        props: vnode.type.props // 构建想要从父组件获得的props
      }
    }
    let { render, data, setup, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, props: propsOption } = componentOptions

    beforeCreate && beforeCreate() // 声明周期

    const state = data ? reactive(data()) : null // 组件的data数据变成响应式数据
    const [props, attrs] = resolveProps(propsOption, vnode.props) // 将想要获得的Props和传入的做一个分类

    const slots = vnode.children || {} // 获取组件的插槽的虚拟DOM

    const instance = { // 组件实例
      state,    // 组件data的睡觉
      props: shallowReactive(props), // 最终想要传入的数据+包含事件
      isMounted: false,
      subTree: null,
      slots,
      mounted: []
    }

    // event事件名称,payload事件传的参数
    function emit(event, ...payload) {
      const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
      const handler = instance.props[eventName];// 到最终的Props中的的属性中中
      if (handler) {
        handler(...payload) // 执行父组件的函数
      } else {
        console.error('事件不存在')
      }
    }

    // setup
    let setupState = null
    if (setup) { //如果使用setup
      const setupContext = { attrs, emit, slots } // setup的参数
      
      const prevInstance = setCurrentInstance(instance) ;//将当前实例存给全局变量currentInstance ,返回以前的实例
      
      const setupResult = setup(shallowReadonly(instance.props), setupContext);//执行setup
     
      setCurrentInstance(prevInstance);// 旧的变新的,新的变旧的
      
      if (typeof setupResult === 'function') { //如果返回的结果是函数就是返回的虚拟DOM
        if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')
        render = setupResult
      } else {   // 返回的是一个对象
        setupState =setupResult
      }
    }

    vnode.component = instance

    const renderContext = new Proxy(instance, {
      get(t, k, r) {
        const { state, props, slots } = t

        if (k === '$slots') return slots

        if (state && k in state) {
          return state[k]
        } else if (k in props) {
          return props[k]
        } else if (setupState && k in setupState) {
          return setupState[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) {
          props[k] = v
        } else if (setupState && k in setupState) {
          setupState[k] = v
        } else {
          console.error('不存在')
        }
      }
    })

    // created
    created && created.call(renderContext);// 这个声明周期中可以读取,组件的插槽的虚拟DOM,也可以读取data,props,setup中返回的响应式数据


    effect(() => {
      const subTree = render.call(renderContext, renderContext)
      if (!instance.isMounted) {  
        beforeMount && beforeMount.call(renderContext)
        patch(null, subTree, container, anchor)
        instance.isMounted = true
        
        mounted && mounted.call(renderContext)
        
        instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))// 执行setup中定义的mounted函数
      } else {
        beforeUpdate && beforeUpdate.call(renderContext)
        patch(instance.subTree, subTree, container, anchor)
        updated && updated.call(renderContext)
      }
      instance.subTree = subTree
    }, {
      scheduler: queueJob
    })
  }

第十二章 组件的实现原理
(二十八)组件的挂载和更新为啥要单独处理?mountComponent的作用是?中的调度器是什么原理?
(1)因为组件的Vnode type是函数或者对象类型,不能直接获取props和children,因为他的Vnode存在于type值中,要用mountComponent和patchComponent单独处理。
(2)当没有旧Vnode的时候调用moutComponent渲染组件,他获取组件的render函数,data函数,获得Vnode和data,执行path进行渲染,Vnode的this执行data返回的对象
(3)mountComponent使用effct来代理,data会被代理,path会被代理,当data中的数据改变,会立即触发执行path,如果是连续更改data中的数据,则会造成性能问题,所以可以用scheduler来缓冲,等所有的数据改变在重执行path一次,
(二十九)组件的生命周期中更新是什么?为啥要在mountComponent中用instance.isMount来判断是不是要使用旧的Vnode?按理说mountComponent说明旧Vnode不存在啊?
(1)在初始化data中的数据之前就执行beforeCreate函数,之后执行created函数
执行完path之后执行mounted函数,当发现组件的实例的instance.isMount是以挂载后执行path后执行updated
(2)我想是虽然就Vnode不存在,但是如果instance.isMount以挂载,说明instance.subTree是存在的,这样比重新渲染要不耗费性能,主要要搞清楚组件的更新是在干什么,什么情况下会更新

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值