阅读 8218

我的前端知识梳理-VUE篇

梳理一下个人开发中遇到的一些vue问题,记录一下个人的理解。
======================相关文章和开源库=======================

系列文章

1.前端知识梳理-HTML,CSS篇

2.前端知识梳理-ES5篇

3.前端知识梳理-ES6篇

4.前端知识梳理-VUE篇

个人维护的开源组件库

1.bin-ui,一个基于vue的pc端组件库

2.树形组织结构组件

3.bin-admin ,基于bin-ui的后台管理系统集成方案

4.bin-data ,基于bin-ui和echarts的数据可视化框架

5.其余生态链接

========================================================

1 Vue的底层原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。 这里文档只推荐官方文档,你能遇到的问题几乎都可以在文档中找的答案,这部分只挑几个常见问题进行讲解说明。以便大家可以避免此类问题的发生。 

2 vue的数据机制

单向数据流: 顾名思义,数据流是单向的。数据流动方向可以跟踪,流动单一,追查问题的时候可以更快捷。缺点就是写起来不太方便。要使UI发生变更就必须创建各种 action 来维护对应的 state 

双向数据绑定:数据之间是相通的,将数据变更的操作隐藏在框架内部。优点是在表单交互较多的场景下,会简化大量与业务无关的代码。缺点就是无法追踪局部状态的变化,增加了出错时 debug 的难度 

具体表现在,一般我们使用自定义组件(如bin-ui中的按钮<b-button size=’small’ v-waves :disabled=’false’>我是按钮<b-button>)中,sizedisabled就是单项数据流的体现,父级组件传参给按钮组件,流动单一,按钮组件只需要对props参数做显示即可,双向数据绑定多体现在form表单组件,如input,v-model是实现双向数据绑定的语法糖,本质的数据流动还是单向的,即v-model相当于绑定:value@input=‘’监听返回值并更新。

3 对 vue数据驱动视图的理解

vue特点:

  1. 各部分之间的通信,都是双向的
  2. 采用双向绑定:View 的变动,自动反映在 ViewModel,反之亦然

传统的开发总是避免不了操作dom,我们总会想到在数据返回后去操作dom元素,但是操作dom元素的开销也是比较大的,而且不容易将视图层和业务层分离,因此,需要改变习惯,当我们获取数据完成时,只需要对当前vue状态值进行更新,剩下的刷新操作,就交给vue虚拟dom去完成吧

4 什么是虚拟dom

虚拟DOM是干什么的?这就要从浏览器本身讲起。

如我们所知,在浏览器渲染网页的过程中,加载到HTML文档后,会将文档解析并构建DOM树,然后将其与解析CSS生成的CSSOM树一起结合产生爱的结晶——RenderObject树,然后将RenderObject树渲染成页面(当然中间可能会有一些优化,比如RenderLayer树)。这些过程都存在与渲染引擎之中,渲染引擎在浏览器中是于JavaScript引擎(JavaScriptCore也好V8也好)分离开的,但为了方便JS操作DOM结构,渲染引擎会暴露一些接口供JavaScript调用。由于这两块相互分离,通信是需要付出代价的,因此JavaScript调用DOM提供的接口性能不咋地。各种性能优化的最佳实践也都在尽可能的减少DOM操作次数。

而虚拟DOM干了什么?它直接用JavaScript实现了DOM树(大致上)。组件的HTML结构并不会直接生成DOM,而是映射生成虚拟的JavaScript DOM结构,通过在这个虚拟DOM上实现了一个 diff 算法找出最小变更,再把这些变更写入实际的DOM中。这个虚拟DOM以JS结构的形式存在,计算性能会比较好,而且由于减少了实际DOM操作次数,性能会有较大提升

虚拟dom的概念这里只是给简单介绍一下,vue的template模板语法,包括jsx语法,本质上最后渲染时都会渲染成render函数,而render函数中渲染的内容,其实就是虚拟dom,在前端开发中,类库的开发和组件开发中,render函数的编写也是必须掌握的技能之一。

5 vue生命周期的理解

vue实例有一个完整的生命周期,生命周期也就是指一个实例从开始创建到销毁的这个过程

beforeCreate() 在实例创建之间执行,数据未加载状态
created() 在实例创建、数据加载后,能初始化数据,dom渲染之前执行
beforeMount() 虚拟dom已创建完成,在数据渲染前最后一次更改数据
mounted() 页面、数据渲染完成,真实dom挂载完成
beforeUpadate() 重新渲染之前触发
updated() 数据已经更改完成,dom 也重新 render 完成,更改数据会陷入死循环
beforeDestory() 和 destoryed() 前者是销毁前执行(实例仍然完全可用),后者则是销毁后执行复制代码

vue 生命周期这里日常开发中常见的就是created和mounted,具体应用就是比如,我在页面加载时需要获取新闻列表和一些原始数据,这时候我们就可以在created钩子函数中调用方法去加载数据,并进行绑定,如果有时候我们需要手动实现图表(echarts),这个时候因为图表的实现机制,我们需要确保dom元素已经渲染完成,图表必须挂载到真实dom元素时,就必须在mounted函数中去调用图表生成方法了。

