Vue

Vue

1. Vue原理
  • Vue是采用数据劫持配合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的gettersetter

  • 在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。

  • 具体执行流程:

1.MVVM作为绑定入口,整合Observe,CompilWatcher三者,通过Observe来监听model的变化。

2.通过Compil来解析编译模版指令,最终利用Watcher搭起ObserveCompil之前的通信桥梁。

3.从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。

2. Vue的生命周期
  • 单个组件的生命周期

1.beforeCreated

2.created

3.beforeMounted

4.mounted

5.activated

6.beforeUpdated

7.updated

8.deactivated

9.beforeDestory

10.destoryed

  • 父子组件的生命周期顺序

1.父组件先执行(beforeCreated -> created -> beforeMounted)函数

2.父组件挂载前,子组件再执行(beforeCreated -> created -> beforeMounted -> mounted )。

3.子组件挂载完成之后,最后执行父组件挂载函数mounted

4.接着是下面的三种情况:

(1)更新

只更新父或子: 局部更新,父或子beforeUpdate -> updated

同时更新父和子: 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

(2)销毁父组件

父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

(3)激活父组件

子activated -> 父activated -> 停止 -> 子deactivated -> 父deactivated

3. Vue响应式原理
  • 使用 defineReactive 函数将深度遍历一个对象(或循环遍历数组),将对象构建成响应式式对象。明显的标志就是 ob 属性 实质是通过 Object.defineProperty对属性(深度遍历)进行 settergetter 拦截。

  • get中主要做依赖收集dep.depend() 【子属性也收集该依赖】

  • set中主要做派发更新 (新的值才更新)dep.notify()调用dep数组中每个渲染watcherupdate方法更新DOM

  • 响应式对象使用应该注意哪些点

1.对象的新增属性,数组的新增元素,因为不是响应式的,所以不会触发视图渲染。此时应该使用 $set

2.改变某一下标的元素,因为Vue未实现监听,所以不会触发视图渲染。此时应该使用 $set

3.删除对象的属性,数组下标的某一元素,确保删除属性能触发视图渲染。此时应该使用 $delete

4. data为什么必须是函数而不是对象?
  • 首先举个栗子

var option = {

  data: {

a: 1

  }

}

class component {

  constructor(opt) {

this.data = opt.data;

Object.defineProperty(this.data, `a`, {

  get: function () {

console.log('get val');

return this._a;

  },

  set: function (newVal) {

console.log('set val:' + newVal);

this._a = newVal;

  }

});

  }

}

var c1 = new component(option);

var c2 = new component(option);

c1.data.a = 3;

c2.data.a = 5;

console.log(`c1 : ` + c1.data.a);//c1 : 5

console.log(`c2 : ` + c2.data.a);//c2 : 5

示例代码中 Object.defineProperty 把传进来组件中的dataa 属性转化为 gettersetter,可以实现 data.a的数据监控。这个转化是Vue.js 响应式的基石。

这样就不难理解data为什么不能是对象了,如果传进来是对象,new出来的两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改。

总结:

1.对象是对于内存地址的引用。直接定义个对象的话,组件之间都会使用这个对象,这样会造成组件之间数据相互影响。

2.组件就是为了抽离开来提高复用的, 如果组件之间数据默认存在关系,就违背了组件的意义。

3.函数 return 一个新的对象,其实还是为 data 定义了一个对象, 只不过这个对象是内存地址的单独引用了,这样组件之间就不会存在数据干扰的问题。

5. v-model基本原理
  • 首先在编译阶段,v-model被当成普通指令解析到el.directives,然后在解析v-model的时候,会根据节点的不同请求去执行不同的逻辑。

1.如果节点是selectcheckbox, radio,则监听的是change事件

2.如果节点是inputtextarea,则监听一般是input事件,在.lazy下的情况下是change事件。

3.如果节点是组件,则是使用自定义的回调函数

  • 在运行的时候,通过相应事件的监听函数去更改数据

v-model实质是一种语法糖,换成模板写法如下:




  • 组件中使用v-model

// 自定义属性名和事件名需要一致

export default {

  model: {

prop: 'num', // 自定义属性名

event: 'addNum' // 自定义事件名

  },

  props: {

num: Number,

  },

  methods: {

add() {

  this.$emit('addNum', this.num + 1)

}

  }

}

