vue 源码运行流程解析
2019-02-15 本文已影响0人
拉面的无聊时光
框架只是工具,了解框架底层原理才能在框架基础上开发出更好的程序。 vue作为前端基础框架之一,更应该清楚其底层运行原理
mvvm模式
vue的mvvm模式如图所示 mvvm.png内部基础对象
-
Observer: 用来深度遍历 model对象 ,对内部的每一个key用
Object.defineProperty
函数对getter
,setter
进行劫持 -
Dep: 初始化vue实例时,Observer劫持model 每个key的
getter
,setter
函数时,会为每一个key值都创建Dep对象,Dep对象内部有数组去存储watcher。 -
Wathcer : 当model数据变动时,触发对应的key的
Dep.notify()
,Dep.notity
触发dep内部所有watcher的watcher.update()
事件
watcher 实例主要有三种情况
1. 主watcher, 每个vue实例都有一个主watcher,负责页面渲染
2. computed-watcher:computed选项生成的watcher,负责触发computed函数
3. watch-watcher:watch选项生成的watcher,负责触发自定义的watch函数
vue在mounted阶段 ,
- 数据如下所示
//name属性对应一个dep对象,dep内部一个数组保存着收集的 wather
name -- {dep : [ watcher ] }
//age属性对应一个dep对象,dep内部一个数组保存着收集的 wather
age -- {dep : [ watcher ] }
- 数据驱动页面 的事件流程
1.name 值变更
2.name的setter 函数执行
3.name对应的dep触发dep.notify()
4.dep内部所有watcher 触发wather.update()
// water.update()触发 1.页面更新 2.computed选项属性更新 3.wath选项属性更新
在vue初始化阶段,有一些重要的事情处理
vue初始化阶段
1.补全vue实例的相关属性,生命周期属性,Event事件注册,渲染函数相关初始化,处理option里面的数据等等。
//...省略
initLifecycle(vm); //给vue实例补充生命周期相关属性如,_isMounted,_isDestroyed等
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
//重点: ininState这里处理了option里面的
//1. data属性(劫持getter,setter),
//目前还未收集watche,此时model是如下
name -- {dep : [ ] }
age -- {dep : [ ] }
//2. computed属性(生成watcher),
//3. watch属性(生成watcher)
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
//...省略
- 生成渲染函数 :源码地址
// 以下代码是我个人便于理解而写的,代码和源码不同,但是逻辑流程是一样的
var render
if(option.render){
render = option.render
}else if(option.template ){
render = compile(option.template)
}else if(option.el) {
rennder = compile(doucment.getElementById(option.el).outerHTML)
}else {
throw new Error("...")
}
以上代码会将html模板转化为渲染函数即
<div id="app">
<h2>{{name}}</h2>
<h4>it is {{age}}</h4>
</div>
//转化为
let _render = function () {
with(this){
return
_c('div',{attrs:{"id":"app"}},
[
_c('h2',[_v(_s(name))]),
_c('h4',[_v("it is "+_s(age))])
]
)
}
}
vm.$option.render = render
//_c :返回vnode(虚拟dom节点)的函数
//_v: 返回vnode(虚拟文本节点)的函数
//_s: String()
转化过程就是:
1.html转astdom树
2.根据astdom树拼接 渲染函数字符串
3. new Function(`渲染函数字符串`)
所以说不写模板标签,直接写option里面写渲染函数性能会提高很多。但代价就是开发效率低,不够直观
3.生成主watcher ,代码如下
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {
before: function before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;
vm._isMounted = true;
callHook(vm, 'mounted');
new Watcher时第四个参数里面没有lazy属性 ,所以此watcher 会在构造函数内部执行watcher.getter(),也就是传入第二个参数updateComponent
看下watcher.getter执行流程
1.watcher.getter()
⬇
2.vm._update(vm._render(),hydrating)
⬇
1. vm._render()
⬇
2.vm.$option.render.call(vm,vm.$createElement)
//也就是以vm为上下文执行渲染函数(此函数在第二步中动态生成)
⬇
3.执行 function () {
with(this){
return
_c('div',{attrs:{"id":"app"}},
[
```执行这里时,触发name的getter函数```
_c('h2',[_v(_s(name))]),
```执行这里时,触发age的getter函数```
_c('h4',[_v("it is "+_s(age))])
]
)
}
}
⬇
4.执行name的get()和age的get()
function defineReactive$$1(obj, key,val,) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
`````````执行这里收集watcher`````
if (Dep.target) {dep.subs.push(Dep.target);}
return val
},
set: function reactiveSetter(newVal) {
if (newVal === val ) {return}
val = newVal;
dep.notify();
}
});
}
⬇
5.执行完get后 此时model是如下
``` name -- {dep : [ watcher ] } ```
``` age -- {dep : [ watcher ] } ```
⬇
6 执行完vm._render后返回vnode,此时watcher收集完毕
⬇
3.vm._update(vnode,hydrating)
⬇
4.vm._update内部执行__patch__ ,
if (!prevVnode) {
//第一次渲染:__patch__根据vnode生成真实dom插入到$el中
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// 更新渲染:__patch__,使用diff算法比较prevVnode和vnode,最小化修正真实dom
vm.$el = vm.__patch__(prevVnode, vnode);
}
以上代码执行完成,vue的初始化就已完成。
在mounted阶段
model此时数据如下:
name -- {dep : [ watcher ] }
age -- {dep : [ watcher ] }
此时当 name的值变更时执行以下函数:
name 的setter() 执行
⬇
dep.notify()
⬇
watcher.update()
⬇
watcher.getter()
⬇
vm._update(vm._render(),hydrating) )
vm._update()执行流程 。上面已经详细的写过了,这里就不写了
在destroy阶段
- 移除vue实例上的所有watcher
- 移除dom上所有事件绑定
- 。。。
vue组件
- 创建时间点:
__patch__
函数内部创建 - 运行流程:和上面写的一样
最外层的vue实例也是vue组件,区别是:最外层的vue组件(vue实例)new的时候需要提供el
属性, 内部vue组件不需要el属性
,因为内层vue组件知道自己真实dom插在哪里。
总结
vue运行总体流程大概就是这样,vue源码还有好多细节比如:
- diff算法
- 防止dep里面多次收集到同一个watcher
- 异步模式下,多个属性更改,收集watcher到一个队列,在下一帧调用一遍队列即可,不必反复触发同一个watcher.
- computed属性的watcher生成和收集
- watch属性的watcher生成和收集