vue-knowledge's People
vue-knowledge's Issues
vue scoped slot
使用element组件库中table组件中table-coloumn的slot="header",如果不传scoped-slot="{row}" 打包之后就不能正常渲染这个slot对应的组件的内容,为什么????
利用vue实现一个抽屉的动画
核心
使用vue的过渡动画
<transition name="slide">
<your-component></your-component>
</transition>
.slide-leave-active,
.slide-enter-active {
transition: 1s;
}
/*进入过渡的开始状态*/
.slide-enter {
transform: translate(100%, 0);
}
/*离开过渡的结束状态*/
.slide-leave-to {
transform: translate(100%, 0);
}
v-enter和v-leave-to的状态一样可以确保组件从哪里来回哪里去
Vue vs Jquery
Jquery
jquery是跨平台的,封装了兼容性问题
Vue
- 声明式渲染(只需要关注编写业务逻辑而不用关心页面变成什么样子, 对于各式各样的状态框架会帮忙计算好在每个状态下的页面长什么样子而不需要自己发现)
- 组件化(提高复用)
[vue] scope-slot的内容不能正常渲染的问题
场景
子组件child接受具名插槽,父组件parent使用子组件child, 并且通过v-for指令将具名插槽传递进去,会有如下情况:
-
- 使用v-for指令,将slot和scope-slot放在和v-for平级的外层template上,插槽内容可以正常展示
Vue.component('parent', {
template: `<div>
<h3>scoped slots: failed</h3>
<child>
<template v-for="item in slotNames" :slot="item" slot-scope="scope">{{ scope }}</template>
</child>
</div>
`,
data: function () {
return {
slotNames: ["test", "test1", "test2"],
test: 'test'
}
}
});
Vue.component('child', {
template: '' +
'<div>' +
'<slot name="test" text="Hello, World!">1' +
'</slot>' +
'<slot name="test1" text="Hello, World1!">2' +
'</slot>' +
'<slot name="test2" text="Hello, World2!">3' +
'</slot>' +
'</div>'
});
new Vue({
el: '#app',
data: {
// slotNames: ["test", "test1", "test2"]
}
})
-
- 使用v-for指令,内部再嵌套一层template指定插糟的内容,不接受scope-slot,插槽的内容可以正常展示
Vue.component('parent', {
template: `<div>
<h3>scoped slots: failed</h3>
<child>
<template v-for="item in slotNames"><template :slot="item">{{ item }}</template></template>
</child>
</div>
`,
data: function () {
return {
slotNames: ["test", "test1", "test2"],
test: 'test'
}
}
});
Vue.component('child', {
template: '' +
'<div>' +
'<slot name="test" text="Hello, World!">1' +
'</slot>' +
'<slot name="test1" text="Hello, World1!">2' +
'</slot>' +
'<slot name="test2" text="Hello, World2!">3' +
'</slot>' +
'</div>'
});
new Vue({
el: '#app',
data: {
// slotNames: ["test", "test1", "test2"]
}
})
-
- 使用v-for指令,内部再嵌套一层template指定插糟的内容,接受scope-slot,插槽的内容却无法正常展示了!!!!!
Vue.component('parent', {
template: `<div>
<h3>scoped slots: failed</h3>
<child>
<template v-for="item in slotNames">
<template :slot="item" slot-scope="props">{{ item }}</template>
</template>
</child>
</div>
`,
data: function () {
return {
slotNames: ["test", "test1", "test2"],
test: 'test'
}
}
});
Vue.component('child', {
template: '' +
'<div>' +
'<slot name="test" text="Hello, World!">1' +
'</slot>' +
'<slot name="test1" text="Hello, World1!">2' +
'</slot>' +
'<slot name="test2" text="Hello, World2!">3' +
'</slot>' +
'</div>'
});
new Vue({
el: '#app',
data: {
// slotNames: ["test", "test1", "test2"]
}
})
在vue中动态引入图片的地址
在vue的项目中我们经常使用如下这种方式来引用我们的图片, 这对于地址是固定的图片来说没有问题, @指向的是项目的根目录./src目录
<img src="@/assets/img/common/meta.png">
但当图片的地址是动态生成,采用字符串拼接的方式就不行了
<img :src="'@/assets/img/assets-search-home' + tool.icon">
需要改成使用require的方式
<img :src="require(`@/assets/img/assets-search-home/${tool.icon}`)">
VUE响应式更新
Vue Diff深入分析
什么是vue
Vue是一个渐进式JavaScript框架,将其很多特性进行分层,从里到外分别是视图层渲染,组件,路由,状态管理,以及构建工具。我所理解的渐进式,是指不必使用框架的所有特性,可以根据项目的需求逐步集成框架的部分特性。例如我们已经有一个JS项目,可以很轻松将vue集成进我们的项目中,随着项目的增大可以利用vue的组件特性来组织我们的项目代码等。
vue的特点
1.声明式
vue是典型的声明式框架。对比传统的命令式框架,比如jQuery,我们不仅需要关注应用的状态(使用的变量就是状态),还需要关注这些状态对应的页面视图,我们需要手写代码告诉浏览器每个时刻对应的视图是什么样子。然而,声明式框架使得我们只需要关注每个时刻应用的状态是什么,从状态到视图的过程由框架帮我们完成。
2.组件化
vue允许我们通过组件的方式来组织项目的结构,这样不仅让我们的代码更易读,还可以提高代码的复用率。
虚拟dom
虚拟dom是vue框架的一个核心概念,也是vue diff算法的载体。相比真实的dom包含很多html属性,虚拟dom只包含生成视图的相关信息,它描述了状态和应用之间的一个对应关系。而vue diff算法的核心就是对比更新前后两次的虚拟dom,只更新有差异的部分,从而将操作dom的次数降低到最小。
1. 如何生成虚拟dom
在vue的生命周期中,在初始化阶段之后就是vue的模板编译阶段, 模板编译的最终目的就是要生成渲染函数(render函数), 执行render函数就可以得到用来描述当前状态对应的视图的所需要信息的虚拟dom。
(1)模板首先经过解析器生成AST(抽象语法树),即用js对象描述的模板结构
(2)优化器遍历AST,找出所有的静态子树并做上标记。静态节点是指那些一旦渲染到页面上就不会再发生变化的点,在vue diff的算法中如果检测到是静态节点就不会再继续进行对比,从而提高效率
(3)代码生成器会根据优化之后的AST生成渲染函数的代码,执行这段代码就可以生成虚拟dom
举例: 模板代码
<p title=“Berwin” @click=“c”>1</p>
优化后的AST如下图所示
let ast = {tag: 'p',type: 1,staticRoot: false,static: false,parent: undefined,attrList: [{name: 'title',value: 'Berwin'}],children: [{type: 3,text: '1',static: true}]
}
根据ast生成的渲染函数代码如下:
let code = `with(this) {
return _c('p', {
attrs: {
title: 'Berwin'
},
on: {
click: c
}
}, [_v("")])
}` //_c, _v等是vue中用来创建不同类型节点的方法
let render = new Function(code);
2. 构成虚拟dom的虚拟node
虚拟dom是由一个个虚拟节点vnode组成的,vnode是一个从VNode类实例化的JS对象,描述了每个节点渲染到视图中需要的信息。比如文本信息text,标签信息tag等
Vnode主要有以下几种类型:
- 注释节点
- 文本节点
- 元素几点
- 组件节点
- 函数式组件节点
- 克隆节点(如果是静态节点,vue会直接将节点克隆一份,而不是生成一个新的节点)
3. AST vs VNode
AST和Vnode看起来很相似,但其所代表的东西完全不一样。AST是用JS对象来描述模板的结构,而Vnode是用JS来描述根据状态生成视图所需要的全部信息。AST不包括状态数据,而Vnode包括状态数据。此外,AST是在编译阶段生成的,而Vnode是运行时生成的。
4. vdom的特点
vdom的引入是因为它具有下述三个重要的特点:
- 提供与真实DOM节点对应的虚拟节点Vnode,最终会根据这些虚拟节点来生成最终的视图
- VDOM提供了一种中等粒度的解决方案
在vue中,并没有为每个绑定了状态的节点生成一个watcher而是为每个组件生成一个watcher。这样可以控制watcher的数目,不会因为依赖状态的节点越多产生很多watcher对象,从而消耗内存。为每个组件生成watcher使得当组件内部的状态发生变化时,按照组件来生成对应的vdom进行对比更新。 - 对比
vue并不会每次都根据新的vnode生成页面最终的视图,而是会缓存之前视图对应的vnode,然后将新的vnode和旧的vnode进行对比,只更新变化的部分,尽量减少dom的操作。
5. 触发Vue Diff
- 当状态发生改变
- 将组件的watcher放入队列
- 调用render函数生成新的VDOM
- 调用update方法执行Vue Diff 对比更新的操作
vue diff算法
Vue diff是一个通过对比每个vnode找出需要更新的节点并跟新dom的过程, 在这个过程中主要调用了三个方法分别是patch, patchVnode和updateChildren. 其中patch是对整棵树的对比,patchVnode是对单个节点的对比, updateChildren是对子节点的对比并且它还会继续调用patchVnode方法,这个过程是一个递归的过程.
1. patch
patch的流程如上图所示,主要完成了三件事:
- 创建新增节点
- 删除已经废弃的节点
- 修改需要更新的节点
其中尤其需要注意的是用来判定是否是同一个节点的sameNode方法, 一旦不满足sameVnode条件,vue就会销毁之前的节点,按照新的vnode生成新的dom节点并插入到页面中去。
2. patchVnode
当两个节点被判定为sameVnode,就会进行一个更细粒度的对比。这是一个边diff边更新dom的过程。
对单个虚拟节点的diff如上图所示,会对节点的属性,文本内容等进行更新。当节点包含有子节点的时候,为了保证尽可能少的dom的操作次数 对孩子节点的对比会采用两头两两对比的方式。
3. updateChildren
updateChildren方法的流程如上图所示,算法会为新旧子节点分别设置指向头尾节点的指针, 然后头尾指针进行两两对比,大致过程如下:
- oldStartIndex指向从头部开始的第一个非空未处理的节点(oldStartVnode),(备注:遇到空节点要跳过,该位置的节点可能会发生位置上的变化,节点一旦发生位置上的移动,原位置会被置空)
- oldEndIndex指向从尾部开始的第一个非空未处理的节点(oldEndVnode)
- oldStartVnode和newStartVnode对比,如果是sameNode就继续调用patchVnode方法,否则继续对比
- oldEndVnode和newEndVnode对比,如果是sameNode就继续调用patchVnode方法,否则继续对比
- oldStartVnode和newEndVnode对比,如果是sameNode就继续调用patchVnode方法,否则继续对比
- oldEndVnode和newStartVnode对比,如果是sameNode就继续调用patchVnode方法,否则继续对比
- 如果上述对比都未找到sameNode,那么就对oldStartVnode和oldEndVnode之间的节点生成一个key到index的映射
- 如果newStartVnode有含有key值,那就直接根据这个key拿到newStartVnode在第七步中生成的映射关系中对应的index值
- 如果newStartVnode不存在key值,就遍历oldStartVnode和oldEndVnode通过sameVnode方法找到和newStartVnode“一样”的节点
- 如果按照第8和第9步没有找到对应的节点,那就根据newStartVnode生成一个新的dom节点然后插入到oldStartVnode前面去
- 如果找到到了对应的节点就将这个节点移动到oldStartVnode前面去,同时将该位置置空,然后继续调用patchVnode进行一个更细粒度的对比
- 如果oldChildren先遍历完,说明newStartVnode和newEndVnode之间的节点是新增的节点,那么就根据这些vnode生成新的节点并插入到页面中去(源代码中是获取newEndVnode的下一个节点在dom中的引用,然后将这些新的节点插入到这个引用的前面)
- 如果newChildren先遍历完,说明oldStartVnode和oldEndVnode之间的节点是需要被删除的节点,那么就将他们从父节点中删除
4. 举例
上图给出了一个关于更新子节点时的一个事例,其中字母a,b,c,d标识每个节点,如果值一样标识它们通过sameVnode方法判定的结果为true.
- newStartVnode和newEndVnode分别指向new中的节点a和b, oldStartVnode和oldEndVnode分别指向节点a和d
- newStartVnode和oldStartVnode是sameVnode,因此a节点对应的位置不变, newStartVnode和oldStartVnode分别指向c和b
- newStartVnode和oldStartVnode不是sameVnode,继续用oldEndVnode和newEndVnode对比(依然不等), oldStartVnode和newEndVnode相等,将b节点(newEndVnode)移到oldEndVnode(d节点)的后面。然后将oldStartVnode指向节点d,newEndVnode指向节点d。
- newEndVnode和oldEndVnode节点是sameVnode,因此该节点不动。将newEndVnode向前进一步指向c, 此时oldEndVnode也向前指。
- 此时oldChildren中的节点已经处理完,但是newChildren还剩下节点C将节点c插入节点d(在newChildren中oldEndVnode的下一个节点)的前面
vue diff的精髓
vue diff算法的一个重要目的就是尽可能少的减少操作dom的次数。
1. 为什么要vue diff
vue设计的核心**之一就是只更新页面中需要更新的部分。如果不进行vue diff,也可以根据新生成的vnode重新生成视图,但当状态更新的时候页面大部分是没有发生变化的,如果根据新的vnode重新生成视图然后替换掉之前的部分无疑这个操作成本是巨大的,当页面中大部分节点是静态节点的时候表现的更为明显。
2. 为什么要两头对比
两头对比的目的并不是为了快速找到节点,减少vue diff的算法复杂度。用js的操作成本来替换操作dom的成本是vue diff设计的核心**,通过两头两两对比是为了减少操作dom的次数。
如果两头两两对比是为了快速找到sameVnode,那么vue框架完全可以强制为节点加上key属性,然后一开始就生成一个key到index的映射,这样在子节点进行对比的过程中查找sameVnode算法复杂度将降低为O(n),n代表newChildren中子节点的个数
两头进行两两对比是为了减少dom的操作次数,尽可能少的移动节点,下面举例说明
如果按照快速找到节点的思路来看这个问题,当newStartVnode指向节点E, oldStartVnode指向节点C的时候,通过用节点E的key在oldChildren中key到index的映射判定到节点E是存在在oldChildren中,那么之后的操作应该是将节点E移动到节点C之前,并且之后需要删除节点C和D。这里发生了两步操作,移动节点E和删除节点C和D。
相反,如果采用两头两两对比的方式,会发现oldEndVnode和newEndVnode相等,保持节点E不动,之后只需要删除节点C和节点D。
所以,vue diff的核心就是尽可能少的操作dom,用js的操作成本来替代操作dom的成本。
最佳实践
1. 为列表渲染设置key
vue的官网上推荐在列表渲染的时候对每个节点设置key属性,并且这个key能尽可能保持唯一。从上面的diff算法中可以看出,key一方面用在判定sameVnode上(一旦key不一样,就可以断定两个节点不是一个节点,无需进行后面的diff操作),另外当调用updateChildren更新子节点的时候,一旦两头两两对比没有找到sameVnode,vue会用这个key来生成一个key到index的映射来判定是否存在newStartVnode,如果没有这个key,那么每次都会遍历oldChildren中没有处理的节点来找到sameVnode。所以为子节点设置key,可以更快速的找到sameVnode,在一定程度上提高效率。
2. 避免用数组的下标作为Key
下图展示了用数组的下标作为key的一个例子,关键代码如图:
<template><div class="border">
<!-- eslint-disable-next-line vue/valid-v-for -->
<list-input v-for="(item, index) in list" :index="index" :word="item" @delete="handleDelete"></list-input>
</div>
</template>
<script>
import ListInput from "./components/ListInput";
export default {
components: {
ListInput
},
data() {
return {
list: ['a', 'b']
};
},
methods: {
handleDelete(index) {
this.list.splice(index, 1);
}
}
};
</script>
<template><div>
word:{{word}}
<input v-model="phone" type="number" /><button @click="$emit('delete', index)">删除</button></div>
</template>
<script>
export default {
props: {
index: {
type: Number,
required: true
},
word: {
type: String,
required: true
}
},
data() {
return {
phone: ""
};
}
};
</script>
<style scoped>
.list-input-item {
border: 1px solid #d9d9d9;
padding: 10px 20px;
margin-bottom: 10px;
}
input {
margin-right: 5px;
}
</style>
渲染的结果如下图所示:
当我们点击第一排的删除按钮之后,渲染的结果如下图所示:
在点击删除按钮之前,页面视图的vdom如下图所示:
oldChilren中的第一个节点被判定为是同一个节点,但是prop word发生了变化,也就是说ListInput组件依赖的状态发生了变化,此时第一个ListInput组件的watcher会被放入队列中等待执行更新。第二个ListInput节点会被删掉,在vue diff算法中被判定为是应该删除的节点。下图展示了第一个ListInput组件节点对应的vdom。
当第一个ListInput组件执行更新的时候,只有word这个prop状态发生了变化,因此只更新了依赖这个状态的第一个文本节点,其他节点依赖的状态没有发生变化因此不更新。
从上面的分析可以看出,尽量避免使用数组的下表作为key。
3. v-if/v-else-if/v-else中使用key
key在 vue diff算法中担当了一个重要的角色,正如前面所说如果key不一样,vue会将节点看做一个全新的节点,如果这个节点是一个组件类型的节点,那么它会再次触发节点的生命周期钩子函数。如果本来不相同的元素被识别为相同会发生一些意料之外的结果,比如不会触发transition动画等。
4. key的作用
- 触发完整的生命周期钩子
- 触发transition
- 提高查找相同节点的效率(vue diff中的key到index映射关系创建)
Vue.nextTick()
Vue组件之间的通信
组件之间的关系
- 父子组件
- 兄弟组件
- 跨级组件
组件通信的方式
- prop
定义一个组件的prop,父组件通过prop向子组件传递数据 - 自定义事件 this.$emit('on-click', data)
为保证单向数据流,子组件不要直接修改父组件直接传递过来的数据,而是通过自定义事件的方式传递给父组件;父组件监听子组件的事件获得数据 - slot
slot是vue中子组件向父组件进行内容分发的组件 - ref, this.$parent 和 this.$children
通过给子组件添加ref属性,父组件可以通过this.$refs.xxx
来引用子组件
父组件也可以通过this.$children来访问所有的子组件,同理子组件通过this.$parent来访问父组件 - provide和inject实现跨级组件通信(非响应式的)
- $attrs 和 $listeners 实现跨级组件通信
- 使用vuex
- 使用事件总线 EventBus
//**事件总线
var bus = new Vue();
var app = new Vue({
el: '#app',
template: `
<div>
<brother1></brother1>
<brother2></brother2>
</div>
`
})
// 在组件 brother1 的 methods 方法中触发事件
bus.$emit('say-hello', 'world')
// 在组件 brother2 的 created 钩子函数中监听事件
bus.$on('say-hello', function(arg) {
console.log('hello ' + arg);
// hello world
})
Vue diff算法
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.