那么,它有什么缺点?javascript
首先你得说说相同点,两个都是MVVM框架,数据驱动视图,无争议。若是说不一样,那可能分为如下这么几点:css
MVVM的核心是数据驱动
即ViewModel,ViewModel
是View和Model的关系映射
。html
MVVM本质就是基于操做数据
来操做视图
进而操做DOM
,借助于MVVM无需直接
操做DOM,开发者只需编写ViewModel
中有业务
,使得View彻底实现自动化
。前端
SPA( single-page application )即一个web项目
就只有一个页面
(即一个HTML文件,HTML 内容的变换是利用路由机制实现的。vue
仅在 Web 页面初始化
时加载
相应的 HTML、JavaScript 和 CSS
。一旦页面加载完成
,SPA 不会
由于用户的操做
而进行页面的从新加载或跳转
;取而代之的是利用路由机制
实现 HTML 内容
的变换,UI 与用户的交互,避免页面的从新加载。java
优势:node
用户体验好、快
,内容的改变不须要从新加载整个页面,避免了没必要要的跳转和重复渲染;服务器压力小
;职责分离
,架构清晰
,前端进行交互逻辑,后端负责数据处理;缺点:react
初次加载耗时多
:为实现单页 Web 应用功能及显示效果,须要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;前进后退路由管理
:因为单页应用在一个页面中显示全部的内容,因此不能使用浏览器的前进后退功能,全部的页面切换须要本身创建堆栈管理;webpack
SEO 难度较大
:因为全部的内容都在一个页面中动态替换显示,因此在 SEO 上其有着自然的弱势。ios
什么是vue生命周期?Vue 实例
从建立
到销毁
的过程
,就是生命周期。
注意:浏览器有8个钩子,可是node
中作服务端渲染的时候只有beforeCreate
和created
new Vue()
以后触发的第一个
钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。 能够作页面拦截。当进一个路由的时候咱们能够判断是否有权限进去,是否安全进去,携带参数是否完整,参数是否安全。使用这个钩子好函数的时候就避免了让页面去判断,省掉了建立一个组建Vue实例。实例建立完成后
,当前阶段已经完成了数据观测
,也就是能够使用数据,更改数据,在这里更改
数据不会
触发updated
函数。能够作一些初始数据的获取,在当前阶段没法
与Dom
进行交互
(由于Dom尚未建立),若是非要想,能够经过vm.$nextTick
来访问Dom。挂载以前
,在这以前template模板已导入渲染函数编译。而当前阶段虚拟Dom
已经建立完成
,即将开始渲染。在此时也能够对数据进行更改,不会触发updated。挂载完成后
,在当前阶段,真实
的Dom
挂载完毕,数据完成双向绑定
,能够访问
到Dom节点
,使用$refs属性对Dom进行操做。更新以前
,也就是响应式数据发生更新,虚拟dom从新渲染以前被触发,你能够在当前阶段进行更改数据,不会形成重渲染。更新完成以后
,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,由于这可能会致使无限循环的更新。实例销毁以前
,在当前阶段实例彻底能够被使用,咱们能够在这时进行善后收尾工做,好比清除计时器,销毁父组件对子组件的重复监听。beforeDestroy(){Bus.$off("saveTheme")}
加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
子组件更新过程 父beforeUpdate->子beforeUpdate->子updated->父updated
父组件更新过程 父 beforeUpdate -> 父 updated
销毁过程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
它的生命周期中有多个事件钩子,让咱们控制
Vue实例过程更加清晰
。
第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个钩子
v-if
事件监听器
和子组件
适当地被销毁和重建
;惰性
的:若是在初始渲染时条件为假,则什么也不作——直到条件第一次变为真时,才会开始渲染条件块。v-show
无论初始条件是什么,元素老是会被渲染,而且只是简单地基于 CSS 的 “display” 属性进行切换。
因此:
不多改变条件
,不须要
频繁切换条件的场景;很是频繁
切换条件的场景。背景:
全部的 prop 都使得其父子 prop 之间造成了一个单向下行绑定
:父级 prop 的更新会向下流动到子组件中,可是反过来则不行。这样会防止从子组件意外改变
父级组件的状态,从而致使你的应用的数据流向变的混乱。
每次父级组件发生更新时,子组件中全部的 prop 都将会刷新为最新的值。这意味着你不该该在一个子组件内部改变 prop。若是你这样作了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能经过 $emit
派发一个自定义事件
,父组件接收到后,由父组件修改
。
有两种常见的试图改变一个 prop 的情形 :
在第2状况下,最好定义一个本地的 data属性并将这个 prop 用做其初始值:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter//定义本地的data属性接收prop初始值 } }
这个 prop 以一种原始的值传入且须要进行转换。
在这种状况下,最好使用这个 prop 的值来定义一个计算属性
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
官方实例
的异步请求是在mounted
生命周期中调用的,而实际上也能够在created生命周期中调用。
本人推荐在 created 钩子函数中调用异步请求,有如下优势:
更快
获取到服务端数据
,减小
页面 loading 时间;ssr
不支持 beforeMount 、mounted 钩子函数,因此放在 created 中有助于一致性;1. 父子props,on
// 子组件
<template> <header> <h1 @click="changeTitle">{{title}}</h1>//绑定一个点击事件 </header> </template> <script> export default { data() { return { title:"Vue.js Demo" } }, methods:{ changeTitle() { this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值” } } } </script>
// 父组件
<template> <div id="app"> <Header @titleChanged="updateTitle" ></Header>//与子组件titleChanged自定义事件保持一致 <h2>{{title}}</h2> </div> </template> <script> import Header from "./Header" export default { data(){ return{ title:"传递的是一个值" } }, methods:{ updateTitle(e){ //声明这个函数 this.title = e; } }, components:{ Header } } </script>
2. parent / $children与 ref
// A 子组件
export default { data () { return { title: 'a组件' } }, methods: { sayHello () { alert('Hello'); } } }
// 父组件
<template> <A ref="comA"></A> </template> <script> export default { mounted () { const comA = this.$refs.comA; console.log(comA.title); // a组件 comA.sayHello(); // 弹窗 } } </script>
3.attrs,listeners
attrs: 包含了父做用域
中不被 prop
所识别
(且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含全部父做用域的绑定 ( class 和 style 除外 ),而且能够经过 v-bind="$attrs" 传入内部组件。一般配合 inheritAttrs 选项一块儿使用。
listeners: :包含了父做用域中的 (不含 .native 修饰器的) v-on 事件监听器。它能够经过 v-on="$listeners" 传入内部组件
// index.vue
<template> <div> <h2>浪里行舟</h2> <child-com1 :foo="foo" :boo="boo" :coo="coo" :doo="doo" title="前端工匠"></child-com1> </div> </template> <script> const childCom1 = () => import("./childCom1.vue"); export default { components: { childCom1 }, data() { return { foo: "Javascript", boo: "Html", coo: "CSS", doo: "Vue" }; } }; </script>
// childCom1.vue
<template class="border"> <div> <p>foo: {{ foo }}</p> <p>childCom1的$attrs: {{ $attrs }}</p> <child-com2 v-bind="$attrs"></child-com2> </div> </template> <script> const childCom2 = () => import("./childCom2.vue"); export default { components: { childCom2 }, inheritAttrs: false, // 能够关闭自动挂载到组件根元素上的没有在props声明的属性 props: { foo: String // foo做为props属性绑定 }, created() { console.log(this.$attrs); // 父组件中的属性,且不在当前组件props中的属性。{ "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script>
// childCom2.vue
<template> <div class="border"> <p>boo: {{ boo }}</p> <p>childCom2: {{ $attrs }}</p> <child-com3 v-bind="$attrs"></child-com3> </div> </template> <script> const childCom3 = () => import("./childCom3.vue"); export default { components: { childCom3 }, inheritAttrs: false, props: { boo: String }, created() { console.log(this.$attrs); // / 父组件中的属性,且不在当前组件props中的属性。{"coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script>
// childCom3.vue
<template> <div class="border"> <p>childCom3: {{ $attrs }}</p> </div> </template> <script> export default { props: { coo: String, title: String } }; </script>
4. Provide、inject的使用:
父组件
<template> <div id="app"> </div> </template> <script> export default { data () { return { datas: [ { id: 1, label: '产品一' } ] } }, provide { return { datas: this.datas } } } </script>
子组件
<template> <div> <ul> <li v-for="(item, index) in datas" :key="index"> {{ item.label }} </li> </ul> </div> </template> <script> export default { inject: ['datas'] } </script>
SSR也就是服务端渲染
,也就是将Vue在客户端把标签
渲染成HTML
的工做放在服务端完成,而后再把html直接返回
给客户端。
服务端渲染 SSR 的优缺点以下:
(1)服务端渲染的优势:
SPA
中是抓取不到
页面经过 Ajax
获取到的内容
;而 SSR
是直接由服务端
返回已经渲染好
的页面(数据已经包含在页面中),因此搜索引擎爬取工具能够抓取渲染好的页面;SPA
会等待
全部 Vue 编译后的 js
文件都下载完成后
,才开始
进行页面的渲染
,文件下载等须要必定的时间等,因此首屏渲染须要必定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,因此 SSR 有更快的内容到达时间;(2) 服务端渲染的缺点:
beforCreate
和 created
两个钩子函数,这会致使一些外部扩展库须要特殊处理,才能在服务端渲染应用程序中运行;而且与能够部署在任何静态文件服务器上的彻底静态单页面应用程序 SPA 不一样,服务端渲染应用程序,须要处于 Node.js server 运行环境;vue-router 有 3 种路由模式:hash
、history
、abstract
,对应的源码以下所示:
switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } }
路由模式的说明以下:
全部浏览器
,包括不支持 HTML5 History Api 的浏览器;JavaScript
运行环境,如 Node.js 服务器端。若是发现没有浏览器的 API,路由会自动强制进入这个模式.(1)hash 模式的实现原理
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。
好比下面这个网站,它的 location.hash 的值为 '#search'
:
https://www.word.com#search
hash 路由模式的实现主要是基于下面几个特性:
客户端
的一种状态
,也就是说当向服务器端发出请求时
,hash 部分不会被发送
;改变
,都会在浏览器的访问历史
中增长
一个记录
。所以咱们能经过浏览器的回退、前进按钮控制hash 的切换;hashchange
事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。(2)history 模式的实现原理
HTML5 提供了 History API
来实现 URL 的变化,其中作最主要的 API 有如下两个:
这两个 API 能够在不进行刷新
的状况下,操做
浏览器的历史纪录
。惟一不一样的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,以下所示:
window.history.pushState(null, null, path); window.history.replaceState(null, null, path);
history 路由模式的实现主要基于存在下面几个特性:
popstate
事件来监听 url 的变化,从而对页面进行跳转(渲染);key 是为 Vue 中 vnode
的惟一标记
,经过这个 key,咱们的 diff
操做能够更准确、更快速
。
Vue 的 diff 过程能够归纳为:
oldCh
和 newCh
各有两个头尾
的变量 oldStartIndex、oldEndIndex
和 newStartIndex、newEndIndex
,它们会新节点和旧节点会进行两两对比
,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,若是以上 4 种比较都没匹配
,若是设置了key
,就会用 key 再进行
比较,在比较的过程当中,遍历会往中间靠
,一旦 StartIdx > EndIdx 代表 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。
因此 Vue 中 key 的做用是:key 是为 Vue 中 vnode 的惟一标记,经过这个 key,咱们的 diff 操做能够更准确、更快速
更快速:利用 key 的惟一性生成 map 对象来获取对应节点,比遍历方式更快,源码以下:
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 }
参考1:Vue2.0 v-for 中 :key 到底有什么用?
虚拟 DOM 的实现原理主要包括如下 3 部分:
优势:
DOM 操做
的实现必须是普适
的,因此它的性能并非最优的;可是比起粗暴的 DOM 操做性能要好不少,所以框架的虚拟 DOM 至少能够保证在你不须要手动优化
的状况下,依然能够提供还不错
的性能
,即保证性能的下限;缺点:
Proxy 的优点以下:
Object.defineProperty 的优点以下:
Proxy 是 ES6 中新增的功能,它能够用来自定义对象中的操做。
let p = new Proxy(target, handler)
添加代理
的对象对象中
的操做
,好比能够用来自定义 set 或者 get 函数。下面来经过 Proxy 来实现一个数据响应式:
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { setBind(value, property) return Reflect.set(target, property, value) } } return new Proxy(obj, handler) } let obj = { a: 1 } let p = onWatch( obj, (v, property) => { console.log(`监听到属性${property}改变为${v}`) }, (target, property) => { console.log(`'${property}' = ${target[property]}`) } ) p.a = 2 // 监听到属性a改变为2 p.a // 'a' = 2
在上述代码中,经过自定义 set 和 get 函数的方式,在本来的逻辑中插入了咱们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
固然这是简单版的响应式实现,若是须要实现一个 Vue 中的响应式,须要在 get 中收集依赖,在 set 派发更新,之因此 Vue3.0 要使用 Proxy 替换本来的 API 缘由在于 Proxy 无需一层层递归为每一个属性添加代理,一次便可完成以上操做,性能上更好,而且本来的实现有一些数据更新不能监听到,可是 Proxy 能够完美监听到任何方式的数据改变,惟一缺陷就是浏览器的兼容性很差。
Vue 框架是经过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操做)进行监听。
vue2:
数组
就是使用 object.defineProperty
从新定义数组的每一项
,能引发数组变化的方法为 pop 、 push 、 shift 、 unshift 、 splice 、 sort 、 reverse
这七种,只要这些方法执行改了数组内容,就更新内容
函数劫持
的方式,重写
了数组方法,具体就是更改了数组的原型,更改为本身的,用户调数组的一些方法的时候,走的就是本身的方法,而后通知视图去更新(本质就是在原有的方法上又调用了更新数据的方法)。对象
,那么就对数组的每一项进行观测vue3:
改用 proxy ,可直接监听对象数组的变化。
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据
输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。 Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。
其中,View 变化更新 Data ,能够经过事件监听的方式来实现,因此 Vue 的数据双向绑定的工做主要是如何根据 Data 变化更新 View。
Vue 主要经过如下 4 个步骤来实现数据双向绑定的:
Object.defineProperty()
对属性都加上 setter
和 getter
。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。变量
都替换成数据
,而后初始化渲染页面视图,并将每一个指令
对应的节点绑定更新函数
,添加监听数据的订阅者,一旦数据有变更,收到通知,调用更新函数进行数据更新。v-model 指令在表单 input、textarea、select 等元素上建立双向数据绑定,v-model 本质上是语法糖
,会在内部为不一样的输入元素使用不一样的属性并抛出不一样的事件:
value
属性和 input
事件;checked
属性和 change
事件;prop
并将 change
做为事件。以 input 表单元素为例:
<input v-model='something'>
至关于
<input :value="something" @input="something = $event.target.value">
为何组件中的 data 必须是一个函数,而后 return 一个对象,而 new Vue 实例里,data 能够直接是一个对象?
// data
data() { return { message: "子组件", childName:this.name } }
// new Vue
new Vue({ el: '#app', router, template: '<App/>', components: {App} })
一个组件被复用屡次的话,也就会建立多个实例,本质上,这些实例用的都是同一个构造函数。
若是data是对象的话,对象属于引用类型,会影响到全部的实例,因此为了保证组件不一样的实例之间data不冲突,data必须是一个函数。
而 new Vue 的实例,是不会被复用的,所以不存在引用对象的问题。
keep-alive 是 Vue 内置
的一个组件
,能够使被包含
的组件保留状态
,避免从新渲染
,其有如下特性:
include
和 exclude
属性,二者都支持字符串或正则表达式, include 表示只有名称匹配
的组件会被缓存
,exclude 表示任何名称匹配
的组件都不会被缓存
,其中 exclude 的优先级
比 include 高
;activated
和 deactivated
,当组件被激活
时,触发钩子函数 activated,当组件被移除
时,触发钩子函数 deactivated。keep-alive的生命周期
好比有父组件 Parent 和子组件 Child,若是父组件监听到子组件挂载 mounted 就作一些逻辑处理,能够经过如下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() { this.$emit("mounted"); }
以上须要手动经过 $emit 触发父组件的事件,更简单的方式能够在父组件引用子组件时经过 @hook 来监听便可,以下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child> doSomething() { console.log('父组件监听到 mounted 钩子函数 ...'); },
// Child.vue
mounted(){ console.log('子组件触发 mounted 钩子函数 ...'); },
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
固然 @hook 方法不只仅是能够监听 mounted,其它的生命周期事件,例如:created,updated 等均可以监听。
因为 JavaScript 的限制,Vue 不能检测到如下数组的变更:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
为了解决第一个问题,Vue 提供了如下操做方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set(Vue.set的一个别名)
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
为了解决第二个问题,Vue 提供了如下操做方法:
// Array.prototype.splice
vm.items.splice(newLength)
使用了函数劫持
的方式,重写
了数组的方法,Vue将data
中的数组
进行了原型链重写
,指向了本身定义
的数组原型方法
。这样当调用数组api时,能够通知依赖更新。若是数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
简单来讲,diff算法有如下过程
同级比较, 再比较子节点,先判断一方有子节点一方没有子节点的状况(若是新的children没有子节点,将旧的子节点移除)
比较都有子节点的状况(核心diff)递归比较子节点
正常Diff两个树
的时间复杂度是O(n^3),但实际状况下咱们不多会进行跨层级的移动DOM,因此Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才须要用核心的Diff算法进行同层级比较。
Vue2的核心Diff算法采用了双端比较
的算法,同时重新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操做。相比React的Diff算法,一样状况下能够减小移动节点次数,减小没必要要的性能损耗,更加的优雅。
Vue3.x借鉴了 ivi算法和 inferno算法 在建立VNode时就肯定其类型,以及在mount/patch的过程当中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提高。 该算法中还运用了动态规划的思想求解最长递归子序列。
简单说,Vue的编译过程就是将template转化为render函数的过程。会经历如下阶段:
首先解析模版
,生成AST
语法树(一种用JavaScript对象的形式来描述整个模板)。
使用大量的正则
表达式对模板
进行解析
,遇到标签
、文本
的时候都会执行
对应的钩子
进行相关处理。
Vue的数据是响应式的,但其实模板中并非全部的数据都是响应式的。有一些数据首次渲染
后就不会
再变化
,对应的DOM
也不会变化。那么优化过程就是深度遍历
AST树,按照相关条件对树节点
进行标记
。这些被标记的节点
(静态节点)咱们就能够跳过
对它们的比对
,对运行时的模板起到很大的优化做用。
编译的最后一步是将优化后的AST树转换
为可执行
的代码
。
computed:
计算属性
,也就是计算值,它更多用于计算值的场景缓存性
,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变以后,下一次获取computed的值时才会从新调用对应的getter来计算watch:
props
$emit
或者本组件
的值,当数据变化时来执行回调进行后续操做不变化
也会执行
小结:
在下次
DOM
更新循环结束以后
执行延迟回调
。在这里面的代码会等到dom更新之后
再执行。
<template> <section> <div ref="hello"> <h1>Hello World ~</h1> </div> <el-button type="danger" @click="get">点击</el-button> </section> </template> <script> export default { methods: { get() { } }, mounted() { console.log(333); console.log(this.$refs['hello']); this.$nextTick(() => { console.log(444); console.log(this.$refs['hello']); }); }, created() { console.log(111); console.log(this.$refs['hello']); this.$nextTick(() => { console.log(222); console.log(this.$refs['hello']); }); } } </script>
Vue在初始化数据时,会使用Object.defineProperty
从新定义data中的全部属性
,当页面使用
对应属性时,首先会进行依赖收集
(收集当前组件的watcher),若是属性发生变化
会通知
相关依赖进行更新操做
(发布订阅)
具体的过程:
initData
初始化用户传入的参数
new Observer
对数据进行观测
对象类型
就会调用 this.walk(value)
对对象进行处理,内部使用 defineeReactive
循环对象属性定义响应式变化,核心就是使用 Object.defineProperty
从新定义数据。Vue3.x改用Proxy替代Object.defineProperty。由于Proxy能够直接监听
对象和数组的变化,而且有多达13种拦截方法。而且做为新标准将受到浏览器厂商重点持续的性能优化。
Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?
判断当前Reflect.get
的返回值是否为Object
,若是是则再经过reactive
方法作代理, 这样就实现了深度观测。
监测数组的时候可能触发屡次get/set,那么如何防止触发屡次呢?
咱们能够判断key
是否为当前被代理对象target
自身属性,也能够判断旧值
与新值
是否相等,只有知足以上两个条件之一时,才有可能执行trigger
。
不能同名 由于无论是计算属性仍是data仍是props 都会被挂载在vm实例
上,所以 这三个都不能同名
找到config/index.js
配置文件,找build
打包对象里的assetsPublicPath
属性 默认值为/
,更改成./
就行了
由于动态添加src
被当作静态资源
处理了,没有进行编译
,因此要加上require。
<img :src="require('../../../assets/images/xxx.png')" />
Object.freeze
适合一些 big data
的业务场景。尤为是作管理后台的时候,常常会有一些超大数据量
的 table
,或者一个含有 n 多数据的图表,这种数据量很大的东西使用起来最明显的感觉就是卡。但其实不少时候其实这些数据其实并不须要响应式变化,这时候你就能够使用 Object.freeze 方法了,它能够冻结一个对象
(注意它不并是 vue 特有的 api)。
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setter,它们让 Vue 能进行追踪依赖,在属性被访问和修改时通知变化。
使用了 Object.freeze 以后,不只能够减小 observer
的开销,还能减小很多内存开销
。
使用方式:
this.item = Object.freeze(Object.assign({}, this.item))
先了解一下,在 vue 中,有不少内置的指令.
好比:
因此,关于指令,咱们能够总结下面几点:
HTML 属性
地方的,<input v-model='name' type='text' />
v-
开头的.Vue自定义指令案例1
例如:咱们须要一个指令,写在某个HTML表单元素上,而后让它在被加载到DOM中时,自动获取焦点.
// 和自定义过滤器同样,咱们这里定义的是全局指令 Vue.directive('focus',{ inserted(el) { el.focus() } }) <div id='app'> <input type="text"> <input type="text" v-focus placeholder="我有v-focus,因此,我获取了焦点"> </div>
先总结几个点:
Vue.directive()
来新建一个全局指令
,(指令使用在HTML元素属性上的)第一个参数
focus是指令名
,指令名在声明的时候,不须要加 v-<input type="text" v-focus placeholder="我有v-focus,因此,我获取了焦点"/>
咱们须要加上 v-.第二个参数
是一个对象
,对象内部
有个 inserted()
的函数,函数有 el
这个参数.DOM元素
,在这里就是后面那个有 placeholder
的 input
,el 就等价于 document.getElementById('el.id')
$(el)
无缝链接 jQuery指令的生命周期
用指令咱们须要:
当一个指令绑定到一个元素上时,其实指令的内部会有五个生命周期事件函数.
bind(){}
当指令绑定
到 HTML 元素
上时触发.只调用一次.inserted()
当绑定了指令的这个HTML元素
插入到父元素
上时触发(在这里父元素是 div#app
).但不保证,父元素已经插入了 DOM 文档.updated()
所在组件的VNode更新
时调用.componentUpdate
指令所在的组件的VNode以及其子VNode 所有更新后
调用.unbind
: 指令和元素解绑
的时候调用,只调用一次Vue 指令的声明周期函数
Vue.directive('gqs',{ bind() { // 当指令绑定到 HTML 元素上时触发.**只调用一次** console.log('bind triggerd') }, inserted() { // 当绑定了指令的这个HTML元素插入到父元素上时触发(在这里父元素是 `div#app`)**.但不保证,父元素已经插入了 DOM 文档.** console.log('inserted triggerd') }, updated() { // 所在组件的`VNode`更新时调用. console.log('updated triggerd') }, componentUpdated() { // 指令所在组件的 VNode 及其子 VNode 所有更新后调用。 console.log('componentUpdated triggerd') }, unbind() { // 只调用一次,指令与元素解绑时调用. console.log('unbind triggerd') } })
HTML
<div id='app' v-gqs></div> 结果: bind triggerd inserted triggerd
发现默认状况下只有 bind 和 inserted 声明周期函数触发了.
那么剩下的三个何时触发呢?
<div id='app' > <p v-gqs v-if="show">v-if是删除或者新建dom元素,它会触发unbind指令声明周期吗?</p> <button @click="show=!show">toggle</button> </div>
当指令绑定的元素被销毁时,会触发指令的 unbind 事件.
(新建并显示,仍然是触发 bind & inserted)
unbind触发.gif
<p v-gqs v-show="show2">v-show设置元素的display:block|none,会触发componentUpdated事件</p> <button @click="show2=!show2">toggle-v-show</button>
根据官方文档定义:
若是在实例建立以后
添加新的属性
到实例上,它不会触发视图更新。
Vue 不容许在已经建立
的实例
上动态添加
新的根级响应式属性
(root-level reactive property)。
然而它能够使用 Vue.set(object, key, value)
方法将响应属性添加到嵌套的对象上。
多个实例
引用了相同或类似的方法或属性
等,可将这些重复的内容抽取出来做为mixins的js,export出去,在须要引用的vue文件经过mixins属性注入,与当前实例
的其余内容
进行merge
。
一个混入对象能够包含任意组件选项
。同一个生命周期,混入对象
会比组件
的先执行
。
//暴露两个mixins对象
export const mixinsTest1 = { methods: { hello1() { console.log("hello1"); } }, created() { this.hello1(); }, } export const mixinsTest2 = { methods:{ hello2(){ console.log("hello2"); } }, created() { this.hello2(); }, }
<template> <div> home </div> </template> <script> import {mixinsTest1,mixinsTest2} from '../util/test.js' export default { name: "Home", data () { return { }; }, created(){ console.log("1212"); }, mixins:[mixinsTest2,mixinsTest1] // 先调用哪一个mixins对象,就先执行哪一个 } </script> hello2 hello1 1212
v-on: .stop .prevent
<input v-model="msg" type="text" v-on="{input:a, focus:b}"/>
安装scss依赖包:
npm install sass-loader --save-dev npm install node-sass --save-dev
在build文件夹下修改 webpack.base.conf.js 文件,在 module 下的 rules 里添加配置,以下:
{ test: /\.scss$/, loaders: ['style', 'css', 'sass'] }
应用:
在vue文件中应用scss时,须要在style样式标签上添加lang="scss",即<style lang="scss">。
watch 中的 immediate 会让监听在初始值声明的时候去执行监听计算,不然就是 created 先执行
created():在实例建立完成后被当即调用。在这一步,实例已完成如下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。
activated():是在路由设置<keep-alive></keep-alive>
时,才会有这个生命周期。在被 keep-alive 缓存的组件激活时调用。
由于在插入数据或者删除数据的时候,会致使后面的数据的key绑定的index变化,进而致使重新渲染,效率会下降
动态组件使用方法
<keep-alive> <component :is="isWhich"></component> </keep-alive> 使用标签保存状态,即切换组件再次回来依然是原来的样子,页面不会刷新,若不须要能够去掉。 经过事件改变is绑定的isWhich值便可切换成不一样的组件,isWhich的值为组件名称。
使用场景:
好比,有一个表单,表单提交成功后,但愿组件恢复到初始状态,重置data数据。
使用Object.assign()
,vm.$data
能够获取当前状态下的data,vm.$options.data
能够获取到组件初始化状态下的data
初始状态下设置data数据的默认值,重置时直接bject.assign(this.$data, this.$options.data())
说明:
this[属性名] = this.$options.data()[属性名]
,如this.message = this.$options.data().message
<template comments> ... </template>
v-data-something
属性,再在选择器时加上对应[v-data-something]
,即CSS属性选择器,以此完成相似做用域的选择方式而后在 index.html 中添加:
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
Babel
默认只转换新的JavaScript句法
(syntax),而不转换新的API
,好比Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise
等全局对象,以及一些定义在全局对象
上的方法(好比Object.assign
)都不会转码。
举例来讲,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。若是想让这个方法运行,必须使用babel-polyfill
,为当前环境提供一个垫片。
强制从新渲染
this.$forceUpdate()
强制从新刷新某组件
//模版上绑定key <SomeComponent :key="theKey"/> //选项里绑定data data(){ return{ theKey:0 } }
//刷新key达到刷新组件的目的
theKey++;
加入.native修饰符
报错 "Method 'xxx' has already been defined as a data property"
键名优先级:props > data > methods
实例建立以后,能够经过 vm.$data
访问原始数据对象
。Vue 实例也代理了 data 对象上全部的属性,所以访问 vm.a
等价于访问 vm.$data.a
。
以 _
或 $
开头的属性 不会
被 Vue 实例代理
,由于它们可能和 Vue 内置
的属性、API 方法冲突
。能够使用 vm.$data._property
的方式访问
这些属性。
使用了history模式,然后端又没有进行相关资源配置。
v-model默认的触发条件是input事件,加了.lazy
修饰符以后,v-model会在change
事件触发的时候去监听
diff算法要求,源码中patch.js中的patchVnode也是根据树状结构进行遍历
生命周期的钩子函数不能使用箭头函数,否者this不能指向vue实例
<template></template>
有什么用包裹嵌套其它元素,使元素具备区域性,自身具备三个特色:
缓存过滤
组见名称
是由vue中组件name决定的解析和转换
.vue 文件,提取
出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理。
设置 path: '*'
, 而且放在最后一个
为何要响应参数变化?
路由参数
发生了变化
,可是页面数据
并未
及时更新,须要强制刷新后才会变化。解决方案:
使用 watch 监听
watch: { $route(to, from){ if(to != from) { console.log("监听到路由变化,作出相应的处理"); } } }
向 router-view 组件中添加 key
<router-view :key="$route.fullPath"></router-view>
$route.fullPath
是完成后解析的URL,包含其查询参数信息和hash完整路径
在路由实例中配置 scrollBehavior(ro,form,savedPosition){ //滚动到顶部 return {x:0,y:0} //保持原先的滚动位置 return {selector:falsy} }
全称: Model-View-ViewModel
, Model 表示数据模型层
, view 表示视图层
, ViewModel 是 View 和 Model 层的桥梁
,数据绑定
到 viewModel
层并自动渲染到页面中,视图变化
通知 viewModel
层更新数据。
事件绑定有几种?
addEventListener
实现。$on
方法 。普通元素
的原生
事件绑定在上是经过@click
进行绑定的组件
的原生
事件绑定是经过@click.native
进行绑定的,组件中的nativeOn
是等价于on的。组件
的自定义
事件是经过@click
绑定的,是经过 $on
方法来实现的,必须有$emit
才能够触发。解释下这2种的区别:
<div @click="getData"></div>
,直接触发的就是原生的点击事件<BtnGroup @click="getName" @click.native="getData"></BtnGroup>
,这时候,要触发原生的点击事件getData,就须要使用修饰符.native
,由于直接使用@click
是接收来自子组件emit
过来的事件getName,这样才不会冲突。let compiler = require('vue-template-compiler'); // vue loader中的包 let r1 = compiler.compile('<div @click="fn()"></div>'); // 给普通标签绑定click事件 // 给组件绑定一个事件,有两种绑定方法 // 一种@click.native,这个绑定的就是原生事件 // 另外一种@click,这个绑定的就是组件自定义事件 let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></mycomponent>'); console.log(r1.render); // {on:{click}} console.log(r2.render); // {nativeOn:{click},on:{click}} // 为何组件要加native?由于组件最终会把nativeOn属性放到on的属性中去,这个on会单独处理 // 组件中的nativeOn 等价于 普通元素on,组件on会单独处理
Vue在更新DOM
时是异步执行
的,只要侦听
到数据变化
,将开启
一个队列
,并缓冲
在同一事件循环中发生的全部数据变动
,若是同一个watcher
被屡次触发
,只会被推入
到队列
中一次
,这种在缓冲
时去除重复数据
对于减小没必要要
的计算和DOM操做
是很是重要的.
而后,在下一个的事件循环tick中,Vue刷新队列并执行实际(已去重的)工做,Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate
,若是执行环境不支持,则会采用setTimeout(fn, 0)
代替。
描述
对于Vue为什么采用异步渲染,简单来讲就是为了提高性能
,由于不采用异步更新,在每次更新
数据都会对当前组件进行从新渲染
,为了性能考虑,Vue会在本轮数据
更新后,再去异步更新视图
,举个例子,让咱们在一个方法内重复更新一个值。
this.msg = 1; this.msg = 2; this.msg = 3;
事实上,咱们真正想要的其实只是最后一次更新而已,也就是说前三次DOM更新都是能够省略的,咱们只须要等全部状态都修改好了以后再进行渲染就能够减小一些性能损耗。
对于渲染方面的问题是很明确的,最终只渲染一次确定比修改以后即渲染所耗费的性能少,在这里咱们还须要考虑一下异步更新队列的相关问题,假设咱们如今是进行了相关处理使得每次更新数据只进行一次真实DOM渲染,来让咱们考虑异步更新队列的性能优化。
假设这里是同步更新队列,this.msg=1,大体会发生这些事:
msg值更新 -> 触发setter -> 触发Watcher的update -> 从新调用 render -> 生成新的vdom -> dom-diff -> dom更新
这里的dom更新并非渲染
(即布局、绘制、合成等一系列步骤),而是更新内存
中的DOM树结构
,以后再运行this.msg=2,再重复上述步骤,以后的第3次更新一样会触发相同的流程,等开始渲染的时候,最新的DOM树中确实只会存在更新完成3,从这里来看,前2次对msg的操做以及Vue内部对它的处理都是无用的操做,能够进行优化处理。
若是是异步更新队列,会是下面的状况:
运行this.msg=1,并非当即进行上面的流程,而是将对msg有依赖的Watcher都保存在队列中,该队列可能这样[Watcher1, Watcher2...]
,当运行this.msg=2后,一样是将对msg有依赖的Watcher保存到队列中,Vue内部
会作去重判断
,此次操做后,能够认为队列数据没有发生变化,第3次更新也是上面的过程。
固然,你不可能只对msg有操做,你可能对该组件中的另外一个属性也有操做,好比this.otherMsg=othermessage,一样会把对otherMsg有依赖的Watcher添加到异步更新队列中,由于有重复判断操做,这个Watcher也只会在队列中存在一次,本次异步任务执行结束后,会进入下一个任务执行流程,其实就是遍历异步更新队列中的每个Watcher,触发其update,而后进行从新调用render -> new vdom -> dom-diff -> dom更新
等流程,可是这种方式和同步更新队列相比,无论操做多少次msg,Vue在内部只会进行一次从新调用真实更新流程。
因此,对于异步更新队列不是节省了渲染成本
,而是节省了Vue内部计算及DOM树操做的成本
,无论采用哪一种方式,渲染确实只有一次。
此外,组件内部实际使用VirtualDOM进行渲染,也就是说,组件内部实际上是不关心哪一个状态发生了变化,它只须要计算一次就能够得知哪些节点须要更新,也就是说,若是更改了N个状态,其实只须要发送一个信号就能够将DOM更新到最新,若是咱们更新多个值。
this.msg = 1; this.age = 2; this.name = 3;
此处咱们分三次修改了三种状态,但其实Vue只会渲染一次,由于VIrtualDOM只须要一次就能够将整个组件的DOM更新到最新,它根本不会关心这个更新的信号究竟是从哪一个具体的状态发出来的。
而为了达到这个目的,咱们须要将渲染操做
推迟到全部
的状态都修改完成
,为了作到这一点只须要将渲染操做推迟到本轮事件循环的最后或者下一轮事件循环,也就是说,只须要在本轮事件循环
的最后
,等前面更新状态
的语句都执行完以后
,执行一次
渲染操做,它就能够无视前面各类更新状态的语法,不管前面写了多少条更新状态的语句,只在最后渲染一次就能够了。
将渲染推迟到本轮事件循环的最后执行渲染的时机会比推迟到下一轮快不少,因此Vue优先将渲染操做推迟到本轮事件循环的最后,若是执行环境不支持会降级到下一轮,Vue的变化侦测机制(setter)决定了它必然会在每次状态发生变化时都会发出渲染的信号,但Vue会在收到信号以后检查队列中是否已经存在这个任务,保证队列中不会有重复,若是队列中不存在则将渲染操做添加到队列中,以后经过异步的方式延迟执行队列中的全部渲染的操做并清空队列,当同一轮事件循环中反复修改状态时,并不会反复向队列中添加相同的渲染操做,因此咱们在使用Vue时,修改状态后更新DOM都是异步的。
当数据变化
后会调用notify
方法,将watcher
遍历,调用update
方法通知watcher进行更新,这时候watcher并不会当即去执行,在update中会调用queueWatcher
方法将watcher放到了一个队列里,在queueWatcher会根据watcher的进行去重,若多个属性依赖一个watcher,则若是队列中没有该watcher就会将该watcher添加到队列中,而后便会在$nextTick
方法的执行队列中加入一个flushSchedulerQueue方法(这个方法将会触发在缓冲队列的全部回调的执行),而后将$nextTick
方法的回调加入$nextTick
方法中维护的执行队列,flushSchedulerQueue中开始会触发一个before的方法,其实就是beforeUpdate,而后watcher.run()才开始真正执行watcher,执行完页面就渲染完成,更新完成后会调用updated钩子。
$nextTick
在上文中谈到了对于Vue为什么采用异步渲染,假如此时咱们有一个需求,须要在页面渲染完成后取得页面的DOM元素,而因为渲染是异步的,咱们不能直接在定义的方法中同步取得这个值的,因而就有了vm.$nextTick
方法,Vue中$nextTick
方法将回调延迟到下次DOM更新循环以后执行,也就是在下次DOM更新循环结束以后执行延迟回调,在修改数据以后当即使用这个方法,可以获取更新后的DOM。简单来讲就是当数据更新时,在DOM中渲染完成后,执行回调函数。
经过一个简单的例子来演示$nextTick
方法的做用,首先须要知道Vue在更新DOM时是异步执行的,也就是说在更新数据时其不会阻塞代码的执行,直到执行栈中代码执行结束以后,才开始执行异步任务队列的代码,因此在数据更新时,组件不会当即渲染,此时在获取到DOM结构后取得的值依然是旧的值,而在$nextTick
方法中设定的回调函数会在组件渲染完成以后执行,取得DOM结构后取得的值即是新的值。
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; console.log("DOM未更新:", this.$refs.msgElement.innerHTML) this.$nextTick(() => { console.log("DOM已更新:", this.$refs.msgElement.innerHTML) }) } }, }) </script> </html>
异步机制#
Js是单线程的,其引入了同步阻塞与异步非阻塞的执行模式,在Js异步模式中维护了一个Event Loop
,Event Loop是一个执行模型
,在不一样的地方有不一样的实现,浏览器和NodeJS基于不一样的技术实现了各自的Event Loop。浏览器的Event Loop是在HTML5
的规范中明肯定义,NodeJS的Event Loop是基于libuv
实现的。
在浏览器中的Event Loop
由执行栈
Execution Stack、后台线程
Background Threads、宏队列
Macrotask Queue、微队列
Microtask Queue组成。
主线程
执行同步任务
的数据结构
,函数调用造成了一个由若干帧组成的栈。setTimeout、setInterval、XMLHttpRequest
等等的执行线程。异步任务
的回调会依次进入宏队列,等待后续被调用,包括setTimeout、setInterval、setImmediate(Node)、requestAnimationFrame、UI rendering、I/O等操做。异步任务
的回调会依次进入微队列,等待后续调用,包括Promise、process.nextTick(Node)、Object.observe、MutationObserver等操做。当Js执行时,进行以下流程:
实例#
// Step 1 console.log(1); // Step 2 setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0); // Step 3 new Promise((resolve, reject) => { console.log(4); resolve(); }).then(() => { console.log(5); }) // Step 4 setTimeout(() => { console.log(6); }, 0); // Step 5 console.log(7); // Step N // ... // Result /* 1 4 7 5 2 3 6 */
分析#
在了解异步任务的执行队列后,回到中$nextTick
方法,当用户数据更新时,Vue将会维护一个缓冲队列
,对于全部的更新数据将要进行的组件渲染与DOM操做进行必定的策略处理后加入缓冲队列,而后便会在$nextTick
方法的执行队列中加入一个flushSchedulerQueue
方法(这个方法将会触发在缓冲队列的全部回调的执行),而后将$nextTick
方法的回调加入$nextTick
方法中维护的执行队列,在异步挂载的执行队列触发时就会首先会首先执行flushSchedulerQueue
方法来处理DOM渲染的任务,而后再去执行$nextTick
方法构建的任务,这样就能够实如今$nextTick
方法中取得已渲染完成的DOM结构。
在测试的过程当中发现了一个颇有意思的现象,在上述例子中的加入两个按钮,在点击updateMsg按钮的结果是3 2 1,点击updateMsgTest按钮的运行结果是2 3 1。
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> <button @click="updateMsgTest">updateMsgTest</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) }, updateMsgTest: function(){ setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) } }, }) </script> </html>
这里假设运行环境中Promise对象是彻底支持的,那么使用setTimeout是宏队列在最后执行这个是没有异议的,可是使用$nextTick
方法以及自行定义的Promise实例是有执行顺序的问题的,虽然都是微队列任务,可是在Vue中具体实现的缘由致使了执行顺序可能会有所不一样,首先直接看一下$nextTick
方法的源码,关键地方添加了注释,请注意这是Vue2.4.2版本的源码,在后期$nextTick
方法可能有所变动。
/** * Defer a task to execute it asynchronously. */ var nextTick = (function () { // 闭包 内部变量 var callbacks = []; // 执行队列 var pending = false; // 标识,用以判断在某个事件循环中是否为第一次加入,第一次加入的时候才触发异步执行的队列挂载 var timerFunc; // 以何种方法执行挂载异步执行队列,这里假设Promise是彻底支持的 function nextTickHandler () { // 异步挂载的执行任务,触发时就已经正式准备开始执行异步任务了 pending = false; // 标识置false var copies = callbacks.slice(0); // 建立副本 callbacks.length = 0; // 执行队列置空 for (var i = 0; i < copies.length; i++) { copies[i](); // 执行 } } // 若是支持promise if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = function (err) { console.error(err); }; timerFunc = function () { p.then(nextTickHandler).catch(logError); // 挂载异步任务队列 if (isIOS) { setTimeout(noop); } }; } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { var counter = 1; var observer = new MutationObserver(nextTickHandler); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // fallback to setTimeout /* istanbul ignore next */ timerFunc = function () { setTimeout(nextTickHandler, 0); }; } return function queueNextTick (cb, ctx) { // nextTick方法真正导出的方法 var _resolve; callbacks.push(function () { // 添加到执行队列中 并加入异常处理 if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); //判断在当前事件循环中是否为第一次加入,如果第一次加入则置标识为true并执行timerFunc函数用以挂载执行队列到Promise // 这个标识在执行队列中的任务将要执行时便置为false并建立执行队列的副本去运行执行队列中的任务,参见nextTickHandler函数的实现 // 在当前事件循环中置标识true并挂载,而后再次调用nextTick方法时只是将任务加入到执行队列中,直到挂载的异步任务触发,便置标识为false而后执行任务,再次调用nextTick方法时就是一样的执行方式而后不断如此往复 if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })();
回到刚才提出的问题上,在更新DOM操做时会先触发$nextTick
方法的回调,解决这个问题的关键在于谁先将异步任务挂载到Promise对象上。
首先对有数据更新的updateMsg按钮触发的方法进行debug,断点设置在Vue.js的715行,版本为2.4.2,在查看调用栈以及传入的参数时能够观察到第一次执行$nextTick
方法的实际上是因为数据更新而调用的nextTick(flushSchedulerQueue)
语句,也就是说在执行this.msg = "Update";的时候就已经触发了第一次的$nextTick
方法,此时在$nextTick
方法中的任务队列会首先将flushSchedulerQueue方法加入队列并挂载$nextTick
方法的执行队列到Promise对象上,而后才是自行自定义的Promise.resolve().then(() => console.log(2))
语句的挂载,当执行微任务队列中的任务时,首先会执行第一个挂载到Promise的任务,此时这个任务是运行执行队列,这个队列中有两个方法,首先会运行flushSchedulerQueue方法去触发组件的DOM渲染操做,而后再执行console.log(3),而后执行第二个微队列的任务也就是() => console.log(2),此时微任务队列清空,而后再去宏任务队列执行console.log(1)。
接下来对于没有数据更新的updateMsgTest按钮触发的方法进行debug,断点设置在一样的位置,此时没有数据更新,那么第一次触发$nextTic
k方法的是自行定义的回调函数,那么此时$nextTick
方法的执行队列才会被挂载到Promise对象上,很显然在此以前自行定义的输出2的Promise回调已经被挂载,那么对于这个按钮绑定的方法的执行流程即是首先执行console.log(2),而后执行$nextTick
方法闭包的执行队列,此时执行队列中只有一个回调函数console.log(3),此时微任务队列清空,而后再去宏任务队列执行console.log(1)。
简单来讲就是谁先挂载Promise对象的问题,在调用$nextTick
方法时就会将其闭包内部维护的执行队列挂载到Promise对象,在数据更新时Vue内部首先就会执行$nextTick
方法,以后便将执行队列挂载到了Promise对象上,其实在明白Js的Event Loop模型后,将数据更新也看作一个$nextTick
方法的调用,而且明白$nextTick
方法会一次性执行全部推入的回调,就能够明白其执行顺序的问题了,下面是一个关于$nextTick方法的最小化的DEMO。
var nextTick = (function(){ var pending = false; const callback = []; var p = Promise.resolve(); var handler = function(){ pending = true; callback.forEach(fn => fn()); } var timerFunc = function(){ p.then(handler); } return function queueNextTick(fn){ callback.push(() => fn()); if(!pending){ pending = true; timerFunc(); } } })(); (function(){ nextTick(() => console.log("触发DOM渲染队列的方法")); // 注释 / 取消注释 来查看效果 setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) nextTick(() => { console.log(3) }) })();