只有那些从不仰望星空的人,才不会跌入坑中。——泰勒斯
1、Vue常用修饰符
- .stop - 调用 event.stopPropagation(),阻止向上冒泡,点击子级,不会触发父级的点击事件。
- .prevent - 调用 event.preventDefault(),阻止默认事件。
- .capture - 事件捕获:在捕获阶段,事件从window开始,之后是document对象,一直到触发事件的元素。
- .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
- .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
- .native - 监听组件根元素的原生事件。
- .once - 只触发一次回调。
- .left - (2.2.0) 只当点击鼠标左键时触发。
- .right - (2.2.0) 只当点击鼠标右键时触发。
- .middle - (2.2.0) 只当点击鼠标中键时触发。
- .passive - (2.3.0) 以 { passive: true } 模式添加侦听器
2、computed和watch
- computed:一个数据受多个数据影响。是基于它的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它才会重新求值。
- watch:一个数据影响多个数据,当需要在数据变化时执行异步或开销较大的操作时;
3、vue中v-if和v-for不建议同时使用
v-if比v-for优先级高,一起使用在性能上会造成极大的浪费,并且官网也并不推荐我们这样做,我们可以选择使用computed过滤掉列表中不需要显示的项目,或者两者分别作用在不同元素上。
<div>
<div v-for="(user,index) in activeUsers" :key="user.index" >
{{ user.name }}
</div>
</div>
data () { // 业务逻辑里面定义的数据
return {
users,: [{
name: '111111',
isShow: true
}, {
name: '22222',
isShow: false
}]
}
}
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isShow;//返回isShow=true的项,添加到activeUsers数组
})
}
}
4、Vue组件间如何通讯,有哪些方式?
一般来说,组件可以有以下几种关系:
A 和 B、B 和 D、C和 E\C和F 都是父子关系,B 和 C 是兄弟关系,A 和 D、A和E、A和F 是隔代关系(可能隔多代)。针对不同的使用场景,如何选择行之有效的通信方式?随着vue版本的迭代,vue提供了很多组件间的通讯方式,这里我针对不同场景总结了vue组件间通信的6种方式:
props和$emit
是最常用的父子组件间通讯的方式。父组件通过props向下传递数据给子组件;子组件通过$emit
触发父组件的事件,并可通过events给父组件发送数据。这俩vue入了门都懂啊,这里就不做代码演示了。
这种方法通过一个空的Vue实例作为中央事件总线,用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、隔代。
举个栗子
假设兄弟组件有三个,分别是A、B、C组件,C组件如何获取A或者B组件的数据
<div id="itany">
<my-a></my-a>
<my-b></my-b>
<my-c></my-c>
</div>
<template id="a">
<div>
<h3>A组件:{{name}}</h3>
<button @click="send">将数据发送给C组件</button>
</div>
</template>
<template id="b">
<div>
<h3>B组件:{{age}}</h3>
<button @click="send">将数组发送给C组件</button>
</div>
</template>
<template id="c">
<div>
<h3>C组件:{{name}},{{age}}</h3>
</div>
</template>
<script>
var Event = new Vue();//定义一个空的Vue实例
var A = {
template: '#a',
data() {
return {
name: 'tom'
}
},
methods: {
send() {
Event.$emit('data-a', this.name);
}
}
}
var B = {
template: '#b',
data() {
return {
age: 20
}
},
methods: {
send() {
Event.$emit('data-b', this.age);
}
}
}
var C = {
template: '#c',
data() {
return {
name: '',
age: ""
}
},
mounted() {//在模板编译完成后执行
Event.$on('data-a',name => {
this.name = name;//箭头函数内部不会产生新的this,这边如果不用=>,this指代Event
})
Event.$on('data-b',age => {
this.age = age;
})
}
}
var vm = new Vue({
el: '#itany',
components: {
'my-a': A,
'my-b': B,
'my-c': C
}
});
</script>
$on
监听了自定义事件 data-a和data-b,因为有时不确定何时会触发事件,一般会在 mounted 或 created 钩子中来监听。
这是Vue2.4 版本提供的方法
$attrs:
包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。$listeners:
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。
这是Vue2.2.0新增的API,这对选项需要一起使用,provide以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,然后在子孙组件中通过inject来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系
- ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
$parent / $children
:访问父 / 子实例
这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化
5、v-model原理
v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。 可以通过model属性的prop和event属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性。
6、Vue组件生命周期
Vue从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。
钩子函数 | 描述 |
---|---|
beforeCreate | 在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | 真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作 |
beforeUpdate | 发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染 |
updated | 发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新 |
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestroy | 发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器 |
destroyed | 发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁 |
组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
组件的销毁操作是先父后子,销毁完成的顺序是先子后父。
具体如下:
- 加载渲染过程:
父beforeCreate-->父created-->父beforeMount-->子beforeCreate-->子created-->子beforeMount-->子mounted-->父mounted
- 子组件更新过程:
父beforeUpdate-->子beforeUpdate-->子updated-->父updated
- 父组件更新过程:
父 beforeUpdate --> 父 updated
- 销毁过程:
父beforeDestroy-->子beforeDestroy-->子destroyed-->父destroyed
7、自定义指令directives
钩子函数:
-
bind :只调用一次, 指令第一次绑定到元素时调用。在这里可以进行一-次性的初始化设置
-
inserted :被绑定元素插入父节点时调用(仅保证父节点存在,但不一-定已被插入文档中)。
-
update :所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。
注意:指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板
-
componentUpdated : 指令所在组件的VNode 及其子VNode 全部更新后调用。
-
unbind:只调用一次,指令与元素解绑时调用。
<template>
<div>
<!-- 基础 -->
<input class="form-control" type="text" v-focus="123" />
</div>
</template>
<script>
export default {
data() {
return {}
},
directives:{
focus:{
// 绑定
bind(el,binding,vNode){
console.log('-------bind-------')
// console.log(el)
console.log(binding)
},
// 插入节点
inserted(el,binding){
console.log('-------inserted-------')
// console.log(el)
// console.log(binding)
el.focus()
},
},
}
}
</script>
8、$nextTick原理
$nextTick在下次 DOM 更新循环结束之后执行延迟回调。
1. JS单线程
JS执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
①、 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
②、主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
③、一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
④、 主线程不断重复上面的第三步。
2. 异步更新队列
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
9、什么是动态组件、异步组件?
<component v-bind:is="currentTabComponent"></component>
currentTabComponent 可以包括已注册组件的名字,或一个组件的选项对象。这里直接引用官网例子:
<!DOCTYPE html>
<html>
<head>
<title>Dynamic Components Example</title>
<script src="https://unpkg.com/vue"></script>
<style>
.tab-button {
padding: 6px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #ccc;
cursor: pointer;
background: #f0f0f0;
margin-bottom: -1px;
margin-right: -1px;
}
.tab-button:hover {
background: #e0e0e0;
}
.tab-button.active {
background: #e0e0e0;
}
.tab {
border: 1px solid #ccc;
padding: 10px;
}
</style>
</head>
<body>
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab"
>
{{ tab }}
</button>
<component v-bind:is="currentTabComponent" class="tab"></component>
</div>
<script>
Vue.component("tab-home", {
template: "<div>Home component</div>"
});
Vue.component("tab-posts", {
template: "<div>Posts component</div>"
});
Vue.component("tab-archive", {
template: "<div>Archive component</div>"
});
new Vue({
el: "#dynamic-component-demo",
data: {
currentTab: "Home",
tabs: ["Home", "Posts", "Archive"]
},
computed: {
currentTabComponent: function() {
return "tab-" + this.currentTab.toLowerCase();
}
}
});
</script>
</body>
</html>
即组件按需加载,当组件比较大和业务复杂时使用,详细的解析建议看官网
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
10、谈谈keep-alive的使用和原理?
keep-alive是vue.js的内置组件,它能够把不活动的组件的实例保存在内存中,而不是直接的销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。
被keep-alive包裹的组件将被缓存,keep-alive组件提供了include和exclude两个属性来进行有条件的缓存,二者都可以用逗号分隔字符串、正则表达式或则数组表示。
//name名为componentA的组件会被缓存起来
<keep-alive include="componentA">
<componentA></componentA>
<componentB></componentB>
</keep-alive>
//name名为componentA的组件将不会被缓存。
<keep-alive exclude="componentA">
<componentA></componentA>
<componentB></componentB>
</keep-alive>
生命钩子
keep-alive提供了两个生命钩子,actived与deactived。
因为keep-alive会把组件保存到内存中,并不会销毁或则重新构建,所以不会调用组件的creted等方法,需要使用actived和deactived两个钩子判断组件是否处于活动状态。具体实现原理可参考 keep-alive实现原理
11、mixins —> Vue组件如何抽离公共逻辑?
混入 (mixins): 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
缺点:
- 变量来源不明确,不利于阅读和维护
- 可能出现命名冲突
- mixin和组件容易出现多对多的关系,复杂度较高
代码演示:
//mixin.js
<template>
</template>
<script>
export default {
name: 'mixin-test',
components: {},
props: {},
data () {
return {
mixinData: 'mixin中的变量'
}
},
methods: {
mixinFunction () {
return '我是mixins里面的公共方法'
},
},
mounted () {
},
computed: {}
}
//index.vue import这个mixin.js文件 ,然后通过mixins:['文件名']来使用就可以了
<template>
<div>
<div @click="handleMixin">调用mixin方法</div>
</div>
</template>
<script>
import MixinItem from './mixin'
export default {
name: 'mixin-test',
props: {},
mixins: [MixinItem],
components: {},
data () {
return {}
},
methods: {
handleMixin () {
console.log('mixin-data=========', this.mixinData)
let mixfun = this.mixinFunction()
console.log('mixin-fun====>>>', mixfun)
},
},
mounted () {
},
computed: {}
}
12、Vuex原理
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
其原理如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sBCwiAtB-1621979298519)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b765e19841114a4c81798b1854ab6f24~tplv-k3u1fbpfcp-watermark.image)]
其运行流程是: 用户根据View内容进行操作,通过Dispatch触发Action,Action提交Mutation更新state,State更新后触发View更新,这一流程遵循单向数据流的原则。
几个概念:
- state:存储应用状态数据的对象,与vue组件中data类似,state的值可以是对象,也可以是返回对象的函数。通过store.state访问状态数据。
- getters:从state中派生的状态数据,接收state作为第一个参数,第二个为可选参数,类似组件中的 computed,派生数据,在数据出门后进行的加工。
- mutations:提交mutation来修改store中的状态(同步操作),每个mutation都有一个字符串事件类型(type)与一个回调函数(handler),在回调函数中修改状态
注意:不能直接去调用mutation的回调函数,需要当mutation类型为increment时,才能调用此函数;mutation必须是同步的;在store中初始化时设置好所有的属性
- actions:与mutations类似,提交修改state的行为,处理异步任务
注意:提交的是mutation,不是直接修改状态可以包含任意异步操作
- modules:将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
13、vue-router的两种模式
首先前端路由就是通过匹配不同的 url 路径,进行解析,加载不同的组件,然后动态的渲染出区域 html 内容。vue-router有hash和history两种模式:
①、hash模式
vue-router默认的就是hash模式:使用URL的hash来模拟一个完整的URL,当#后面的hash发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,每次 hash 值的变化,会触发hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange来实现更新页面部分内容的操作。
hash模式背后的原理是onhashchange事件,可以在window对象上监听这个事件:
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
document.body.style.color = hash;
}
对于hash模式会创建hashHistory对象,在访问不同的路由的时候,会发生两件事:
HashHistory.push()将新的路由添加到浏览器访问的历史的栈顶,和HasHistory.replace()替换到当前栈顶的路由
②、history模式
主要使用HTML5的pushState()和replaceState()这两个api来实现的,pushState()可以改变url地址且不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器记录进行修改
window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)
切换历史状态
包括back,forward,go三个方法,对应浏览器的前进forward,后退back,跳转go操作:
history.go(-2);//后退两次
history.go(2);//前进两次
history.back(); //后退
hsitory.forward(); //前进
③、两者区别
- 前面的hashchange,你只能改变#后面的url片段。而pushState设置的新URL可以是与当前URL同源的任意URL。
- history模式会将URL修改得就和正常请求后端的URL一样,如后端没有配置对应/user/id的路由处理,则会返回404错误。当用户刷新页面之类的操作时,浏览器会给服务器发送请求,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
14、MVVM
MVVM(Model-View-ViewModel)。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,负责视图显示逻辑和监听视图变化。
当用户操作View时,ViewModel监听到View的变化,会通知Model中对应的方法进行业务逻辑和数据处理,处理完毕后,ViewModel会监听到自动让View做出相应的更新。ViewModel可以对应多个View,具有很强的复用性
15、Vue 响应式原理
Vue采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 getter,setter在数据变动时发布消息给订阅者,触发响应的监听回调。
- Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
- Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。
- Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种
Watcher 和 Dep 的关系
watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。
- initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
- initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
- render()的过程,触发 render watcher 依赖收集
- re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。
- 组件中对响应的数据进行了修改,触发 setter 的逻辑
- 调用 dep.notify()
- 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。
当创建Vue实例时,vue会遍历data选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。
每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例,之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。
Object.defineProperty的缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听引用类型的属性新增和删除
- 无法原生监听数组,需要特殊处理
Vue3.0已带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。
下面看一下Proxy的基本使用:
const proxyData = new Proxy(data, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
console. log( 'get' , key)
return result //返回结果
},
set(target, key, val, receiver) {
const result = Reflect.set(target, key, val, receiver)
console. log( 'set' , key, val)
return result //是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console. log( 'delete property', key)
return result //是否删除成功
}
})
//准备数据
const data = {
name: 'Jake Zhang',
age: 25,
info: {
school:'湖工大' // 需要深度监听
},
arr: ['a','b','c']
}
//更新视图
function updateView () {
console.log('update view')
}
// 重新定义属性,进行监听
function defineReactive (target,key,value) {
// 深度监听
observer(value)
// 核心API
Object.defineProperty (target,key,{
get () {
return value
},
set (newVal) {
if (newVal !== value) {
// 深度监听
observer(newVal)
// 设置新值,value一直在闭包中,此处设置完之后,再get是也能拿到最新的值
value = newVal
// 触发视图更新
updateView()
}
}
})
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype;
// 创建新对象 原型指向oldArrayProperty,再拓展新的方法不会影响原型 如:arrProto.push = function(){} !== arrProto.__proto__.push()
const arrProto = Object.create(oldArrayProperty);
['push','pop','shift','unshift','splice'].forEach(methodName =>{
arrProto[methodName] = function () {
updateView() // 更新视图
oldArrayProperty[methodName].call(this,...arguments)
}
})
// 监听对象属性
function observer (target) {
if(target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 监听数组
if(Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性
for(let key in target) {
defineReactive (target,key,target[key])
}
}
// 调用监听函数
observer(data)
data.name = 'Jake'
data.age = 20
data.info.school = '清华大学' // 深度监听
data.sex = 'man' // 监听不到新增属性,所以有Vue.set()
delete data.name // 删除属性也监听不到,所以有 Vue.delete();
data.arr.push('e') // 监听数组
//后面接着继续实现Dep和Watcher就行,此处不再展开,如果有需要的老哥,评论留言,我再补充
16、虚拟Dom,如何实现vdom
①、vdom: 用js模拟DOM结构,新旧vdom对比,计算出最小的变更,最后更新DOM
<div id="virtual" class="container" title="one" data-index="0">
<p style="font-size:20px">虚拟DOM树</p>
</div>
//使用JS来模拟:
var div = {
tagName:'div',
attrs:{
className:'container',
id:'virtual',
title:'one',
'data-index':'0'
},
children:[
tagName:'p',
attrs:{
style:'font-size:20px',
},
children:[
'虚拟DOM树'
]
}
]
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<!--引入snabbdom的js-->
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
</head>
<body>
<div id="container">
</div>
<button id='c_button'>change</button>
<script type="text/javascript">
var snabbdom = window.snabbdom;
//定义patch函数 实现dom节点更新的核心方法
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
var h = snabbdom.h; //定义h函数
var container = document.getElementById('container'); //获取页面原始的DOM节点
// 新建一个虚拟dom 通过h函数建立虚拟dom的
var vnode = h('ul#list', {}, [
h('ul.item', {}, 'item1'),
h('ul.item', {}, 'item2')
]);
patch(container, vnode); //第一次渲染,vnode去替换container节点内容
document.getElementById('c_button').addEventListener('click', function () {
var newNode = h('ul#list', {}, [
h('ul.item', {}, 'item1'),
h('ul.item', {}, 'itemC'),
h('ul.item', {}, 'itemB')
]);
patch(vnode, newNode); //新的虚拟dom 替换之前的dom元素,只会修改发生变化的dom
})
</script>
</body>
</html>
17、Vue的diff算法
vue和react虽然都采用了diff算法。但 是dif设计是截然不同的,vue采用依赖收集追踪,可以更加细粒度的更新组件,即给模板使用到的每一个属性绑定监听,而react是采用自顶而下的更新策略,每次小的改动都会生成一个全新的vdom。不管是什么dif算法,核心都是一样的。篇幅有限这里只做简单介绍。
Vue的diff算法:
特点1:只做同层级比较,不做跨层级比较
特点2:在diff比较的过程中,循环从两边向中间收拢
循环对比首尾节点的同时对新老节点数组的开始和结尾节点设置标记索引,循环的过程中向中间移动索引,这样既能实现排序也能减小时间复杂度。
key的作用
key的作用主要是为了高效的更新虚拟DOM列表,key值是用来判断VDOM
元素项的唯一依据。 但使用key不保证100%比不使用快,这
就和Vdom不保证比操作原生DOM快是一样的,这只是一种权衡,如果渲染是一个简单的无状态的列表,如不依赖子组件状态或临时DOM状态(例如:表单输入值)的列表渲染输出,不用key性能会更好,因为不用key采用的是“就地更新”的策略。如果数据项的顺序被改变, Vue将不会移动DOM元素来匹配数据项的顺序,而是就地更新每个元素。
18、Vue的template编译
Vue的模板编译在$mount之后,通过compile方法,经过parse、optimize、generate方法,最后生成render function来生成虚拟DOM,虚拟DOM通过diff算法,来更新DOM。
具体功能如下:
- parse 函数解析 template
- optimize 函数优化静态内容
- generate 函数创建 render 函数字符串
细节请移步从源码理解 Vue 模板编译
19、组件的渲染和更新
从黄色部分开始,执行render函数,生成虚拟DOM树,此时监听数据变动,一旦变动,触发 setter函数,通知watcher,重新执行render 函数,循环往复。具体如下:
-
解析模板为 render 函数: 把 vue 语法编译 成 js 语法,通过执行vue-template-compiler的compiler函数,得到 render
-
触发响应式: 将模版初次渲染使用到的变量绑定到 Object.defineProperty() 中,监听data属性的getter和setter,首次渲染会触发getter。
-
执行render函数,生成vnode
-
更新:修改data,触发setter(此前在getter中已被监听),然后重新执行render函数,生成newVnode
20、Vue项目常用优化手段
编码阶段
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 事件的销毁
- SPA 页面采用keep-alive缓存组件
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
打包优化
- 压缩图片/代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 提取组件的 CSS
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
💕看完三件事:
- 点赞 | 你可以点击——>收藏——>退出一气呵成,但别忘了点赞🤭
- 关注 | 点个关注,下次不迷路😘
- 也可以到GitHub拿我所有文章源文件🤗