vue内部运行机制浅析

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

vue组件的三个API:prop,event,slot

1
2
3
4
5
6
7
8
<template>
<button :class="'i-button-size'+size" :disabled='disabled' @click="handleClick">
<slot></slot>
<!-- 具名插槽 -->
<slot name="icon"></slot>
</button>
</template>
<script>

判断参数是否是其中之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function oneOf(value,sizeList){
for(let i=0;i<sizeList.length;i++){
if(value===sizeList[i]){
return true
}
}
return false
}
export default {
props:{
size:{
validator(value){
return oneOf(value,['small','large','default']);
},
default:'default'
},
disabled:{
type:Boolean,
default:false
}
},
methods:{
handleClick(event){
this.$emit('on-click',event)
}
}
}
</script>

<i-button size="large" disabled @on-click="handleClick"></i-button>
<i-button @click.native="handleClick"></i-button>

组件通信

无依赖的组件通信方法provide/inject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//A.vue

export default{
provide:{
name:'Aresn'
}
}
//B.vue
export default{
inject:['name'],
mounted(){
console.log(this.name) //aresn
}
}

app.vue 是整个项目第一个被渲染的组件,而且只会渲染一次(即使切换路由,app.vue 也不会被再次渲染),
利用这个特性,很适合做一次性全局的状态数据管理,例如,我们将用户的登录信息保存起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script>
export default {
provide(){
return{
app:this
}
},
data(){
return {
userInfo:null
}
},
methods:{
getUserInfo(){
//伪代码
$.ajax('/user/info',(data)=>{
this.userInfo=data
})
}
},
mounted(){
this.getUserInfo()
}
}
</script>

<template>
<div>{{app.userinfo}}</div>
</template>
<script>
export default {
inject:['app'],
methods:{
changeUserInfo(){
//伪代码
$.ajax('/user/update',()=>{
this.app.getUserInfo()
})
}
}
}
</script>

如果你的项目足够复杂,或需要多人协同开发时,在 app.vue 里会写非常多的代码,多到结构复杂难以维护。这时可以使用 Vue.js 的混合 mixins,将不同的逻辑分开到不同的 js 文件里。

比如上面的用户信息,就可以放到混合里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
import mixins_user from '../mixins/user.js'
export default {
mixins: [mixins_user]
}
</script>
function broadcast(compoentName,event,params){
this.$children.forEach(child=>{
const name=child.$options.name
if(name===componentName){
child.$emit.apply(child,[eventName].concat(params))
}else{
broadcast.apply(child,[componentName,eventName].concat(params))
}
})
}
export default{
methods:{
dispatch(compnentName,eventName,params){
let parent=this.$parent||this.$root;
let name=parent.$options.name
while(parent&&(!name||name!=componentName)){
parent=parent.$parent
if(parent){
name=parent.$options.name
}
}
if(parent){
parent.$emit.apply(parent,[eventName].concat(params))
}
}
},

}

得理解思路才行,具体代码可以不用太认真,有一个总体的概念
动态渲染.vue文件的组件 —display

运行机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
new vue =>init=>$mount=>render function =>vnode
init 对数据进行响应式化
render function 会被转化成 VNode 节点
obj: 目标对象
prop: 需要操作的目标对象的属性名
descriptor: 描述符
Object.defineProperty(obj,prop,descrptor)

Object.defineProperty 用来把对象变成可观察的

function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
enumerable:true, //属性可枚举
configurable:true, //属性可被修改或删除
get:function reactiveGetter(){
return val;
},
set:function reactiveSetter(newVal){
if(newVal===val) return ;
cb(newVal)
}
})
}

