和女朋友争论了1个小时,在vue用throttle居然这么黑盒?

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 首先我们都知道,throttle(节流) 和 debounce(防抖) 是性能优化的利器。本文会简单介绍一下这两个的概念,但是并不会对这两个函数再进行老生常谈地说原理了,而是会说它和 vue 之间的爱恨情仇~,但是在步入正题以前,我们得先知道它的一些简介。

开篇



首先我们都知道,throttle(节流)debounce(防抖) 是性能优化的利器。


本文会简单介绍一下这两个的概念,但是并不会对这两个函数再进行老生常谈地说原理了,而是会说它和 vue 之间的爱恨情仇~,但是在步入正题以前,我们得先知道它的一些简介。


函数节流(throttle) 是指一定时间内 js 方法只运行一次。


节流节流就是节省水流的意思,就像水龙头在流水,我们可以手动让水流(在一定时间内)小一点,但是他会一直在流。


函数节流的情况下,函数将每隔 n 秒执行一次,常见的场景为:


  • DOM 元素的拖拽功能实现(mousemove)
  • 搜索联想(keyup)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)


函数防抖(debounce) 只当有足够的空闲时间,才运行代码一次。


比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。(其实只要记住了节流的思想就能通过排除法判断节流和防抖了)


函数防抖的情况下,函数将一直推迟执行,造成不会被执行的效果,常见的场景为:


  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)


vue throttle



那么它们和 vue 结合会擦除怎么样的火花呢?你有了以上的基础知识后,下面正片就正式开始了~ 最近和女朋友谈了下 vue throttle 相关的问题,一开始以为是简单的的东西,没想到真的讨论了1个小时.... 前方高能硬核,层层递进涉及到 vue 源码。


初舞台


问题形态一:


<input @input="download" />
...
methods: {
 download: () {
  this.throttle(xxx)
 },
}
...


我们来分析为什么这样是不行,首先我们来看看正常情况下 throttle 是怎么写的,再来拆分拆分 throttle 。


window.addEventListener('mousemove', throttle(xxx));


进一步拆分


const handleMove = throttle(xxx)
window.addEventListener('mousemove', handleMove);


我们一直调用的是 handleMove 方法,而 throttle 的原理是依赖于 JS 的闭包原理,依赖于handleMove 中的闭包变量。而如果你在 handleMove 外层再套一层 download 函数,贼无法让 handleMove 中的闭包内的变量进行了缓存,因此也失去了throttle 的效果。


升温


那我们来改造一下,看起来是正确地形态。


<input @input="throttle(download(xxx))">
...
methods: {
   download: (xxx) {
    ..
   },
   throttle: ...
}
...


开始一顿疑惑,没错呀,这的确就是 throttle 正确写法的样子,为什么这样就不行呢,再加上好久没有写 vue 的黑魔法了,一时不知道如何解释。


赶紧偷偷查资料,默默地在谷歌输入下了 vue debounce ...


image.png


搜到了一些正确的打开方式。


image.png


发现它这样是可以使用的,而我将他写到模板中不行。


emm。查不到,那开始思考?为什么这个写法不行?等等,我刚刚说了什么?把时间倒退 3.3 秒前... (为什么是3.3秒,因为人类平均说话语速是200字/分钟)


写法?对啊,是写法,这个只是 vue 的模板语法,真实浏览器运行的并不是这个样子啊。


感觉有思路了!快快快,快找 vue 模板编译完后的样子


在浏览器输入下下了vue 模板 在线这几个关键词。


image.png


很快我们就查到了这个地址 https://template-explorer.vuejs.org/


我们将我们的模板输入到左侧的输入框。


image.png


我们得到了这样的一个解析后的 render 函数。


function render() {
  with(this) {
    return _c('input', {
      on: {
        "input": function ($event) {
          throttle(download(xxx));
        }
      }
    })
  }
}


在这里我们看到,我们能大概知道,通过解析后,input 监听方法已经被包裹了一层函数。也很容猜出,最终解析成真正的绑定的函数会变成以下这个样子。


xxxx.addEventListener('input', function ($event) {
  throttle(download(xxx));
})


如果是这个样子的 throttle ,我相信有了解 throttle 的朋友们一眼就能看出来,这样子的 throttle 是完全不起效果的。


而我们刚才资料中查询到的方式呢?


<template>
  <input @input="click" />
</template>
<script>
...
click: _.throttle(() => {
  ...
})
...
</script>


function render() {
  with(this) {
    return _c('input', {
      on: {
        "input": click
      }
    })
  }
}


这种方式下,vue 是直接传递绑定的实践方法的,并不会有任何包装。


所以真相只有一个


果然是 vue 模板的黑魔法!!!!!


进阶