6 v-if和v-show的区别

使用了 v-if 的时候,如果值为 false ,那么页面将不会有这个 html 标签生成。

v-show 则是不管值为 true 还是 false ,html 元素都会存在,只是 CSS 中的 display 显示或隐藏

使用技巧:

1.这两个在使用时会有一些小问题,比如v-if在使用的时候,如果子元素内有使用{{}}绑定的响应属性,如{{current.name}},这时候如果current不存在,那么在渲染这个元素的时候“可能”会报错,为啥是可能,是因为v-if有可能判断为false而导致内层元素根本就没渲染,而使用v-show的话就会报错,因为无论是true或false,内层元素都会渲染,这时如果current没有初始化,则一定会报错。所以,我们如果通过v-show来显示隐藏元素的时候,需要确保内层绑定值已经初始化完成了。

2.个人推荐,如果是内层元素不变化的,如图片,部分样式内容等开启隐藏的,可以用v-show来实现,配合transition能实现比较好的性能要求。如果是需要动态渲染的可以使用v-if

3.有些情况由于刷新机制问题,我们可以通过v-if来强制开启vue进行重绘元素,如element和bin-ui,表格的生成都是基于配置宽高动态生成的,这就需要监听窗口大小去调用组件提供的接口重新刷新大小,但还有个暴力的解决办法就是v-if,比如弹窗的时候再渲染表格,关闭时直接=false来强制清除元素,以便保证每次都刷新重绘元素。

7 什么是NextTick函数

this.$nextTick()函数,官方释义是下次DOM更新循环结束之后执行的延迟回调,一般我们会在修改数据之后使用$nextTick(),则可以在回调后获取更新后的DOM元素。

这个函数的具体使用我举个例子,上文中说道element,包括我的bin-ui重的table都会提供一个刷新大小的接口函数来手动获取table组件并重绘大小,但是这个函数有时候你编码了确并未实现,那这是为什么呢,原因就是vue在更新DOM的时候是异步执行的,只要侦听到数据变化,vue将会开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM的操作是非常重要的。然后再下一个事件循环“tick”中,vue刷新队列并行执行实际(已去重)的工作。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})
复制代码

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
复制代码

因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}复制代码

回到上文所述的现象上,由于dom刷新是异步的,而且被加入到一个队列中去,类似setTimeout异步队列,我们无法确定你当前希望刷新表格大小事件是否在当前渲染帧中执行。因此,在我们动态计算了宽度/高度后,我们需要准确的获取已经更新dom元素后的组件,我们就要在nextTick函数中去获取执行刷新函数,这样就可以保证元素重绘正常而不需要使用v-if强制刷新了。 

注意这里使用this.$nextTick是官方提供的方法,我们也可以用setTimeout(func,20)默认20毫秒来模拟nextTick函数,20毫秒是经验值,但还是推荐使用nextTick函数。 

8 Vue中key值的作用

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用主要是为了高效的更新虚拟DOM。

这块的注意点主要在动态组件和v-for时,为了标识独有dom,key值一般我们取类似id这种唯一且 不变的变量,如果仅为了区分dom,切元素不会频繁更新(增删改)则可使用index索引

9 组件通信

1.父组件向子组件通信:子组件通过 props 属性,绑定父组件数据,实现双方通信

2.子组件向父组件通信:将父组件的事件在子组件中通过 $emit 触发

3.非父子组件、兄弟组件之间的数据传递

  •   eventBus中央事件总线let EventBus = new Vue();
  •    Vuex 数据管理
  •   3.3 provide和inject 注入(多用于组件form)

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

组件通信,结合4.2的部分,如 <b-button size=’small’ v-waves :disabled=’false’>我是按钮<b-button> 这其中size,:disabled 都是prop传值,也就是父组件传值给子组件,这里有个注意点,静态数据绑定,类似size=’small’ :disabled=’false’都属于静态绑定,即不会动态根据父组件 data中的响应值做变化,这里数据绑定有个规定,如果数据值为静态数据切为字符串,则可以省略v-bind:即像size=’small’,否则所有的数据绑定都需要使用v-bind:,一般我们会省略,已冒号开头,后面跟随的就是绑定值,这个值也可以是各种数据类型,表达式,甚至是函数返回,如 :data=’[1,2,3]’ :data=’list? list : []’ :data=’33’ 等 如果是布尔值的传值还有个建议写法,如,直接在组件中写 <b-button disabled>我是按钮<b-button> 这里 的disabled 就相当于 :disabled=’true’ 

10 插槽

这里简单介绍一个插槽概念,组件编写时可以提供props和插槽两种类型的传值方式,区别不同于props传值的是,插槽 更加灵活多变,如上文的按钮 

<b-button size=’small’ v-waves :disabled=’false’>我是按钮<b-button> 按钮中间的‘我是按钮’就是插槽,插槽允许你往其中插入你想要的任何内容,如 

<b-button><i class=’iconfont close’></i><span>我是按钮</span><b-button> 你可以插入你想定制的任意内容给组件,当前前提是组件提供了一个<slot></slot>的默认插槽 这就像你小时候玩的卡带游戏机,你想玩什么游戏就插入什么样的卡带即可  

