v-model 是如何实现的,语法糖理论是什么?
(1)作用在表单元素上 动静绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动静把 message 设置为目标值:
<input v-model="sth" />
// 等同于
<input v-bind:value="message" v-on:input="message=$event.target.value"
>
//$event 指代以后触发的事件对象;//$event.target 指代以后触发的事件对象的 dom;//$event.target.value 就是以后 dom 的 value 值;// 在 @input 办法中,value => sth;// 在:value 中,sth => value;
(2)作用在组件上 在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件
实质是一个父子组件通信的语法糖,通过 prop 和 $.emit 实现。 因而父组件 v-model 语法糖实质上能够批改为:
<child :value="message" @input="function(e){message = e}"></child>
在组件的实现中,能够通过 v-model 属性来配置子组件接管的 prop 名称,以及派发的事件名称。
例子:
// 父组件
<aa-input v-model="aa"></aa-input>
// 等价于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>
// 子组件:<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
props:{value:aa,}
methods:{onmessage(e){$emit('input',e.target.value)
}
}
默认状况下,一个组件上的 v -model 会把 value 用作 prop 且把 input 用作 event。然而一些输出类型比方单选框和复选框按钮可能想应用 value prop 来达到不同的目标。应用 model 选项能够回避这些状况产生的抵触。js 监听 input 输入框输出数据扭转,用 oninput,数据扭转当前就会立即登程这个事件。通过 input 事件把数据 $emit 进来,在父组件承受。父组件设置 v -model 的值为 input $emit
过去的值。
组件中写 name 属性的益处
能够标识组件的具体名称不便调试和查找对应属性
// 源码地位 src/core/global-api/extend.js
// enable recursive self-lookup
if (name) {Sub.options.components[name] = Sub // 记录本人 在组件中递归本人 -> jsx
}
如何监听 pushState 和 replaceState 的变动呢?
利用自定义事件 new Event()
创立这两个事件,并全局监听:
<body>
<button onclick="goPage2()"> 去 page2</button>
<div>Page1</div>
<script>
let count = 0;
function goPage2 () {history.pushState({ count: count++}, `bb${count}`,'page1.html')
console.log(history)
}
// 这个不能监听到 pushState
// window.addEventListener('popstate', function (event) {// console.log(event)
// })
function createHistoryEvent (type) {var fn = history[type]
return function () {
// 这里的 arguments 就是调用 pushState 时的三个参数汇合
var res = fn.apply(this, arguments)
let e = new Event(type)
e.arguments = arguments
window.dispatchEvent(e)
return res
}
}
history.pushState = createHistoryEvent('pushState')
history.replaceState = createHistoryEvent('replaceState')
window.addEventListener('pushState', function (event) {// { type: 'pushState', arguments: [...], target: Window, ... }
console.log(event)
})
window.addEventListener('replaceState', function (event) {console.log(event)
})
</script>
</body>
v-once 的应用场景有哪些
剖析
v-once
是 Vue
中内置指令,很有用的API
,在优化方面常常会用到
体验
仅渲染元素和组件一次,并且跳过将来更新
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
答复范例
v-once
是vue
的内置指令,作用是仅渲染指定组件或元素一次,并跳过将来对其更新- 如果咱们有一些元素或者组件在初始化渲染之后不再须要变动,这种状况下适宜应用
v-once
,这样哪怕这些数据变动,vue
也会跳过更新,是一种代码优化伎俩 - 咱们只须要作用的组件或元素上加上
v-once
即可 vue3.2
之后,又减少了v-memo
指令,能够有条件缓存局部模板并管制它们的更新,能够说控制力更强了- 编译器发现元素下面有
v-once
时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而防止再次计算
原理
上面例子应用了v-once
:
<script setup>
import {ref} from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1 v-once>{{msg}}</h1>
<input v-model="msg">
</template>
咱们发现 v-once
呈现后,编译器会缓存作用元素或组件,从而防止当前更新时从新计算这一部分:
// ...
return (_ctx, _cache) => {return (_openBlock(), _createElementBlock(_Fragment, null, [
// 从缓存获取 vnode
_cache[0] || (_setBlockTracking(-1),
_cache[0] = _createElementVNode("h1", null, [_createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
]),
_setBlockTracking(1),
_cache[0]
),
// ...
Vue 的 diff 算法详细分析
1. 是什么
diff
算法是一种通过同层的树节点进行比拟的高效算法
其有两个特点:
- 比拟只会在同层级进行, 不会跨层级比拟
- 在 diff 比拟的过程中,循环从两边向两头比拟
diff
算法在很多场景下都有利用,在 vue
中,作用于虚构 dom
渲染成实在 dom
的新旧 VNode
节点比拟
2. 比拟形式
diff
整体策略为:深度优先,同层比拟
- 比拟只会在同层级进行, 不会跨层级比拟
- 比拟的过程中,循环从两边向两头收拢
上面举个 vue
通过 diff
算法更新的例子:
新旧 VNode
节点如下图所示:
第一次循环后,发现旧节点 D 与新节点 D 雷同,间接复用旧节点 D 作为 diff
后的第一个实在节点,同时旧节点 endIndex
挪动到 C,新节点的 startIndex
挪动到了 C
第二次循环后,同样是旧节点的开端和新节点的结尾 (都是 C) 雷同,同理,diff
后创立了 C 的实在节点插入到第一次创立的 D 节点前面。同时旧节点的 endIndex
挪动到了 B,新节点的 startIndex
挪动到了 E
第三次循环中,发现 E 没有找到,这时候只能间接创立新的实在节点 E,插入到第二次创立的 C 节点之后。同时新节点的 startIndex
挪动到了 A。旧节点的 startIndex
和 endIndex
都放弃不动
第四次循环中,发现了新旧节点的结尾 (都是 A) 雷同,于是 diff
后创立了 A 的实在节点,插入到前一次创立的 E 节点前面。同时旧节点的 startIndex
挪动到了 B,新节点的startIndex
挪动到了 B
第五次循环中,情景同第四次循环一样,因而 diff
后创立了 B 实在节点 插入到前一次创立的 A 节点前面。同时旧节点的 startIndex
挪动到了 C,新节点的 startIndex 挪动到了 F
新节点的 startIndex
曾经大于 endIndex
了,须要创立 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点 F,间接创立 F 节点对应的实在节点放到 B 节点前面
3. 原理剖析
当数据产生扭转时,set
办法会调用 Dep.notify
告诉所有订阅者 Watcher
,订阅者就会调用patch
给实在的 DOM
打补丁,更新相应的视图
源码地位:src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {if (isUndef(vnode)) { // 没有新节点,间接执行 destory 钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,间接用新节点生成 dom 元素
} else {const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点本身一样,统一执行 patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则间接销毁及旧节点,依据新节点生成 dom 元素
if (isRealElement) {if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}
patch
函数前两个参数位为oldVnode
和 Vnode
,别离代表新的节点和之前的旧节点,次要做了四个判断:
- 没有新节点,间接触发旧节点的
destory
钩子 - 没有旧节点,阐明是页面刚开始初始化的时候,此时,基本不须要比拟了,间接全是新建,所以只调用
createElm
- 旧节点和新节点本身一样,通过
sameVnode
判断节点是否一样,一样时,间接调用patchVnode
去解决这两个节点 - 旧节点和新节点本身不一样,当两个节点不一样的时候,间接创立新节点,删除旧节点
上面次要讲的是 patchVnode
局部
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点统一,什么都不做
if (oldVnode === vnode) {return}
// 让 vnode.el 援用到当初的实在 dom,当 el 批改时,vnode.el 会同步变动
const elm = vnode.elm = oldVnode.elm
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {vnode.isAsyncPlaceholder = true}
return
}
// 如果新旧都是动态节点,并且具备雷同的 key
// 当 vnode 是克隆节点或是 v -once 指令管制的节点时,只须要把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上
// 也不必再有其余操作
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果 vnode 不是文本节点或者正文节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用 updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的 vnode 有子节点
} else if (isDef(ch)) {if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm 曾经援用了老的 dom 节点,在老的 dom 节点上增加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新 vnode 没有子节点,而 vnode 有子节点,间接删除老的 oldCh
} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, '')
}
// 如果新 vnode 和老 vnode 是文本节点或正文节点
// 然而 vnode.text != oldVnode.text 时,只须要更新 vnode.elm 的文本内容就能够
} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode
次要做了几个判断:
- 新节点是否是文本节点,如果是,则间接更新
dom
的文本内容为新节点的文本内容 - 新节点和旧节点如果都有子节点,则解决比拟更新子节点
- 只有新节点有子节点,旧节点没有,那么不必比拟了,所有节点都是全新的,所以间接全副新建就好了,新建是指创立出所有新
DOM
,并且增加进父节点 - 只有旧节点有子节点而新节点没有,阐明更新后的页面,旧节点全副都不见了,那么要做的,就是把所有的旧节点删除,也就是间接把
DOM
删除
子节点不完全一致,则调用updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode 的第一个 child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode 的最初一个 child
let newStartVnode = newCh[0] // newVnode 的第一个 child
let newEndVnode = newCh[newEndIdx] // newVnode 的最初一个 child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果 oldStartVnode 和 oldEndVnode 重合,并且新的也都重合了,证实 diff 完了,循环完结
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果 oldVnode 的第一个 child 不存在
if (isUndef(oldStartVnode)) {
// oldStart 索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果 oldVnode 的最初一个 child 不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd 索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode 和 newStartVnode,索引左移,持续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode 和 newEndVnode,索引右移,持续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode 和 newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldStartVnode.eml 挪动到 oldEndVnode.elm 之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart 索引右移,newEnd 索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果 oldEndVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode 和 newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldEndVnode.elm 挪动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd 索引左移,newStart 索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在 oldChildren 中寻找和 newStartVnode 的具备雷同的 key 的 Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,阐明 newStartVnode 是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创立一个新 Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和 newStartVnodej 具备雷同的 key 的 Vnode,叫 vnodeToMove
} else {vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error.' +
'Make sure each v-for item has a unique key.'
)
}
// 比拟两个具备雷同的 key 的新节点是否是同一个节点
// 不设 key,newCh 和 oldCh 只会进行头尾两端的互相比拟,设 key 后,除了头尾两端的比拟外,还会从用 key 生成的对象 oldKeyToIdx 中查找匹配的节点,所以为节点设置 key 能够更高效的利用 dom。if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove 和 newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 革除
oldCh[idxInOld] = undefined
// 如果 removeOnly 是 false,则将找到的和 newStartVnodej 具备雷同的 key 的 Vnode,叫 vnodeToMove.elm
// 挪动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果 key 雷同,然而节点不雷同,则创立一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
while
循环次要解决了以下五种情景:
- 当新老
VNode
节点的start
雷同时,间接patchVnode
,同时新老VNode
节点的开始索引都加 1 - 当新老
VNode
节点的end
雷同时,同样间接patchVnode
,同时新老VNode
节点的完结索引都减 1 - 当老
VNode
节点的start
和新VNode
节点的end
雷同时,这时候在patchVnode
后,还须要将以后实在dom
节点挪动到oldEndVnode
的前面,同时老VNode
节点开始索引加 1,新VNode
节点的完结索引减 1 - 当老
VNode
节点的end
和新VNode
节点的start
雷同时,这时候在patchVnode
后,还须要将以后实在dom
节点挪动到oldStartVnode
的后面,同时老VNode
节点完结索引减 1,新VNode
节点的开始索引加 1 -
如果都不满足以上四种情景,那阐明没有雷同的节点能够复用,则会分为以下两种状况:
- 从旧的
VNode
为key
值,对应index
序列为value
值的哈希表中找到与newStartVnode
统一key
的旧的VNode
节点,再进行patchVnode
,同时将这个实在dom
挪动到oldStartVnode
对应的实在dom
的后面 - 调用
createElm
创立一个新的dom
节点放到以后newStartIdx
的地位
- 从旧的
小结
- 当数据产生扭转时,订阅者
watcher
就会调用patch
给实在的DOM
打补丁 - 通过
isSameVnode
进行判断,雷同则调用patchVnode
办法 -
patchVnode
做了以下操作:- 找到对应的实在
dom
,称为el
- 如果都有都有文本节点且不相等,将
el
文本节点设置为Vnode
的文本节点 - 如果
oldVnode
有子节点而VNode
没有,则删除el
子节点 - 如果
oldVnode
没有子节点而VNode
有,则将VNode
的子节点实在化后增加到el
- 如果两者都有子节点,则执行
updateChildren
函数比拟子节点
- 找到对应的实在
-
updateChildren
次要做了以下操作:- 设置新旧
VNode
的头尾指针 - 新旧头尾指针进行比拟,循环向两头聚拢,依据状况调用
patchVnode
进行patch
反复流程、调用createElem
创立一个新节点,从哈希表寻找key
统一的VNode
节点再分状况操作
- 设置新旧
Vue3 的设计指标是什么?做了哪些优化
1、设计指标
不以解决理论业务痛点的更新都是耍流氓,上面咱们来列举一下 Vue3
之前咱们或者会面临的问题
- 随着性能的增长,简单组件的代码变得越来越难以保护
- 短少一种比拟「洁净」的在多个组件之间提取和复用逻辑的机制
- 类型推断不够敌对
bundle
的工夫太久了
而 Vue3
通过长达两三年工夫的筹备,做了哪些事件?
咱们从后果反推
- 更小
- 更快
- TypeScript 反对
- API 设计一致性
- 进步本身可维护性
- 凋谢更多底层性能
一句话概述,就是更小更快更敌对了
更小
Vue3
移除一些不罕用的API
- 引入
tree-shaking
,能够将无用模块“剪辑”,仅打包须要的,使打包的整体体积变小了
更快
次要体现在编译方面:
diff
算法优化- 动态晋升
- 事件监听缓存
SSR
优化
更敌对
vue3
在兼顾 vue2
的options API
的同时还推出了composition API
,大大增加了代码的逻辑组织和代码复用能力
这里代码简略演示下:
存在一个获取鼠标地位的函数
import {toRefs, reactive} from 'vue';
function useMouse(){const state = reactive({x:0,y:0});
const update = e=>{
state.x = e.pageX;
state.y = e.pageY;
}
onMounted(()=>{window.addEventListener('mousemove',update);
})
onUnmounted(()=>{window.removeEventListener('mousemove',update);
})
return toRefs(state);
}
咱们只须要调用这个函数,即可获取 x
、y
的坐标,齐全不必关注实现过程
试想一下,如果很多相似的第三方库,咱们只须要调用即可,不用关注实现过程,开发效率大大提高
同时,VUE3
是基于 typescipt
编写的,能够享受到主动的类型定义提醒
2、优化计划
vue3
从很多层面都做了优化,能够分成三个方面:
- 源码
- 性能
- 语法 API
源码
源码能够从两个层面开展:
- 源码治理
- TypeScript
源码治理
vue3
整个源码是通过 monorepo
的形式保护的,依据性能将不同的模块拆分到 packages
目录上面不同的子目录中
这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易浏览、了解和更改所有模块源码,进步代码的可维护性
另外一些 package
(比方 reactivity
响应式库)是能够独立于 Vue
应用的,这样用户如果只想应用 Vue3
的响应式能力,能够独自依赖这个响应式库而不必去依赖整个 Vue
TypeScript
Vue3
是基于 typeScript
编写的,提供了更好的类型查看,能反对简单的类型推导
性能
vue3
是从什么哪些方面对性能进行进一步优化呢?
- 体积优化
- 编译优化
- 数据劫持优化
这里讲述数据劫持:
在 vue2
中,数据劫持是通过Object.defineProperty
,这个 API 有一些缺点,并不能检测对象属性的增加和删除
Object.defineProperty(data, 'a',{get(){// track},
set(){// trigger}
})
只管 Vue
为了解决这个问题提供了 set
和 delete
实例办法,然而对于用户来说,还是减少了肯定的心智累赘
同时在面对嵌套层级比拟深的状况下,就存在性能问题
default {
data: {
a: {
b: {
c: {d: 1}
}
}
}
}
相比之下,vue3
是通过 proxy
监听整个对象,那么对于删除还是监听当然也能监听到
同时Proxy
并不能监听到外部深层次的对象变动,而 Vue3
的解决形式是在getter
中去递归响应式,这样的益处是真正拜访到的外部对象才会变成响应式,而不是无脑递归
语法 API
这里当然说的就是composition API
,其两大显著的优化:
- 优化逻辑组织
- 优化逻辑复用
逻辑组织
一张图,咱们能够很直观地感触到 Composition API
在逻辑组织方面的劣势
雷同性能的代码编写在一块,而不像 options API
那样,各个性能的代码混成一块
逻辑复用
在 vue2
中,咱们是通过 mixin
实现性能混合,如果多个 mixin
混合,会存在两个非常明显的问题:命名抵触和数据起源不清晰
而通过 composition
这种模式,能够将一些复用的代码抽离进去作为一个函数,只有的应用的中央间接进行调用即可
同样是上文的获取鼠标地位的例子
import {toRefs, reactive, onUnmounted, onMounted} from 'vue';
function useMouse(){const state = reactive({x:0,y:0});
const update = e=>{
state.x = e.pageX;
state.y = e.pageY;
}
onMounted(()=>{window.addEventListener('mousemove',update);
})
onUnmounted(()=>{window.removeEventListener('mousemove',update);
})
return toRefs(state);
}
组件应用
import useMousePosition from './mouse'
export default {setup() {const { x, y} = useMousePosition()
return {x, y}
}
}
能够看到,整个数据起源清晰了,即便去编写更多的 hook
函数,也不会呈现命名抵触的问题
参考 前端进阶面试题具体解答
Vue-router 除了 router-link 怎么实现跳转
申明式导航
<router-link to="/about">Go to About</router-link>
编程式导航
// literal string path
router.push('/users/1')
// object with path
router.push({path: '/users/1'})
// named route with params to let the router build the url
router.push({name: 'user', params: { username: 'test'} })
答复范例
vue-router
导航有两种形式:申明式导航和编程形式导航- 申明式导航形式应用
router-link
组件,增加to
属性导航;编程形式导航更加灵便,可传递调用router.push()
,并传递path
字符串或者RouteLocationRaw
对象,指定path
、name
、params
等信息 - 如果页面中简略示意跳转链接,应用
router-link
最快捷,会渲染一个 a 标签;如果页面是个简单的内容,比方商品信息,能够增加点击事件,应用编程式导航 - 实际上外部两者调用的导航函数是一样的
Vue3.0 和 2.0 的响应式原理区别
Vue3.x 改用 Proxy 代替 Object.defineProperty。因为 Proxy 能够间接监听对象和数组的变动,并且有多达 13 种拦挡办法。
相干代码如下
import {mutableHandlers} from "./baseHandlers"; // 代理相干逻辑
import {isObject} from "./util"; // 工具办法
export function reactive(target) {
// 依据不同参数创立不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {if (!isObject(target)) {return target;}
const observed = new Proxy(target, baseHandler);
return observed;
}
const get = createGetter();
const set = createSetter();
function createGetter() {return function get(target, key, receiver) {
// 对获取的值进行喷射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回以后对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {return function set(target, key, value, receiver) {const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {console.log("属性值被批改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此办法
set, // 当批改属性时调用此办法
};
理解 nextTick 吗?
异步办法,异步渲染最初一步,与 JS 事件循环分割严密。次要应用了宏工作微工作(setTimeout
、promise
那些),定义了一个异步办法,屡次调用 nextTick
会将办法存入队列,通过异步办法清空以后队列。
computed 和 watch 的区别和使用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值产生扭转,下一次获取 computed 的值时才会从新计算 computed 的值;
watch: 更多的是「察看」的作用,相似于某些数据的监听回调,每当监听的数据变动时都会执行回调进行后续操作;
使用场景:
- 当咱们须要进行数值计算,并且依赖于其它数据时,应该应用 computed,因为能够利用 computed 的缓存个性,防止每次获取值时,都要从新计算;
- 当咱们须要在数据变动时执行异步或开销较大的操作时,应该应用 watch,应用 watch 选项容许咱们执行异步操作 (拜访一个 API),限度咱们执行该操作的频率,并在咱们失去最终后果前,设置中间状态。这些都是计算属性无奈做到的。
Vuex 为什么要分模块并且加命名空间
- 模块 : 因为应用繁多状态树,利用的所有状态会集中到一个比拟大的对象。当利用变得非常复杂时,
store
对象就有可能变得相当臃肿。为了解决以上问题,Vuex
容许咱们将store
宰割成模块(module
)。每个模块领有本人的state
、mutation
、action
、getter
、甚至是嵌套子模块 - 命名空间:默认状况下,模块外部的
action
、mutation
和getter
是注册在全局命名空间的——这样使得多个模块可能对同一mutation
或action
作出响应。如果心愿你的模块具备更高的封装度和复用性,你能够通过增加namespaced: true
的形式使其成为带命名空间的模块。当模块被注册后,它的所有getter
、action
及mutation
都会主动依据模块注册的门路调整命名
什么是 mixin?
- Mixin 使咱们可能为 Vue 组件编写可插拔和可重用的性能。
- 如果心愿在多个组件之间重用一组组件选项,例如生命周期 hook、办法等,则能够将其编写为 mixin,并在组件中简略的援用它。
- 而后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
Vue template 到 render 的过程
vue 的模版编译过程次要如下:template -> ast -> render 函数
vue 在模版编译版本的码中会执行 compileToFunctions 将 template 转化为 render 函数:
// 将模板编译为 render 函数 const {render, staticRenderFns} = compileToFunctions(template,options// 省略}, this)
CompileToFunctions 中的次要逻辑如下∶ (1)调用 parse 办法将 template 转化为 ast(形象语法树)
constast = parse(template.trim(), options)
- parse 的指标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的模式来形容整个模板。
- 解析过程:利用正则表达式程序解析模板,当解析到开始标签、闭合标签、文本的时候都会别离执行对应的 回调函数,来达到结构 AST 树的目标。
AST 元素节点总共三种类型:type 为 1 示意一般元素、2 为表达式、3 为纯文本
(2)对动态节点做优化
optimize(ast,options)
这个过程次要剖析出哪些是动态节点,给其打一个标记,为后续更新渲染能够间接跳过动态节点做优化
深度遍历 AST,查看每个子树的节点元素是否为动态节点或者动态节点根。如果为动态节点,他们生成的 DOM 永远不会扭转,这对运行时模板更新起到了极大的优化作用。
(3)生成代码
const code = generate(ast, options)
generate 将 ast 形象语法树编译成 render 字符串并将动态局部放到 staticRenderFns 中,最初通过 new Function(
` render`)
生成 render 函数。
什么是作用域插槽
插槽
- 创立组件虚构节点时,会将组件儿子的虚构节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类
{a:[vnode],b[vnode]}
- 渲染组件时会拿对应的
slot
属性的节点进行替换操作。(插槽的作用域为父组件)
<app>
<div slot="a">xxxx</div>
<div slot="b">xxxx</div>
</app>
slot name="a"
slot name="b"
作用域插槽
- 作用域插槽在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件)
- 一般插槽渲染的作用域是父组件,作用域插槽的渲染作用域是以后子组件。
// 插槽
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<my-component>
<div slot="header">node</div>
<div>react</div>
<div slot="footer">vue</div>
</my-component> `
)
// with(this) {
// return _c('my-component', [_c('div', {// attrs: { "slot": "header"},
// slot: "header"
// }, [_v("node")] // _文本及诶点 )
// , _v(" "),
// _c('div', [_v("react")]), _v(""), _c('div', {// attrs: { "slot": "footer"},
// slot: "footer" }, [_v("vue")])])
// }
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<div>
<slot name="header"></slot>
<slot name="footer"></slot>
<slot></slot>
</div> `
);
with(this) {return _c('div', [_v("node"), _v(""), _t(_v("vue")])]), _v(" "), _t("default")], 2)
}
// _t 定义在 core/instance/render-helpers/index.js
// 作用域插槽:
let ele = VueTemplateCompiler.compile(` <app>
<div slot-scope="msg" slot="footer">{{msg.a}}</div>
</app> `
);
// with(this) {
// return _c('app', { scopedSlots: _u([{
// // 作用域插槽的内容会被渲染成一个函数
// key: "footer",
// fn: function (msg) {// return _c('div', {}, [_v(_s(msg.a))]) } }])
// })
// }
// }
const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `);
// with(this) {return _c('div', [_t("footer", null, { "a": "1", "b": "2"})], 2) }
v-model 的原理?
咱们在 vue 我的项目中次要应用 v-model 指令在表单 input、textarea、select 等元素上创立双向数据绑定,咱们晓得 v-model 实质上不过是语法糖,v-model 在外部为不同的输出元素应用不同的属性并抛出不同的事件:
- text 和 textarea 元素应用 value 属性和 input 事件;
- checkbox 和 radio 应用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
父组件:<ModelChild v-model="message"></ModelChild>
子组件:<div>{{value}}</div>
props:{value: String},
methods: {test1(){this.$emit('input', '小红')
},
},
Vue 修饰符有哪些
vue 中修饰符分为以下五种
- 表单修饰符
- 事件修饰符
- 鼠标按键修饰符
- 键值修饰符
v-bind
修饰符
1. 表单修饰符
在咱们填写表单的时候用得最多的是 input
标签,指令用得最多的是v-model
对于表单的修饰符有如下:
lazy
在咱们填完信息,光标来到标签的时候,才会将值赋予给 value
,也就是在change
事件之后再进行信息同步
<input type="text" v-model.lazy="value">
<p>{{value}}</p>
trim
主动过滤用户输出的首空格字符,而两头的空格不会过滤
<input type="text" v-model.trim="value">
number
主动将用户的输出值转为数值类型,但如果这个值无奈被 parseFloat
解析,则会返回原来的值
<input v-model.number="age" type="number">
2. 事件修饰符
事件修饰符是对事件捕捉以及指标进行了解决,有如下修饰符
.stop
阻止了事件冒泡,相当于调用了event.stopPropagation
办法
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>
// 只输入 1
.prevent
阻止了事件的默认行为,相当于调用了event.preventDefault
办法
<form v-on:submit.prevent="onSubmit"></form>
.capture
应用事件捕捉模式,使事件触发从蕴含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
// 输入构造: 1 2 4 3
.self
只当在event.target
是以后元素本身时触发处理函数
<div v-on:click.self="doThat">...</div>
应用修饰符时,程序很重要;相应的代码会以同样的程序产生。因而,用
v-on:click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素本身的点击
.once
绑定了事件当前只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
.passive
通知浏览器你不想阻止事件的默认行为
在挪动端,当咱们在监听元素滚动事件的时候,会始终触发 onscroll
事件会让咱们的网页变卡,因而咱们应用这个修饰符的时候,相当于给 onscroll
事件整了一个 .lazy
修饰符
<!-- 滚动事件的默认行为 (即滚动行为) 将会立刻触发 -->
<!-- 而不会期待 `onScroll` 实现 -->
<!-- 这其中蕴含 `event.preventDefault()` 的状况 -->
<div v-on:scroll.passive="onScroll">...</div>
- 不要把
.passive
和.prevent
一起应用, 因为.prevent
将会被疏忽,同时浏览器可能会向你展现一个正告。passive
会通知浏览器你不想阻止事件的默认行为
native
让组件变成像html
内置标签那样监听根元素的原生事件,否则组件上应用v-on
只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>
<!-- 应用.native 修饰符来操作一般 HTML 标签是会令事件生效的 -->
3. 鼠标按钮修饰符
鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:
.left
左键点击.right
右键点击.middle
中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>
4. 键盘事件的修饰符
键盘修饰符是用来润饰键盘事件(onkeyup
,onkeydown
)的,有如下:
keyCode
存在很多,但 vue 为咱们提供了别名,分为以下两种:
- 一般键(
enter
、tab
、delete
、space
、esc
、up
、down
、left
、right
…) - 零碎润饰键(
ctrl
、alt
、meta
、shift
…)
<!-- 只有按键为 keyCode 的时候才触发 -->
<input type="text" @keyup.keyCode="shout()">
还能够通过以下形式自定义一些全局的键盘码别名
Vue.config.keyCodes.f2 = 113
5. v-bind 修饰符
v-bind
修饰符次要是为属性进行操作,用来别离有如下:
- async 能对
props
进行一个双向绑定
// 父组件
<comp :myMessage.sync="bar"></comp>
// 子组件
this.$emit('update:myMessage',params);
以上这种办法相当于以下的简写
// 父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){this.bar = e;}
// 子组件 js
func2(){this.$emit('update:myMessage',params);
}
应用 async
须要留神以下两点:
- 应用
sync
的时候,子组件传递的事件名格局必须为update:value
,其中value
必须与子组件中props
中申明的名称完全一致 - 留神带有
.sync
修饰符的v-bind
不能和表达式一起应用 - prop 设置自定义标签属性,防止裸露数据,避免净化 HTML 构造
<input id="uid" title="title1" value="1" :index.prop="index">
- camel 将命名变为驼峰命名法,如将
view-Box
属性名转换为viewBox
<svg :viewBox="viewBox"></svg>
利用场景
依据每一个修饰符的性能,咱们能够失去以下修饰符的利用场景:
.stop
:阻止事件冒泡.native
:绑定原生事件.once
:事件只执行一次.self
:将事件绑定在本身身上,相当于阻止事件冒泡.prevent
:阻止默认事件.caption
:用于事件捕捉.once
:只触发一次.keyCode
:监听特定键盘按下.right
:右键
Vue 子组件和父组件执行程序
加载渲染过程:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
更新过程:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
销毁过程:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 destoryed
Vue 的基本原理
当一个 Vue 实例创立时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0 应用 proxy)将它们转为 getter/setter,并且在外部追踪相干依赖,在属性被拜访和批改时告诉变动。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会告诉 watcher 从新计算,从而以致它关联的组件得以更新。
实现双向绑定
咱们还是以 Vue
为例,先来看看 Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化解决,这个过程产生Observe
中- 同时对模板执行编译,找到其中动静绑定的数据,从
data
中获取并初始化视图,这个过程产生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,未来对应数据变动时Watcher
会调用更新函数 - 因为
data
的某个key
在⼀个视图中可能呈现屡次,所以每个key
都须要⼀个管家Dep
来治理多个Watcher
- 未来 data 中数据⼀旦发生变化,会首先找到对应的
Dep
,告诉所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对 data
执行响应化解决
class Vue {constructor(options) {
this.$options = options;
this.$data = options.data;
// 对 data 选项做响应式解决
observe(this.$data);
// 代理 data 到 vm 上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
对 data
选项执行响应化具体操作
function observe(obj) {if (typeof obj !== "object" || obj == null) {return;}
new Observer(obj);
}
class Observer {constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {Object.keys(obj).forEach((key) => {defineReactive(obj, key, obj[key]);
});
}
}
编译Compile
对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数
class Compile {constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取 dom
if (this.$el) {this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {return node.nodeType == 1;}
isInterpolation(node) {return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依赖收集
视图中会用到 data
中某 key
,这称为依赖。同⼀个key
可能呈现屡次,每次都须要收集进去用⼀个 Watcher
来保护它们,此过程称为依赖收集多个 Watcher
须要⼀个 Dep
来治理,须要更新时由 Dep
统⼀告诉
实现思路
defineReactive
时为每⼀个key
创立⼀个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创立⼀个watcher1
- 因为触发
name1
的getter
办法,便将watcher1
增加到name1
对应的Dep
中 - 当
name1
更新,setter
触发时,便可通过对应Dep
告诉其治理所有Watcher
更新
// 负责更新视图
class Watcher {constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创立实例时,把以后实例指定到 Dep.target 动态属性上
Dep.target = this
// 读一下 key,触发 get
vm[key]
// 置空
Dep.target = null
}
// 将来执行 dom 更新函数,由 dep 调用的
update() {this.updaterFn.call(this.vm, this.vm[this.key])
}
}
申明Dep
class Dep {constructor() {this.deps = []; // 依赖治理
}
addDep(dep) {this.deps.push(dep);
}
notify() {this.deps.forEach((dep) => dep.update());
}
}
创立 watcher
时触发getter
class Watcher {constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创立 Dep
实例
function defineReactive(obj, key, val) {this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {get() {Dep.target && dep.addDep(Dep.target);// Dep.target 也就是 Watcher 实例
return val;
},
set(newVal) {if (newVal === val) return;
dep.notify(); // 告诉 dep 执行更新办法},
});
}
v-model 实现原理
咱们在
vue
我的项目中次要应用v-model
指令在表单input
、textarea
、select
等元素上创立双向数据绑定,咱们晓得v-model
实质上不过是语法糖(能够看成是value + input
办法的语法糖),v-model
在外部为不同的输出元素应用不同的属性并抛出不同的事件:
text
和textarea
元素应用value
属性和input
事件checkbox
和radio
应用checked
属性和change
事件select
字段将value
作为prop
并将change
作为事件
所以咱们能够 v -model 进行如下改写:
<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />
当在
input
元素中应用v-model
实现双数据绑定,其实就是在输出的时候触发元素的input
事件,通过这个语法糖,实现了数据的双向绑定
- 这个语法糖必须是固定的,也就是说属性必须为
value
,办法名必须为:input
。 - 晓得了
v-model
的原理,咱们能够在自定义组件上实现v-model
//Parent
<template>
{{num}}
<Child v-model="num">
</template>
export default {data(){
return {num: 0}
}
}
//Child
<template>
<div @click="add">Add</div>
</template>
export default {props: ['value'], // 属性必须为 value
methods:{add(){
// 办法名为 input
this.$emit('input', this.value + 1)
}
}
}
原理
会将组件的 v-model
默认转化成value+input
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>');
// 察看输入的渲染函数:// with(this) {
// return _c('el-checkbox', {
// model: {// value: (check),
// callback: function ($$v) {check = $$v},
// expression: "check"
// }
// })
// }
// 源码地位 core/vdom/create-component.js line:155
function transformModel (options, data: any) {const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {on[event] = [callback].concat(existing)
}
} else {on[event] = callback
}
}
原生的 v-model
,会依据标签的不同生成不同的事件和属性
const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');
// with(this) {
// return _c('input', {// directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }],
// domProps: {"value": (value) },
// on: {"input": function ($event) {// if ($event.target.composing) return;
// value = $event.target.value
// }
// }
// })
// }
编译时:不同的标签解析出的内容不一样
platforms/web/compiler/directives/model.js
if (el.component) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime
return false
}
运行时:会对元素解决一些对于输入法的问题
platforms/web/runtime/directives/model.js
inserted (el, binding, vnode, oldVnode) {if (vnode.tag === 'select') { // #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {mergeVNodeHook(vnode, 'postpatch', () => {directive.componentUpdated(el, binding, vnode)
})
} else {setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */
if (isIE9) {el.vmodel = true}
}
}
}