那我们通过 vue 的源码来探索一下,vue 的模板解析的原理,来加深一些我们的印象。

由于这里部分是 vue 事件编译相关的代码,我们很容易地找到了 vue 源码(目前看的是 v2.6.12版本)的位置。


https://github.com/vuejs/vue/blob/v2.6.12/src/compiler/codegen/events.js#L96


我们看到 vue 源码中含关于事件生成是以下代码。


const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
const fnInvokeRE = /\([^)]*?\);*$/
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
...
const isMethodPath = simplePathRE.test(handler.value)
const isFunctionExpression = fnExpRE.test(handler.value)
const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, ''))
if (!handler.modifiers) {
  // 判断如果是个方法或者是函数表达式,就返回 value
  if (isMethodPath || isFunctionExpression) {
    return handler.value
  }
  /* istanbul ignore if */
  if (__WEEX__ && handler.params) {
    return genWeexHandler(handler.params, handler.value)
  }
  // 如果不满足以上的情况就会包一层方法
  return `function($event){${
    isFunctionInvocation ? `return ${handler.value}` : handler.value
  }}` // inline statement
} else {
 ...
}


由于我们的是没有 修饰符(modifiers)的,因此我们关于含有修饰符的代码注释了,防止不必要的干扰。


为了能更好地梳理情况,我们将 isMethodPath 称作方法路径,而将 isFunctionExpression称作函数表达式,isFunctionInvocation称为函数调用(虽然英文就是这个意思,但是为了大家都能看明白吧)


通过以上代码我们能明白,如果这个事件的写法,满足 isMethodPath 或者满足isFunctionExpression。那么我们在事件中的写法会被直接返回,否则的话,会被包一层 function


我们一一来看看关于事件的情景。isMethodPath 的判断方法是const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/, 乍一看有点长,我们通过可视化工具分析分析。


https://jex.im/regulex/


image.png


通过可视化可以看出,我们的事件方式如果是以上形态就会通过正则的检验(例如 handle, handle['xx'], handle["xx"],handle[xxx], handle[0], console.log )这些情况都是不会被包裹一层函数。


还有一种情况就是 正则 const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/


image.png


简单来讲就是写一个匿名函数, (xx) => {} 或者 funciton(){}


除了以上两种情况之外的所有情况都会被包含一层方法。


还记得 vue 的官方教程中,我们写模板语法的时候,以下两种方式是等价的。


1.<div @click="handler"></div>

2.<div @click="handler()"></div>


因为在编译的时候,他们会分别被编译成以下形态。


xxx.onclick = handle
xxx.onclick = function($event) {
 return handler();
}


通过包一层函数来达到相同的目的,现在你能明白了吧?在 vue 中写,怎么写都不会出问题,有时候可能是你偶然手误,它都讲这些情况考虑在内了,就像是吃饭一样,饭已经喂到我们嘴边了。


而在被函数包裹的情况又分了两种情况。


isFunctionInvocation ? return ${handler.value} : handler.value

isFunctionInvocation的检测就是将函数调用的部分去掉,如果去掉后,满足方法路径的情况,那么就会多一个 return


image.png


我们来画个图总结一下。


image.png


而我们的情况是怎么样的呢?


throttle(download(xxx))


显然我们既不满足方法路径、也不满足函数表达式,因此就会出现我们上述的 "bug",让我们的 throttle 失效了。


至此,我们已经清楚了关于 vue 中的黑魔法了,vue 给我们带来便利的同时,我们运用的不好,或者说不理解它的一些思想原理,就会发生一些神奇的事情。


最佳


所以上述说了这么多,我们需要有个最佳的实践方案。


<template>
  <input @click="download(xxx)" />
</template>
<script>
import {debounce} from 'lodash';
...
methods: {
  download: debounce((xxx) => {
    ...
  })
}
...
</script>


升华


那么我们再来解释一个问题,外部导入和内部 methods 的差异性?


<template>
  <input @click="debounce(download(xxx))" />
</template>
<script>
import {debounce} from 'lodash';
</script>


先说以上写法是会出错的。


因为在我们模板中写的方法,必须是 methods 中的方法,否则就会找不到。


也许这样我们直接像在模板中写 throttle 就必须将这个函数定义在 methods 中,这样是非常不友好的,因为会反直觉,对于太久没写的我(T T忘记了)。


那为什么不可以直接写在模板上面呢,其实这也和 vue 的编译相关的,因为 vue 模板中的方法都会被编译成 _vm.xxx,举个例子。


<template>
 <input @click="debounce(download(xxx))" />
</template>


以上模板代码会被编译成这个样子。


