Vue组件data为什么必须是个函数?
- 根实例对象
data
能够是对象也能够是函数 (根实例是单例),不会产生数据净化状况 - 组件实例对象
data
必须为函数 一个组件被复用屡次的话,也就会创立多个实例。实质上,这些实例用的都是同一个构造函数。如果data
是对象的话,对象属于援用类型,会影响到所有的实例。所以为了保障组件不同的实例之间data
不抵触,data
必须是一个函数,
简版了解
// 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类// Vue.extend 依据用户定义产生一个新的类function Vue() {}function Sub() { // 会将data存起来 this.data = this.constructor.options.data();}Vue.extend = function(options) { Sub.options = options; // 动态属性 return Sub;}let Child = Vue.extend({ data:()=>( { name: 'zf' })});// 两个组件就是两个实例, 心愿数据互不感化let child1 = new Child();let child2 = new Child();console.log(child1.data.name);child1.data.name = 'poetry';console.log(child2.data.name);// 根不须要 任何的合并操作 根才有vm属性 所以他能够是函数和对象 然而组件mixin他们都没有vm 所以我就能够判断 以后data是不是个函数
相干源码
// 源码地位 src/core/global-api/extend.jsexport function initExtend (Vue: GlobalAPI) { Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) } const Sub = function VueComponent (options) { this._init(options) } // 子类继承大Vue父类的原型 Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // allow further extension/mixin/plugin usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub // 记录本人 在组件中递归本人 -> jsx } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // cache constructor cachedCtors[SuperId] = Sub return Sub }}
理解history有哪些办法吗?说下它们的区别
history
这个对象在html5
的时候新退出两个api
history.pushState()
和history.repalceState()
这两个API
能够在不进行刷新的状况下,操作浏览器的历史纪录。惟一不同的是,前者是新增一个历史记录,后者是间接替换以后的历史记录。
从参数上来说:
window.history.pushState(state,title,url)//state:须要保留的数据,这个数据在触发popstate事件时,能够在event.state里获取//title:题目,根本没用,个别传null//url:设定新的历史纪录的url。新的url与以后url的origin必须是一样的,否则会抛出谬误。url能够时绝对路径,也能够是相对路径。//如 以后url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/window.history.replaceState(state,title,url)//与pushState 基本相同,但她是批改以后历史纪录,而 pushState 是创立新的历史纪录
另外还有:
window.history.back()
后退window.history.forward()
后退window.history.go(1)
后退或者后退几步
从触发事件的监听上来说:
pushState()
和replaceState()
不能被popstate
事件所监听- 而前面三者能够,且用户点击浏览器后退后退键时也能够
Vue 模板编译原理
Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
第一步是将 模板字符串 转换成 element ASTs(解析器)第二步是对 AST 进行动态节点标记,次要用来做虚构DOM的渲染优化(优化器)第三步是 应用 element ASTs 生成 render 函数代码字符串(代码生成器)
delete和Vue.delete删除数组的区别
delete
只是被删除的元素变成了empty/undefined
其余的元素的键值还是不变。Vue.delete
间接删除了数组 扭转了数组的键值。
参考:前端vue面试题具体解答
Vue性能优化
编码优化:
- 事件代理
keep-alive
- 拆分组件
key
保障唯一性- 路由懒加载、异步组件
- 防抖节流
Vue加载性能优化
- 第三方模块按需导入(
babel-plugin-component
) - 图片懒加载
用户体验
app-skeleton
骨架屏shellap
p壳pwa
SEO优化
- 预渲染
slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容散发机制,组件外部的模板引擎应用slot元素作为承载散发内容的进口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
- 默认插槽:又名匿名查抄,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件能够呈现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,能够是匿名插槽,也能够是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,能够将子组件外部的数据传递给父组件,让父组件依据子组件的传递过去的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,寄存在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,应用$slot
中的内容进行替换,此时能够为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
写过自定义指令吗 原理是什么
指令实质上是装璜器,是 vue 对 HTML 元素的扩大,给 HTML 元素减少自定义性能。vue 编译 DOM 时,会找到指令对象,执行指令的相干办法。
自定义指令有五个生命周期(也叫钩子函数),别离是 bind、inserted、update、componentUpdated、unbind
1. bind:只调用一次,指令第一次绑定到元素时调用。在这里能够进行一次性的初始化设置。2. inserted:被绑定元素插入父节点时调用 (仅保障父节点存在,但不肯定已被插入文档中)。3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变动。通过比拟更新前后的绑定值,能够疏忽不必要的模板更新。4. componentUpdated:被绑定元素所在模板实现一次更新周期时调用。5. unbind:只调用一次,指令与元素解绑时调用。
原理
1.在生成 ast 语法树时,遇到指令会给以后元素增加 directives 属性
2.通过 genDirectives 生成指令代码
3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
4.当执行指令对应钩子函数时,调用对应指令定义的办法
说说Vue的生命周期吧
什么时候被调用?
- beforeCreate :实例初始化之后,数据观测之前调用
- created:实例创立万之后调用。实例实现:数据观测、属性和办法的运算、
watch/event
事件回调。无$el
. - beforeMount:在挂载之前调用,相干
render
函数首次被调用 - mounted:了被新创建的
vm.$el
替换,并挂载到实例下来之后调用改钩子。 - beforeUpdate:数据更新前调用,产生在虚构DOM从新渲染和打补丁,在这之后会调用改钩子。
- updated:因为数据更改导致的虚构DOM从新渲染和打补丁,在这之后会调用改钩子。
- beforeDestroy:实例销毁前调用,实例依然可用。
- destroyed:实例销毁之后调用,调用后,Vue实例批示的所有货色都会解绑,所有事件监听器和所有子实例都会被移除
每个生命周期外部能够做什么?
- created:实例曾经创立实现,因为他是最早触发的,所以能够进行一些数据、资源的申请。
- mounted:实例曾经挂载实现,能够进行一些DOM操作。
- beforeUpdate:能够在这个钩子中进一步的更改状态,不会触发重渲染。
- updated:能够执行依赖于DOM的操作,然而要防止更改状态,可能会导致更新无线循环。
- destroyed:能够执行一些优化操作,清空计时器,解除绑定事件。
ajax放在哪个生命周期?:个别放在 mounted
中,保障逻辑统一性,因为生命周期是同步执行的, ajax
是异步执行的。复数服务端渲染 ssr
同一放在 created
中,因为服务端渲染不反对 mounted
办法。 什么时候应用beforeDestroy?:以后页面应用 $on
,须要解绑事件。分明定时器。解除事件绑定, scroll mousemove
。
子组件能够间接扭转父组件的数据吗?
子组件不能够间接扭转父组件的数据。这样做次要是为了保护父子组件的单向数据流。每次父级组件产生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中收回正告。
Vue提倡单向数据流,即父级 props 的更新会流向子组件,然而反过来则不行。这是为了避免意外的扭转父组件状态,使得利用的数据流变得难以了解,导致数据流凌乱。如果毁坏了单向数据流,当利用简单时,debug 的老本会十分高。
只能通过 $emit
派发一个自定义事件,父组件接管到后,由父组件批改。
Vue是如何收集依赖的?
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由一般对象变成响应式对象,在这个过程中便会进行依赖收集的相干逻辑,如下所示∶
function defieneReactive (obj, key, val){ const dep = new Dep(); ... Object.defineProperty(obj, key, { ... get: function reactiveGetter () { if(Dep.target){ dep.depend(); ... } return val } ... })}
以上只保留了要害代码,次要就是 const dep = new Dep()
实例化一个 Dep 的实例,而后在 get 函数中通过 dep.depend()
进行依赖收集。 (1)Dep Dep是整个依赖收集的外围,其要害代码如下:
class Dep { static target; subs; constructor () { ... this.subs = []; } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.sub, sub) } depend () { if(Dep.target){ Dep.target.addDep(this) } } notify () { const subs = this.subds.slice(); for(let i = 0;i < subs.length; i++){ subs[i].update() } }}
Dep 是一个 class ,其中有一个关 键的动态属性 static,它指向了一个全局惟一 Watcher,保障了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的治理,再看看 Watcher 的相干代码∶
(2)Watcher
class Watcher { getter; ... constructor (vm, expression){ ... this.getter = expression; this.get(); } get () { pushTarget(this); value = this.getter.call(vm, vm) ... return value } addDep (dep){ ... dep.addSub(this) } ...}function pushTarget (_target) { Dep.target = _target}
Watcher 是一个 class,它定义了一些办法,其中和依赖收集相干的次要有 get、addDep 等。
(3)过程
在实例化 Vue 时,依赖收集的相干过程如下∶
初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 局部便是用来依赖收集的。
初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 办法,
updateComponent = () => { vm._update(vm._render())}new Watcher(vm, updateComponent)
get 办法中的 pushTarget 实际上就是把 Dep.target 赋值为以后的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 办法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 办法,也就会执行 Dep.target.addDep(this)。方才 Dep.target 曾经被赋值为 watcher,于是便会执行 addDep 办法,而后走到 dep.addSub() 办法,便将以后的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目标是为后续数据变动时候能告诉到哪些 subs 做筹备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便曾经实现了一个依赖收集的过程。
双向数据绑定的原理
Vue.js 是采纳数据劫持联合发布者-订阅者模式的形式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时公布音讯给订阅者,触发相应的监听回调。次要分为以下几个步骤:
- 须要observe的数据对象进行递归遍历,包含子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变动
- compile解析模板指令,将模板中的变量替换成数据,而后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,增加监听数据的订阅者,一旦数据有变动,收到告诉,更新视图
- Watcher订阅者是Observer和Compile之间通信的桥梁,次要做的事件是: ①在本身实例化时往属性订阅器(dep)外面增加本人 ②本身必须有一个update()办法 ③待属性变动dep.notice()告诉时,能调用本身的update()办法,并触发Compile中绑定的回调,则功成身退。
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听本人的model数据变动,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变动 -> 视图更新;视图交互变动(input) -> 数据model变更的双向绑定成果。
vue的长处
轻量级框架:只关注视图层,是一个构建数据的视图汇合,大小只有几十kb;
简略易学:国人开发,中文文档,不存在语言障碍 ,易于了解和学习;
双向数据绑定:保留了angular的特点,在数据操作方面更为简略;
组件化:保留了react的长处,实现了html的封装和重用,在构建单页面利用方面有着独特的劣势;
视图,数据,构造拆散:使数据的更改更为简略,不须要进行逻辑代码的批改,只须要操作数据就能实现相干操作;
虚构DOM:dom操作是十分消耗性能的,不再应用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom不过是换了另一种形式;
运行速度更快:相比拟与react而言,同样是操作虚构dom,就性能而言,vue存在很大的劣势。
形容下Vue自定义指令
在 Vue2.0 中,代码复用和形象的次要模式是组件。然而,有的状况下,你依然须要对一般 DOM 元素进行底层操作,这时候就会用到自定义指令。
个别须要对DOM元素进行底层操作时应用,尽量只用来操作 DOM展现,不批改外部的值。当应用自定义指令间接批改 value 值时绑定v-model的值也不会同步更新;如必须批改能够在自定义指令中应用keydown事件,在vue组件中应用 change事件,回调中批改vue数据;
(1)自定义指令根本内容
- 全局定义:
Vue.directive("focus",{})
- 部分定义:
directives:{focus:{}}
钩子函数:指令定义对象提供钩子函数
o bind:只调用一次,指令第一次绑定到元素时调用。在这里能够进行一次性的初始化设置。
o inSerted:被绑定元素插入父节点时调用(仅保障父节点存在,但不肯定已被插入文档中)。
o update:所在组件的VNode更新时调用,然而可能产生在其子VNode更新之前调用。指令的值可能产生了扭转,也可能没有。然而能够通过比拟更新前后的值来疏忽不必要的模板更新。
o ComponentUpdate:指令所在组件的 VNode及其子VNode全副更新后调用。
o unbind:只调用一次,指令与元素解绑时调用。
钩子函数参数
o el:绑定元素o bing: 指令外围对象,形容指令全副信息属性
o name
o value
o oldValue
o expression
o arg
o modifers
o vnode 虚构节点
o oldVnode:上一个虚构节点(更新钩子函数中才有用)
(2)应用场景
- 一般DOM元素进行底层操作的时候,能够应用自定义指令
- 自定义指令是用来操作DOM的。只管Vue推崇数据驱动视图的理念,但并非所有状况都适宜数据驱动。自定义指令就是一种无效的补充和扩大,不仅可用于定义任何的DOM操作,并且是可复用的。
(3)应用案例
高级利用:
- 鼠标聚焦
- 下拉菜单
- 绝对工夫转换
- 滚动动画
高级利用:
- 自定义指令实现图片懒加载
- 自定义指令集成第三方插件
v-model 能够被用在自定义组件上吗?如果能够,如何应用?
能够。v-model 实际上是一个语法糖,如:
<input v-model="searchText">
实际上相当于:
<input v-bind:value="searchText" v-on:input="searchText = $event.target.value">
用在自定义组件上也是同理:
<custom-input v-model="searchText">
相当于:
<custom-input v-bind:value="searchText" v-on:input="searchText = $event"></custom-input>
显然,custom-input 与父组件的交互如下:
- 父组件将
searchText
变量传入custom-input 组件,应用的 prop 名为value
; - custom-input 组件向父组件传出名为
input
的事件,父组件将接管到的值赋值给searchText
;
所以,custom-input 组件的实现应该相似于这样:
Vue.component('custom-input', { props: ['value'], template: ` <input v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > `})
Vue 初始化页面闪动问题如何解决?
呈现该问题是因为在 Vue 代码尚未被解析之前,尚无法控制页面中 DOM 的显示,所以会看见模板字符串等代码。
解决方案是,在 css 代码中增加 v-cloak 规定,同时在待编译的标签上增加 v-cloak 属性:
[v-cloak] { display: none; }<div v-cloak> {{ message }}</div>
虚构 DOM 的优缺点?
长处:
- 保障性能上限: 框架的虚构 DOM 须要适配任何下层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;然而比起粗犷的 DOM 操作性能要好很多,因而框架的虚构 DOM 至多能够保障在你不须要手动优化的状况下,仍然能够提供还不错的性能,即保障性能的上限;
- 无需手动操作 DOM: 咱们不再须要手动去操作 DOM,只须要写好 View-Model 的代码逻辑,框架会依据虚构 DOM 和 数据双向绑定,帮咱们以可预期的形式更新视图,极大进步咱们的开发效率;
- 跨平台: 虚构 DOM 实质上是 JavaScript 对象,而 DOM 与平台强相干,相比之下虚构 DOM 能够进行更不便地跨平台操作,例如服务器渲染、weex 开发等等。
毛病:
- 无奈进行极致优化: 尽管虚构 DOM + 正当的优化,足以应答绝大部分利用的性能需求,但在一些性能要求极高的利用中虚构 DOM 无奈进行针对性的极致优化。
Vue模版编译原理晓得吗,能简略说一下吗?
简略说,Vue的编译过程就是将template
转化为render
函数的过程。会经验以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的模式来形容整个模板)。 应用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相干解决。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变动,对应的DOM也不会变动。那么优化过程就是深度遍历AST树,依照相干条件对树节点进行标记。这些被标记的节点(动态节点)咱们就能够跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最初一步是将优化后的AST树转换为可执行的代码
。
$nextTick 是什么?
Vue 实现响应式并不是在数据产生后立刻更新 DOM,应用 vm.$nextTick
是在下次 DOM 更新循环完结之后立刻执行提早回调。在批改数据之后应用,则能够在回调中获取更新后的 DOM。
Vue中的key到底有什么用?
key
是为Vue中的vnode标记的惟一id,通过这个key,咱们的diff操作能够更精确、更疾速
diff算法的过程中,先会进行新旧节点的首尾穿插比照,当无奈匹配的时候会用新节点的key
与旧节点进行比对,而后超出差别.
diff程能够概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量互相比拟,一共有4种比拟形式。如果4种比拟都没匹配,如果设置了key,就会用key进行比拟,在比拟的过程中,变量会往两头靠,一旦StartIdx>EndIdx表明oldCh和newCh至多有一个曾经遍历完了,就会完结比拟,这四种比拟形式就是首、尾、旧尾新头、旧头新尾.
- 精确: 如果不加
key
,那么vue会抉择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug. - 疾速: key的唯一性能够被Map数据结构充分利用,相比于遍历查找的工夫复杂度O(n),Map的工夫复杂度仅仅为O(1).
为什么在 Vue3.0 采纳了 Proxy,摈弃了 Object.defineProperty?
Object.defineProperty 自身有肯定的监控到数组下标变动的能力,然而在 Vue 中,从性能/体验的性价比思考,尤大大就弃用了这个个性。为了解决这个问题,通过 vue 外部解决后能够应用以下几种办法来监听数组
push();pop();shift();unshift();splice();sort();reverse();
因为只针对了以上 7 种办法进行了 hack 解决,所以其余数组的属性也是检测不到的,还是具备肯定的局限性。
Object.defineProperty 只能劫持对象的属性,因而咱们须要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么须要深度遍历,显然如果能劫持一个残缺的对象是才是更好的抉择。
Proxy 能够劫持整个对象,并返回一个新的对象。Proxy 不仅能够代理对象,还能够代理数组。还能够代理动静减少的属性。