关于vue.js:社招前端一面必会vue面试题

0次阅读

共计 29228 个字符,预计需要花费 74 分钟才能阅读完成。

DIFF 算法的原理

在新老虚构 DOM 比照时:

  • 首先,比照节点自身,判断是否为同一节点,如果不为雷同节点,则删除该节点从新创立节点进行替换
  • 如果为雷同节点,进行 patchVnode,判断如何对该节点的子节点进行解决,先判断一方有子节点一方没有子节点的状况(如果新的 children 没有子节点,将旧的子节点移除)
  • 比拟如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff 外围)。
  • 匹配时,找到雷同的子节点,递归比拟子节点

在 diff 中,只对同层的子节点进行比拟,放弃跨级的节点比拟,使得工夫简单从 O(n3)升高值 O(n),也就是说,只有当新旧 children 都为多个子节点时才须要用外围的 Diff 算法进行同层级比拟。

vuex 是什么?怎么应用?哪种性能场景应用它?

Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源寄存地,对应于个别 vue 对象外面的 data 外面寄存的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据产生扭转,依赖这相数据的组件也会产生更新它通过 mapState 把全局的 stategetters 映射到以后组件的 computed 计算属性

  • vuex 个别用于中大型 web 单页利用中对利用的状态进行治理,对于一些组件间关系较为简单的小型利用,应用 vuex 的必要性不是很大,因为齐全能够用组件 prop 属性或者事件来实现父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 应用 Vuex 解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据齐全独立于组件,因而将组件间共享的数据置于 State 中能无效解决多层级组件嵌套的跨组件通信问题

vuexState 在单页利用的开发中自身具备一个“数据库”的作用,能够将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,个别什么样的数据会放在 State 中呢?目前次要有两种数据会应用 vuex 进行治理:

  • 组件之间全局共享的数据
  • 通过后端异步申请的数据

包含以下几个模块

  • stateVuex 应用繁多状态树, 即每个利用将仅仅蕴含一个store 实例。外面寄存的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据产生扭转,依赖这相数据的组件也会产生更新。它通过 mapState 把全局的 stategetters 映射到以后组件的 computed 计算属性
  • mutations:更改 Vuexstore中的状态的惟一办法是提交mutation
  • gettersgetter 能够对 state 进行计算操作,它就是 store 的计算属性尽管在组件内也能够做计算属性,然而 getters 能够在多给件之间复用如果一个状态只在一个组件内应用,是能够不必 getters
  • actionaction 相似于 muation, 不同在于:action 提交的是 mutation, 而不是间接变更状态action 能够蕴含任意异步操作
  • modules:面对简单的应用程序,当治理的状态比拟多时;咱们须要将 vuexstore对象宰割成模块(modules)

modules:我的项目特地简单的时候,能够让每一个模块领有本人的statemutationactiongetters,使得构造十分清晰,方便管理

答复范例

思路

  • 给定义
  • 必要性论述
  • 何时应用
  • 拓展:一些集体思考、实践经验等

答复范例

  1. Vuex 是一个专为 Vue.js 利用开发的 状态管理模式 + 库。它采纳集中式存储,治理利用的所有组件的状态,并以相应的规定保障状态以一种可预测的形式发生变化。
  2. 咱们期待以一种简略的“单向数据流”的形式治理利用,即 状态 -> 视图 -> 操作单向循环 的形式。但当咱们的利用遇到多个组件共享状态时,比方:多个视图依赖于同一状态或者来自不同视图的行为须要变更同一状态。此时单向数据流的简洁性很容易被毁坏。因而,咱们有必要把组件的共享状态抽取进去,以一个全局单例模式治理。通过定义和隔离状态治理中的各种概念并通过强制规定维持视图和状态间的独立性,咱们的代码将会变得更结构化且易保护。这是 vuex 存在的必要性,它和 react 生态中的 redux 之类是一个概念
  3. Vuex 解决状态治理的同时引入了不少概念:例如 statemutationaction 等,是否须要引入还须要依据利用的理论状况掂量一下:如果不打算开发大型单页利用,应用 Vuex 反而是繁琐冗余的,一个简略的 store 模式就足够了。然而,如果要构建一个中大型单页利用,Vuex 根本是标配。
  4. 我在应用 vuex 过程中感触到一些等

