1. 尽量避免使用行内事件代码(inline script)
这是一个行内事件代码 的例子
<div @click="alert('hello world'); doSomething();" />
这种代码虽然第一次写起来很简单,但是很容易出bug。这样做有两个缺点。
VS Code 无法检查行内事件代码的错误
VS Code没有办法帮你检查模板中的行内事件代码,所以请尽量不要写行内事件代码。你可能会觉得有些代码很简单,你可以直接看出有没有错误。但实际上,大多数时间我们都看不出一些简单的错误。比如我们在模板中有两句简单的代码
<div @click="alert('hello world'); doSomething();" />
以下是其中一行代码doSomething()
的定义
doSomething(event: Event) {
// do something ...
}
VS Code不会给你提示任何的错误。但是如果你运行这些代码,你会发现doSomething
的 event
对象是undefined
。原因很简单,因为我们在模板中写的是doSomething()
。如果你不用行内事件代码,而是把需要处理的逻辑全部抽取到一个事件处理方法里面。 把所有的js逻辑放到一个统一的地方,情况就不一样了。比如我们把事件处理逻辑移到sayHelloAndDoSomething
方法中
sayHelloAndDoSomething() {
console.log('do something');
this.doSomething();
}
这回VS Code就发现了你的错误。VS Code帮你发现了doSomething
需要一个必须的参数 event
使用行内事件代码更糟糕的是它看起来可以运行,有的时候真的可以运行,只不过有一些不会引发代码崩溃的bug被隐藏了起来。虽然上面这个例子看起来很简单,但是在实际工作中,我经常可以看到类似的错误。
使用行内事件代码会给生产环境调试造成困难
由于在生产环境上的代码多半是被混淆和压缩过的,如果你没有使用Sourcemap。当生产环境出现问题时,当你打开浏览器的调试工具想通过调试来排查问题的时候,你会发现编译后的html文件是一个很长的单行代码。你根本没有办法往里面加断点来调试。但是如果你把代码放到一个方法里面,你还是可以使用浏览器的调试工具来加断点。
2. 尽量避免使用侦听属性
Vue.js 的官方文档已经明确建议大家使用侦听属性之前请三思。
为什么侦听属性不好呢?因为侦听属性其实是一种隐式依赖。
什么是隐式依赖
当一个组件或者方法明确的调用另外一个组件或者方法的时候,我们称之为显式依赖。比如
class Foo {
getName: () {
return "Terry";
}
}
class Bar {
sayHello: () {
const foo = new Foo();
retur "Hello! " + foo.getName();
}
}
在这个例子中 Foo.getName
和 Bar.sayHello
之间有明确的现实依赖。当输出有问题的时候,你知道去哪里找问题。如果你使用了IDE,当你点击foo.getName()
的时候,你会自动跳转到 getName
的定义代码。
隐式依赖跟显示依赖相对,在这种关系中的两个组件或者方法之间往往没有明确的关联关系。你通过IDE的自动关联功能是无法自动关联上它们的。你需要使用搜索功能来找到这些代码之间的关联。
可能很多人很痛很基于事件总线的观察者模式。我曾经有一个领导说他再也不想用事件总线了。我以前也开发过基于事件总线的代码。当你在调试的时候,代码总是在项目的各个角落来回跳转,有时候你根本不知道哪一个代码会被某一个监听器触发。有的时候各个监听器触发的代码的结果会互相覆盖。基于事件总线的观察者模式最大的问题就在于隐式依赖。那些散落在项目各处的监听器代码跟事件广播代码之间的关系就是隐式依赖。比如
在A.js中你可以广播一个事件
this.$bus.sendEvent('sayHello')
然后在 B.js和C.js中都可以监听这个事件
B.js
this.$bus.listenEvent('sayHello', function() {
console.log("I'm B. I got hello!")
})
C.js
this.$bus.listenEvent('sayHello', function() {
console.log("I'm C. I got hello!")
})
当事件被广播的时候你无法控制被触发的代码的执行顺序,你甚至都不知道有多少处代码被触发了。最可怕的是当你改动了事件的广播代码的时候,你不知道哪个部分的代码被你破坏了。因为你很容易漏改某个逻辑。
侦听属性(Watcher)的地雷
此外,侦听属性本身还有两个初学者容易踩的雷:
- 侦听属性默认是懒加载(lazy loading)的。所以当组建被第一次创建时,它不会被触发。所以如果你有一些代码既想在组建初始化时被运行,又想当某些属性变化时被触发,你就必须在
created
或者mounted
中写一遍,并且在侦听属性中再写一遍。你也可以给侦听属性加上immediate: true
属性,这样侦听属性就会在组建创建的时候被执行一次。但是这样做可能容易出现死循环。 - 侦听属性是不能被暂停的。你可以销毁它或者根据Evan在这个回答中所说的一样,增加一个变量来达到在某些情况下跳过侦听属性内部的逻辑的效果。我在使用侦听属性的时候,会增加一个变量
watcherEnabled
来在某些情况下关闭侦听属性。但是这个属性也给我带来了很多麻烦。有些bug就是因为没有及时改变这个属性的值造成的。而且对于模板渲染来说,你在同一个tick
中对一个变量多次赋值,它并不会多次渲染。比如,如果你在同一个tick
中先把watcherEnable
赋值为true
,然后赋值为false
,最后再赋值为true
,对于模版渲染来说,这个值等于没变。所以我必须用vue.nextTick
来让模板"感受"到这个值的变化。
比侦听属性(Watcher)更好的方式
以下是5种比侦听属性(Watcher)更好的方式。
从子组件到父组件
如果是想让子组件调用父组件的方法,我们可以使用:自定义事件。通过使用自定义事件,子组件可以通知父组件调用指定的方法。
从父组件到子组件
如果你想让父组件调用子组件的方法或者让子组件的某些属性更新。如果要触发的方法在子方法中。与其使用$refs,不如将该方法移动到父组件。然后,您可以直接调用它或使用自定义事件
使用ref属性
如果您想触发的方法不能移动到父方法。或者您不想冒险重构任务。你可以使用ref属性
但是请记住,使用$refs
并不是一个好的实践。$refs
也有自己的问题。组件$refs
在呈现时是未定义的。所以如果你在模板中使用$refs
,它可能没有值。如果你使用$refs
,computed有可能会坏掉。这是一个使用computed来获取子组件scrollLeft属性的例子。
get childScollLeft() {
return this.$refs.child.scrollLeft;
}
这个computed其实不会有用。因为当你想要在computed中观察的变量在开始时是未定义的,那么这个computed就会失去响应性。就算$refs
加载之后,它也不会生效。
使用props
如果你想更新子组件UI。你可以使用props
从任何地方
使用 Vuex或者Pinia
如果你想更新父/子/兄弟组件UI。你可以使用Vuex或者Pinia。唯一的缺点是这可能会增加代码的复杂性。
3. 不要通过props传递函数
在Vue.js中用props传递函数是一个反模式中有一句话非常好。
随着应用程序的规模越来越大,其他开发人员加入,他们将查看子组件代码,并必须弄清楚它是哪个prop引入了这个函数以及这个函数来自哪里。
这就是我在自己项目中遇到的问题。有时我需要弄清楚这些方法是从哪里来的。我非常同意他的观点。但这并不是我不建议将函数作为props传递的主要原因。主要原因是,当我阅读代码时,需要花费一些精力来熟悉这个组件的上下文。如果有一个函数作为prop传递,那么我需要熟悉另一个组件的上下文。如果仅仅是这样,其实也没那么糟。更糟糕的是,当阅读代码时,我还需要在不同的组件上下文之间来回切换。
如果你想传递函数,我建议使用自定义事件,而不是通过prop传递函数。