当然这是不够的,我们需要在上面再封装一层 observer 。这个函数传入一个 value(需要「响应式」化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。
(注:实际上 observer 会进行递归调用,为了便于理解去掉了递归的过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function observer(obj){
if(!obj||(typeof obj!=='object')){
return
}
Object.keys(obj).forEach(key=>{
defineReactive(obj,key,obj[key])
})
}
class Vue{
constructor(options){
this._data=options.data;
observer(this._data)
}
}

订阅者dep (Dependency)
主要作用是存放watcher观察者对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dep{
constructor(){
//用来存放watcher对象的数组
this.subs=[]
}
//在subs中添加一个watch对象
addSub(sub){
this.subs.push(sub)
}
//通知所有watcher对象视图更新
notify(){
this.subs.forEach(sub=>{
sub.update()
})
}
}

完成两件事
1 用addSub方法可以在目前的Dep对象中增加一个watcher的订阅操作
2 用notify方法通知目前对象dep对象的subs中的所有watcher对象触发更新操作

观察者watcher

1
2
3
4
5
6
7
8
9
class watcher{
constructor(){
/*在new一个watch对象时将该对象赋值给Dep.target,在get中用到*/
Dep.target=this;
}
update(){
console.log('视图更新啦)
}
}

修改一下 defineReactive 以及 Vue 的构造函数,来完成依赖收集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function defineReactive(obj,key,val){
//一个Dep类对象
const dep=new Dep()

Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get:function reactiveGetter(){
dep.addSub(Dep.target)
return val;
},
set:function reactiveSetter(newVal){
if(newVal===val) return;
dep.notify()
}
})
}
class Vue{
constructor(options){
this._data=options.data;
observer(this._data)
/*新建一个watch观察者对象,这时候Dep.target会指向这个watcher对象*/
new watcher()
//模拟render过程,触发test属性的get函数
console.log('render~',this._data.test)

}
}

数据状态更新时的差异diff及patch机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const nodeOps={
setTextContent(text){
if(platform==='weex'){
node.parentNode.setAttr('value',text)
}else if(platform==='web'){
node.setTextContent=text
}
},
parentNode(){

},
removeChild(){

},
nextSibling(){

},
insertBefore(){

}

}

patch的diff算法,是通过同层的树节点进行比较,而非对树进行逐层遍历搜索,时间复杂度O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
patch的简单过程代码
function patch(oldVnode,vnode,parentElm){
if(!oldVnode){
addVnodes(parentElm,null,vnode,0,vnode.length-1)
}else if(!vnode){
removeVnodes(parentElm,oldVnode,0,oldVnode.length-1)
}else{
if(sameVnode(oldVnode,vnode)){
patchVnode(oldVNode,vnode)
}else{
removeVnodes(parentElm,oldVNode,0,oldVNode.length-1)
addVnodes(parentElm,null,vnode,0,vnode.length-1)
}
}
}

sameVnode的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
function sameVnode(){
return (
a.key===b.key&&
a.tag===b.tag&&
a.isComment===b.isComment&&
(!!a.data)===(!!b.data) &&
sameInputType(a,b)
)
}
function sameInputType(a,b){
if(a.tag!=='input') return true
let i
const typeA=(i=a.data)&&(i=i.attrs)&&i.type
const typeB=(i=b.data)&&(i=i.attrs)&&i.type
return typeA===typeB
}

/**对比相同的节点有哪些变化**/

function patchVnode(oldVnode,vnode){
if(oldVNode===vnode){
return
}
if(vnode.isStatic&&oldVnode.isStatic&&vnode.key===oldnode.key){
vnode.elm=oldVnode.elm;
vnode.componentInstance=oldVnode.componentInstance
return
}
const elm=vnode.elm=oldVNode.elm
const oldCh=oldVnode.children;
const ch=vnode.children

if(vnode.text){
nodeOps.setTextContent(elm,vnode.text)
}else
if(oldCh&&ch&&(oldCh!==ch)){
updateChildren(elm,oldCh,ch)
}else if(ch){
if(oldVnode.text) nodeOps.setTextContent(elm,'')
addVnodes(elm,null,ch,0,ch.length-1)
}else if(oldCh){
removeVnodes(elm,oldCh,0,oldCh.length-1)
}else if(oldVNode.text){
nodeOps.setTextContent(elm,'')
}
}
//updateChildren
function updateChildren(parentElm,oldCh,newCh){
let oldStartIdx=0;
let newStartIdx=0;
let oldEndIdx=oldCh.length-1
let oldStartVnode=oldCh[0]
let oldEndVnode=oldCh[oldEndIdx]
let newEndIdx=newCh.length-1
let newStartVnode=newCh[0]
let newEndVnode=newCh[newEndIdx]
let oldKeyToIdx,idxInOld,elmToMove,refElm;

while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){
if(!oldStartVnode){
oldStartVnode=oldCh[++oldStartIdx]
}else if(!oldEndVnode){
oldEndVnode=oldCh[--oldEndIdx]
}else if ( sameVnode(oldStartVnode,newStartVnode) ){
patchVnode(oldStartVnode,newStartVnode)
oldStartVnode=oldCh[++oldStartIdx]
newStartVnode=newCh[++newStartIdx]
}else if(sameVnode(oldEndVnode,newEndVnode)){
patchVnode(oldEndVnode,newEndVnode)
oldEndVnode=oldCh[--oldEndIdx]
newEndVnode=newCh[--newEndIdx]
}else if(sameVnode(oldStartVnode,newEndVnode)){
patchVnode(oldStartVnode,newEndVnode)
nodeOps.insertBefore(parentElm,oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.oldStartVnode=oldCh[++oldStartIdx]))
newEndVnode=newCh[--newEndIdx]
}else if(sameVnode(oldEndVnode,newStartVnode)){
patchVnode(oldEndVnode,newStartVnode)
nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)
oldEndVnode=oldCh[--oldEndIdx]
newStartVnode=newCh[++newStartIdx]
}else{
let elmToMove=oldCh[idxInOld]
if(!oldKeyToIdx) oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)
idxInOld=newStartVnode.key?oldKeyToIdx[newStartVnode.key]:null
if(!idexInOld){
createElm(newStartVnode,parentElm);
newStartVnode=newCh[++newStartIdx]
}else{
elmToMove=oldCh[idxInOld]
if(sameVnode(elmToMove,newStartVnode)){
patchVnode(elmToMove,newStartVnode)
oldCh[idxInOld]=undefined
nodeOps.insertBefore(parentElm,newStartVnode.elm,oldStartVnode.elm)
newStartVnode=newCh[++newStartIdx]
}else{
createElm(newStartVnode,parentElm)
newStartVnode=newCh[++newStartIdx]
}
}
}
}
if(oldStartIdx>oldEndIdx){
refElm=(newCh[newEndIdx+1])?newCh[newEndIdx+1].elm:null;
addVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx)
}else if(newStartIdx>newEndIdx){
removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)
}
}

