vue
- 最大的特点就是数据驱动视图
- 公式:UI = render(state)
- 解析:输入状态一旦改变,页面输出也随着改变,vue扮演render()
变化侦测 — 使用属性拦截的方式实现的
- 释义:追踪状态,即数据一旦发生变化,就要去更新视图
- vue:通过对比虚拟DOM–VNode类来实现侦测
- react:通过对比虚拟DOM来实现侦测
数据变的“可观测”
- 释义:知道数据什么时候被读取或改写
- 方法:通过Object.defineProperty方法,只能观测Object的取值或写入,观测不到向object数据里添加或删除key/value,但是可以用两个全局API:Vue.set和 Vue.delete解决
- 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| let car = { 'brand': 'BMW', 'price': 3000 } let car= {} let val=3000 Object.defineProperty(car,'price',{ enumerable: true, configurable: true, get(){ console.log('price属性被读取') return val }, set(newVal){ console.log('price属性被修改了') val = newVal } })
|
依赖收集
- 释义:把“谁用到了这个数据”称为“谁依赖了这个数据”
- 用法:在getter中收集依赖,在setter中通知依赖更新
虚拟DOM
- 释义:用一个JS对象来描述一个DOM节点
- 存在的原因:数据驱动视图会操作DOM,但操作真实DOM耗性能。
- 方法:对比变化前后的虚拟DOM节点,通过DOM-Diff算法计算出需要更新的地方,然后去更新需要更新的视图。
Vue中的虚拟DOM — VNode
类
- 释义:包含了描述一个真实DOM节点所需要的一系列属性
- 作用:在视图渲染之前,把写好的template模板先编译成VNode并缓存下来,等到数据发生变化页面需要重新渲染的时候,把变化后生成的VNode与前一次缓存下来的VNode进行对比,找出差异,然后有差异的VNode对应的真实DOM节点就是需要重新渲染的节点,最后根据有差异的VNode创建出真实的DOM节点再插入到视图中,最终完成一次视图更新。
注释节点(源码)
1 2 3 4 5 6
| export const createEmptyVNode = (text: string = '' => { const node = new VNode() node.text = text node.isComment = true return node })
|
文本节点(源码)
1 2 3
| export function createTextVNode (val: string | number){ return new VNode(undefined, undefined,unfined,String(val)) }
|
元素节点
- 接近真实的DOM节点,反应到VNode节点上:tag表示标签名,data表示标签的属性id等,children表示子节点数组
组件节点
- 除了有元素节点具有的属性之外,它还有两个特有的属性:componentOptions、componentInstance
函数式组件节点
- 相较于组件节点,它又有两个特有的属性:fnContext、fnOptions
克隆节点
- 把现有节点的属性全部复制到新节点中,而现有节点和新克隆得到的节点之间唯一的不同就是克隆得到的节点isCloned为true
DOM-Diff 算法
- 释义:在vue中,把DOM-Diff过程叫做patch过程,意为”补丁”,本质就是对比新旧两份VNode的过程,以新的VNode为基准,改造旧的oldNVNode使之成为跟新的VNode一样,这就是patch过程要干的事
创建节点(源码)
- 前提:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建
- 方法:判断新的VNode中有而旧的VNode中没有的这个节点是属于元素节点、文本节点、注释节点中的哪一种,从而调用不同的方法创建并插入到DOM中
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function createElm (vnode, parentElm, refElm) { const tag = vode.tag if(isDef(tag)){ vnode.elm = nodeOps.createElement(tag, vnode) createChildren(vnode, vnode.children, insertedVnodeQueue) insert(parentElm, vonode.elm, refElm) } else if (isTrue(vnode,isComment)) { vnode.elm = nodeDps.createComment(vnode.text) insert(parentElm, vnode.elm,refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }
|
删除节点(源码)
- 前提:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除
1 2 3 4 5 6
| function removeNode (el) { const parent = nodeOps.parentNode(el) if (isDef(parent)) { nodeOps.removeChild(parent, el) } }
|
更新节点
- 前提:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode
- 如果VNode和oldVNode均为静态节点,不处理,跳过
- 如果VNode是文本节点,看oldVNode是否也是文本节点,
- 是,就更新oldVNode节点,
- 不是,直接调用setTextNode方法改成文本节点,并且内容与VNode相同
- 如果VNode是元素节点
- 该节点包含子节点,看oldVNode是否包含子节点
是
,就需要递归对比更新字节点。
- 不是,则oldVNode可能是空节点或者是文本节点。
- 空节点就把VNode的子节点创建一份然后插入到oldVNode节点里面。
- 文本节点则把文本清空,然后把VNode的子节点创建一份然后插入到oldVNode节点里面。
- 不包含子节点,直接清空oldVNode里面的内容
更新子节点
- 当新旧节点都是元素节点但并且都包含子节点时,这两个节点的VNode实例上的children属性就是所包含的子节点数组。
- 把newChildren里面的元素与oldChildren里的元素一一进行对比,外层循环newChildren数组,内层循环oldChildren数组,每循环外层newChildren数组里的一个子节点,就去内层oldChildren数组里去找看有没有与之相同的子节点。
创建子节点
- newChildren的某个节点在oldChildren中找不到相同的子节点,就创建节点,并插入到所有未处理节点之前
删除子节点
- newChildren里面的每一个子节点都循环完毕,发现oldChildren还有未处理子节点,则删除未处理子节点
移动子节点
- newChildren的某个节点在oldChildren中找到相同的子节点,但位置不同,就以newChildren为基准,调整oldChildren里节点的位置.所有未处理节点之前就是我们要移动的目的位置
更新节点
- newChildren的某个节点在oldChildren中找到相同的子节点,位置相同,就更新oldChildren节点
优化策略
- 新前与旧前
- 把newChildren数组里的所有未处理子节点的第一个子节点>和oldChildren数组里所有未处理子节点的第一个子节点比对
- 新后与旧后
- 把newChildren数组里的所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点比对
相同
,更新节点,并且不需要移动节点
- 新后与旧前
- 把newChildren数组里的所有未处理子节点的最后一个子节点>和oldChildren数组里`所有未处理子节点的第一个子节点比对
相同
,更新节点,再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置
- 新前与旧后
- 把newChildren数组里的所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点比对
相同
,更新节点,再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置
模板编译(用户写的模板进行编译,就会产生VNode)
把用户在<template></template>
标签中写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生HTML找出来,经过一系列的逻辑处理生成渲染函数 — 即render函数的过程
抽象语法树AST
- 释义:以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
作用:
- 将用户在
<template></template>
标签中写的模板解析并提取除元素、属性、变量插值
流程:
- 模板解析阶段
- 将一堆模板字符串用正则等方式解析成AST
- 模板解析阶段——解析器——源码路径:src/compiler/parser/index.js
- 优化阶段
- 遍历AST,找出其中的静态节点,并打上标记
- 优化阶段——优化器——源码路径:src/compiler/optimizer.js
- 静态节点 — 全是文本没有动态的变量
- 静态根节点 — 包含静态节点的容器
- 代码生成阶段
- 将AST转换成渲染函数
- 代码生成阶段——代码生成器——源码路径:src/compiler/codegen/index.js
- 方法:根据已有的AST生成对应的render函数供组件挂载时调用,组件只要调用的这个render函数就可以得到AST对应的虚拟DOM的VNode
- 元素节点
- _c(tagName,data,children)
- tagName:节点标签名;
- data:节点属性;判断plain属性是否为true,true则表示节点没有属性,将data赋值为undefined;不为true则调用genData函数获取节点属性data数据
- chiledren:节点的子节点列表;遍历AST的children属性中的元素,然后根据元素属性的不同生成不同的VNode创建函数调用字符串
- 文本节点
- _v(text)
- 如果文本是动态文本,则使用动态文本AST节点的expression属性,如果是纯静态文本,则使用text属性
- 注释节点
解析器
就是把用户所写的模板根据一定的解析规则解析出有效的信息,最后用这些信息形成AST
- HTML解析器是主线,先用HTML解析器进行解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。
- HTML解析器: 源码中,HTML解析器就是parseHTML函数
- 内部运行流程
- 当解析开始,调用start (tag, attrs, unary) {}
- 当解析结束,调用end () {}
- 当解析到文本时,调用chars (text) {}
- 当解析到注释时,调用comment (text) {}
- html解析器内容解析
- 解析HTML注释:找以
<!--
开头,并且后面有-->
结尾
- 解析条件注释:找以
<![
开头,并且后面存在]>
结尾
- 解析DOCTYPE:匹配是否存在
/^<!DOCTYPE [^>]+>/i
- 解析开始标签:html.match(new RegExp(
^<((?:([a-zA-Z_][\\w\\-\\.]*)\\:)?([a-zA-Z_][\\w\\-\\.]*))
))
- 解析结束标签:html.match(new RegExp(
^<\\/((?:([a-zA-Z_][\\w\\-\\.]*)\\:)?([a-zA-Z_][\\w\\-\\.]*))[^>]*>
))
- 解析文本:判断字符串是不是以
<
开头,不是就是文本
生命周期 — vue实例的生命周期大致分为4个阶段
初始化阶段:为Vue实例上初始化一些属性,事件以及响应式数据
- new Vue() — 创建一个vue实例
- 作用:执行了vue类的构造函数,构造函数会执行_init方法。_init方法把Vue实例赋值给变量vm,并且把用户传递的options选项与当前构造函数的options属性及其父级构造函数的options属性合并,得到新的options选项赋值给$options属性,并将$options属性挂载到vue实例。接着,调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据。在所有的初始化工作都完成以后,会判断用户是否传入了el选项,传入了则调用
$mount
函数进入模板编译与挂载阶段,没有传入el选项,需要用户手动执行vm.$mount
方法才进入下一个生命周期阶段。
- initLifecycle函数 — 初始化了一些属性,包括以$开头的供用户使用的外部属性,也包括以_开头的供内部使用的内部属性。
- 作用:给Vue实例上挂载了一些属性并设置了默认值,值得一提的是挂载$parent属性和$root属性。
- initEvents函数 — 初始化实例的事件系统
- 作用:初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件
- initInjections函数 — 初始化实例中的inject选项的
- initState函数 — 初始化实例的状态选项(props、data、methods、computed、watch)
模板编译阶段:将模板编译成渲染函数;只存在与完整版(有编译器)
- 调用vm.$mount标志着初始化阶段的结束和进入模板编译阶段。
- 完整版的$mount会先将$mount方法缓存起来,记作mount。然后等模板编译完成,再执行mount方法
- 作用:从用户传入的el选项和template选项中获取到用户传入的内部或外部模板,然后将获取到的模板编译成渲染函数
挂载阶段:将实例挂载到指定的DOM上,即将模板渲染到真实DOM中;
- 作用:创建Vue实例并用其替换el选项对应的DOM元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。
- 将模板渲染到视图上,第二部分是开启对模板中数据的监控,直到实例被销毁,挂在阶段完成,调用mounted
销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器;
- 作用:当调用了vm.$destroy方法,Vue实例就进入了销毁阶段,该阶段所做的主要工作是将当前的Vue实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器。
实例方法
与数据相关:vm.$set、vm.$delete和vm.$watch
- vm.$watch(expOrFn:string | Function,callback: Function | Object,[options:Object])
- options
- deep: boolean: 深度监听
- immediate: boolean: 是否立即触发回调,如果存在immediate,第一次回调不能取消侦听给定的property
- 返回值:unwatch: Function: 返回一个取消观察函数,用来停止回调 var unwatch = vm.$watch(expOrFn,callback,[options]) unwatch()
- 用法:观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
- vm.$set(target: Object | Array, propertyName/index: string | number, value: any)
- 返回值:设置的值。
- 用法:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。必须用于向响应式对象上添加新属性.对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
- vm.$delete(target: Object | Array, propertyName/index: string | number)
- 用法:删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。
与事件相关:vm.$on、vm.$emit、vm.$off和vm.$once
$on和$emit
的内部原理是设计模式中最典型的发布订阅模式,首先定义一个事件中心,通过$on订阅事件
,将事件存储在事件中心里面,然后通过$emit触发
事件中心里面存储的订阅事件。
- vm.$on(event: string | Array, callback: Function)
- 作用:监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
- 示例:
1 2
| vm.$on('test',function(msg){console.log(msg)}) vm.$emit('test','hi')
|
- vm.$emit(eventName: string, […args])
- 作用:触发当前实例上的事件。附加参数都会传给监听器回调
- vm.$off([event: string | Array, callback: Function])
- 作用:移除自定义事件监听器。
- 如果没有提供参数,则移除所有事件的监听器
- 如果只提供了事件,则移除该事件所有的监听器
- 如果同时提供了事件与回调,则移除这个回调的监听器
- vm.$once(event: string, callback: Function)
- 作用:监听一个自定义事件,但是只触发一次。一旦触发,监听器就会被移除。
与生命周期相关:vm.$mount、vm.$forceUpdate、vm.$nextTick和vm.$destory
- vm.$mount([elementOrSelector: Element | string],[hydrating: boolean])
- 返回值:vm
- 作用:实例化时没有收到el选项,处于未挂载状态。使用vm.$mount手动挂载
- vm.$forceUpdate()
- 作用: 迫使 Vue 实例重新渲染.仅仅影响实例本身和插入插槽内容的子组件
- vm.$nextTick([callback: Function])
- 用法:将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。回调的 this 自动绑定到调用它的实例上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <div id="example">{{message}}</div> </template> <script> var vm = new Vue({ el: '##example', data: { message: '123' } }) vm.message = 'new message' console.log(vm.$el.innerHTML) Vue.nextTick(function () { console.log(vm.$el.innerHTML) }) </script>
|
JS的运行机制:
事件循环
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦”执行栈”中所有同步任务执行完毕,系统就会读取任务列表,将任务列表中的异步任务放进执行栈中执行,主线程不断重复该步骤
主线程的执行过程就是一个tick,而所有的异步结果都是通过 “任务队列” 来调度。任务队列中存放宏任务和微任务,并且每执行完一个宏任务,都要去清空对应的微任务
vm.$destroy()
- 用法:完全销毁一个实例。清理它与其他实例的连接,解绑它的全部指令即事件监听器。触发 beforeDestroy 和 destroyed 的钩子。
全局API
- Vue.extend(options: Object)
- 作用:用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
- Vue.nextTick([callback:Function, context: Object])
- Vue.set(target: Object | Array, propertyName/index: string | number, value: any)
- Vue.delete(target: Object | Array, propertyName/index: string | number)
- Vue.directive(id: string, [definition: Function | Object])
- Vue.filter(id: string, [definition: Function])
- Vue.component(id: string, [definition: Function | Object])
- 作用:注册或获取全局组件。注册还会自动使用给定的id设置组件的名称
- Vue.use(plugin: Object | Function)
- 作用:安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。需要在调用 new Vue() 之前被调用。当 install 方法被同一个插件多次调用,插件只会安装一次。
- Vue.mixin(mixin: Object)
- 作用:全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。
- Vue.compile(template: string)
- 作用:在 render 函数中编译模板字符串。只在独立构建时有效
- Vue.observable(object:Object)
- 作用:让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。返回的对象可以直接用于渲染函数和计算属性内,并且会在发生改变时触发相应的更新。也可以作为最小化的跨组件状态存储器
- Vue.version
- 作用:提供字符串形式的 Vue 安装版本号
- 用法:
if (version === 2) {} else if (version === 1) {} else {}
过滤器
使用方式
- 双大括号插值中被添加在js表达式尾部,由‘|’指示:
{{message | capitablize}}
,存在标签文本中
- 和v-bind表达式中被添加在js表达式尾部,由‘|’指示:
{{}}
,存在标签属性中
定义
- 在组件中使用filters选项来定义本地过滤器,也可以使用Vue.filter来定义全局过滤器
串联过滤器
{{ message | filterA('arg1', arg2) }}
,还可以传参
自定义指令
钩子函数名称 |
触发时机 |
回调参数 |
init |
已创建VNode,在patch期间发现新的虚拟节点时被触发 |
VNode |
create |
已基于VNode创建了DOM元素 |
emptyNode和VNode |
activate |
keep-alive组件被创建 |
emptyNode和VNode |
insert |
VNode对应的DOM元素被插入到父节点中时被触发 |
VNode |
prepatch |
一个VNode即将被patch之前触发 |
oldVNode和VNode |
update |
一个VNode更新时触发 |
oldVNode和VNode |
postpatch |
一个VNode被patch完毕时触发 |
oldVNode和VNode |
destory |
一个VNode对应的DOM元素从DOM中移除时或者它的父元素从DOM中移除时触发 |
VNode |
remove |
一个VNode对应的DOM元素从DOM中移除时触发。与destory不同的是,如果是直接将该VNode的父元素从DOM中移除导致该元素被移除不会触发 |
VNode和removeCallback |
指令钩子函数 — 对应指令的状态
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
- inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
- unbind:只调用一次,指令与元素解绑时调用
keep-alive
<keep-alive include="字符串|正则表达式" exclude="字符串|正则表达式" max="Number"/>
- include:只有名称匹配的组件会被缓存
- exclude:任何名称匹配的组件都不会被缓存
- max:最多可以缓存多少组件实例.一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <keep-alive include="a,b"> <component :is="view"></component> </keep-alive>
<keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive>
<keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive>
|
- 组件一旦被
<keep-alive>
缓存,那么再次渲染的时候就不会执行created、mounted等钩子
- 如果希望能再次渲染时使用
activated
和deactivated
两个钩子,它的执行时机是<keep-alive>
包裹的组件激活时调用和停用时调用