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-onceVue中内置指令,很有用的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-oncevue的内置指令,作用是仅渲染指定组件或元素一次,并跳过将来对其更新
  • 如果咱们有一些元素或者组件在初始化渲染之后不再须要变动,这种状况下适宜应用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整体策略为:深度优先,同层比拟

  1. 比拟只会在同层级进行, 不会跨层级比拟

  1. 比拟的过程中,循环从两边向两头收拢

上面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

第一次循环后,发现旧节点D与新节点D雷同,间接复用旧节点D作为diff后的第一个实在节点,同时旧节点endIndex挪动到C,新节点的 startIndex 挪动到了 C

第二次循环后,同样是旧节点的开端和新节点的结尾(都是 C)雷同,同理,diff 后创立了 C 的实在节点插入到第一次创立的 D 节点前面。同时旧节点的 endIndex 挪动到了 B,新节点的 startIndex 挪动到了 E

第三次循环中,发现E没有找到,这时候只能间接创立新的实在节点 E,插入到第二次创立的 C 节点之后。同时新节点的 startIndex 挪动到了 A。旧节点的 startIndexendIndex 都放弃不动

第四次循环中,发现了新旧节点的结尾(都是 A)雷同,于是 diff 后创立了 A 的实在节点,插入到前一次创立的 E 节点前面。同时旧节点的 startIndex 挪动到了 B,新节点的startIndex 挪动到了 B

第五次循环中,情景同第四次循环一样,因而 diff 后创立了 B 实在节点 插入到前一次创立的 A 节点前面。同时旧节点的 startIndex挪动到了 C,新节点的 startIndex 挪动到了 F

新节点的 startIndex 曾经大于 endIndex 了,须要创立 newStartIdxnewEndIdx 之间的所有节点,也就是节点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函数前两个参数位为oldVnodeVnode ,别离代表新的节点和之前的旧节点,次要做了四个判断:

  • 没有新节点,间接触发旧节点的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
  • 如果都不满足以上四种情景,那阐明没有雷同的节点能够复用,则会分为以下两种状况:

    • 从旧的 VNodekey 值,对应 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在兼顾vue2options 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);}

咱们只须要调用这个函数,即可获取xy的坐标,齐全不必关注实现过程

试想一下,如果很多相似的第三方库,咱们只须要调用即可,不用关注实现过程,开发效率大大提高

同时,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为了解决这个问题提供了 setdelete实例办法,然而对于用户来说,还是减少了肯定的心智累赘

同时在面对嵌套层级比拟深的状况下,就存在性能问题

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对象,指定pathnameparams等信息
  • 如果页面中简略示意跳转链接,应用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事件循环分割严密。次要应用了宏工作微工作(setTimeoutpromise那些),定义了一个异步办法,屡次调用nextTick会将办法存入队列,通过异步办法清空以后队列。

computed 和 watch 的区别和使用的场景?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值产生扭转,下一次获取 computed 的值时才会从新计算 computed 的值;

watch: 更多的是「察看」的作用,相似于某些数据的监听回调 ,每当监听的数据变动时都会执行回调进行后续操作;

使用场景:

  • 当咱们须要进行数值计算,并且依赖于其它数据时,应该应用 computed,因为能够利用 computed 的缓存个性,防止每次获取值时,都要从新计算;
  • 当咱们须要在数据变动时执行异步或开销较大的操作时,应该应用 watch,应用 watch 选项容许咱们执行异步操作 ( 拜访一个 API ),限度咱们执行该操作的频率,并在咱们失去最终后果前,设置中间状态。这些都是计算属性无奈做到的。

Vuex 为什么要分模块并且加命名空间

  • 模块 : 因为应用繁多状态树,利用的所有状态会集中到一个比拟大的对象。当利用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 容许咱们将 store 宰割成模块(module)。每个模块领有本人的 statemutationactiongetter、甚至是嵌套子模块
  • 命名空间 :默认状况下,模块外部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块可能对同一 mutationaction 作出响应。如果心愿你的模块具备更高的封装度和复用性,你能够通过增加 namespaced: true 的形式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会主动依据模块注册的门路调整命名

什么是 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. 键盘事件的修饰符

键盘修饰符是用来润饰键盘事件(onkeyuponkeydown)的,有如下:

keyCode存在很多,但vue为咱们提供了别名,分为以下两种:

  • 一般键entertabdeletespaceescupdownleftright...)
  • 零碎润饰键ctrlaltmetashift...)
<!-- 只有按键为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 子组件和父组件执行程序

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed

Vue的基本原理

当一个Vue实例创立时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0应用proxy )将它们转为 getter/setter,并且在外部追踪相干依赖,在属性被拜访和批改时告诉变动。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会告诉watcher从新计算,从而以致它关联的组件得以更新。

实现双向绑定

咱们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化解决,这个过程产生Observe
  2. 同时对模板执行编译,找到其中动静绑定的数据,从data中获取并初始化视图,这个过程产生在Compile
  3. 同时定义⼀个更新函数和Watcher,未来对应数据变动时Watcher会调用更新函数
  4. 因为data的某个key在⼀个视图中可能呈现屡次,所以每个key都须要⼀个管家Dep来治理多个Watcher
  5. 未来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统⼀告诉

实现思路

  1. defineReactive时为每⼀个key创立⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创立⼀个watcher1
  3. 因为触发name1getter办法,便将watcher1增加到name1对应的Dep
  4. 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 指令在表单 inputtextareaselect 等元素上创立双向数据绑定,咱们晓得 v-model 实质上不过是语法糖(能够看成是value + input办法的语法糖),v-model 在外部为不同的输出元素应用不同的属性并抛出不同的事件:
  • texttextarea 元素应用 value 属性和 input 事件
  • checkboxradio 应用 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             }        }    }}