2024-09-18 15:08:22 +08:00

299 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 事件绑定与更新
>面试题:说一下 Vue 内部是如何绑定和更新事件的?
```vue
<p @click="clickHandler">text</p>
```
对应的 vnode 如下:
```js
const vnode = {
type: 'p',
props: {
// 事件其实就是一种特殊的属性放置于props里面
onClick: ()=>{
// ...
}
},
children: 'text'
}
```
所以在渲染器内部可以检测以 on 开头的属性,说明就是事件,例如:
```js
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 方法:
```js
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(
//...
){
// ...
}
}
```
如果涉及到事件的更新,则需要先把上一次的**事件卸载**掉,然后绑定新的事件:
```js
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(
//...
){
// ...
}
}
```
上面的方式虽然能够正常工作,但是会涉及到反复的绑定和卸载事件。
一种更加优雅的方式是将事件处理器作为一个对象的属性,之后只要更新该对象的属性即可。
```js
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(
//...
){
// ...
}
}
```
不过目前仍然有问题,同一时刻只能缓存一个事件处理函数,而一个元素其实是可以绑定多种事件的,例如:
```js
const vnode = {
type: 'p',
props: {
onClick: ()=>{
// ...
},
onContextmenu: ()=>{
// ...
}
},
children: 'text'
}
```
把 el._eventHandler 由对应的一个函数改为一个对象,对象的键就是事件的名称,对象的值则是对应的事件处理函数:
```js
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(
//...
){
// ...
}
}
```
另外还有一种情况我们需要解决,那就是同种事件类型绑定多个事件处理函数的情况,例如:
```js
el.addEventListener('click', fn1);
el.addEventListener('click', fn2);
```
```js
// 对应的 vnode 结构
const vnode = {
type: 'p',
props: {
// 事件其实就是一种特殊的属性放置于props里面
onClick: [
()=>{},
()=>{}
]
},
children: 'text'
}
```
```js
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 内部是如何绑定和更新事件的?
>
>参考答案:
>
>开发者在模板中书写事件绑定:
>
>```vue
><p @click='clickHandler'>text</p>
>```
>
>模板被编译器编译后会生成渲染函数,渲染函数的执行得到的是虚拟 DOM.
>
>事件在虚拟 DOM 中其实就是以 Props 的形式存在的。在渲染器内部,会有一个专门针对 Props 进行处理的方法,当遇到以 on 开头的 Prop 时候,会认为这是一个事件,从而进行事件的绑定操作。
>
>为了避免事件更新时频繁的卸载旧事件绑定新事件所带来的性能消耗Vue 内部将事件作为一个对象的属性,更新事件的时候只需要更新对象的属性值即可。该对象的结构大致为:
>
>```js
>{
> onClick: [
> ()=>{},
> ()=>{},
> ],
> onContextmenu: ()=>{}
> // ...
>}
>```
>
>这种结构能做到:
>
>1. 一个元素绑定多种事件
>2. 支持同种事件类型绑定多个事件处理函数
---
-EOF-