可能的诘问

  1. vuex有什么毛病吗?你在开发过程中有遇到什么问题吗?
  2. 刷新浏览器,vuex中的 state 会从新变为初始状态。解决方案 - 插件 vuex-persistedstate
  3. actionmutation 的区别是什么?为什么要辨别它们?
  • action中解决异步,mutation不能够
  • mutation做原子操作
  • action能够整合多个 mutation 的汇合
  • mutation 是同步更新数据(外部会进行是否为异步形式更新数据的检测) $watch 严格模式下会报错
  • action 异步操作,能够获取数据后调佣 mutation 提交最终数据
  • 流程程序:“相应视图—> 批改 State”拆分成两局部,视图触发ActionAction 再触发Mutation`。
  • 基于流程程序,二者表演不同的角色:Mutation:专一于批改 State,实践上是批改State 的惟一路径。Action:业务代码、异步申请
  • 角色不同,二者有不同的限度:Mutation:必须同步执行。Action:能够异步,但不能间接操作State

组件中写 name 属性的益处

能够标识组件的具体名称不便调试和查找对应属性

// 源码地位 src/core/global-api/extend.js

// enable recursive self-lookup
if (name) {Sub.options.components[name] = Sub // 记录本人 在组件中递归本人  -> jsx
}

实现双向绑定

咱们还是以 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 执行更新办法},  
  });  
}  

watch 原理

watch 实质上是为每个监听属性 setter 创立了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下

  • deep:深度监听对象,为对象的每一个属性创立一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。次要起因在于对象属于援用类型,单个属性的更新并不会触发对象 setter,因而引入 deep 可能很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,防止性能节约。
  • immediate:在初始化时间接调用回调函数,能够通过在 created 阶段手动调用回调函数实现雷同的成果

Vue computed 实现

  • 建设与其余属性(如:dataStore)的分割;
  • 属性扭转后,告诉计算属性从新计算

实现时,次要如下

  • 初始化 data,应用 Object.defineProperty 把这些属性全副转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,应用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性从新计算。
  • 若呈现以后 computed 计算属性嵌套其余 computed 计算属性时,先进行其余的依赖收集

参考 前端进阶面试题具体解答

keep-alive 应用场景和原理

  • keep-aliveVue 内置的一个组件,能够实现组件缓存 ,当组件切换时不会对以后组件进行卸载。 个别联合路由和动静组件一起应用,用于缓存组件
  • 提供 includeexclude 属性,容许组件有条件的进行缓存。两者都反对字符串或正则表达式,include 示意只有名称匹配的组件会被缓存,exclude 示意任何名称匹配的组件都不会被缓存,其中 exclude 的优先级比 include
  • 对应两个钩子函数 activateddeactivated,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated
  • keep-alive 的中还使用了 LRU(最近起码应用) 算法,抉择最近最久未应用的组件予以淘汰
  • <keep-alive></keep-alive> 包裹动静组件时,会缓存不流动的组件实例, 次要用于保留组件状态或防止从新渲染
  • 比方有一个列表和一个详情,那么用户就会常常执行关上详情 => 返回列表 => 关上详情…这样的话列表和详情都是一个频率很高的页面,那么就能够对列表组件应用 <keep-alive></keep-alive> 进行缓存,这样用户每次返回列表的时候,都能从缓存中疾速渲染,而不是从新渲染

对于 keep-alive 的根本用法

<keep-alive>
  <component :is="view"></component>
</keep-alive>

应用 includesexclude

<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (应用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (应用 `v-bind`) -->
<keep-alive :include="['a','b']">
  <component :is="view"></component>
</keep-alive>

匹配首先查看组件本身的 name 选项,如果 name 选项不可用,则匹配它的部分注册名称 (父组件 components 选项的键值),匿名组件不能被匹配

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated
  • 再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > deactivated

应用场景

应用准则:当咱们在某些场景下不须要让页面从新加载时咱们能够应用keepalive

举个栗子:

当咱们从 首页 –> 列表页 –> 商详页 –> 再返回,这时候列表页应该是须要keep-alive

首页 –> 列表页 –> 商详页 –> 返回到列表页 (须要缓存)–> 返回到首页 (须要缓存)–> 再次进入列表页(不须要缓存),这时候能够按需来管制页面的keep-alive

在路由中设置 keepAlive 属性判断是否须要缓存

{
  path: 'list',
  name: 'itemList', // 列表页
  component (resolve) {require(['@/pages/item/list'], resolve)
 },
 meta: {
  keepAlive: true,
  title: '列表页'
 }
}

应用<keep-alive>

<div id="app" class='wrapper'>
    <keep-alive>
        <!-- 须要缓存的视图组件 --> 
        <router-view v-if="$route.meta.keepAlive"></router-view>
     </keep-alive>
      <!-- 不须要缓存的视图组件 -->
     <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

思考题:缓存后如何获取数据

解决方案能够有以下两种:

  • beforeRouteEnter:每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
    next(vm=>{console.log(vm)
        // 每次进入路由执行
        vm.getData()  // 获取数据})
},
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
// 留神:服务器端渲染期间 avtived 不被调用
activated(){this.getData() // 获取数据
},

扩大补充:LRU 算法是什么?

LRU 的核心思想是如果数据最近被拜访过,那么未来被拜访的几率也更高,所以咱们将命中缓存的组件 key 从新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据行将来被拜访几率越低,所以当缓存数量达到最大值时,咱们就删除未来被拜访几率最低的数据,即 this.keys 中第一个缓存的组件

相干代码

keep-alivevue 中内置的一个组件

源码地位:src/core/components/keep-alive.js

export default {
  name: "keep-alive",
  abstract: true, // 形象组件

  props: {
    include: patternTypes, // 要缓存的组件
    exclude: patternTypes, // 要排除的组件
    max: [String, Number], // 最大缓存数
  },

  created() {this.cache = Object.create(null); // 缓存对象  {a:vNode,b:vNode}
    this.keys = []; // 缓存组件的 key 汇合 [a,b]
  },

  destroyed() {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    // 动静监听 include  exclude
    this.$watch("include", (val) => {pruneCache(this, (name) => matches(val, name));
    });
    this.$watch("exclude", (val) => {pruneCache(this, (name) => !matches(val, name));
    });
  },

  render() {
    const slot = this.$slots.default; // 获取包裹的插槽默认值 获取默认插槽中的第一个组件节点
    const vnode: VNode = getFirstComponentChild(slot); // 获取第一个子组件
    // 获取该组件节点的 componentOptions
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // 获取该组件节点的名称,优先获取组件的 name 字段,如果 name 不存在则获取组件的 tag
      const name: ?string = getComponentName(componentOptions);
      const {include, exclude} = this;
      // 不走缓存 如果 name 不在 inlcude 中或者存在于 exlude 中则示意不缓存,间接返回 vnode
      if (
        // not included  不蕴含
        (include && (!name || !matches(include, name))) ||
        // excluded  排除外面
        (exclude && name && matches(exclude, name))
      ) {
        // 返回虚构节点
        return vnode;
      }

      const {cache, keys} = this;
      // 获取组件的 key 值
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      // 拿到 key 值后去 this.cache 对象中去寻找是否有该值,如果有则示意该组件有缓存,即命中缓存
      if (cache[key]) {
        // 通过 key 找到缓存 获取实例
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key); // 通过 LRU 算法把数组外面的 key 删掉
        keys.push(key); // 把它放在数组开端
      } else {cache[key] = vnode; // 没找到就换存下来
        keys.push(key); // 把它放在数组开端
        // prune oldest entry  // 如果超过最大值就把数组第 0 项删掉
        if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }

      vnode.data.keepAlive = true; // 标记虚构节点曾经被缓存
    }
    // 返回虚构节点
    return vnode || (slot && slot[0]);
  },
};

能够看到该组件没有 template,而是用了render,在组件渲染的时候会主动执行render 函数

this.cache是一个对象,用来存储须要缓存的组件,它将以如下模式存储:

this.cache = {
  'key1':'组件 1',
  'key2':'组件 2',
  // ...
}

在组件销毁的时候执行 pruneCacheEntry 函数

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {const cached = cache[key]
  /* 判断以后没有处于被渲染状态的组件,将其销毁 */
  if (cached && (!current || cached.tag !== current.tag)) {cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

mounted 钩子函数中观测 includeexclude 的变动,如下:

mounted () {
  this.$watch('include', val => {pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))
  })
}

如果 includeexclude 产生了变动,即示意定义须要缓存的组件的规定或者不须要缓存的组件的规定产生了变动,那么就执行pruneCache 函数,函数如下

function pruneCache (keepAliveInstance, filter) {const { cache, keys, _vnode} = keepAliveInstance
  for (const key in cache) {const cachedNode = cache[key]
    if (cachedNode) {const name = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

在该函数内对 this.cache 对象进行遍历,取出每一项的 name 值,用其与新的缓存规定进行匹配,如果匹配不上,则示意在新的缓存规定下该组件曾经不须要被缓存,则调用 pruneCacheEntry 函数将其从 this.cache 对象剔除即可

对于 keep-alive 的最弱小缓存性能是在 render 函数中实现

首先获取组件的 key 值:

const key = vnode.key == null? 
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key

拿到 key 值后去 this.cache 对象中去寻找是否有该值,如果有则示意该组件有缓存,即命中缓存,如下:

/* 如果命中缓存,则间接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {vnode.componentInstance = cache[key].componentInstance
    /* 调整该组件 key 的程序,将其从原来的中央删掉并从新放在最初一个 */
    remove(keys, key)
    keys.push(key)
} 

间接从缓存中拿 vnode 的组件实例,此时从新调整该组件 key 的程序,将其从原来的中央删掉并从新放在 this.keys 中最初一个

this.cache对象中没有该 key 值的状况,如下:

/* 如果没有命中缓存,则将其设置进缓存 */
else {cache[key] = vnode
    keys.push(key)
    /* 如果配置了 max 并且缓存的长度超过了 this.max,则从缓存中删除第一个 */
    if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
}

表明该组件还没有被缓存过,则以该组件的 key 为键,组件 vnode 为值,将其存入 this.cache 中,并且把 key 存入 this.keys

此时再判断 this.keys 中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉

Vue 组件渲染和更新过程

渲染组件时,会通过 Vue.extend 办法构建子组件的构造函数,并进行实例化。最终手动调用 $mount() 进行挂载。更新组件时会进行 patchVnode 流程,外围就是diff 算法

Vue 中如何扩大一个组件

此题属于实际题,考查大家对 vue 罕用 api 应用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同

答题思路:

  • 依照逻辑扩大和内容扩大来列举

    • 逻辑扩大有:mixinsextendscomposition api
    • 内容扩大有slots
  • 别离说出他们应用办法、场景差别和问题。
  • 作为扩大,还能够说说 vue3 中新引入的 composition api 带来的变动

答复范例:

  1. 常见的组件扩大办法有:mixinsslotsextends
  2. 混入 mixins 是散发 Vue 组件中可复用性能的非常灵活的形式。混入对象能够蕴含任意组件选项。当组件应用混入对象时,所有混入对象的选项将被混入该组件自身的选项
// 复用代码:它是一个配置对象,选项和组件外面一样
const mymixin = {
   methods: {dosomething(){}}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)

// 部分混入:做数组项设置到 mixins 选项,仅作用于以后组件
const Comp = {mixins: [mymixin]
}
  1. 插槽次要用于 vue 组件中的内容散发,也能够用于组件扩大

子组件 Child

<div>
  <slot> 这个内容会被父组件传递的内容替换 </slot>
</div>

父组件 Parent

<div>
   <Child> 来自父组件内容 </Child>
</div>

如果要准确散发到不同地位能够应用 具名插槽,如果要应用子组件中的数据能够应用作用域插槽

  1. 组件选项中还有一个不太罕用的选项extends,也能够起到扩大组件的目标
// 扩大对象
const myextends = {
   methods: {dosomething(){}}
}
// 组件扩大:做数组项设置到 extends 选项,仅作用于以后组件
// 跟混入的不同是它只能扩大单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {extends: myextends}
  1. 混入的数据和办法不能明确判断起源且可能和以后组件内变量产生命名抵触,vue3中引入的 composition api,能够很好解决这些问题,利用独立进去的响应式模块能够很不便的编写独立逻辑并提供响应式的数据,而后在setup 选项中组合应用,加强代码的可读性和维护性。例如
// 复用逻辑 1
function useXX() {}
// 复用逻辑 2
function useYY() {}
// 逻辑组合
const Comp = {setup() {const {xx} = useXX()
      const {yy} = useYY()
      return {xx, yy}
   }
}

异步组件是什么?应用场景有哪些?

剖析

因为异步路由的存在,咱们应用异步组件的次数比拟少,因而还是有必要两者的不同。

体验

大型利用中,咱们须要宰割利用为更小的块,并且在须要组件时再加载它们

import {defineAsyncComponent} from 'vue'
// defineAsyncComponent 定义异步组件,返回一个包装组件。包装组件依据加载器的状态决定渲染什么内容
const AsyncComp = defineAsyncComponent(() => {
  // 加载函数返回 Promise
  return new Promise((resolve, reject) => {
    // ... 能够从服务器加载组件
    resolve(/* loaded component */)
  })
})
// 借助打包工具实现 ES 模块动静导入
const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

答复范例

  1. 在大型利用中,咱们须要宰割利用为更小的块,并且在须要组件时再加载它们。
  2. 咱们不仅能够在路由切换时懒加载组件,还能够在页面组件中持续应用异步组件,从而实现更细的宰割粒度。
  3. 应用异步组件最简略的形式是间接给 defineAsyncComponent 指定一个 loader 函数,联合 ES 模块动静导入函数 import 能够疾速实现。咱们甚至能够指定 loadingComponenterrorComponent选项从而给用户一个很好的加载反馈。另外 Vue3 中还能够联合 Suspense 组件应用异步组件。
  4. 异步组件容易和路由懒加载混同,实际上不是一个货色。异步组件不能被用于定义懒加载路由上,解决它的是 vue 框架,解决路由组件加载的是vue-router。然而能够在懒加载的路由组件中应用异步组件

Vue 中修饰符.sync 与 v -model 的区别

sync的作用

  • .sync修饰符能够实现父子组件之间的双向绑定,并且能够实现子组件同步批改父组件的值,相比拟与 v-model 来说,sync修饰符就简略很多了
  • 一个组件上能够有多个 .sync 修饰符
<!-- 失常父传子 -->
<Son :a="num" :b="num2" />

<!-- 加上 sync 之后的父传子 -->
<Son :a.sync="num" :b.sync="num2" />

<!-- 它等价于 -->
<Son 
  :a="num" 
  :b="num2" 
  @update:a="val=>num=val" 
  @update:b="val=>num2=val" 
/>

<!-- 相当于多了一个事件监听,事件名是 update:a, -->
<!-- 回调函数中,会把接管到的值赋值给属性绑定的数据项中。-->

v-model的工作原理

<com1 v-model="num"></com1>
<!-- 等价于 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
  • 相同点

    • 都是语法糖,都能够实现父子组件中的数据的双向通信
  • 区别点

    • 格局不同:v-model="num", :num.sync="num"
    • v-model: @input + value
    • :num.sync: @update:num
    • v-model只能用一次;.sync能够有多个

函数式组件劣势和原理

函数组件的特点

  1. 函数式组件须要在申明组件是指定 functional:true
  2. 不须要实例化,所以没有 this,this 通过 render 函数的第二个参数 context 来代替
  3. 没有生命周期钩子函数,不能应用计算属性,watch
  4. 不能通过 $emit 对外裸露事件,调用事件只能通过context.listeners.click 的形式调用内部传入的事件
  5. 因为函数式组件是没有实例化的,所以在内部通过 ref 去援用组件时,理论援用的是HTMLElement
  6. 函数式组件的 props 能够不必显示申明,所以没有在 props 外面申明的属性都会被主动隐式解析为 prop, 而一般组件所有未声明的属性都解析到$attrs 外面,并主动挂载到组件根元素下面 (能够通过inheritAttrs 属性禁止)

长处

  1. 因为函数式组件不须要实例化,无状态,没有生命周期,所以渲染性能要好于一般组件
  2. 函数式组件构造比较简单,代码构造更清晰

应用场景:

  • 一个简略的展现组件,作为容器组件应用 比方 router-view 就是一个函数式组件
  • “高阶组件”——用于接管一个组件作为参数,返回一个被包装过的组件

例子

Vue.component('functional',{ // 构造函数产生虚构节点的
    functional:true, // 函数式组件 // data={attrs:{}}
    render(h){return h('div','test')
    }
})
const vm = new Vue({el: '#app'})

源码相干

// functional component
if (isTrue(Ctor.options.functional)) { // 带有 functional 的属性的就是函数式组件
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 解决原生事件

// install component management hooks onto the placeholder node
installComponentHooks(data) // 装置组件相干钩子(函数式组件没有调用此办法,从而性能高于一般组件)

Vue 中 v -html 会导致哪些问题

  • 可能会导致 xss 攻打
  • v-html 会替换掉标签外部的子元素
let template = require('vue-template-compiler'); 
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) 

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} 
console.log(r.render);

// _c 定义在 core/instance/render.js 
// _s 定义在 core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {if (vnode.children) vnode.children.length = 0 
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property 
    if (elm.childNodes.length === 1) {elm.removeChild(elm.childNodes[0]) 
    } 
}

双向绑定的原理是什么

咱们都晓得 Vue 是数据双向绑定的框架,双向绑定由三个重要局部形成

  • 数据层(Model):利用的数据及业务逻辑
  • 视图层(View):利用的展现成果,各类 UI 组件
  • 业务逻辑层(ViewModel):框架封装的外围,它负责将数据与视图关联起来

而下面的这个分层的架构计划,能够用一个专业术语进行称说:MVVM这里的管制层的外围性能便是“数据双向绑定”。天然,咱们只需弄懂它是什么,便能够进一步理解数据绑定的原理

了解 ViewModel

它的主要职责就是:

  • 数据变动后更新视图
  • 视图变动后更新数据

当然,它还有两个次要局部组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数

Vue 为什么须要虚构 DOM?优缺点有哪些

因为在浏览器中操作 DOM是很低廉的。频繁的操作 DOM,会产生肯定的性能问题。这就是虚构 Dom 的产生起因。Vue2Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 实质就是用一个原生的 JS 对象去形容一个 DOM 节点,是对实在 DOM 的一层形象

长处:

  • 保障性能上限:框架的虚构 DOM 须要适配任何下层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;然而比起粗犷的 DOM 操作性能要好很多,因而框架的虚构 DOM 至多能够保障在你不须要手动优化的状况下,仍然能够提供还不错的性能,即保障性能的上限;
  • 无需手动操作 DOM:咱们不再须要手动去操作 DOM,只须要写好 View-Model 的代码逻辑,框架会依据虚构 DOM 和 数据双向绑定,帮咱们以可预期的形式更新视图,极大进步咱们的开发效率;
  • 跨平台:虚构 DOM 实质上是 JavaScript 对象, 而 DOM 与平台强相干,相比之下虚构 DOM 能够进行更不便地跨平台操作,例如服务器渲染、weex 开发等等。

毛病:

  • 无奈进行极致优化:尽管虚构 DOM + 正当的优化,足以应答绝大部分利用的性能需求,但在一些性能要求极高的利用中虚构 DOM 无奈进行针对性的极致优化。
  • 首次渲染大量 DOM 时,因为多了一层虚构 DOM 的计算,会比 innerHTML 插入慢。

虚构 DOM 实现原理?

虚构 DOM 的实现原理次要包含以下 3 局部:

  • JavaScript 对象模仿实在 DOM 树,对实在 DOM 进行形象;
  • diff 算法 — 比拟两棵虚构 DOM 树的差别;
  • pach 算法 — 将两个虚构 DOM 对象的差别利用到真正的 DOM 树。

说说你对虚构 DOM 的了解?答复范例

思路

  • vdom是什么
  • 引入 vdom 的益处
  • vdom如何生成,又如何成为dom
  • 在后续的 diff 中的作用

答复范例

  1. 虚构 dom 顾名思义就是虚构的 dom 对象,它自身就是一个 JavaScript 对象,只不过它是通过不同的属性去形容一个视图构造
  2. 通过引入 vdom 咱们能够取得如下益处:
  3. 将实在元素节点形象成 VNode,无效缩小间接操作 dom 次数,从而进步程序性能

    • 间接操作 dom 是有限度的,比方:diffclone 等操作,一个实在元素上有许多的内容,如果间接对其进行 diff 操作,会去额定 diff 一些没有必要的内容;同样的,如果须要进行 clone 那么须要将其全部内容进行复制,这也是没必要的。然而,如果将这些操作转移到 JavaScript 对象上,那么就会变得简略了
    • 操作 dom 是比拟低廉的操作,频繁的 dom 操作容易引起页面的重绘和回流,然而通过形象 VNode 进行两头解决,能够无效缩小间接操作 dom 的次数,从而缩小页面重绘和回流
  • 不便实现跨平台

    • 同一 VNode 节点能够渲染成不同平台上的对应的内容,比方:渲染在浏览器是 dom 元素节点,渲染在 Native(iOS、Android)变为对应的控件、能够实现 SSR、渲染到 WebGL 中等等
    • Vue3 中容许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染
  • vdom如何生成?在 vue 中咱们经常会为组件编写模板 – template,这个模板会被编译器 – compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用 render 函数,返回的对象就是虚构 dom。但它们还不是真正的dom,所以会在后续的patch 过程中进一步转化为dom
  1. 挂载过程完结后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件从新 render,此时就会生成新的vdom,和上一次的渲染后果diff 就能失去变动的中央,从而转换为最小量的 dom 操作,高效更新视图

为什么要用 vdom?案例解析

当初有一个场景,实现以下需要:

[{ name: "张三", age: "20", address: "北京"},    
  {name: "李四", age: "21", address: "武汉"},    
  {name: "王五", age: "22", address: "杭州"},
]

将该数据展现成一个表格,并且轻易批改一个信息,表格也跟着批改。用 jQuery 实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="container"></div>
  <button id="btn-change"> 扭转 </button>

  <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
  <script>
    const data = [{
        name: "张三",
        age: "20",
        address: "北京"
      },
      {
        name: "李四",
        age: "21",
        address: "武汉"
      },
      {
        name: "王五",
        age: "22",
        address: "杭州"
      },
    ];
    // 渲染函数
    function render(data) {const $container = $('#container');
      $container.html('');
      const $table = $('<table>');
      // 重绘一次
      $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));
      data.forEach(item => {
        // 每次进入都重绘
        $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`))
      })
      $container.append($table);
    }

    $('#btn-change').click(function () {data[1].age = 30;
      data[2].address = '深圳';
      render(data);
    });
  </script>
