11.vue3中组件实现原理(中)

1.理解props与组件的被动更新

1.理解组件props传参

再虚拟DOM的层面,组件的props和普调的HTML标签属性差距不大,假设我们的虚拟DOM代码如下

js 复制代码
 const vnode = {
   type: MyComponent,
   props: {
     title: 'A big Title',
     other: this.val
   }
 }

在编写组件时,我们需要显式地指定组件会接收哪些 props 数据,如下面的代码所示:

js 复制代码
const MyComponent = {
   name: 'MyComponent',
   // 组件接收名为 title 的 props,并且该 props 的类型为 String
   props: {
     title: String
   },
   render() {
     return {
     type: 'div',
     children: `count is: ${this.title}` // 访问 props 数据
   }
 }

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

  • 为组件传递的 props 数据,即组件的 vnode.props 对象;
  • 组件选项对象中定义的 props 选项,即 MyComponent.props 对象。

2.再mountComponent中解析props

我们需要结合这两个选项来解析出组件在渲染时需要用到的 props 数据,具体实现如下:

js 复制代码
 function mountComponent(vnode, container, anchor) {
       //省略其他代码
    const componentOptions = vnode.type;
    const { render, data, props: propsOptions } = componentOptions;
    // 调用resolveProps解析出最终的props数据和attrs数据
    const [props, attrs] = resolvePorps(propsOptions, vnode.props)

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

function resolvePorps(options, propsData) {
      const props = {}
      const attrs = {}
      for (const key in propsData) {
          if (key in options) {
              props[key] = propsData[key]
          } else {
              attrs[key] = propsData[key]
          }
      }
      return [props, attrs]
}

再上面代码中,我们最终解析出来需要使用的props和attrs数据。props本质上是父组件的数据,当props发生变化的时候,会触发父组件的重新渲染

3.父组件自更新时,渲染子节点

当响应式的数据title发生变化的时候,父组件会重新渲染执行,当父组件进行子更新的时候。再更新过程中,渲染器发现父组件subTree包含组件类型的虚拟节点 ,所以会调用patchComponent函数完成子组件的更新,如下面的patch函数

js 复制代码
 function patch(n1, n2, container, anchor) {
   if (typeof type === 'object') {
       // vnode.type 的值是选项对象,作为组件来处理
     if (!n1) {
       mountComponent(n2, container, anchor)
     } else {
       // 更新组件
       patchComponent(n1, n2, anchor)
     }
   }
 }

其中,patchComponent函数用来完成子组件的更新,我们吧由父组件子更新引起的子组件更新叫做子组件的被动更新。当子组件发生更新时,我们只需要做以下两个事情

  • 判断子组件是否真的需要更新,因为props可能是不变的
  • 如果需要更新,那就更新子组件的props。slots等内容
js 复制代码
 function patchComponent(n1, n2,) {
      const instance = (n2.component = n1.component)
      //获取当前的props数据
      const { props } = instance;
      //判断porps是否发生了变化
      if (hasPropsChanged(n1.props, n2.props)) {
          const [nextProps] = resolvePorps(n2.type.props, n2.props)
          //更新props
          for (const k in nextProps) {
              props[k] = nextProps[k]
          }
          //删除不存在的props
          for (const k in props) {
              if (!(k in props)) 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]
          if (nextProps[key] !== prevProps[key]) {
              return true
          }
      }
      return false
  }
  • 需要将组件实例添加到新的组件 vnode 对象上,n2.component=n1.component否则下次更新时将无法取 得组件实例;
  • instance.props 对象本身是浅响应的(即 shallowReactive)。因此在更新组件的 props 时,只需要设置 instance.props 对象下的属性值即可触发组件重新渲染。

4.封装渲染上下文

由于 props 数据与组件自身的状态数据都需要暴露到渲染函数中,并使得渲染函数能够通过 this 访问它们,因此我们需要封装一个渲染上下文对象,如下面的代码所示:

js 复制代码
 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(`Attempting to mutate prop "${k}". Props
    are readonly.`)
     } else {
       console.error('不存在')
     }
   }
 })

 // 生命周期函数调用时要绑定渲染上下文对象
 created && created.call(renderContext)
}

再上述代码中,我们创建了一个代理对象,它的意义在于拦截数据状态的读取和设置操作,每当再渲染函数或生命周期钩子中通过this来读取数据,都会优化从组件自身的状态中读取,如果组件本身没有对应的数据,则再从props数据中读取