6. vue2.0响应式的缺陷
  • Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应

  • Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,弃用了这个特性。

  • Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历

7. Vue3.0为什么使用Proxy实现响应式
  • Proxy可以劫持整个对象,并返回一个新的对象。

  • Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

8. Vue的通信方式
  • props$emit

  • $parent$children

  • vueBus: 中央事务总线,一个发布订阅中心

  • vuex:状态树管理(单一的)

  • refrefs

  • $attr$listener: v-bind="$attrs" v-on="$listeners"

  • provideinject: 实质就是递归父组件帮你寻找对应的provider

  • provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

9. Vue.nextTick的原理
  • Vue.nextTick是在执行render渲染后运行的,即在render渲染后的下一次tickevent loop最开始的时候执行)

  • Vue.nextTikc的降级顺序(优先使用) Promise.then(microtask) , MutationObserver(microtask) , setImmediate(task) , setTimeout(fn, 0)(task)

  • Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

  • 应用场景

1.在Vue生命周期的created()钩子函数进行DOM操作一定要放到Vue.nextTick()的回调函数中。

2.在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

  • 10. new Vue会做什么操作

Vue.prototype._init = function (options) {

const vm = this

// ...忽略,从第45行看起

if (process.env.NODE_ENV !== 'production') {

  initProxy(vm) // 作用域代理,拦截组件内访问其它组件的数据

} else {

  vm._renderProxy = vm

}

// expose real self

vm._self = vm

initLifecycle(vm) // 建立父子组件关系,在当前实例上添加一些属性和生命周期标识。

initEvents(vm) // 用来存放除 @hook:生命周期钩子名称="绑定的函数"事件的对象。如:$on、 $emit等

initRender(vm) // 用于初始化 $slots、 $attrs、 $listeners

callHook(vm, 'beforeCreate')

initInjections(vm) // resolve injections before data/props  // 初始化 inject,一般用于更深层次的组件通信,相当于加强版子组件的 props。用于组件库开发较多

initState(vm) // 是很多选项初始化的汇总,包括:props、methods、data、computed和watch 等。

initProvide(vm) // resolve provide after data/props  // 初始化 provide

callHook(vm, 'created')

// ...忽略

if (vm.$options.el) {

  vm.$mount(vm.$options.el)  // 挂载实例

}

  }

11. Vue的diff原理
  • 主要执行的是patch函数。主要流程如下:

function patch (oldVnode, vnode, hydrating, removeOnly)

1.如果oldVnode不存在,即是新添加的节点,则创建vnode的DOM

2.如果不是真实的节点且是相同类型的节点,则进入结点diff,即patchVnode函数。否则会用新的节点替换老的。这里的相同类型指的是具有相同的key值和一些其他条件,例如标签相同等等。

3.如果oldVnode === vnode,则认为没有变化, 如果oldVnodeisAsyncPlaceholder属性为true时,跳过检查异步组件,return

4.如果oldVnodevnode都是静态节点(实例不会发生变化),且具有相同的key,并且当vnode是克隆节点或是v-once指令控制的节点时,则把oldVnode.elmoldVnode.child都复制到vnode上;

5.如果vnode不是文本节点或注释节点

(1)如果vnodeoldVnode都有子节点并且两者的子节点不一致时,就调用updateChildren更新子节点

(2)如果只有vnode有子节点,则调用addVnodes创建子节点

(3)如果只有oldVnode有子节点,则调用removeVnodes把这些子节点都删除

(4)如果vnode文本为undefined,则清空vnode.elm文本;

6.如果vnode是文本节点但是vnode.text != oldVnode.text时只需要更新vnode.elm的文本内容就可以。

7.在updateChildren主要是子节点数组对比,思路是通过首尾两端对比,如果是相同类型的节点也会使用patchVnode函数。

  • 在做对比中key 的作用 主要是

1.决定节点是否可以复用

2.建立key-index的索引,主要是替代遍历,提升性能

12. computed 和 watcher
  • computed是计算属性,依赖其他属性计算,并且computed的值有缓存,只有当计算值发生变化才会返回内容。所以,对于任何复杂逻辑,你都应当使用计算属性。

  • watch主要用于监控vue实例的变化,它监控的变量当然必须在data里面声明才可以,它可以监控一个变量,也可以是一个对象。比较适合的场景是一个数据影响多个数据。

  • watch支持异步。

  • watcher的分类

