百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

vue3源码分析——实现props,emit,事件处理等

bigegpt 2024-08-29 11:25 1 浏览


引言

<<往期回顾>>

  1. vue3源码分析——rollup打包monorepo
  2. vue3源码分析——实现组件的挂载流程

本期来实现,setup里面使用props,父子组件通信props和emit等,所有的源码请查看

本期的内容与上一期的代码具有联动性,所以需要明白本期的内容,最后是先看下上期的内容哦!

实现render中的this

在render函数中,可以通过this,来访问setup返回的内容,还可以访问this.$el等

测试用例

由于是测试dom,jest需要提前注入下面的内容,让document里面有app节点,下面测试用例类似在html中定义一个app节点哦


let appElement: Element;

  beforeEach(() => {
    appElement = document.createElement('div');
    appElement.id = 'app';
    document.body.appendChild(appElement);
  });

  afterEach(() => {
    document.body.innerHTML = '';
  })
复制代码

本功能的测试用例正式开始

test('实现代理对象,通过this来访问', () => {
   let that;
    const app = createApp({
      render() {
      // 在这里可以通过this来访问
        that = this;
        return h('div', { class: 'container' }, this.name);
      },
      setup() {
        return {
          name: '123'
        }
      }
    });
    const appDoc = document.querySelector('#app')
    app.mount(appDoc);
    // 绑定值后的html
    expect(document.body.innerHTML).toBe('<div id="app"><div class="container">123</div></div>');
    
     const elDom = document.querySelector('#container')
    // el就是当前组件的真实dom
    expect(that.$el).toBe(elDom);
  })
复制代码

分析

上面的测试用例

  1. setup返回是对象的时候,绑定到render的this上面
  2. $el则是获取的是当前组件的真实dom

解决这两个需求:

  1. 需要在render调用的时候,改变当前函数的this指向,但是需要思考的一个问题是:this是啥,它既要存在setup,也要存在el,咋们是不是可以用一个proxy来绑定呢?在哪里创建呢 可以在处理组件状态setupStatefulComponent来完成改操作
  2. el则是在mountElement中挂载真实dom的时候,把当前的真实dom绑定在vnode当中

编码

针对上面的分析,需要在setupStatefulComponent中来创建proxy并且绑定到instance当中,并且setup的执行结果如果是对象,也已经存在instance中了,可以通过instance.setupState来进行获取

function setupStatefulComponent(instance: any) {
 instance.proxy = new Proxy({}, {
     get(target, key){
       // 判断当前的key是否存在于instance.setupState当中
       if(key in instance.setupState){
         return instance.setupState[key]
       }
     }
 })
 // ...省略其他
}
// 然后在setupRenderEffect调用render的时候,改变当前的this执行,执行为instance.proxy

function setupRenderEffect(instance: any, vnode: any, container: any) {
  // 获取到vnode的子组件,传入proxy进去
  const { proxy } = instance

  const subtree = instance.render.call(proxy)
  // ...省略其他
}
复制代码

通过上面的操作,从render中this.xxx获取setup返回对象的内容就ok了,接下来处理el

需要在mountElement中,创建节点的时候,在vnode中绑定下,el,并且在setupStatefulComponent 中的代理对象中判断当前的key

// 代理对象进行修改
 instance.proxy = new Proxy({}, {
     get(target, key){
       // 判断当前的key是否存在于instance.setupState当中
       if(key in instance.setupState){
         return instance.setupState[key]
       }else if(key === '$el'){
           return instance.vnode.el
       }
     }
 })
 
 // mount中需要在vnode中绑定el
 
 function mountElement(vnode: any, container: any) {
  // 创建元素
  const el = document.createElement(vnode.type)
  // 设置vnode的el
  vnode.el = el
  
  //…… 省略其他
  }
复制代码

看似没有问题吧,但是实际上是有问题的,请仔细思考一下,mountElement是不是比setupStatefulComponent 后执行,setupStatefulComponent执行的时候,vnode.el不存在,后续mountelement的时候,vnode就会有值,那么上面的测试用例肯定是报错的,$el为null

解决这个问题的关键,mountElement的加载顺序是 render -> patch -> mountElement,并且render函数返回的subtree是一个vnode,改vnode中上面是mount的时候,已经赋值好了el,所以在patch后执行下操作


