前言
When we learn a new technology,we need to know what it is,why we learn it and how to use it best
最近在github上遇到了一个项目,是去剖析vue内部mvvm的实现原理。感觉挺有意思的,于是就花了差不多1周的时间看了看这个项目
GitHub项目地址:https://github.com/DMQ/mvvm
这个项目主要是解析vue mvvm的实现原理,关于该项目其他说明可以阅读项目的readme。而本文主要是从该项目的代码来讲讲该项目是如何实现mvvm
必备的基础知识
由于该项目相当于是去自己手动实现vue,因此在该项目会使用到JavaScript一些底层语法,涉及到的语法如下
1. [].slice.call(this):将伪数组转换为真数组
2. node.nodeType:得到节点类型(document,element,attribute,text)
3. Object.defineProperties(obj,props): 给当前对象添加属性
4. Object.keys(obj):得到对象自身并且可枚举属性组成的数组
5. obj.hasOwnProperty(prop):判断prop是否是obj自身的属性
6. DocumentFragemnt(Node接口的一个实现类)
关于上述语法的具体使用,我们可以从MDN这个网站中查询,该网站其实已经讲得很清楚了。而在这里我就来说说Object.defineProperties(obj,props)这个语法的具体使用。因为这个语法其实就是vue实现数据双向绑定的JavaScript底层语法
Object.defineProperties(obj,props)
-
Object.defineProperties(obj,props) 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象
-
在这里该语法语法可以有两个参数obj和props,obj是要添加或者修改属性的对象,而props其实是一个配置对象,是对要添加或者修改的属性的描述对象,具体可看官方文档。而在这里props中对于vue中最重要的两个属性就是get和set
-
get:作为该属性的 getter 函数,如果没有 getter 则为
undefined
。函数返回值将被用作属性的值。 -
set:作为属性的 setter 函数,如果没有 setter 则为
undefined
。函数将仅接受参数赋值给该属性的新值(注意:set在JavaScript基本都是监听的意思,在这里是当监听属性发生变化时就会触发相应的回调函数,这也是vue实现数据双向绑定最核心的语法) -
example
var obj = { firstName: "Jack", lastName: "Black" } // 输出obj console.log(obj); // 在obj对象上定义属性fullName,为该属性设置对应的get和set方法 Object.defineProperties(obj, { "fullName": { // 当去修改fullName值时会触发set的回调函数 set: function (value) { console.log("the attribute has change! and new value is " + value); }, // 当去获取fullName值时会触发get的回调函数 get: function () { console.log("the attribute has getted") // this是指当前触发该回调函数的对象 // 若使用箭头表达式则this就会变为windows对象(在浏览器中),箭头函数是不会修改当前this所指向的对象 // console.log(this); return this.firstName + " " + this.lastName; }, // get:()=>{ // console.log(this); // return this.firstName + " " + this.lastName; // } } }) // 输出 obj.fullName // 获取 fullName就会触发get的回调函数 console.log(obj.fullName); // 修改fullName就会触发set的回调函数 obj.fullName = "hello world"; // 当去修改firstName时就会修改fullName // 因为fullName值的计算是基于firstName得到的 // 在获取fullName值时就会触发get方法 obj.firstName = "Bob"; console.log(obj.fullName); /** * 注意,我们只有直接去修改fullName的值才会触发fullName的set回调函数 * 而当我们去修改firstName值使得fullName值发生变化,属于间接修改fullName值,是不会触发fullName的set的回调函数的 */
项目代码结构
刚刚说了一下一些在研究该项目时所必须的基础知识,如果还不清楚的可以查阅文档
接下来,就正式开始对于该项目的研究
首先是该项目的文件目录结构图
其实最主要的就是js文件夹中的那个4个文件夹,那四个js文件就是实现mvvm思想的代码文件
其中
- mvvm.js是其实是创建mvvm对象的入口文件
- compiler.js是负责编译一些指令,将html标签上的一些指令属性转化为js代码并执行操作
- observer.js是负责对vm实例中data的监视(我们知道在创建vue实例对象中有一个data属性供我们配置,我们在data属性中配置的数据可以在被vue处理过的html中使用)
- watcher.js是负责当数据(准确说应该是data)发送变化时做出相应的操作,例如更新视图,相关数据发生变化
mvvm实现流程
首先,是一副mvvm初始化及更新的图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0excSGUO-1580803528301)(images/2020012101.PNG)]
该图涉及到以下对象
- MVVM ViewModel
- Observer 数据劫持实现数据绑定
- Dep 与data中的每一个属性名对应
- Watcher 与html中的每一个指令表达式/大括号表达式对应
- Updater 更新视图工具类
接下来我将会从两个方面来剖析mvvm
初始化
当我们new一个MVVM实例对象时,它其实完成了以下工作
-
数据代理 实现vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key) { me._proxyData(key); });
-
数据绑定 对data中所有的层次属性通过数据劫持实现数据绑定
observe(data, this);
-
模板解析
this.$compile = new Compile(options.el || document.body, this)
-
大致流程
- 创建MVVM对象
- 通过劫持数据实现数据绑定
- 为每一个中data中所有的层次属性名关联一个dep
- 为在html中每一个表达式关联一个watcher
- 建立watcher与dep的关系
- 通过使用Compiler对象解析指令属性/大括号表达式
更新
当data中的数据发生变化时,mvvm会自动帮助我们更新视图
// 给data重新定义属性,添加set/get
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();// 建立watcher与dep之间的关系
}
return val;
},
set: function(newVal) {// 监事key属性的方法,更新界面
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知所有相关的订阅者
dep.notify();
}
});
上述代码就是实现数据变化视图更新的关键代码,主要涉及到了对象dep,它会通知相关的更新器根据数据去更新视图
刚刚我们在初始化时,mvvm为data中的每一个层次属性名关联一个dep,为html中的每一个表达式关联一个watcher,并且mvvm也为每一个dep和watcher建立关联(在获取该属性的值时建立)
那么如何判断一个dep和一个watcher是否有关联
<div id="test">
<h1>{{msg1}}</h1> // watcher1
<h1 v-text="msg1"></h1> // watcher2
<h1>{{msg2}}</h1> // watcher3
</div>
<script>
new MVVM({
el:"#test",
data:{
msg1:"hello world1", // dep1
msg2:"hello world2" // dep2
}
})
</script>
在上面的代码中,MVVM会创建2个dep(data中只有两个层次属性名)和3个watcher(html代码中只有3个表达式)
而dep1会与watcher1,watcher2关联
dep2会与watcher3关联
因为watcher1,wacther2中都使用到了msg1,而msg1与dep1关联,因此dep1会与watcher1,watcher2关联
watcher3同理
而data中的某个属性值发生变化时,会触发初始化定义的set函数,而set函数会触发该属性值相关的dep方法notify(),dep的notify()方法会通知相关的watcher去更新视图
// Dep.prototype
notify: function() {
// 遍历所有的watcher,通知watcher更新
this.subs.forEach(function(sub) {
sub.update();
});
}
// 表达式相watcher的定义
// 主要在编译指令时创建watcher
// 为表达式创建一个对应的watcher,实现节点的更新显示
// 当表达式对应包含的任意一个属性值发送变化时调用
// 更新界面中的指定的更新节点
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
大致就是这样了
总体说一下
刚刚分别是从mvvm初始化和更新界面两个方面来讲,接下来就总体说一下
1. 当创建MVVM实例对象时,MVVM的构造函数会获取关于其的配置参数
2. 获取配置参数中的data数据,并代理到实例对象的_data上
3. 查看是否有关于计算属性的相关配置,如果有则将计算属性中的属性通过Object.defineProperty挂到vm实例对象上
4. 对data中的所有层次的属性通过数据劫持实现数据绑定
1. 使用Object.defineProperty重新定义data中的所有属性,添加set和get方法
2. 为每一个属性关联一个dep对象、
3. 当要获取该属性的值时会触发get方法将它的dep与相关的watcher进行关联
4. 当要修改该属性的值时会触发set方法调用它的dep方法notify(),通知该dep相关的watcher调用更新视图方法,实现数据修改视图更新
5. 创建一个编译对象,解析标签中的指令属性/大括号表达式
1. 将el元素中所有的子节点保存到一个fragment容器中
2. 在fragment容器中进行指令的解析
1. 取出最外层的节点,判断节点的类型
1. 若是元素节点,则开始解析该元素节点是否有相关指令
1. 获取该元素节点的所有属性节点
2. 如果是vue相关指令,则开始解析指令
1. 如果是事件指令,则为该元素注册事件函数
2. 如果是普通指令,则根据指令名称做出相关操作
3. 完成指令解析后移除指令属性
2. 若是含有大括号表达式的文本节点,则开始解析大括号表达式
3. 如果当前节点还有子节点,则递归调用编译节点的方法
2. 在进行指令解析时,会为每一个表达式创建watcher(主要在compiler.compileUtil.bind函数中)
3. 当完成指令解析后将这个fragment放到元素的后面
基本情况就是这样
这是我在研究该项目时所做的笔记,或许会跟github上的有些不一样,因为我也尝试了修改了一些代码
地址:
最后
通过这次的学习,起码对JavaScript的一些基础有了一些基本的认识,但是不懂的东西还有很多,要学习的东西还有很多。
除了该项目,我还参考了b站的一个关于vue学习的视频,这个视频对我的帮助很大,在这里我也安利一下。
或许写的比较乱,意思有可能没有表达很清楚,望能多多包含!
放到元素的后面
基本情况就是这样
最后
通过这次的学习,起码对JavaScript的一些基础有了一些基本的认识,但是不懂的东西还有很多,要学习的东西还有很多。
除了该项目,我还参考了b站的一个关于vue学习的视频,这个视频对我的帮助很大,在这里我也安利一下。
或许写的比较乱,意思有可能没有表达很清楚,望能多多包含!