</body>
</html>
  • 这样点击按钮,会有相应的视图变动,然而你审查以下元素,每次改变之后,table标签都得从新创立,也就是说 table 上面的每一个栏目,不论是数据是否和原来一样,都得从新渲染,这并不是现实中的状况,当其中的一栏数据和原来一样,咱们心愿这一栏不要从新渲染,因为 DOM 重绘相当耗费浏览器性能。
  • 因而咱们采纳 JS 对象模仿的办法,将 DOM 的比对操作放在 JS 层,缩小浏览器不必要的重绘,提高效率。
  • 当然有人说虚构 DOM 并不比实在的 DOM 快,其实也是有情理的。当上述 table 中的每一条数据都扭转时,显然实在的 DOM 操作更快,因为虚构 DOM 还存在 jsdiff算法的比对过程。所以,上述性能劣势仅仅实用于大量数据的渲染并且扭转的数据只是一小部分的状况。

如下 DOM 构造:

<ul id="list">
    <li class="item">Item1</li>
    <li class="item">Item2</li>
</ul>

映射成虚构 DOM 就是这样:

{
  tag: "ul",
  attrs: {id: "list"},
  children: [
    {
      tag: "li",
      attrs: {className: "item"},
      children: ["Item1"]
    }, {
      tag: "li",
      attrs: {className: "item"},
      children: ["Item2"]
    }
  ]
} 

