关于前端:美团前端常见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 
            }
        }
    }
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理