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
把全局的state
和getters
映射到以后组件的computed
计算属性
vuex
个别用于中大型web
单页利用中对利用的状态进行治理,对于一些组件间关系较为简单的小型利用,应用vuex
的必要性不是很大,因为齐全能够用组件prop
属性或者事件来实现父子组件之间的通信,vuex
更多地用于解决跨组件通信以及作为数据中心集中式存储数据。- 应用
Vuex
解决非父子组件之间通信问题vuex
是通过将state
作为数据中心、各个组件共享state
实现跨组件通信的,此时的数据齐全独立于组件,因而将组件间共享的数据置于State
中能无效解决多层级组件嵌套的跨组件通信问题
vuex
的State
在单页利用的开发中自身具备一个“数据库”的作用,能够将组件中用到的数据存储在State
中,并在Action
中封装数据读写的逻辑。这时候存在一个问题,个别什么样的数据会放在State
中呢?目前次要有两种数据会应用vuex
进行治理:
- 组件之间全局共享的数据
- 通过后端异步申请的数据
包含以下几个模块
state
:Vuex
应用繁多状态树, 即每个利用将仅仅蕴含一个store
实例。外面寄存的数据是响应式的,vue
组件从store
读取数据,若是store
中的数据产生扭转,依赖这相数据的组件也会产生更新。它通过mapState
把全局的state
和getters
映射到以后组件的computed
计算属性mutations
:更改Vuex
的store
中的状态的惟一办法是提交mutation
getters
:getter
能够对state
进行计算操作,它就是store
的计算属性尽管在组件内也能够做计算属性,然而getters
能够在多给件之间复用如果一个状态只在一个组件内应用,是能够不必getters
action
:action
相似于muation
, 不同在于:action
提交的是mutation
, 而不是间接变更状态action
能够蕴含任意异步操作modules
:面对简单的应用程序,当治理的状态比拟多时;咱们须要将vuex
的store
对象宰割成模块(modules
)
modules
:我的项目特地简单的时候,能够让每一个模块领有本人的state
、mutation
、action
、getters
,使得构造十分清晰,方便管理
答复范例
思路
- 给定义
- 必要性论述
- 何时应用
- 拓展:一些集体思考、实践经验等
答复范例
Vuex
是一个专为Vue.js
利用开发的 状态管理模式 + 库。它采纳集中式存储,治理利用的所有组件的状态,并以相应的规定保障状态以一种可预测的形式发生变化。- 咱们期待以一种简略的“单向数据流”的形式治理利用,即 状态 -> 视图 -> 操作单向循环 的形式。但当咱们的利用遇到多个组件共享状态时,比方:多个视图依赖于同一状态或者来自不同视图的行为须要变更同一状态。此时单向数据流的简洁性很容易被毁坏。因而,咱们有必要把组件的共享状态抽取进去,以一个全局单例模式治理。通过定义和隔离状态治理中的各种概念并通过强制规定维持视图和状态间的独立性,咱们的代码将会变得更结构化且易保护。这是
vuex
存在的必要性,它和react
生态中的redux
之类是一个概念 Vuex
解决状态治理的同时引入了不少概念:例如state
、mutation
、action
等,是否须要引入还须要依据利用的理论状况掂量一下:如果不打算开发大型单页利用,应用Vuex
反而是繁琐冗余的,一个简略的store
模式就足够了。然而,如果要构建一个中大型单页利用,Vuex
根本是标配。- 我在应用
vuex
过程中感触到一些等
可能的诘问
vuex
有什么毛病吗?你在开发过程中有遇到什么问题吗?- 刷新浏览器,
vuex
中的state
会从新变为初始状态。解决方案 - 插件vuex-persistedstate
action
和mutation
的区别是什么?为什么要辨别它们?
action
中解决异步,mutation
不能够mutation
做原子操作action
能够整合多个mutation
的汇合mutation
是同步更新数据(外部会进行是否为异步形式更新数据的检测)$watch
严格模式下会报错action
异步操作,能够获取数据后调佣mutation
提交最终数据
- 流程程序:“相应视图—> 批改 State”拆分成两局部,视图触发
Action
,Action 再触发
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
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化解决,这个过程产生Observe
中- 同时对模板执行编译,找到其中动静绑定的数据,从
data
中获取并初始化视图,这个过程产生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,未来对应数据变动时Watcher
会调用更新函数 - 因为
data
的某个key
在⼀个视图中可能呈现屡次,所以每个key
都须要⼀个管家Dep
来治理多个Watcher
- 未来 data 中数据⼀旦发生变化,会首先找到对应的
Dep
,告诉所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对 data
执行响应化解决
class Vue {constructor(options) {
this.$options = options;
this.$data = options.data;
// 对 data 选项做响应式解决
observe(this.$data);
// 代理 data 到 vm 上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
对 data
选项执行响应化具体操作
function observe(obj) {if (typeof obj !== "object" || obj == null) {return;}
new Observer(obj);
}
class Observer {constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {Object.keys(obj).forEach((key) => {defineReactive(obj, key, obj[key]);
});
}
}
编译Compile
对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数
class Compile {constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取 dom
if (this.$el) {this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {return node.nodeType == 1;}
isInterpolation(node) {return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依赖收集
视图中会用到 data
中某 key
,这称为依赖。同⼀个key
可能呈现屡次,每次都须要收集进去用⼀个 Watcher
来保护它们,此过程称为依赖收集多个 Watcher
须要⼀个 Dep
来治理,须要更新时由 Dep
统⼀告诉
实现思路
defineReactive
时为每⼀个key
创立⼀个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创立⼀个watcher1
- 因为触发
name1
的getter
办法,便将watcher1
增加到name1
对应的Dep
中 - 当
name1
更新,setter
触发时,便可通过对应Dep
告诉其治理所有Watcher
更新
// 负责更新视图
class Watcher {constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创立实例时,把以后实例指定到 Dep.target 动态属性上
Dep.target = this
// 读一下 key,触发 get
vm[key]
// 置空
Dep.target = null
}
// 将来执行 dom 更新函数,由 dep 调用的
update() {this.updaterFn.call(this.vm, this.vm[this.key])
}
}
申明Dep
class Dep {constructor() {this.deps = []; // 依赖治理
}
addDep(dep) {this.deps.push(dep);
}
notify() {this.deps.forEach((dep) => dep.update());
}
}
创立 watcher
时触发getter
class Watcher {constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创立 Dep
实例
function defineReactive(obj, key, val) {this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {get() {Dep.target && dep.addDep(Dep.target);// Dep.target 也就是 Watcher 实例
return val;
},
set(newVal) {if (newVal === val) return;
dep.notify(); // 告诉 dep 执行更新办法},
});
}
watch 原理
watch
实质上是为每个监听属性 setter
创立了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创立一个watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。次要起因在于对象属于援用类型,单个属性的更新并不会触发对象setter
,因而引入deep
可能很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,防止性能节约。immediate
:在初始化时间接调用回调函数,能够通过在created
阶段手动调用回调函数实现雷同的成果
Vue computed 实现
- 建设与其余属性(如:
data
、Store
)的分割; - 属性扭转后,告诉计算属性从新计算
实现时,次要如下
- 初始化
data
,应用Object.defineProperty
把这些属性全副转为getter/setter
。 - 初始化
computed
, 遍历computed
里的每个属性,每个computed
属性都是一个watch
实例。每个属性提供的函数作为属性的getter
,应用Object.defineProperty
转化。 Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性从新计算。- 若呈现以后
computed
计算属性嵌套其余computed
计算属性时,先进行其余的依赖收集
参考 前端进阶面试题具体解答
keep-alive 应用场景和原理
keep-alive
是Vue
内置的一个组件,能够实现组件缓存 ,当组件切换时不会对以后组件进行卸载。 个别联合路由和动静组件一起应用,用于缓存组件- 提供
include
和exclude
属性,容许组件有条件的进行缓存。两者都反对字符串或正则表达式,include
示意只有名称匹配的组件会被缓存,exclude
示意任何名称匹配的组件都不会被缓存,其中exclude
的优先级比include
高 - 对应两个钩子函数
activated
和deactivated
,当组件被激活时,触发钩子函数activated
,当组件被移除时,触发钩子函数deactivated
keep-alive
的中还使用了LRU
(最近起码应用) 算法,抉择最近最久未应用的组件予以淘汰
<keep-alive></keep-alive>
包裹动静组件时,会缓存不流动的组件实例, 次要用于保留组件状态或防止从新渲染- 比方有一个列表和一个详情,那么用户就会常常执行关上详情 => 返回列表 => 关上详情…这样的话列表和详情都是一个频率很高的页面,那么就能够对列表组件应用
<keep-alive></keep-alive>
进行缓存,这样用户每次返回列表的时候,都能从缓存中疾速渲染,而不是从新渲染
对于 keep-alive 的根本用法
<keep-alive>
<component :is="view"></component>
</keep-alive>
应用 includes
和exclude
:
<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
缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
- 首次进入组件时:
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-alive
是 vue
中内置的一个组件
源码地位: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
钩子函数中观测 include
和 exclude
的变动,如下:
mounted () {
this.$watch('include', val => {pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))
})
}
如果 include
或exclude
产生了变动,即示意定义须要缓存的组件的规定或者不须要缓存的组件的规定产生了变动,那么就执行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 应用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同
答题思路:
-
依照逻辑扩大和内容扩大来列举
- 逻辑扩大有:
mixins
、extends
、composition api
- 内容扩大有
slots
;
- 逻辑扩大有:
- 别离说出他们应用办法、场景差别和问题。
- 作为扩大,还能够说说
vue3
中新引入的composition api
带来的变动
答复范例:
- 常见的组件扩大办法有:
mixins
,slots
,extends
等 - 混入
mixins
是散发Vue
组件中可复用性能的非常灵活的形式。混入对象能够蕴含任意组件选项。当组件应用混入对象时,所有混入对象的选项将被混入该组件自身的选项
// 复用代码:它是一个配置对象,选项和组件外面一样
const mymixin = {
methods: {dosomething(){}}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)
// 部分混入:做数组项设置到 mixins 选项,仅作用于以后组件
const Comp = {mixins: [mymixin]
}
- 插槽次要用于
vue
组件中的内容散发,也能够用于组件扩大
子组件 Child
<div>
<slot> 这个内容会被父组件传递的内容替换 </slot>
</div>
父组件 Parent
<div>
<Child> 来自父组件内容 </Child>
</div>
如果要准确散发到不同地位能够应用 具名插槽
,如果要应用子组件中的数据能够应用作用域插槽
- 组件选项中还有一个不太罕用的选项
extends
,也能够起到扩大组件的目标
// 扩大对象
const myextends = {
methods: {dosomething(){}}
}
// 组件扩大:做数组项设置到 extends 选项,仅作用于以后组件
// 跟混入的不同是它只能扩大单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {extends: myextends}
- 混入的数据和办法不能明确判断起源且可能和以后组件内变量产生命名抵触,
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')
)
答复范例
- 在大型利用中,咱们须要宰割利用为更小的块,并且在须要组件时再加载它们。
- 咱们不仅能够在路由切换时懒加载组件,还能够在页面组件中持续应用异步组件,从而实现更细的宰割粒度。
- 应用异步组件最简略的形式是间接给
defineAsyncComponent
指定一个loader
函数,联合 ES 模块动静导入函数import
能够疾速实现。咱们甚至能够指定loadingComponent
和errorComponent
选项从而给用户一个很好的加载反馈。另外Vue3
中还能够联合Suspense
组件应用异步组件。 - 异步组件容易和路由懒加载混同,实际上不是一个货色。异步组件不能被用于定义懒加载路由上,解决它的是
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
能够有多个
- 格局不同:
函数式组件劣势和原理
函数组件的特点
- 函数式组件须要在申明组件是指定
functional:true
- 不须要实例化,所以没有
this
,this
通过render
函数的第二个参数context
来代替 - 没有生命周期钩子函数,不能应用计算属性,
watch
- 不能通过
$emit
对外裸露事件,调用事件只能通过context.listeners.click
的形式调用内部传入的事件 - 因为函数式组件是没有实例化的,所以在内部通过
ref
去援用组件时,理论援用的是HTMLElement
- 函数式组件的
props
能够不必显示申明,所以没有在props
外面申明的属性都会被主动隐式解析为prop
, 而一般组件所有未声明的属性都解析到$attrs
外面,并主动挂载到组件根元素下面 (能够通过inheritAttrs
属性禁止)
长处
- 因为函数式组件不须要实例化,无状态,没有生命周期,所以渲染性能要好于一般组件
- 函数式组件构造比较简单,代码构造更清晰
应用场景:
- 一个简略的展现组件,作为容器组件应用 比方
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
的产生起因。Vue2
的Virtual 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
中的作用
答复范例
- 虚构
dom
顾名思义就是虚构的dom
对象,它自身就是一个JavaScript
对象,只不过它是通过不同的属性去形容一个视图构造 - 通过引入
vdom
咱们能够取得如下益处: -
将实在元素节点形象成
VNode
,无效缩小间接操作dom
次数,从而进步程序性能- 间接操作
dom
是有限度的,比方:diff
、clone
等操作,一个实在元素上有许多的内容,如果间接对其进行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
。
- 挂载过程完结后,
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
还存在js
中diff
算法的比对过程。所以,上述性能劣势仅仅实用于大量数据的渲染并且扭转的数据只是一小部分的状况。
如下 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
性能的库,相比vue
、react
,对于vdom
这块更加繁难,适宜咱们学习vdom
。vdom
外面有两个外围的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 渲染大量数据时应该怎么优化?说下你的思路!
剖析
企业级我的项目中渲染大量数据的状况比拟常见,因而这是一道十分好的综合实际题目。
答复
- 在大型企业级我的项目中常常须要渲染大量数据,此时很容易呈现卡顿的状况。比方大数据量的表格、树
- 解决时要依据状况做不同解决:
- 能够采取分页的形式获取,防止渲染大量数据
- 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
设置setter
与getter
函数,用来实现「响应式 」以及「 依赖收集」,前面会具体讲到,这里只有有一个印象即可。初始化之后调用
$mount
会挂载组件,如果是运行时编译,即不存在 render function 然而存在 template 的状况,须要进行「编译」步骤。
编译
compile 编译能够分成 parse
、optimize
与 generate
三个阶段,最终须要失去 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 字符串。
- 在经验过
parse
、optimize
与generate
这三个阶段当前,组件中就会存在渲染VNode
所需的render function
了。
响应式
接下来也就是 Vue.js 响应式外围局部。
这里的
getter
跟setter
曾经在之前介绍过了,在init
的时候通过Object.defineProperty
进行了绑定,它使得当被设置的对象被读取的时候会执行getter
函数,而在当被赋值的时候会执行setter
函数。
- 当
render function
被渲染的时候,因为会读取所需对象的值,所以会触发getter
函数进行「依赖收集 」,「 依赖收集」的目标是将观察者Watcher
对象寄存到以后闭包中的订阅者Dep
的subs
中。造成如下所示的这样一个关系。
在批改对象的值的时候,会触发对应的
setter
,setter
告诉之前「依赖收集」失去的 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
组件,这是一个十分常见且有用的优化伎俩,vue3
中 keep-alive
有比拟大的更新,能说的点比拟多
思路
- 缓存用
keep-alive
,它的作用与用法 - 应用细节,例如缓存指定 / 排除、联合
router
和transition
- 组件缓存后更新能够利用
activated
或者beforeRouteEnter
- 原理论述
答复范例
- 开发中缓存组件应用
keep-alive
组件,keep-alive
是vue
内置组件,keep-alive
包裹动静组件component
时,会缓存不流动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,避免反复渲染DOM
<keep-alive>
<component :is="view"></component>
</keep-alive>
- 联合属性
include
和exclude
能够明确指定缓存哪些组件或排除缓存指定组件。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>
- 缓存后如果要获取数据,解决方案能够有以下两种
beforeRouteEnter
:在有vue-router 的
我的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
next(vm=>{console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据})
},
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
activated(){this.getData() // 获取数据
},
keep-alive
是一个通用组件,它外部定义了一个map
,缓存创立过的组件实例,它返回的渲染函数外部会查找内嵌的component
组件对应组件的vnode
,如果该组件在map
中存在就间接返回它。因为component
的is
属性是个响应式数据,因而只有它变动,keep-alive
的render
函数就会从新执行
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
执行后,其后果会保留在Vnode
的text
属性中,渲染到视图