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-router 路由有哪些模式?
个别有两种模式:
(1)**hash 模式**:前面的 hash 值的变动,浏览器既不会向服务器发出请求,浏览器也不会刷新,每次 hash 值的变动会触发 hashchange 事件。 (2)**history 模式**:利用了 HTML5 中新增的 pushState() 和 replaceState() 办法。这两个办法利用于浏览器的历史记录栈,在以后已有的 back、forward、go 的根底之上,它们提供了对历史记录进行批改的性能。只是当它们执行批改时,尽管扭转了以后的 URL,但浏览器不会立刻向后端发送申请。
diff算法
<details open=""><summary>答案</summary>
<p>
</p><p>工夫复杂度: 个树的齐全 diff
算法是一个工夫复杂度为 O(n*3)
,vue进行优化转化成 O(n)
。</p>
<p>了解:</p>
<ul>
<li>
<p>最小量更新, key
很重要。这个能够是这个节点的惟一标识,通知 diff
算法,在更改前后它们是同一个DOM节点</p>
<ul>
<li>扩大 v-for
为什么要有 key
,没有 key
会暴力复用,举例子的话轻易说一个比方挪动节点或者减少节点(批改DOM),加 key
只会挪动缩小操作DOM。</li>
</ul>
</li>
<li>
<p>只有是同一个虚构节点才会进行精细化比拟,否则就是暴力删除旧的,插入新的。</p>
</li>
<li>
<p>只进行同层比拟,不会进行跨层比拟。</p>
</li>
</ul>
<p>diff算法的优化策略:四种命中查找,四个指针</p>
<ol>
<li>
<p>旧前与新前(先比结尾,后插入和删除节点的这种状况)</p>
</li>
<li>
<p>旧后与新后(比结尾,前插入或删除的状况)</p>
</li>
<li>
<p>旧前与新后(头与尾比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧后之后)</p>
</li>
<li>
<p>旧后与新前(尾与头比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧前之前)</p>
</li>
</ol>
<p></p>
</details>
--- 问完下面这些如果都能很分明的话,根本O了 ---
以下的这些简略的概念,你必定也是没有问题的啦
Vue中封装的数组办法有哪些,其如何实现页面更新
在Vue中,对响应式解决利用的是Object.defineProperty对数据进行拦挡,而这个办法并不能监听到数组外部变动,数组长度变动,数组的截取变动等,所以须要对这些操作进行hack,让Vue能监听到其中的变动。 那Vue是如何实现让这些数组办法实现元素的实时更新的呢,上面是Vue中对这些办法的封装:
// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 须要进行性能拓展的办法const methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse"];/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(method) { // 缓存原生数组办法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组性能 const result = original.apply(this, args); // 响应式解决 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice办法,如果传入了第三个参数,也会有索引退出,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 告诉依赖更新 // 返回原生数组办法的执行后果 return result; });});
简略来说就是,重写了数组中的那些原生办法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray持续对新的值察看变动(也就是通过target__proto__ == arrayMethods
来扭转了数组实例的型),而后手动调用notify,告诉渲染watcher,执行update。
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, // 当批改属性时调用此办法};
Vue的长处
- 轻量级框架:只关注视图层,是一个构建数据的视图汇合,大小只有几十
kb
; - 简略易学:国人开发,中文文档,不存在语言障碍 ,易于了解和学习;
- 双向数据绑定:保留了
angular
的特点,在数据操作方面更为简略; - 组件化:保留了
react
的长处,实现了html
的封装和重用,在构建单页面利用方面有着独特的劣势; - 视图,数据,构造拆散:使数据的更改更为简略,不须要进行逻辑代码的批改,只须要操作数据就能实现相干操作;
- 虚构DOM:
dom
操作是十分消耗性能的,不再应用原生的dom
操作节点,极大解放dom
操作,但具体操作的还是dom
不过是换了另一种形式; - 运行速度更快:相比拟于
react
而言,同样是操作虚构dom
,就性能而言,vue
存在很大的劣势。
参考 前端进阶面试题具体解答
Vue 中的 key 到底有什么用?
key 是给每一个 vnode 的惟一 id,依附 key,咱们的 diff 操作能够更精确、更疾速 (对于简略列表页渲染来说 diff 节点也更快,但会产生一些暗藏的副作用,比方可能不会产生过渡成果,或者在某些节点有绑定数据(表单)状态,会呈现状态错位。)
diff 算法的过程中,先会进行新旧节点的首尾穿插比照,当无奈匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
更精确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 比照中能够防止就地复用的状况。所以会更加精确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更疾速 : key 的唯一性能够被 Map 数据结构充分利用,相比于遍历查找的工夫复杂度 O(n),Map 的工夫复杂度仅仅为 O(1),源码如下:
function createKeyToOldIdx(children, beginIdx, endIdx) { let i, key; const map = {}; for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key; if (isDef(key)) map[key] = i; } return map;}
谈谈对keep-alive的理解
keep-alive 能够实现组件的缓存,当组件切换时不会对以后组件进行卸载。罕用的2个属性
include/exclude ,2个生命周期
activated ,
deactivated
如何保留页面的以后的状态
既然是要放弃页面的状态(其实也就是组件的状态),那么会呈现以下两种状况:
- 前组件会被卸载
- 前组件不会被卸载
那么能够依照这两种状况别离失去以下办法:
组件会被卸载:
(1)将状态存储在LocalStorage / SessionStorage
只须要在组件行将被销毁的生命周期 componentWillUnmount
(react)中在 LocalStorage / SessionStorage 中把以后组件的 state 通过 JSON.stringify() 贮存下来就能够了。在这外面须要留神的是组件更新状态的机会。
比方从 B 组件跳转到 A 组件的时候,A 组件须要更新本身的状态。然而如果从别的组件跳转到 B 组件的时候,实际上是心愿 B 组件从新渲染的,也就是不要从 Storage 中读取信息。所以须要在 Storage 中的状态退出一个 flag 属性,用来管制 A 组件是否读取 Storage 中的状态。
长处:
- 兼容性好,不须要额定库或工具。
- 简略快捷,根本能够满足大部分需要。
毛病:
- 状态通过 JSON 办法贮存(相当于深拷贝),如果状态中有非凡状况(比方 Date 对象、Regexp 对象等)的时候会失去字符串而不是原来的值。(具体参考用 JSON 深拷贝的毛病)
- 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会生效,导致从其余页面进入 A 组件页面时 A 组件会从新读取 Storage,会造成很奇怪的景象
(2)路由传值
通过 react-router 的 Link 组件的 prop —— to 能够实现路由间传递参数的成果。
在这里须要用到 state 参数,在 B 组件中通过 history.location.state 就能够拿到 state 值,保留它。返回 A 组件时再次携带 state 达到路由状态放弃的成果。
长处:
- 简略快捷,不会净化 LocalStorage / SessionStorage。
- 能够传递 Date、RegExp 等非凡对象(不必放心 JSON.stringify / parse 的有余)
毛病:
- 如果 A 组件能够跳转至多个组件,那么在每一个跳转组件内都要写雷同的逻辑。
组件不会被卸载:
(1)单页面渲染
要切换的组件作为子组件全屏渲染,父组件中失常贮存页面状态。
长处:
- 代码量少
- 不须要思考状态传递过程中的谬误
毛病:
- 减少 A 组件保护老本
- 须要传入额定的 prop 到 B 组件
- 无奈利用路由定位页面
除此之外,在Vue中,还能够是用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行
被包裹在keep-alive中的组件的状态将会被保留:
<keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view></kepp-alive>
router.js
{ path: '/', name: 'xxx', component: ()=>import('../src/views/xxx.vue'), meta:{ keepAlive: true // 须要被缓存 }},
delete和Vue.delete删除数组的区别
delete
只是被删除的元素变成了empty/undefined
其余的元素的键值还是不变。Vue.delete
间接删除了数组 扭转了数组的键值。
action 与 mutation 的区别
mutation
是同步更新,$watch
严格模式下会报错action
是异步操作,能够获取数据后调用mutation
提交最终数据
v-if、v-show、v-html 的原理
- v-if会调用addIfCondition办法,生成vnode的时候会疏忽对应节点,render的时候就不会渲染;
- v-show会生成vnode,render的时候也会渲染成实在节点,只是在render过程中会在节点的属性中批改show属性值,也就是常说的display;
- v-html会先移除节点下的所有节点,调用html办法,通过addProp增加innerHTML属性,归根结底还是设置innerHTML为v-html的值。
$nextTick 原理及作用
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种利用。
nextTick 的外围是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 办法来模仿对应的微/宏工作的实现,实质是为了利用 JavaScript 的这些异步回调工作队列来实现 Vue 框架中本人的异步回调队列。
nextTick 不仅是 Vue 外部的异步队列的调用办法,同时也容许开发者在理论我的项目中应用这个办法来满足理论利用中对 DOM 更新数据机会的后续逻辑解决
nextTick 是典型的将底层 JavaScript 执行原理利用到具体案例中的示例,引入异步更新队列机制的起因∶
- 如果是同步更新,则屡次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,能够缩小一些无用渲染
- 同时因为 VirtualDOM 的引入,每一次状态发生变化后,状态变动的信号会发送给组件,组件外部应用 VirtualDOM 进行计算得出须要更新的具体的 DOM 节点,而后对 DOM 进行更新操作,每次更新状态后的渲染过程须要更多的计算,而这种无用功也将节约更多的性能,所以异步渲染变得更加至关重要
Vue采纳了数据驱动视图的思维,然而在一些状况下,依然须要操作DOM。有时候,可能遇到这样的状况,DOM1的数据产生了变动,而DOM2须要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就须要用到了nextTick
了。
因为Vue的DOM操作是异步的,所以,在下面的状况中,就要将DOM2获取数据的操作写在$nextTick
中。
this.$nextTick(() => { // 获取数据的操作...})
所以,在以下状况下,会用到nextTick:
- 在数据变动后执行的某个操作,而这个操作须要应用随数据变动而变动的DOM构造的时候,这个操作就须要办法在
nextTick()
的回调函数中。 - 在vue生命周期中,如果在created()钩子进行DOM操作,也肯定要放在
nextTick()
的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没方法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()
的回调函数中。
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
过去的值。
Vue 模板编译原理
Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
第一步是将 模板字符串 转换成 element ASTs(解析器)第二步是对 AST 进行动态节点标记,次要用来做虚构DOM的渲染优化(优化器)第三步是 应用 element ASTs 生成 render 函数代码字符串(代码生成器)
相干代码如下
export function compileToFunctions(template) { // 咱们须要把html字符串变成render函数 // 1.把html代码转成ast语法树 ast用来形容代码自身造成树结构 不仅能够形容html 也能形容css以及js语法 // 很多库都使用到了ast 比方 webpack babel eslint等等 let ast = parse(template); // 2.优化动态节点 // 这个有趣味的能够去看源码 不影响外围性能就不实现了 // if (options.optimize !== false) { // optimize(ast, options); // } // 3.通过ast 从新生成代码 // 咱们最初生成的代码须要和render函数一样 // 相似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))) // _c代表创立元素 _v代表创立文本 _s代表文Json.stringify--把对象解析成文本 let code = generate(ast); // 应用with语法扭转作用域为this 之后调用render函数能够应用call扭转this 不便code外面的变量取值 let renderFn = new Function(`with(this){return ${code}}`); return renderFn;}
nextTick 应用场景和原理
nextTick 中的回调是在下次 DOM 更新循环完结之后执行的提早回调。在批改数据之后立刻应用这个办法,获取更新后的 DOM。次要思路就是采纳微工作优先的形式调用异步办法去执行 nextTick 包装的办法
相干代码如下
let callbacks = [];let pending = false;function flushCallbacks() { pending = false; //把标记还原为false // 顺次执行回调 for (let i = 0; i < callbacks.length; i++) { callbacks[i](); }}let timerFunc; //定义异步办法 采纳优雅降级if (typeof Promise !== "undefined") { // 如果反对promise const p = Promise.resolve(); timerFunc = () => { p.then(flushCallbacks); };} else if (typeof MutationObserver !== "undefined") { // MutationObserver 次要是监听dom变动 也是一个异步办法 let counter = 1; const observer = new MutationObserver(flushCallbacks); const textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true, }); timerFunc = () => { counter = (counter + 1) % 2; textNode.data = String(counter); };} else if (typeof setImmediate !== "undefined") { // 如果后面都不反对 判断setImmediate timerFunc = () => { setImmediate(flushCallbacks); };} else { // 最初降级采纳setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0); };}export function nextTick(cb) { // 除了渲染watcher 还有用户本人手动调用的nextTick 一起被收集到数组 callbacks.push(cb); if (!pending) { // 如果屡次调用nextTick 只会执行一次异步 等异步队列清空之后再把标记变为false pending = true; timerFunc(); }}
双向数据绑定的原理
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变更的双向绑定成果。
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简略得多——不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的 “display” 属性进行切换。
所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简略得多——不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的 “display” 属性进行切换。
所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。
Vue 组件间通信有哪几种形式?
Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信。
(1)props / $emit
实用 父子组件通信
这种办法是 Vue 组件的根底,置信大部分同学耳闻能详,所以此处就不举例开展介绍。
(2)ref
与 $parent / $children
实用 父子组件通信
ref
:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例$parent
/$children
:拜访父 / 子实例
(3)EventBus ($emit / $on)
实用于 父子、隔代、兄弟组件通信
这种办法通过一个空的 Vue 实例作为地方事件总线(事件核心),用它来触发事件和监听事件,从而实现任何组件间的通信,包含父子、隔代、兄弟组件。
(4)$attrs
/$listeners
实用于 隔代组件通信
$attrs
:蕴含了父作用域中不被 prop 所辨认 (且获取) 的个性绑定 ( class 和 style 除外 )。当一个组件没有申明任何 prop 时,这里会蕴含所有父作用域的绑定 ( class 和 style 除外 ),并且能够通过v-bind="$attrs"
传入外部组件。通常配合 inheritAttrs 选项一起应用。$listeners
:蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过v-on="$listeners"
传入外部组件
(5)provide / inject
实用于 隔代组件通信
先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。 provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系。
(6)Vuex 实用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。每一个 Vuex 利用的外围就是 store(仓库)。“store” 基本上就是一个容器,它蕴含着你的利用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
- 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。