也不知道哪股风潮,钻研源码竟成了深入理解的标配。我只想说一句,说的很对

准备工作

  1. 从GitHub上面下载vue的源码(https://github.com/vuejs/vue
  2. 了解下Flow,Flow 是 facebook 出品的 JavaScript 静态类型检查工具。Vue.js 的源码利用了 Flow 做了静态类型检查
  3. vue.js 源码目录设计,vue.js的源码都在 src 目录下(\vue-dev\src)
    src
    ├── compiler # 编译相关
    ├── core # 核心代码
    ├── platforms # 不同平台的支持
    ├── server # 服务端渲染
    ├── sfc # .vue 文件解析
    ├── shared # 共享代码

    core 目录:包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。这里的代码可谓是 Vue.js 的灵魂

    platform目录:Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。比如现在比较火热的mpvue框架其实就是在这个目录下面多了一个小程序的运行平台相关内容。

  1. vue2.0的生命周期分为4主要个过程:
    4.1 create:创建---实例化Vue(new Vue) 时,会先进行create。
    4.2 mount:挂载---根据el, template, render方法等属性,会生成DOM,并添加到对应位置。
    4.3 update:更新---当数据发生变化后,更新DOM。
    4.4 destory:销毁---销毁时执行

new Vue()发生了什么

在vue的生命周期上第一个就是 new Vue() 创建一个vue实例出来,对应到源码在\vue-dev\src\core\instance\index.js


import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index' function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
} initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue) export default Vue

可以通过index.js中的代码看到,其实就是一个function,在es5中实现class的方式,在function vue中还加入了if判断,表示vue必须通过new关键字进行实例化。这里有个疑问就是为什么vue中没有使用es6的方式进行定义?通过看下面的方法可以得到解答。

function vue下定义了许多Mixin这种方法,并且把vue类当作参数传递进去,下面来进入initMixin(Vue)下,来自import { initMixin } from './init',选取了部分代码如下


export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++ let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

可以看到initMixin方法就是往vue的原型上挂载了一个_init方法,其他的Mixin也是同理,都是往vue的原型上挂载各种方法,而最开始创建vue类时通过es5 function的方式创建也是为了后面可以更加灵活操作,可以将方法写入到各个js文件,不用一次写在一个下面,更加方便代码后期的维护,这个也是选择es5创建的原因。

当调用new Vue的时候,事实上就调用的Vue原型上的_init方法.

vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等

Vue的双向绑定原理

从index.js入口分析后,越往里发现各个文件之间的引用理不乱剪还乱,于是乎从原来的看源码变成模仿着写雏形,这种方式可能会理解的更加深刻一些,和大家共勉。

vue中的双向数据是通过数据劫持(Object.defineProperty())结合发布者-订阅者模式来实现的,Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

Object.defineProperty()使用

做个小案例,定义一个svue.js文件,定义一个book对象,并赋值输出


var Book = {
name: 'vue权威指南'
};
console.log(Book.name); // vue权威指南

得到的结果就是“vue权威指南”,如果想要在执行console.log(book.name)的同时,直接给书名加个书名号怎么做?可以使用Object.defineProperty()来完成,修改后的代码


var Book = {}
var name = '';
Object.defineProperty(Book, 'name', {
set: function (value) {
name = value;
console.log('你取了一个书名叫做' + value);
},
get: function () {
return '《' + name + '》'
}
}) Book.name = 'vue权威指南'; // 你取了一个书名叫做vue权威指南
console.log(Book.name); // 《vue权威指南》

通过Object.defineProperty()对Book对象的name属性的get和set进行了重写操作,当访问name属性时会触发get执行。

动手模拟写数据双向绑定

实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据。分为3个步骤来做:

(1) 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者
Observer是一个数据监听器,其实现核心方法就是前文所说的Object.defineProperty( )。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行Object.defineProperty( )处理。

对应到源码的目录是:/vue-dev/src/core/observer/index.js


function defineReactive(data, key, val) {
observe(val); // 递归遍历所有子属性
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val;
},
set: function(newVal) {
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
}
});
} function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}; var library = {
book1: {
name: ''
},
book2: ''
};
observe(library);
library.book1.name = 'vue权威指南'; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = '没有此书籍'; // 属性book2已经被监听了,现在值为:“没有此书籍”

因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的,所以要修改下代码