1.内部-watcher vue组件上的每一条数据绑定指令(例如{{myData}})和computed属性,通过compile最后都会生成一个对应的 watcher 对象。

2.user--watcherwatch属性中,由用户自己定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数

3.render-watcher每一个组件都会有一个 render-watcher, function () {vm._update(vm._render(), hydrating);}, 当 data / computed中的属性改变的时候,会调用该 render-watcher 来更新组件的视图

watcher 也有固定的执行顺序,分别是:内部-watcher -> user-watcher -> render-watcher

13. Vue指令

// 全局

Vue.directive('my-click', config)

// 局部

new Vue({

directives:{

focus: config // v-focus

}

}})

  • 配置参数

1.一个指令定义对象可以提供如下几个钩子函数 (均为可选):

(1)bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

(2)inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

(3)update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。

(4)componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

(5)unbind:只调用一次,指令与元素解绑时调用。

每个钩子函数都有四个参数el、binding、vnode 和 oldVnode

14. 混入 (mixin)
  • 混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

  • 全局和局部mixin


var mixin = {

  data: function () {

return {

  message: 'hello',

  foo: 'abc'

}

  }

}



Vue.mixin(mixin)



new Vue({

  mixins: [mixin],

})

  • 合并策略

1.钩子函数将合并成数组,且混入的函数先执行

2.其他的值为对象的将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

3.默认的合并策略可以使用下面的方面更改


Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {

  // 返回合并后的值

}

15. vue-router
  • Vue RouterVue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌

  • 路由模式

