vue源码学习

前言

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)
  1. Object.defineProperties(obj,props) 方法直接在一个对象上定义新的属性修改现有属性,并返回该对象

  2. 在这里该语法语法可以有两个参数obj和props,obj是要添加或者修改属性的对象,而props其实是一个配置对象,是对要添加或者修改的属性的描述对象,具体可看官方文档。而在这里props中对于vue中最重要的两个属性就是getset

  3. get:作为该属性的 getter 函数,如果没有 getter 则为undefined。函数返回值将被用作属性的值。

  4. set:作为属性的 setter 函数,如果没有 setter 则为undefined。函数将仅接受参数赋值给该属性的新值(注意:set在JavaScript基本都是监听的意思,在这里是当监听属性发生变化时就会触发相应的回调函数,这也是vue实现数据双向绑定最核心的语法)

  5. 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的回调函数的
     */
    

    example

项目代码结构

刚刚说了一下一些在研究该项目时所必须的基础知识,如果还不清楚的可以查阅文档

接下来,就正式开始对于该项目的研究

首先是该项目的文件目录结构图

项目结构

其实最主要的就是js文件夹中的那个4个文件夹,那四个js文件就是实现mvvm思想的代码文件

其中

  1. mvvm.js是其实是创建mvvm对象的入口文件
  2. compiler.js是负责编译一些指令,将html标签上的一些指令属性转化为js代码并执行操作
  3. observer.js是负责对vm实例中data的监视(我们知道在创建vue实例对象中有一个data属性供我们配置,我们在data属性中配置的数据可以在被vue处理过的html中使用)
  4. watcher.js是负责当数据(准确说应该是data)发送变化时做出相应的操作,例如更新视图,相关数据发生变化

mvvm实现流程

首先,是一副mvvm初始化及更新的图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0excSGUO-1580803528301)(images/2020012101.PNG)]

该图涉及到以下对象

  1. MVVM ViewModel
  2. Observer 数据劫持实现数据绑定
  3. Dep 与data中的每一个属性名对应
  4. Watcher 与html中的每一个指令表达式/大括号表达式对应
  5. Updater 更新视图工具类

接下来我将会从两个方面来剖析mvvm

初始化

当我们new一个MVVM实例对象时,它其实完成了以下工作

  1. 数据代理 实现vm.xxx -> vm._data.xxx

    Object.keys(data).forEach(function(key) {
            me._proxyData(key);
    });
    
  2. 数据绑定 对data中所有的层次属性通过数据劫持实现数据绑定

    observe(data, this);
    
  3. 模板解析

    this.$compile = new Compile(options.el || document.body, this)
    
  4. 大致流程

    1. 创建MVVM对象
    2. 通过劫持数据实现数据绑定
      1. 为每一个中data中所有的层次属性名关联一个dep
      2. 为在html中每一个表达式关联一个watcher
      3. 建立watcher与dep的关系
    3. 通过使用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学习的视频,这个视频对我的帮助很大,在这里我也安利一下。

或许写的比较乱,意思有可能没有表达很清楚,望能多多包含!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值