应用 snabbdom 实现 vdom

这是一个繁难的实现 vdom 性能的库,相比 vuereact,对于vdom 这块更加繁难,适宜咱们学习 vdomvdom 外面有两个外围的 api,一个是h 函数,一个是 patch 函数,前者用来生成 vdom 对象,后者的性能在于做虚构 dom 的比对和将 vdom 挂载到实在 DOM

简略介绍一下这两个函数的用法:

h('标签名', {属性}, [子元素])
h('标签名', {属性}, [文本])
patch(container, vnode) // container 为容器 DOM 元素
patch(vnode, newVnode)

当初咱们就来用 snabbdom 重写一下方才的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="container"></div>
  <button id="btn-change"> 扭转 </button>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
  <script>
    let snabbdom = window.snabbdom;

    // 定义 patch
    let patch = snabbdom.init([
      snabbdom_class,
      snabbdom_props,
      snabbdom_style,
      snabbdom_eventlisteners
    ]);

    // 定义 h
    let h = snabbdom.h;

    const data = [{
        name: "张三",
        age: "20",
        address: "北京"
      },
      {
        name: "李四",
        age: "21",
        address: "武汉"
      },
      {
        name: "王五",
        age: "22",
        address: "杭州"
      },
    ];
    data.unshift({name: "姓名", age: "年龄", address: "地址"});

    let container = document.getElementById('container');
    let vnode;
    const render = (data) => {let newVnode = h('table', {}, data.map(item => {let tds = [];
        for(let i in item) {if(item.hasOwnProperty(i)) {tds.push(h('td', {}, item[i] + ''));
          }
        }
        return h('tr', {}, tds);
      }));

      if(vnode) {patch(vnode, newVnode);
      } else {patch(container, newVnode);
      }
      vnode = newVnode;
    }

    render(data);

    let btnChnage = document.getElementById('btn-change');
    btnChnage.addEventListener('click', function() {data[1].age = 30;
      data[2].address = "深圳";
      //re-render
      render(data);
    })
  </script>