但是这样仍然不够强大,如果我想我插入内容时默认就有个字符或者元素在那呢,这里就是你需要在组件中<slot>….</slot>中间写入你想要的默认元素内容就行了

类比于电脑主板,不同的生产厂商都会按照一种共同的标准提供不同的接口插槽,为的就是方便用户按需扩展,vue的组件插槽也提供了这种方式。有时候我们也会有多个插槽,如 

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>
复制代码

对于这样的情况,<slot> 元素有一个特殊的特性:name。这个特性可以用来定义额外的插槽:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
复制代码

一个不带 name 的 <slot> 出口会带有隐含的名字“default”。

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
复制代码

现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。

插槽不仅有默认和具名插槽,还用作用域插槽,如父级插入子组件的插槽可以访问子组件的数据,这就是作用域插槽,具体使用参考bin-ui,element-ui中表格的用法

11 混入mixin

在新项目运用vue实现数据绑定的同事可能会发现,每个vue实例下都写了一行

mixins:[mixin],那这个是干嘛的呢

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项

mixin混入在vue开发中十分常见,它的用法也非常的简单,你只需要记住,公共的方法函数属性统统都可以放置到mixin中,主要就是为了复用代码,减少代码冗余,如,公共的请求封装,公共的分页属性,公共的查询跳转等都可以。

mixin的混入策略可以简单的理解为以下几点

1.数据对象,data中的属性,会进行递归合并,类比es6的展开运算符,如果mixin中存在的,泽一组件定义的数据优先。

2.同名的钩子函数会合并数组,如mounted ,如果组件和mixin都定义了,则都会执行,且组件内定义的后置。即混入的代码会优先执行。

3.同名的函数会覆盖。类似data中的属性,会将组件内函数和mixin中函数递归合并,同名覆盖,这点类似于es5的,同名函数覆盖,组件内函数覆盖混入函数。

12 vue如何获取dom元素呢

jQuery时代的核心就是获取dom元素,并进行一系列的操作,但是vue数据驱动视图时如果有必要获取dom元素(如获取元素绘制echarts,绘制canvas)时该如何获取呢。

vue提供了一个方法,首先在dom元素中编写ref=’table’,这其实就是类似设置id或class,只是为了给vue进行识别使用。

获取方式也非常简单,只需要this.$refs.tablethis.$refs[‘table’]即可获得dom元素

如果你给一个vue组件设置ref并使用this.$refs获取的则是这个组件的实例,你可以通过这个实例调用组件的内部方法,如上文提到的table刷新方法。

this.$refs.table.handleResize()

13 vue为什么不触发响应式更新

这个问题一般有两种原因。

1.没有设置响应式对象属性的添加删除

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:

var vm = new Vue({
  data:{
    a:1
  }
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
复制代码

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。例如,对于:

Vue.set(vm.someObject, 'b', 2)复制代码

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.someObject,'b',2)复制代码

有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign()_.extend()。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`复制代码
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })复制代码

分析:这就是前面说说的对象拷贝问题,如果你只使用注释的内容来去扩展对象的话,这里a和b实际还是相当于你直接写this.someObject.a=1 , this.someObject.b=2,这里仍然不会触发更新。

如果采用下面的方式进行编码,本质上实际相当于创建了一个新的对象并变更了整个响应式对象someObject的引用地址。因此,vue会重新触发更新。

2.数组索引值设置一个项或数组长度改变

改变数组的长度或者设置一个项

var vm = new Vue({
  data(){
	return{
            list:[{id:1,name:’张三’},{id:2,name:’李四’}]
        }
  }
})
this.list[1]= {id:3,name:’王五’}

this.list.length = 1
复制代码

实际输出的效果就是数据变化了但不会更新视图变化复制代码

数组问题的解决办法有两种 

  1. 创建新数组整体替换原有数组值。
  2. 使用Js中的数组操作函数(本质上还是返回了一个新的数组)也是数组的替换原理。 支持的方法有,push,pop,shift,unshift,splice,sort,reverse 不支持的方法有filter,concat,slice 

因此,我们再遇到数组操作时,一般推荐创建新数组来操作并更新视图,这里就是上文中提到的深拷贝。因为我们不能保证我们当前操作的数组是不是包含对象或者其他引用类型。  

最后结合上文提到的判断精准类型,来实现一个递归调用的深拷贝函数 

function  typeOf (obj) {
  const  toString = Object.prototype.toString
  const  map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object'
  }
  return map[toString.call(obj)]
}
// 深拷贝函数
function  deepCopy (data) {
  const t = typeOf(data)
  let o
  if (t === 'array') {
    o = []
  } else if (t === 'object') {
    o = {}
  } else {
    return data
  }
  if (t === 'array') {
    for (let i = 0; i < data.length; i++) {
      o.push(deepCopy(data[i]))
    }
  } else if (t === 'object') {
    for (let i in data) {
      o[i] = deepCopy(data[i])
    }
  }
  return o
}
复制代码


关注下面的标签,发现更多相似文章
评论