2.setup函数的作用和实现

1.setup的基础定义

定义:setup主要用于配合组合式API,用于建立组合逻辑,创建响应数据,创建通用函数,注册生命周期钩子等能力。再组件的整个生命周期中,setup函数只会被挂载时执行一次,他的返回值有两种情况

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

js 复制代码
 const Comp = {
   setup() {
   // setup 函数可以返回一个函数,该函数将作为组件的渲染函数
     return () => {
       return { type: 'div', children: 'hello' }
     }
   }
 }

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

js 复制代码
 const Comp = {
   setup() {
     const count = ref(0)
   // 返回一个对象,对象中的数据会暴露到渲染函数中
     return {
       count
     }
   },
   render() {
     // 通过 this 可以访问 setup 暴露出来的响应式数据
     return { type: 'div', children: `count is: ${this.count}` }
   }
 }

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

另外,setup 函数接收两个参数。第一个参数是 props 数据对象,第二个参数也是一对象,通常称为 setupContext,如下面的代码所示:

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

从上面的代码可以看出,我们可以通过 setup 函数的第一个参数 取得外部为组件传递的 props 数据对象。同时,setup 函数还接收第 二个参数 setupContext 对象,其中保存着与组件接口相关的数据和方法,如下所示。

2.setup的基本实现

我们了解setup的基础定义后,那我们就围绕上述能力来尝试实现setup组件选项,如下:

1.从vnode中取出参数,调用setup,setup执行,传入props和setupContext

js 复制代码
 function mountComponent(vnode, container, anchor) {
        const componentOptions = vnode.type;
        let { render, data, setup } = componentOptions;

        const state = data ? reactive(data()) : null; // 处理data选项
        const [props, attrs] = resolvePorps(propsOptions, vnode.props)
        const instance = {
            state,
            props: shallowReactive(props), // 处理props选项
            isMounted: false,
            subTree: null
        }

        const setupContext = { attrs }
        const setupResult = setup(shallowReadonly(instance.props), setupContext) // 处理setup选项
        let setupState = null;
        if(typeof setupResult === 'function'){
            if(render) { console.log('setup函数返回渲染函数,render函数被忽略')}
            render = setupResult
        }else{
            setupState = setupResult
        }

        //省略后面
    }

2.处理setup中的值,支持setup

js 复制代码
    function mountComponent(vnode, container, anchor) {
        //省略前面
        vnode.component = instance
        const renderContext = new Proxy(instance,{
            get(t,k,r){
                const {state,props} = t
                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.warn('不存在该属性')
                }
            },
            set(t,k,v,r){
                const {state,props} = t
                if(state && k in state){
                    state[k] = v
                }else if(k in props){
                    console.warn('不能修改props')
                }else if(setupState && k in setupState){
                    setupState[k] = v
                }else{
                    console.warn('不存在该属性')

                }
            }
        })
    }

3.setup总结

  • 我们通过检测setup函数的返回值类型来决定应该如何处理它,如果它的返回值是函数,则直接将其作为组件的渲染函数。
  • 渲染上下文renderContext应该争取的处理setupState,因为setup函数返回的数据状态也应该暴露到渲染环境
相关推荐
houzhizhen10 分钟前
SQL JOIN 关联条件和 where 条件的异同
前端·数据库·sql
^小桃冰茶4 小时前
CSS知识总结
前端·css
运维@小兵4 小时前
vue注册用户使用v-model实现数据双向绑定
javascript·vue.js·ecmascript
巴巴_羊5 小时前
yarn npm pnpm
前端·npm·node.js
chéng ௹6 小时前
vue2 上传pdf,拖拽盖章,下载图片
前端·css·pdf
嗯.~6 小时前
【无标题】如何在sheel中运行Spark
前端·javascript·c#
A_aspectJ9 小时前
【Bootstrap V4系列】学习入门教程之 组件-输入组(Input group)
前端·css·学习·bootstrap·html
兆。9 小时前
电子商城后台管理平台-Flask Vue项目开发
前端·vue.js·后端·python·flask
互联网搬砖老肖9 小时前
Web 架构之负载均衡全解析
前端·架构·负载均衡
sunbyte10 小时前
Tailwind CSS v4 主题化实践入门(自定义 Theme + 主题模式切换)✨
前端·javascript·css·tailwindcss