function setupRenderEffect(instance: any, vnode: any, container: any) {
  // 获取到vnode的子组件,传入proxy进去
  const { proxy } = instance

  const subtree = instance.render.call(proxy)
  
  patch(subtree, container)
 // 赋值vnode.el,上面执行render的时候,vnode.el是null
  vnode.el = subtree.el
}
复制代码

至此,上面的测试用例就能ok通过啦!

实现on+Event注册事件

在vue中,可以使用onEvent来写事件,那么这个功能是怎么实现的呢,咋们一起来看看

测试用例

  test('测试on绑定事件', () => {
    let count = 0
    console.log = jest.fn()
    const app = createApp({
      render() {
        return h('div', {
          class: 'container',
          onClick() {
            console.log('click')
            count++
          },
          onFocus() {
            count--
            console.log(1)
          }
        }, '123');
      }
    });
    const appDoc = document.querySelector('#app')
    app.mount(appDoc);
    const container = document.querySelector('.container') as HTMLElement;
    
    // 调用click事件
    container.click();
    expect(console.log).toHaveBeenCalledTimes(1)

    // 调用focus事件
    container.focus();
    expect(count).toBe(0)
    expect(console.log).toHaveBeenCalledTimes(2)

  })
复制代码

分析

在本功能的测试用例中,可以分析以下内容:

  1. onEvent事件是在props中定义的
  2. 事件的格式必须是 on + Event的格式

解决问题:

这个功能比较简单,在处理prop中做个判断, 属性是否满足 /^on[A-Z]/i这个格式,如果是这个格式,则进行事件注册,但是vue3会做事件缓存,这个是怎么做到?

缓存也好实现,在传入当前的el中增加一个属性 el._vei || (el._vei = {}) 存在这里,则直接使用,不能存在则创建并且存入缓存

编码

在mountElement中增加处理事件的逻辑

 const { props } = vnode
  for (let key in props) {
    // 判断key是否是on + 事件命,满足条件需要注册事件
    const isOn = (p: string) => p.match(/^on[A-Z]/i)
    if (isOn(key)) {
      // 注册事件
      el.addEventListener(key.slice(2).toLowerCase(), props[key])
    }
    // ... 其他逻辑
    el.setAttribute(key, props[key])
  }
复制代码

事件处理就ok啦

父子组件通信——props

父子组件通信,在vue中是非常常见的,这里主要实现props与emit

测试用例

 test('测试组件传递props', () => {
    let tempProps;
    console.warn = jest.fn()
    const Foo = {
      name: 'Foo',
      render() {
        // 2. 组件render里面可以直接使用props里面的值
        return h('div', { class: 'foo' }, this.count);
      },
      setup(props) {
        // 1. 此处可以拿到props
        tempProps = props;

        // 3. readonly props
        props.count++
      }
    }

    const app = createApp({
      name: 'App',
      render() {
        return h('div', {
          class: 'container',
        }, [
          h(Foo, { count: 1 }),
          h('span', { class: 'span' }, '123')
        ]);
      }
    });
    const appDoc = document.querySelector('#app')
    app.mount(appDoc);
    // 验证功能1
    expect(tempProps.count).toBe(1)

    // 验证功能3,修改setup内部的props需要报错
    expect(console.warn).toBeCalled()
    expect(tempProps.count).toBe(1)

    // 验证功能2,在render中可以直接使用this来访问props里面的内部属性
    expect(document.body.innerHTML).toBe(`<div id="app"><div class="container"><div class="foo">1</div><span class="span">123</span></div></div>`)
  })
复制代码

分析

根据上面的测试用例,分析props的以下内容:

  1. 父组件传递的参数,可以给到子组件的setup的第一个参数里面
  2. 在子组件的render函数中,可以使用this来访问props的值
  3. 在子组件中修改props会报错,不允许修改

解决问题:

问题1: 想要在子组件的setup函数中第一个参数,使用props,那么在setup函数调用的时候,把当前组件的props传入到setup函数中即可 问题2: render中this想要问题,则在上面的那个代理中,在加入一个判断,key是否在当前instance的props中 问题3: 修改报错,那就是只能读,可以使用以前实现的api shallowReadonly来包裹一下既可

