乐趣区

关于前端:美团前端常见vue面试题必备

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-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 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 对象,指定 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;}

// 子组件 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 子组件和父组件执行程序

加载渲染过程:

  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: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}
        }
    }
}
退出移动版