function defineReactive(data, key, val) {
observe(val); // 递归遍历所有子属性
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) { //是否需要添加订阅者(Dep.target后类中加入)
dep.addSub(Dep.target); // 在这里添加一个订阅者
}
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
dep.notify(); // 如果数据变化,通知所有订阅者
}
});
}
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}; function Dep () {
this.subs = []; //订阅者的list
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};

在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数
(2) 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图


function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
} Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};

简单版的Watcher设计完毕,这时候只要将Observer和Watcher关联起来,就可以实现一个简单的双向绑定数据了,定义index.js


function SelfVue (data, el, exp) {
this.data = data; //传递进来的{}对象数据
observe(data);//监听
el.innerHTML = this.data[exp]; // 初始化模板数据的值 this.data[name]
//订阅者 function更新会在watcher.js中回调
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}

定义index.html引入以上3个js文件进行测试


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1 id="name">{{name}}</h1>
</body>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="index.js"></script>
<script type="text/javascript">
var ele = document.querySelector('#name');
var selfVue = new SelfVue({
name: 'hello world'
}, ele, 'name'); window.setTimeout(function () {
console.log('name值改变了');
selfVue.data.name = '66666666';
}, 2000); </script>
</html>

(3) 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器
虽然上面已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析dom节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器Compile来做解析和绑定工作


function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
} Compile.prototype = {
init: function () {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
} else {
console.log('Dom元素不存在');
}
},
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
compileElement: function (el) {
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent; if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令
self.compileText(node, reg.exec(text)[1]);
} if (node.childNodes && node.childNodes.length) {
self.compileElement(node); // 继续递归遍历子节点
}
});
},
compileText: function(node, exp) {
var self = this;
var initText = this.vm[exp];
this.updateText(node, initText); // 将初始化的数据初始化到视图中
new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数
self.updateText(node, value);
});
},
updateText: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
isTextNode: function(node) {
return node.nodeType == 3;
}
}

修改index.js


function SelfVue (options) {
var self = this;
this.vm = this;
this.data = options.data; Object.keys(this.data).forEach(function(key) {
self.proxyKeys(key);
}); observe(this.data);
new Compile(options.el, this.vm);
return this;
} SelfVue.prototype = {
proxyKeys: function (key) {
var self = this;
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function proxyGetter() {
return self.data[key];
},
set: function proxySetter(newVal) {
self.data[key] = newVal;
}
});
}
}

修改index.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{title}}</h2>
<h1>{{name}}</h1>
</div>
</body>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script src="index.js"></script>
<script type="text/javascript">
var selfVue = new SelfVue({
el: '#app',
data: {
title: 'hello world',
name: ''
}
}); window.setTimeout(function () {
selfVue.title = '你好';
}, 2000); window.setTimeout(function () {
selfVue.name = 'canfoo';
}, 2500); </script>
</html>

现在已经可以解析出{{}}的内容,如果想要支持更多的指令,继续完善compile.js


function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
} Compile.prototype = {
init: function () {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
} else {
console.log('Dom元素不存在');
}
},
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
compileElement: function (el) {
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent; if (self.isElementNode(node)) {
self.compile(node);
} else if (self.isTextNode(node) && reg.test(text)) {
self.compileText(node, reg.exec(text)[1]);
} if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes;
var self = this;
Array.prototype.forEach.call(nodeAttrs, function(attr) {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
if (self.isEventDirective(dir)) { // 事件指令
self.compileEvent(node, self.vm, exp, dir);
} else { // v-model 指令
self.compileModel(node, self.vm, exp, dir);
}
node.removeAttribute(attrName);
}
});
},
compileText: function(node, exp) {
var self = this;
var initText = this.vm[exp];
this.updateText(node, initText);
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value);
});
},
compileEvent: function (node, vm, exp, dir) {
var eventType = dir.split(':')[1];
var cb = vm.methods && vm.methods[exp]; if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false);
}
},
compileModel: function (node, vm, exp, dir) {
var self = this;
var val = this.vm[exp];
this.modelUpdater(node, val);
new Watcher(this.vm, exp, function (value) {
self.modelUpdater(node, value);
}); node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[exp] = newValue;
val = newValue;
});
},
updateText: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
},
isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function(dir) {
return dir.indexOf('on:') === 0;
},
isElementNode: function (node) {
return node.nodeType == 1;
},
isTextNode: function(node) {
return node.nodeType == 3;
}
}

修改index.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
</div>
</body>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script src="index.js"></script>
<script type="text/javascript">
var selfVue = new SelfVue({
el: '#app',
data: {
title: 'hello world',
name: ''
}
}); </script>
</html>

