剖析vue原理及运行机制(上)
这是笔者最近一段时间的学习收获,笔者历时半天将其整理成文,以便日后复习,也可能对各位有些许帮助。文章稍长,但绝对精粹。也算是笔者为各位献上的一份“元旦大礼”吧~~
(因文章过长,故将其整理为上、下两篇,之间连续贯通)
Vue运行机制全局预览
初始化及挂载
在new了vue之后,Vue会调用_init函数进行(全局)初始化。初始化生命周期、事件、props、methods、data、computed、watch等。其中最重要的是通过Object.defineProperty
设置setter和getter函数,用来实现【响应式】和【依赖收集】。
初始化后调用 $mount 挂载组件 —— 如果是运行时编译,即不存在 render function 但是存在templete的情况,(此后)还需进行【编译】步骤。
编译
- parse :用正则等方法解析templete模板中的指令、class、style等数据,形成AST(抽象语法树——源代码的抽象语法结构的树状表现形式)
- optimize :主要为了标记static节点。这是vue的一处优化——后面当update更新界面时,会有一个 patch 的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能
- generate :将AST转化为 render function 字符串的过程。得到 render & staticRenderFns 字符串
经历以上三个阶段后,组件中就会存在渲染VNode所需的render function了。
响应式
Vue的响应式中做出卓越贡献的也就getter和setter了。
当 render function 被渲染时,因为会读取所需对象的值,所以会触发getter进行【依赖收集】,其目的是将观察者Watcher对象存放到当前闭包中的订阅者Dep的subs中,形成如下关系:
在修改对象值时,会触发对应的setter,setter通知之前【依赖收集】得到的Dep中每一个Watcher,告诉他们自己的值改变了,需要重新渲染视图。这时候这些Watcher就开始调用update来更新视图 —— 当然,这中间还有一个patch的过程,以及使用队列来异步更新的策略。
Virtual DOM
前面说 render function 会转化为VNode节点。而Virtual DOM其实是一棵以JavaScript对象为基础的树。用对象属性描述节点。它实际上只是一层对真是DOM的抽象。
正是由于Virtual DOM是以JavaScript对象为基础而不依赖平台环境。所以使他拥有了跨平台的能力。
一个简单的例子:
// AST
{
tag:'div',
children:[
{
tag:'a',
data:{
directives:[ //属性
{
rawName:'v-show',
expression:'isShow',
name:'show',
value:true
}
],
staticClass:'demo'
},
text:'click me'
}
]
}
渲染之后就得到:
<div>
<a class="demo" v-show="isShow">click me</a>
</div>
这两段代码的逆向过程,其实就是一个HTML解析器解析标签的过程。让我们来逐步看看:
响应式系统
对vue稍有深究的人都一定知道,Vue的【响应式】是基于Object.defineProperty
实现的。
其使用方法:
Object.defineProperty(obj,prop,descriptor)
//目标对象、需要操作的目标对象的属性名、描述符(属性聚集地)
比如,一般更喜欢这样写:
Object.defineProperty(obj,prop,{
enumerable:是否可以枚举
configurable:是否可以被修改或删除
get:
set:
})
实现Observer
首先定义一个函数cb,用来模拟视图更新。内部是一些更新视图的方法:
function cb(val){
//渲染视图
console.log('视图更新了...');
}
然后我们定义一个defineReactive,这个方法通过Object.defineProperty
来实现对对象的【响应式】化,入参当然是obj(需要绑定的对象)、key(obj的一些属性)、val(具体值),经过defineReactive处理后,obj的key属性会在读的时候触发reactiveGetter方法,在写的时候触发reactiveSetter方法:
function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
enumable:true,
configurable:true,
get:function reactiveGetter(){
return val;
},
set:function reactiveSetter(newVal){
if(newVal===val) return;
cb(newVal);
}
});
}
这些当然是不够的。我们需要在这上面再封装一层observer —— 它传入一个value(需要“响应式”化的对象)参数,通过遍历他所有属性的方式来对该对象的每一个属性做defineReactive处理:
function observer(value){
if(!value || (typeof value !== 'object')){
return;
}
Object.keys(value).forEach((key)=>{
defineReactive(value,key,value[key]);
});
}
最后,我们把它们封装到“vue”中:
class Vue{
constructor(options){
this._data=options.data;
observer(this._data);
}
}
这时,就可以“为所欲为了”:
let vm=new Vue({
data:{
test:"I'm mxc"
}
});
vm._data.test="hello world!"; //“视图更新了...”
这样代码就“完美”了吗?
响应式系统的依赖收集追踪原理
为什么要“追踪”?
假如我们现在有一个Vue对象:
new Vue({
templete:`<div>
<span>{{text1}}</span>
<span>{{text2}}</span>
</div>`,
data:{
text1:'text1',
text2:'text2',
text3:'text3'
}
});
然后我们这么做了:
this.text3="I'm text3";
如上,我们修改了text3的数据,但视图中并不需要text3,所以我们并不需要调用cb函数。
要解决这个问题,就需要大名鼎鼎的【订阅】 & 【观察者】模式了:
订阅者Dep —— 存放Watcher对象 (->其实这可以说成是一个“消息管理中心”)
class Dep{
constructor(){
this.subs=[]
}
addSub(sub){
this.subs.push(sub);
}
//通知所有Watcher更新视图
notify(){
this.subs.forEach((sub)=>{
sub.update();
});
}
}
观察者Watcher:
class Watcher{
constructor(){
//在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到
Dep.target=this;
}
update(){
console.log('视图更新啦...');
}
}
Dep.target=null;
接下来要去修改一下defineReactive以及Vue的构造函数,来完成【依赖收集的注入】:(我们不再需要“cb”了)
function defineReactive(obj,key,val){
const dep=new Dep();
Object.defineProperty(obj,key,{
enumable: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);
new Watcher();
//这里模拟render的过程,为了触发test属性的get函数
console.log('render',this._data.test);
}
})
我们在闭包中增加了一个Dep类的对象,用来收集Watcher对象。在对象被“读”的时候,会触发reactiveGetter,把当前Watcher对象(存放在Dep.target中)收集到Dep类中去;之后“写”的时候,触发reactiveSetter,通知类Dep调用notify来触发所有Watcher的update更新对应视图。
专栏链接(免费):vue实现原理及运行机制分析