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-lookupif (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 pathrouter.push('/users/1')// object with pathrouter.push({ path: '/users/1' })// named route with params to let the router build the urlrouter.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;}//子组件jsfunc2(){ 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:155function 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 } } }}