nextTick原理

Vue.js 实现了一个 nextTick 函数,传入一个 cb ,这个 cb 会被存储到一个队列中,在下一个 tick 时触发队列中的所有 cb 事件。

因为目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。

笔者用 setTimeout 来模拟这个方法,当然,真实的源码中会更加复杂,笔者在小册中只讲原理,有兴趣了解源码中 nextTick 的具体实现的同学可以参考next-tick。

首先定义一个 callbacks 数组用来存储 nextTick,在下一个 tick 处理这些回调函数之前,所有的 cb 都会被存在这个 callbacks 数组中。pending 是一个标记位,代表一个等待的状态。

setTimeout 会在 task 中创建一个事件 flushCallbacks ,flushCallbacks 则会在执行时将 callbacks 中的所有 cb 依次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let callbacks=[]
let pending=false;
function nexttick(cb){
callbacks.push(cb)
if(!pending){
pending=true;
setTimeout(flushCallbacks,0)
}
}

function flushCallbacks(){
pending=false;
const copies=callbacks.slice(0)
callbacks.length=0;
for(let i=0;i<copies.length;i++){
copies[i]()
}
}

//update 方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let uid=0;
class watcher{
constructor(){
this.id=++uid;
}
update(){
console.log('watch'+this.id+'update')
queueWatcher(this)
}
run(){
console.log('watch'+this.id+'视图更新啦')
}
}

queueWatcher

我们使用一个叫做 has 的 map,里面存放 id -> true ( false ) 的形式,用来判断是否已经存在相同的 Watcher 对象 (这样比每次都去遍历 queue 效率上会高很多)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let has={}
let queue=[]
let waiting=false

function queueWatcher(watcher){
const id=watcher.id;
if(has[id]==null){
has[id]=true;
queue.push(watcher)
if(!waiting){
waiting=true;
nextTick(flushSchedulerQueue)
}
}
}

//flushSchedulerQueue

1
2
3
4
5
6
7
8
9
10
function flushSchedulerQueue(){
let watcher ,id;
for(index=0;index<queue.length;index++){
watcher=queue[index]
id=watcher.id
has[id]=null
watcher.run();
}
waiting=false
}

store
理解 Vuex 的核心在于理解其如何与 Vue 本身结合,如何利用 Vue 的响应式机制来实现核心 Store 的「响应式化」。

1
2
3
4
5
6
7
8
9
10
11
12
13
let Vue;
export default install(_Vue){
Vue.mixin({beforeCreate:vuexInit})
Vue=_Vue
}
function vuexInit(){
const options=this.$options;
if(options.store){
this.$store=options.store
}else{
this.$store=options.parent.$store
}
}