1.HashHistory模式:实质是监听onhashchange事件 (window.location API - location.hash

2.HTML5History模式:实质是使用h5的 window.history API, 监听popstate事件(pushState, replaceState)。使用该模式,服务器和前端需要做好页面404的处理

3.AbstractHistory模式:在不支持上面两种方式的环境下使用,如node环境,实际是使用数组模拟路由历史栈

  • 导航守卫

// 全局守卫

// 在项目中,一般在beforeEach这个钩子函数中进行路由跳转的一些信息判断。

判断是否登录,是否拿到对应的路由权限等等。

router.beforeEach((to, from, next) => {

  to: Route:  // 即将要进入的目标 路由对象

  from: Route: // 当前导航正要离开的路由

  next: Function: // 一定要调用该方法来 resolve 这个钩子。

})

router.afterEach((to, from) => {})

router.beforeResolve((to, from) => {}) 

// 与afterEach类似, 区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用

// 路由独享守卫

const router = new VueRouter({

  routes: [

{

  path: '/foo',

  component: Foo,

  beforeEnter: (to, from, next) => {},

  ...

}

  ]

})

// 组件内守卫

const Foo = {

  template: `...`,

  beforeRouteEnter (to, from, next) {

// 在渲染该组件的对应路由被 confirm 前调用

// 不!能!获取组件实例 `this`

// 因为当守卫执行前,组件实例还没被创建

  },

  beforeRouteUpdate (to, from, next) {

// 在当前路由改变,但是该组件被复用时调用

// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,

// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。

// 可以访问组件实例 `this`

  },

  beforeRouteLeave (to, from, next) {

// 导航离开该组件的对应路由时调用

// 可以访问组件实例 `this`

  }

16. VueRouter
  • 基于vue的插件机制,全局混入beforeCreateddestroyed的生命钩子

  • 查找根实例上的route,注入到每个组件上,监听current变化


Vue.util.defineReactive(this, '_route', this._router.history.current)

  • vue原型上添加两个属性$router$route, 拦截get操作,限制set操作

Object.defineProperty(Vue.prototype, '$router', {

get () { return this._routerRoot._router }

})

  • 注册全局组件RouterView 和 RouterLink
17. Vuex
  • Vue.js 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  • 核心概念

1.state:使用单一状态树,用一个对象就包含了全部的应用层级状态。

2.getter:可看成数据的computed计算属性

3.mutation:唯一更改数据的方法 通过 store.commit 使用相应的 mutation方法

4.Action:支持异步的提交mutation 通过 store.dispatch 使用相应的Action方法

5.module:数据分模块。如moduleA.xx

  • 如何注入

在使用 Vue.use(vuex)的时候会执行install 方法在(vue插件机制)。这个方法会混入一个minxin


Vue.mixin({

beforeCreate() {

const options = this.$options

// store injection

// 非根组件指向其父组件的$store,使得所有组件的实例,都指向根的store对象

if (options.store) {

  this.$store = typeof options.store === 'function'

? options.store()

: options.store

} else if (options.parent && options.parent.$store) {

  this.$store = options.parent.$store

}

}

})

  • 如何实现响应式

通过添加到data中实现响应式


store._vm = new Vue({

  data: {

$$state: state

  },

  computed // 这里是store的getter

})

18. 首屏加载慢的优化方案
  • webpack来打包Vue项目,vendor.js过大问题解决

1.造成过大的原因是因为在main.js导入第三库太多时,webpack合并js时生成了vendor.js(我们习惯把第三方库放在vendor里面)造成的,js文件过多,拖慢加载速度,所以:首先在index.html中,使用CDN的资源














2.在bulid/webpack.base.conf.js文件中,添加externals,导入index.html下所需的资源模块:


module.exports = {

  context: path.resolve(__dirname, '../'),

  entry: {

app: ['babel-polyfill', 'lib-flexible', './src/main.js']

  },

  externals: { // <-添加

vue: 'Vue',

vuex: 'Vuex',

'vue-router': 'VueRouter',

VueAwesomeSwiper: 'VueAwesomeSwiper'

  },

3.在main.js里将以下 import 注释 替换 require 引入模块


// import Vue from 'vue'

// import VueAwesomeSwiper from 'vue-awesome-swiper'

const Vue = require('vue')

const VueAwesomeSwiper = require('VueAwesomeSwiper')

Vue.use(VueAwesomeSwiper)

4.当然可以在生产环境当中删除掉不必要的console.log,打开build/webpack.prod.conf.jsplugins里添加以下代码


plugins: [

new webpack.optimize.UglifyJsPlugin({ //添加-删除console.log

  compress: {

warnings: false,

drop_debugger: true,

drop_console: true

  },

  sourceMap: true

}),

5.执行npm run build之后,会发现文件的体积明显小了很多,如果把一些Ui库也替换成CDN的方式,可能体积会更小,渲染解析更快。

  • Vue-cli开启打包压缩 和后台配合 gzip访问开启打包压缩 和后台配合 gzip访问

1.首先打开 config/index.js,找到 build 对象中的productionGzip ,改成 true

2.打开 productionGzip: true 之前,先要安装依赖 compression-webpack-plugin ,官方推荐的命令是:


npm install --save-dev compression-webpack-plugin

//(此处有坑) 如果打包报错,应该是版本问题 ,先卸载之前安装的此插件 ,然后安装低版本

npm install --save-dev [email protected]

3.等安装好了,重新打包 npm run build,此时打包的文件会 新增 .gz文件。是不是比原来的js文件小很多呢,之后项目访问的文件就是这个.gz文件

4.后台nginx开启gzip模式访问,浏览器访问项目,自动会找到 .gz的文件。加载速度明显提高。


http {  //在 http中配置如下代码,

  gzip on;

  gzip_disable "msie6";

  gzip_vary on;

  gzip_proxied any;

  gzip_comp_level 8; #压缩级别

  gzip_buffers 16 8k;

  #gzip_http_version 1.1;

  gzip_min_length 100; #不压缩临界值

  gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

}

19. Vue核心之虚拟DOM
  • 真实DOM和其解析流程,浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树-->创建StyleRules-->创建Render树-->布局Layout-->绘制Painting

1.用HTML分析器,创建DOM树。

2.用CSS分析器,生成样式规则表。

3.关联DOM树和规则表,生成渲染树。

4.通过渲染树计算节点属性。

5.通过计算好的节点属性,渲染页面

DOM树的构建是文档加载完成开始的?构建DOM数是一个渐进过程,为达到更好用户体验,渲染引擎会尽快将内容显示在屏幕上。它不必等到整个HTML文档解析完毕之后才开始构建render数和布局。

Render树是DOM树和CSSOM树构建完毕才开始构建的吗?这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。

CSS的解析是从右往左逆向解析的(从DOM树的下-上解析比上-下解析效率高),嵌套标签越多,解析越慢。

  • JS操作真实DOM的代价

用我们传统的开发模式,原生JSJQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验。

  • 虚拟DOM有什么好处?虚拟DOM,其实是一个大对象

1.Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化。

2.虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attchDOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。

你可能感兴趣的:(Vue)