就能看到v-model的效果了

未完待续

来源:https://segmentfault.com/a/1190000015846104

vue2.x源码理解的更多相关文章

  1. Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步

    目录 写在前面 成员变量的含义及作用 构造与析构 内存同步管理 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 在Caffe源码理解1中介绍了Blob类,其中的数据成 ...

  2. 基于SpringBoot的Environment源码理解实现分散配置

    前提 org.springframework.core.env.Environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(profiles)和属性.Envi ...

  3. jedis的源码理解-基础篇

    [jedis的源码理解-基础篇][http://my.oschina.net/u/944165/blog/127998] (关注实现关键功能的类)   基于jedis 2.2.0-SNAPSHOT   ...

  4. VUEJS2.0源码理解--优

    VUEJS2.0源码理解 http://jiongks.name/blog/vue-code-review/#pingback-112428

  5. AdvanceEast源码理解

    目录 文章思路 源码理解 一. 标签点形式 按顺序排列四个点,逆时针旋转,且第一个点为左上角点(刚开始选择最左边的点, 二. 标签切边 三. loss计算 四. NMS 最后说明 文章思路 大神的gi ...

  6. Pytorch学习之源码理解:pytorch/examples/mnists

    Pytorch学习之源码理解:pytorch/examples/mnists from __future__ import print_function import argparse import ...

  7. .NET Core 3.0之深入源码理解Startup的注册及运行

    原文:.NET Core 3.0之深入源码理解Startup的注册及运行   写在前面 开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程 ...

  8. 深入源码理解Spring整合MyBatis原理

    写在前面 聊一聊MyBatis的核心概念.Spring相关的核心内容,主要结合源码理解Spring是如何整合MyBatis的.(结合右侧目录了解吧) MyBatis相关核心概念粗略回顾 SqlSess ...

  9. HashMap源码理解一下?

    HashMap 是一个散列桶(本质是数组+链表),散列桶就是数据结构里面的散列表,每个数组元素是一个Node节点,该节点又链接着多个节点形成一个链表,故一个数组元素 = 一个链表,利用了数组线性查找和 ...

随机推荐

  1. Spring Boot + Elastic stack 记录日志

    原文链接:https://piotrminkowski.wordpress.com/2019/05/07/logging-with-spring-boot-and-elastic-stack/ 作者: ...

  2. 笔记-迎难而上之Java基础进阶5

    Lambda表达式无参数无返回值的练习 //定义一个接口 public interface Cook{ public abstract void makeFood(); } public class ...

  3. Java获取当前时间戳/时间戳转换

    时间戳精度有两个概念:1是精确到秒,2是精确到毫秒. 要操作时间戳和时间戳转换为时间一般对应的对象就是Date,而Date各种转换离不开SimpleDateFormat: 如果是要获取时间指定的年月日 ...

  4. mac安装.net core

    https://www.microsoft.com/net/core#macos Install for macOS 10.11 or higher (64 bit) 1 Install pre-re ...

  5. c#列表操作

    Enumerable[从元数据]   //        // 摘要:         //     从序列的开头返回指定数量的连续元素.        //        // 参数:        ...

  6. xcode 5.0 以上去掉icon高亮方法&iOS5白图标问题

    之前的建议方法是把在xxx.info.plist文件里把 icon already includes gloss and bevel effects 设置YES 在Xcode5下,重复实现不成功,今天 ...

  7. Mysql多线程性能测试工具sysbench 安装、使用和测试

    From:http://www.cnblogs.com/zhoujinyi/archive/2013/04/19/3029134.html 摘要:      sysbench是一个开源的.模块化的.跨 ...

  8. RobotFramework --RIDE介绍

    RIDE是robotframework的图形操作前端,我们在RIDE上进行测试用例设计和编写测试脚本,并执行自动化测试.下面来全面的认识下这个操作工具. 在右边编辑页面有三大模块,Edit,TextE ...

  9. Intel® RAID Software User’s Guide

    Intel® RAID Software User’s Guide: •Intel ® Embedded Server RAID Technology 2 •Intel ® IT/IR RAID •I ...

  10. 《好好说话》zz

    最近,<奇葩说>闹出来了一些不愉快. 在半决赛中,姜思达惜败,愤怒的粉丝把矛头指向那场比赛的其他人.最终,马薇薇.黄执中和网友们吵起来了. 这件事本不算大事,毕竟娱乐业就是这个样子.刚刚好 ...