沐游虞笔记
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 课程导读-必看
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
    • computed面试题
    • watch面试题
    • 图解双端diff
    • 图解快速diff
    • 最长递增子序列
    • 模板编译器
    • 模板编译提升
    • 组件name作用
    • Vue项目性能优化
    • 路由传参方式
    • vue3笔面试题汇总
    luzhichang
    2024-09-27
    目录

    事件绑定与更新

    # 事件绑定与更新

    面试题:说一下 Vue 内部是如何绑定和更新事件的?

    <p @click="clickHandler">text</p>
    

    对应的 vnode 如下:

    const vnode = {
      type: 'p',
      props: {
        // 事件其实就是一种特殊的属性,放置于props里面
        onClick: ()=>{ 
          // ...
        }
      },
      children: 'text'
    }
    

    所以在渲染器内部可以检测以 on 开头的属性,说明就是事件,例如:

    function renderer(vnode, container) {
      // 使用 vnode.tag 作为标签名称创建 DOM 元素
      const el = document.createElement(vnode.tag);
      // 遍历 vnode.props,将属性、事件添加到 DOM 元素
      for (const key in vnode.props) {
        if(/^on/.test(key)){
          // 说明是事件
          el.addEventListenser(
          	key.substr(2).toLowerCase(), // 事件名称 onClick --> click
            vnode.props[key]
          )
        }
      }
    
      // 处理 children
      if (typeof vnode.children === "string") {
        // 如果 children 是字符串,说明它是元素的文本子节点
        el.appendChild(document.createTextNode(vnode.children));
      } else if (Array.isArray(vnode.children)) {
        // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
        vnode.children.forEach((child) => renderer(child, el));
      }
    
      // 将元素添加到挂载点下
      container.appendChild(el);
    }
    

    不过在 Vue 源码中,渲染器内部其实有一个 patchProps 方法:

    function patchProps(el, key, prevValue, nextValue){
      if(/^on/.test{key}){
       	// 说明是事件,做事件的绑定操作
        const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
       	el.addEventListenser(name, vnode.props[key])
      } else if(key === 'class'){
        // ...
      } else if( 
        //... 
      ){
        // ...
      }
    }
    

    如果涉及到事件的更新,则需要先把上一次的事件卸载掉,然后绑定新的事件:

    function patchProps(el, key, prevValue, nextValue){
      if(/^on/.test{key}){
        // 说明是事件,做事件的绑定操作
        const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
        // 移除上一次绑定的事件
        prevValue && el.removeEventListenser(name, prevValue);
        // 再来绑定新的事件处理函数
       	el.addEventListenser(name, vnode.props[key])
      } else if(key === 'class'){
        // ...
      } else if( 
        //... 
      ){
        // ...
      }
    }
    

    上面的方式虽然能够正常工作,但是会涉及到反复的绑定和卸载事件。

    一种更加优雅的方式是将事件处理器作为一个对象的属性,之后只要更新该对象的属性即可。

    function patchProps(el, key, prevValue, nextValue){
      if(/^on/.test{key}){
        // 说明是事件,做事件的绑定操作
        const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
        // 这是一个自定义的属性,回头会被赋值为一个函数,该函数会作为事件处理函数
        let invoker = el._eventHandler; 
        if(nextValue){
          // 说明有新的事件处理函数
          // 这里又有两种情况:1. 第一次绑定事件(事件的初始化)2.非第一次(事件的更新)
          if(!invoker){
            // 事件的初始化
            invoker = el._eventHandler = (e)=>{
              // 执行真正的事件处理函数
              invoker.value(e)
            }
            // 将新的事件处理函数挂载 invoker 的 value 属性上面
            invoker.value = nextValue;
            // 因此是第一次,需要做事件的挂载
            el.addEventListenser(name, invoker)
          } else {
            // 事件的更新
            // 更新的时候不需要再像之前一样先卸载事件,直接更新invoker的value属性值即可
            invoker.value = nextValue;
          }
        } else {
          // 新的事件处理器不存在,那么就需要卸载旧的事件处理器
          el.removeEventListenser(name, invoker);
        }
      } else if(key === 'class'){
        // ...
      } else if( 
        //... 
      ){
        // ...
      }
    }
    

    不过目前仍然有问题,同一时刻只能缓存一个事件处理函数,而一个元素其实是可以绑定多种事件的,例如:

    const vnode = {
      type: 'p',
      props: {
        onClick: ()=>{ 
          // ...
        },
        onContextmenu: ()=>{
          // ...
        }
      },
      children: 'text'
    }
    

    把 el._eventHandler 由对应的一个函数改为一个对象,对象的键就是事件的名称,对象的值则是对应的事件处理函数:

    function patchProps(el, key, prevValue, nextValue){
      if(/^on/.test{key}){
        // 说明是事件,做事件的绑定操作
        const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
        // 这是一个自定义的属性,回头会被赋值为一个函数,该函数会作为事件处理函数
        const invokers = el._eventHandler || (el._eventHandler = {})
        let invoker = invokers[key]; 
        if(nextValue){
          // 说明有新的事件处理函数
          // 这里又有两种情况:1. 第一次绑定事件(事件的初始化)2.非第一次(事件的更新)
          if(!invoker){
            // 事件的初始化
            invoker = el._eventHandler[key] = (e)=>{
              // 执行真正的事件处理函数
              invoker.value(e)
            }
            // 将新的事件处理函数挂载 invoker 的 value 属性上面
            invoker.value = nextValue;
            // 因此是第一次,需要做事件的挂载
            el.addEventListenser(name, invoker)
          } else {
            // 事件的更新
            // 更新的时候不需要再像之前一样先卸载事件,直接更新invoker的value属性值即可
            invoker.value = nextValue;
          }
        } else {
          // 新的事件处理器不存在,那么就需要卸载旧的事件处理器
          el.removeEventListenser(name, invoker);
        }
      } else if(key === 'class'){
        // ...
      } else if( 
        //... 
      ){
        // ...
      }
    }
    

    另外还有一种情况我们需要解决,那就是同种事件类型绑定多个事件处理函数的情况,例如:

    el.addEventListener('click', fn1);
    el.addEventListener('click', fn2);
    
    // 对应的 vnode 结构
    const vnode = {
      type: 'p',
      props: {
         // 事件其实就是一种特殊的属性,放置于props里面
        onClick: [
          ()=>{},
          ()=>{}
        ]
      },
      children: 'text'
    }
    
    function patchProps(el, key, prevValue, nextValue){
      if(/^on/.test{key}){
        // 说明是事件,做事件的绑定操作
        const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
        // 这是一个自定义的属性,回头会被赋值为一个函数,该函数会作为事件处理函数
        const invokers = el._eventHandler || (el._eventHandler = {})
        let invoker = invokers[key]; 
        if(nextValue){
          // 说明有新的事件处理函数
          // 这里又有两种情况:1. 第一次绑定事件(事件的初始化)2.非第一次(事件的更新)
          if(!invoker){
            // 事件的初始化
            invoker = el._eventHandler[key] = (e)=>{
              // 这里需要进行判断,判断是否为数组,如果是数组,说明有多个事件处理函数
              if(Array.isArray(invoker.value)){
                invoker.value.forEach(fn=>fn(e))
              } else {
                // 执行真正的事件处理函数
              	invoker.value(e)
              }
            }
            // 将新的事件处理函数挂载 invoker 的 value 属性上面
            invoker.value = nextValue;
            // 因此是第一次,需要做事件的挂载
            el.addEventListenser(name, invoker)
          } else {
            // 事件的更新
            // 更新的时候不需要再像之前一样先卸载事件,直接更新invoker的value属性值即可
            invoker.value = nextValue;
          }
        } else {
          // 新的事件处理器不存在,那么就需要卸载旧的事件处理器
          el.removeEventListenser(name, invoker);
        }
      } else if(key === 'class'){
        // ...
      } else if( 
        //... 
      ){
        // ...
      }
    }
    

    面试题:说一下 Vue 内部是如何绑定和更新事件的?

    参考答案:

    开发者在模板中书写事件绑定:

    <p @click='clickHandler'>text</p>
    

    模板被编译器编译后会生成渲染函数,渲染函数的执行得到的是虚拟 DOM.

    事件在虚拟 DOM 中其实就是以 Props 的形式存在的。在渲染器内部,会有一个专门针对 Props 进行处理的方法,当遇到以 on 开头的 Prop 时候,会认为这是一个事件,从而进行事件的绑定操作。

    为了避免事件更新时频繁的卸载旧事件,绑定新事件所带来的性能消耗,Vue 内部将事件作为一个对象的属性,更新事件的时候只需要更新对象的属性值即可。该对象的结构大致为:

    {
         onClick: [
             ()=>{},
             ()=>{},
         ],
         onContextmenu: ()=>{}
         // ...
    }
    

    这种结构能做到:

    1. 一个元素绑定多种事件
    2. 支持同种事件类型绑定多个事件处理函数

    -EOF-

    渲染器核心功能
    computed面试题

    ← 渲染器核心功能 computed面试题→

    Theme by Vdoing | Copyright © 2021-2024 蜀ICP备2024068710号-1
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式