编码

1. 在setup函数调用的时候,传入instance.props之前,需要在实例上挂载props

export function setupComponent(instance) {
  // 获取props和children
  const { props } = instance.vnode

  // 处理props
  instance.props = props || {}
  
  // ……省略其他
 }
 
 //2. 在setup中进行调用时作为参数赋值
 function setupStatefulComponent(instance: any) {
   // ……省略其他
  // 获取组件的setup
  const { setup } = Component;

  if (setup) {
    // 执行setup,并且获取到setup的结果,把props使用shallowReadonly进行包裹,则是只读,不能修改
    const setupResult = setup(shallowReadonly(instance.props));

   // …… 省略其他
  }
}

// 3. 在propxy中在加入判断
 instance.proxy = new Proxy({}, {
     get(target, key){
       // 判断当前的key是否存在于instance.setupState当中
       if(key in instance.setupState){
         return instance.setupState[key]
       }else if(key in instance.props){
          return instance.props[key]
       }else if(key === '$el'){
           return instance.vnode.el
       }
     }
 })
复制代码

做完之后,可以发现咋们的测试用例是运行没有毛病的

组件通信——emit

上面实现了props,那么emit也是少不了的,那么接下来就来实现下emit

测试用例

test('测试组件emit', () => {
    let count;
    const Foo = {
      name: 'Foo',
      render() {
        return h('div', { class: 'foo' }, this.count);
      },
      setup(props, { emit }) {
        // 1. setup对象的第二个参数里面,可以结构出emit,并且是一个函数

        // 2. emit 函数可以父组件传过来的事件
        emit('click')

        // 验证emit1,可以执行父组件的函数
        expect(count.value).toBe(2)

        // 3 emit 可以传递参数
        emit('clickNum', 5)
        // 验证emit传入参数
        expect(count.value).toBe(7)
        // 4 emit 可以使用—的模式
        emit('click-num', -5)
        expect(count.value).toBe(2)
      }
    }

    const app = createApp({
      name: 'App',
      render() {
        return h('div', {}, [
          h(Foo, { onClick: this.click, onClickNum: this.clickNum, count: this.count })
        ])
      },
      setup() {
        const click = () => {
          count.value++
        }
        count = ref(1)

        const clickNum = (num) => {
          count.value = Number(count.value) + Number(num)
        }
        return {
          click,
          clickNum,
          count
        }
      }
    })

    const appDoc = document.querySelector('#app')
    app.mount(appDoc);
    // 验证挂载
    expect(document.body.innerHTML).toBe(`<div id="app"><div><div class="foo">1</div></div></div>`)
  })
复制代码

分析

根据上面的测试用例,可以分析出:

  1. emit 的参数是在父组件的props里面,并且是以 on + Event的形式
  2. emit 作为setup的第二个参数,并且可以结构出来使用
  3. emit 函数里面是触发事件的,事件名称,事件名称可以是小写,或者是 xxx-xxx的形式
  4. emit 函数的后续可以传入多个参数,作为父组件callback的参数

解决办法: 问题1: emit 是setup的第二个参数,那么可以在setup函数调用的时候,传入第二个参数 问题2: 关于emit的第一个参数,可以做条件判断,把xxx-xxx的形式转成xxxXxx的形式,然后加入on,最后在props中取找,存在则调用,不存在则不调用 问题3:emit的第二个参数,则使用剩余参数即可

编码

// 1. 在setup函数执行的时候,传入第二个参数
 const setupResult = setup(shallowReadonly(instance.props), { emit: instance.emit });

// 2. 在setup中传入第二个参数的时候,还需要在实例上添加emit属性哦

export function createComponentInstance(vnode) {
  const instance = {
    // ……其他属性
    // emit函数
    emit: () => { },
  }
  
  

  instance.emit = emit.bind(null, instance);
  
  function emit(instance, event, ...args) {
      const { props } = instance
      // 判断props里面是否有对应的事件,有的话执行,没有就不执行,处理emit的内容,详情请查看源码
      const key = handlerName(capitalize(camize(event)))
      const handler = props[key]
      handler && handler(...args)
  }

  
  return instance
}


复制代码

到此就圆满成功啦!

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...