</body>
</html>

你会发现,只有扭转的栏目才闪动,也就是进行重绘,数据没有扭转的栏目还是放弃原样,这样就大大节俭了浏览器从新渲染的开销

vue 中应用 h 函数 生成虚构 DOM 返回

const vm = new Vue({
  el: '#app',
  data: {user: {name:'poetry'}
  },
  render(h){// h()
    // h(App)
    // h('div',[])
    let vnode = h('div',{},'hello world');
    return vnode
  }
});

</details>

应用 vue 渲染大量数据时应该怎么优化?说下你的思路!

剖析

企业级我的项目中渲染大量数据的状况比拟常见,因而这是一道十分好的综合实际题目。

答复

  1. 在大型企业级我的项目中常常须要渲染大量数据,此时很容易呈现卡顿的状况。比方大数据量的表格、树
  2. 解决时要依据状况做不同解决:
  3. 能够采取分页的形式获取,防止渲染大量数据
  • vue-virtual-scroller (opens new window)等虚构滚动计划,只渲染视口范畴内的数据
  • 如果不须要更新,能够应用 v -once 形式只渲染一次
  • 通过 v -memo (opens new window)能够缓存后果,联合 v-for 应用,防止数据变动时不必要的 VNode 创立
  • 能够采纳懒加载形式,在用户须要的时候再加载数据,比方 tree 组件子树的懒加载
  • 还是要看具体需要,首先从设计上防止大数据获取和渲染;切实须要这样做能够采纳虚表的形式优化渲染;最初优化更新,如果不须要更新能够 v-once 解决,须要更新能够 v-memo 进一步优化大数据更新性能。其余能够采纳的是交互方式优化,无线滚动、懒加载等计划