/* template */
var __vue_render__ = function() {
    var _vm = this;
    var _h = _vm.$createElement;
    var _c = _vm._self._c || _h;
    return _c("input", {
      on: {
        click: function($event) {
          _vm.debounce(_vm.download(_vm.xxx));
        }
      }
    })
};


以上才是真正在浏览器执行的代码,所以我们可以很清楚地看到 _vm 中是不存在 debounce,这也是 template 只能访问 vue 中定义的方法与变量。


试探边缘


我们再来探究一下 vue 3.0 是否对这个有改动。


答案是: 没有。


我特地去找了  @vue/compiler-sfc 进行了测试。


const sfc = require('@vue/compiler-sfc');
const template = sfc.compileTemplate({
    filename: 'example.vue',
    source: '<input @input="throttle(download(xxx))" />',
    id: ''
});


// output
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("input", {
    onInput: _cache[1] || (_cache[1] = $event => (_ctx.throttle(_ctx.download(_ctx.download(_ctx.xxx))));


结尾



从这一次的探索来看,vue 自身模板语言需要很多心智模型,而在本实例中,vue给了我们很多语法糖,让我们沉醉其中,不得不说这样的方式很舒服,但是总有一天我们独自承受这些苦楚。


这就不得不讨论到 React 的 JSX,虽然它麻烦,对我们很残酷,但是我们对自身的行为更加可控(虽然 vue 也可以用 JSX,但是 Templates 依旧是是官方推荐的方法)我也能理解 vue 上述的这些表现,因为它帮我们做了很多处理,对于某些情况它需要给我们注入 $event, 也就是我们常用的事件对象,但是别人帮我们手把手处理了这些事情,也使得我们慢慢忘记了它原本的形态,一旦出现问题,会让我们举手无措。而 JSX 中则要求我们写出完整的代码,这样的方式使得我们写什么都需要付出额外的劳动,也许像 vue 官方文档中所说,谈论 JSX 和 vue 的 Templates 是肤浅的的,但是不管怎么样,每个人都会对它有不一样的理解,不一样的喜好,所以自己总结了一下。


都学就完si儿了 :)


相关文章
|
8天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
10天前
|
JavaScript
Vue 指令速查表
【10月更文挑战第12天】Vue 指令速查表
|
8天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
27 9
|
7天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
6天前
|
JavaScript
如何在 Vue 中使用具名插槽
【10月更文挑战第25天】通过使用具名插槽,你可以更好地组织和定制组件的模板结构,使组件更具灵活性和可复用性。同时,具名插槽也有助于提高代码的可读性和可维护性。
13 2
|
6天前
|
JavaScript
Vue 中的插槽
【10月更文挑战第25天】插槽的使用可以大大提高组件的复用性和灵活性,使你能够根据具体需求在组件中插入不同的内容,同时保持组件的结构和样式的一致性。
11 2
|
6天前
|
前端开发 JavaScript 容器
在 vite+vue 中使用@originjs/vite-plugin-federation 模块联邦
【10月更文挑战第25天】模块联邦是一种强大的技术,它允许将不同的微前端模块组合在一起,形成一个统一的应用。在 vite+vue 项目中,使用@originjs/vite-plugin-federation 模块联邦可以实现高效的模块共享和组合。通过本文的介绍,相信你已经了解了如何在 vite+vue 项目中使用@originjs/vite-plugin-federation 模块联邦,包括安装、配置和使用等方面。在实际开发中,你可以根据自己的需求和项目的特点,灵活地使用模块联邦,提高项目的可维护性和扩展性。
|
7天前
|
JavaScript 前端开发 UED
vue 提高 tree shaking 的效果
【10月更文挑战第23天】提高 Vue 中 Tree shaking 的效果需要综合考虑多个因素,包括模块的导出和引用方式、打包工具配置、代码结构等。通过不断地优化和调整,可以最大限度地发挥 Tree shaking 的优势,为 Vue 项目带来更好的性能和用户体验。
|
11天前
|
JavaScript 前端开发 开发者
Vue 的优缺点
【10月更文挑战第16天】Vue 具有众多优点,使其成为前端开发中备受青睐的框架之一。尽管它也存在一些局限性,但通过合理的应用和技术选型,这些问题可以得到一定程度的解决。在实际项目中,开发者可以根据项目的需求和特点,权衡 Vue 的优缺点,选择最适合的技术方案。同时,随着 Vue 不断的发展和完善,相信它将在前端开发领域继续发挥重要作用。
19 6
|
11天前
|
JavaScript 前端开发 编译器
在 Vue 项目中使用 ES 模块格式的优点
【10月更文挑战第20天】在 Vue 项目中使用 ES 模块格式具有众多优点,这些优点共同作用,使得项目能够更高效、更可靠地开发和运行。当然,在实际应用中,还需要根据项目的具体情况和需求进行合理的选择和配置。
21 6