了解 Vue 运行机制全局概览

全局概览

首先咱们来看一下笔者画的外部流程图。

大家第一次看到这个图肯定是一头雾水的,没有关系,咱们来一一讲一下这些模块的作用以及调用关系。置信讲完之后大家对 Vue.js 外部运行机制会有一个大略的意识。

初始化及挂载

new Vue() 之后。Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、props、methods、data、computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「响应式 」以及「 依赖收集」,前面会具体讲到,这里只有有一个印象即可。

初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 然而存在 template 的状况,须要进行「编译」步骤。

编译

compile 编译能够分成 parseoptimizegenerate 三个阶段,最终须要失去 render function。

1. parse

parse 会用正则等形式解析 template 模板中的指令、class、style 等数据,造成 AST。

2. optimize

optimize 的次要作用是标记 static 动态节点,这是 Vue 在编译过程中的一处优化,前面当 update 更新界面时,会有一个 patch 的过程,diff 算法会间接跳过动态节点,从而缩小了比拟的过程,优化了 patch 的性能。

3. generate

generate 是将 AST 转化成 render function字符串的过程,失去后果是 render 的字符串以及 staticRenderFns 字符串。

  • 在经验过 parseoptimizegenerate 这三个阶段当前,组件中就会存在渲染 VNode 所需的 render function 了。

响应式

接下来也就是 Vue.js 响应式外围局部。

这里的 gettersetter 曾经在之前介绍过了,在 init 的时候通过 Object.defineProperty 进行了绑定,它使得当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。

  • render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集 」,「 依赖收集」的目标是将观察者 Watcher 对象寄存到以后闭包中的订阅者 Depsubs 中。造成如下所示的这样一个关系。

在批改对象的值的时候,会触发对应的 settersetter 告诉之前「依赖收集」失去的 Dep 中的每一个 Watcher,通知它们本人的值扭转了,须要从新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这两头还有一个 patch 的过程以及应用队列来异步更新的策略,这个咱们前面再讲。

Virtual DOM

咱们晓得,render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为根底的树,用对象属性来形容节点,实际上它只是一层对实在 DOM 的形象。最终能够通过一系列操作使这棵树映射到实在环境上。因为 Virtual DOM 是以 JavaScript 对象为根底而不依赖实在平台环境,所以使它具备了跨平台的能力,比如说浏览器平台、Weex、Node 等。

比如说上面这样一个例子:

{
    tag: 'div',                 /* 阐明这是一个 div 标签 */
    children: [                 /* 寄存该标签的子节点 */
        {
            tag: 'a',           /* 阐明这是一个 a 标签 */
            text: 'click me'    /* 标签的内容 */
        }
    ]
}

渲染后能够失去

<div>
    <a>click me</a>
</div>

这只是一个简略的例子,实际上的节点有更多的属性来标记节点,比方 isStatic(代表是否为动态节点)、isComment(代表是否为正文节点)等。

更新视图

  • 后面咱们说到,在批改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来批改对应的视图,那么最终是如何更新视图的呢?
  • 当数据变动后,执行 render function 就能够失去一个新的 VNode 节点,咱们如果想要失去新的视图,最简略粗犷的办法就是间接解析这个新的 VNode 节点,而后用 innerHTML 间接全副渲染到实在 DOM 中。然而其实咱们只对其中的一小块内容进行了批改,这样做仿佛有些「节约」。
  • 那么咱们为什么不能只批改那些「扭转了的中央」呢?这个时候就要介绍咱们的「patch」了。咱们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比拟,通过 diff 算法得出它们的「差别 」。最初咱们只须要将这些「 差别」的对应 DOM 进行批改即可。

再看全局

回过头再来看看这张图,是不是大脑中曾经有一个大略的脉络了呢?

怎么缓存以后的组件?缓存后怎么更新

缓存组件应用 keep-alive 组件,这是一个十分常见且有用的优化伎俩,vue3keep-alive 有比拟大的更新,能说的点比拟多

思路

  • 缓存用keep-alive,它的作用与用法
  • 应用细节,例如缓存指定 / 排除、联合 routertransition
  • 组件缓存后更新能够利用 activated 或者beforeRouteEnter
  • 原理论述

答复范例

  1. 开发中缓存组件应用 keep-alive 组件,keep-alivevue 内置组件,keep-alive包裹动静组件 component 时,会缓存不流动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,避免反复渲染DOM
<keep-alive>
  <component :is="view"></component>
</keep-alive>
  1. 联合属性 includeexclude能够明确指定缓存哪些组件或排除缓存指定组件。vue3中联合 vue-router 时变动较大,之前是 keep-alive 包裹 router-view,当初须要反过来用router-view 包裹keep-alive
<router-view v-slot="{Component}">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. 缓存后如果要获取数据,解决方案能够有以下两种
  2. beforeRouteEnter:在有 vue-router 的 我的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
  next(vm=>{console.log(vm)
    // 每次进入路由执行
    vm.getData()  // 获取数据})
},
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
activated(){this.getData() // 获取数据
},
  1. keep-alive是一个通用组件,它外部定义了一个 map,缓存创立过的组件实例,它返回的渲染函数外部会查找内嵌的component 组件对应组件的 vnode,如果该组件在map 中存在就间接返回它。因为 componentis属性是个响应式数据,因而只有它变动,keep-aliverender 函数就会从新执行

Vue.extend 作用和原理

官网解释:Vue.extend 应用根底 Vue 结构器,创立一个“子类”。参数是一个蕴含组件选项的对象。

其实就是一个子类结构器 是 Vue 组件的外围 api 实现思路就是应用原型继承的办法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

  • extend是结构一个组件的语法器。而后这个组件你能够作用到 Vue.component 这个全局注册办法里还能够在任意 vue 模板里应用组件。也能够作用到 vue 实例或者某个组件中的 components 属性中并在外部应用 apple 组件。
  • Vue.component你能够创立,也能够取组件。

相干代码如下

export default function initExtend(Vue) {
  let cid = 0; // 组件的惟一标识
  // 创立子类继承 Vue 父类 便于属性扩大
  Vue.extend = function (extendOptions) {
    // 创立子类的构造函数 并且调用初始化办法
    const Sub = function VueComponent(options) {this._init(options); // 调用 Vue 初始化办法
    };
    Sub.cid = cid++;
    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
    Sub.prototype.constructor = Sub; //constructor 指向本人
    Sub.options = mergeOptions(this.options, extendOptions); // 合并本人的 options 和父类的 options
    return Sub;
  };
}

Vue 中的过滤器理解吗?过滤器的利用场景有哪些?

过滤器本质不扭转原始数据,只是对数据进行加工解决后返回过滤后的数据再进行调用解决,咱们也能够了解其为一个纯函数

Vue 容许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废除filter

如何用

vue 中的过滤器能够用在两个中央:双花括号插值和 v-bind 表达式,过滤器应该被增加在 JavaScript 表达式的尾部,由“管道”符号批示:

<!-- 在双花括号中 -->
{message | capitalize}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

定义 filter

在组件的选项中定义本地的过滤器

filters: {capitalize: function (value) {if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

定义全局过滤器:

Vue.filter('capitalize', function (value) {if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({// ...})

留神:当全局过滤器和部分过滤器重名时,会采纳部分过滤器

过滤器函数总接管表达式的值 (之前的操作链的后果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数

过滤器能够串联:

{message | filterA | filterB}

在这个例子中,filterA 被定义为接管单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。而后持续调用同样被定义为接管单个参数的过滤器函数 filterB,将 filterA 的后果传递到 filterB 中。

过滤器是 JavaScript函数,因而能够接管参数:

{{message | filterA('arg1', arg2) }}

这里,filterA 被定义为接管三个参数的过滤器函数。

其中 message 的值作为第一个参数,一般字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数

举个例子:

<div id="app">
  <p>{{msg | msgFormat('疯狂','--')}}</p>
</div>

<script>
    // 定义一个 Vue 全局的过滤器,名字叫做  msgFormat
    Vue.filter('msgFormat', function(msg, arg, arg2) {
        // 字符串的  replace 办法,第一个参数,除了可写一个 字符串之外,还能够定义一个正则
        return msg.replace(/ 单纯 /g, arg+arg2)
    })
</script>

小结:

  • 部过滤器优先于全局过滤器被调用
  • 一个表达式能够应用多个过滤器。过滤器之间须要用管道符“|”隔开。其执行程序从左往右

利用场景

平时开发中,须要用到过滤器的中央有很多,比方 单位转换 数字打点 文本格式化 工夫格式化 之类的等

比方咱们要实现将30000 => 30,000,这时候咱们就须要应用过滤器

Vue.filter('toThousandFilter', function (value) {if (!value) return ''
  value = value.toString()
  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})

原理剖析

应用过滤器

{{message | capitalize}}

在模板编译阶段过滤器表达式将会被编译为过滤器函数,次要是用过parseFilters,咱们放到最初讲

_s(_f('filterFormat')(message))

首先剖析一下_f

_f 函数全名是:resolveFilter,这个函数的作用是从 this.$options.filters 中找出注册的过滤器并返回

// 变为
this.$options.filters['filterFormat'](message) // message 为参数

对于resolveFilter

import {indentity,resolveAsset} from 'core/util/index' 

export function resolveFilter(id){return resolveAsset(this.$options,'filters',id,true) || identity
}

外部间接调用 resolveAsset,将option 对象,类型,过滤器id,以及一个触发正告的标记作为参数传递,如果找到,则返回过滤器;

resolveAsset的代码如下:

export function resolveAsset(options,type,id,warnMissing){ // 因为咱们找的是过滤器,所以在 resolveFilter 函数中调用时 type 的值间接给的 'filters', 理论这个函数还能够拿到其余很多货色
  if(typeof id !== 'string'){ // 判断传递的过滤器 id 是不是字符串,不是则间接返回
      return 
  }
  const assets = options[type]  // 将咱们注册的所有过滤器保留在变量中
  // 接下来的逻辑便是判断 id 是否在 assets 中存在,即进行匹配
  if(hasOwn(assets,id)) return assets[id] // 如找到,间接返回过滤器
  // 没有找到,代码继续执行
  const camelizedId  = camelize(id) // 万一你是驼峰的呢
  if(hasOwn(assets,camelizedId)) return assets[camelizedId]
  // 没找到,继续执行
  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
  if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
  // 如果还是没找到,则查看原型链(即拜访属性)
  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  // 如果仍然没找到,则在非生产环境的控制台打印正告
  if(process.env.NODE_ENV !== 'production' && warnMissing && !result){warn('Failed to resolve' + type.slice(0,-1) + ':' + id, options)
  }
  // 无论是否找到,都返回查找后果
  return result
}

上面再来剖析一下_s

_s 函数的全称是 toString, 过滤器解决后的后果会当作参数传递给 toString函数,最终 toString函数执行后的后果会保留到 Vnode 中的 text 属性中,渲染到视图中

function toString(value){
  return value == null
  ? '': typeof value ==='object'
    ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来管制字符串外面的间距
    : String(value)
}

最初,在剖析下parseFilters,在模板编译阶段应用该函数阶段将模板过滤器解析为过滤器函数调用表达式

function parseFilters (filter) {let filters = filter.split('|')
    let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该办法会更改原数组
    let i
    if (filters) {for(i = 0;i < filters.length;i++){experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的 expression 实际上是管道符号后面的字符串,即过滤器的第一个参数
        }
    }
    return expression
}
// warpFilter 函数实现
function warpFilter(exp,filter){
    // 首先判断过滤器是否有其余参数
    const i = filter.indexof('(')
    if(i<0){ // 不含其余参数,间接进行过滤器表达式字符串的拼接
        return `_f("${filter}")(${exp})`
    }else{const name = filter.slice(0,i) // 过滤器名称
        const args = filter.slice(i+1) // 参数,但还多了‘)’return `_f('${name}')(${exp},${args}` // 留神这一步少给了一个 ')'
    }
}

小结:

  • 在编译阶段通过 parseFilters 将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的后果是后一个过滤器函数的参数)
  • 编译后通过调用 resolveFilter 函数找到对应过滤器并返回后果
  • 执行后果作为参数传递给 toString 函数,而 toString 执行后,其后果会保留在 Vnodetext属性中,渲染到视图
正文完
 0