前言
本篇文章 分析 vue 作为一个 MVVM 框架的基本实现原理 :数据代理 ;模板解析 ;数据绑定
不直接看 vue.js 的源码 ,剖析 github 上某基友仿 vue 实现的 mvvm 库 地址:https://github.com/DMQ/mvvm
必备的知识点
在开始分析vue源码之前,必须要知道的以下知识点
[].slice.call(lis): 将伪数组转换为真数组
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul id="fragment_test">
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul>
<script type="text/javascript">
const lis = document.getElementsByTagName('li') // lis是伪数组(是一个特别的对象, 具有length和数值下标属性)
// 判断lis是否是对象,是否是真数组
console.log(lis instanceof Object, lis instanceof Array)
// 数组的slice()截取数组中指定部分的元素, 生成一个新的数组 [1, 3, 5, 7, 9], slice(0, 3)
// slice2(),数组的slice()方法实现方式
Array.prototype.slice2 = function (start, end) {
start = start || 0
end = start || this.length
const arr = []
for (var i = start; i < end; i++) {
arr.push(this[i])
}
return arr
}
const lis2 = Array.prototype.slice.call(lis) // lis.slice()
console.log(lis2 instanceof Object, lis2 instanceof Array)
// lis2.forEach() //lis2已经是真数组,可以使用forEach进行遍历
</script>
</body>
</html>
或者使用ES6中的语法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul id="fragment_test">
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul>
<script type="text/javascript">
const lis = document.getElementsByTagName('li') // lis是伪数组(是一个特别的对象, 具有length和数值下标属性)
// 判断lis是否是对象,是否是真数组
console.log(lis instanceof Object, lis instanceof Array) // true ,false
const lis2 = Array.from(lis) // lis.slice()
console.log(lis2 instanceof Object, lis2 instanceof Array) // true , true
// lis2.forEach() //lis2已经是真数组,可以使用forEach进行遍历
</script>
</body>
</html>
node.nodeType: 得到节点类型
节点类型:document(整个html文档),Element(某个标签元素),attr(标签元素的属性),text(标签元素的文本内容)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="test">test</div>
<script type="text/javascript">
const elementNode = document.getElementById('test')
const attrNode = elementNode.getAttributeNode('id')
const textNode = elementNode.firstChild
console.log(elementNode.nodeType, attrNode.nodeType, textNode.nodeType)
</script>
</body>
</html>
Object.defineProperty(obj, propertyName, {}): 给对象添加/修改属性(指定描述符)
PS:IE8以下是不支持这个语法的,vue不支持IE8的根本原因也是这个语法不支持
configurable: true/false(默认) 是否可以重新define
enumerable: true/false(默认) 是否可以枚举(for..in / keys())
value: 指定初始值
writable: true/false(默认) value是否可以修改存取(访问)描述符
get: 函数, 用来得到当前属性值
set: 函数, 用来监视当前属性值的变化
Object.defineProperty(obj, 'fullName', {
configurable: false, //是否可以重新define
enumerable: true, // 是否可以枚举(for..in / keys())
value: 'A-B', // 指定初始值
writable: false // value是否可以修改
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript">
const obj = {
firstName: 'A',
lastName: 'B'
}
// 给obj这个对象添加fullname属性,obj.fullName = 'A-B'
Object.defineProperty(obj, 'fullName', {
// 整个是属性描述符
// 数据描述符
// 访问描述符
// 当读取对象此属性值时自动调用, 将函数返回的值作为属性值, this为obj
get () {
return this.firstName + "-" + this.lastName
},
// 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this为obj
set (value) {
const names = value.split('-')
this.firstName = names[0]
this.lastName = names[1]
}
})
console.log(obj.fullName) // A-B
obj.fullName = 'C-D'
console.log(obj.firstName, obj.lastName) // C D
Object.defineProperty(obj, 'fullName2', {
configurable: false, //是否可以重新define
enumerable: true, // 是否可以枚举(for..in / keys())
value: 'A-B', // 指定初始值
writable: false // value是否可以修改
})
console.log(obj.fullName2) // A-B
obj.fullName2 = 'E-F'
console.log(obj.fullName2) // A-B
</script>
</body>
</html>
Object.keys(obj): 得到对象自身可枚举的属性名的数组
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript">
const obj = {
firstName: 'A',
lastName: 'B'
}
// 给obj这个对象添加fullname属性,obj.fullName = 'A-B'
Object.defineProperty(obj, 'fullName', {
// 整个是属性描述符
// 数据描述符
// 访问描述符
// 当读取对象此属性值时自动调用, 将函数返回的值作为属性值, this为obj
get () {
return this.firstName + "-" + this.lastName
},
// 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this为obj
set (value) {
const names = value.split('-')
this.firstName = names[0]
this.lastName = names[1]
}
})
console.log(obj.fullName) // A-B
obj.fullName = 'C-D'
console.log(obj.firstName, obj.lastName) // C D
Object.defineProperty(obj, 'fullName2', {
configurable: false, //是否可以重新define
enumerable: true, // 是否可以枚举(for..in / keys())
value: 'A-B', // 指定初始值
writable: false // value是否可以修改
})
console.log(obj.fullName2) // A-B
obj.fullName2 = 'E-F'
console.log(obj.fullName2) // A-B
const names = Object.keys(obj) // 其中fullName是不可以枚举的
console.log(names)
</script>
</body>
</html>
DocumentFragment: 文档碎片(高效批量更新多个节点)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul id="fragment_test">
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul>
<script type="text/javascript">
// document: 对应显示的页面, 包含n个elment 一旦更新document内部的某个元素界面更新(做法是遍历每个li,然后依次替换)
// documentFragment: 内存中保存n个element的容器对象(不与界面关联), 如果更新framgnet中的某个element, 界面不变
/*
<ul id="fragment_test">
<li>test1</li>
<li>test2</li>
<li>test3</li>
</ul>
*/
const ul = document.getElementById('fragment_test')
// 1. 创建fragment
const fragment = document.createDocumentFragment()
// 2. 取出ul中所有子节点取出保存到fragment
let child
while(child = ul.firstChild) { // 一个节点只能有一个父亲
fragment.appendChild(child) // 先将child从ul中移除, 添加为fragment子节点
}
// 3. 更新fragment中所有li的文本(先将伪数组变成真数组)
Array.prototype.slice.call(fragment.childNodes).forEach(node => {
if (node.nodeType===1) { // 元素节点 <li>
node.textContent = 'fragment'
}
})
// 4. 将fragment插入ul
ul.appendChild(fragment)
</script>
</body>
</html>
obj.hasOwnProperty(prop): 判断prop是否是obj自身的属性(原型链上的是不属于自身的属性)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript">
const obj = {
firstName: 'A',
lastName: 'B'
}
// 给obj这个对象添加fullname属性,obj.fullName = 'A-B'
Object.defineProperty(obj, 'fullName', {
get () {
return this.firstName + "-" + this.lastName
},
// 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this为obj
set (value) {
const names = value.split('-')
this.firstName = names[0]
this.lastName = names[1]
}
})
console.log(obj.hasOwnProperty('fullName'), obj.hasOwnProperty('toString')) // true false
</script>
</body>
</html>
Vue 源码分析—数据代理
数据代理: 通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)
vue 数据代理: 通过 vm 对象来代理 data 对象中所有属性的操作
好处: 更方便的操作 data 中的数据
基本实现流程
- 通过 Object.defineProperty()给 vm 添加与 data 对象的属性对应的属性描述符
- 所有添加的属性都包含 getter/setter
- getter/setter 内部去操作 data 中对应的属性数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>02_数据代理_vue</title>
</head>
<body>
<div id="test"></div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
const vm = new Vue({
el: "#test",
data: {
name: '张三'
}
})
console.log(vm)
console.log(vm.name) // 读取的是data中的name, vm代理对data的读操作
vm.name = '李四' // 数据保存到data中的name上, vm代理对data的写操作
console.log(vm.name, vm._data.name)
</script>
</body>
</html>
接下来我们来分析数据代理的过程是怎么样的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>数据代理</title>
</head>
<body>
<!--
1. vue数据代理: data对象的所有属性的操作(读/写)由vm对象来代理操作
2. 好处: 通过vm对象就可以方便的操作data中的数据
3. 实现:
1). 通过Object.defineProperty(vm, key, {})给vm添加与data对象的属性对应的属性
2). 所有添加的属性都包含get/set方法
3). 在get/set方法中去操作data中对应的属性
-->
<script type="text/javascript" src="js/mvvm/compile.js"></script>
<script type="text/javascript" src="js/mvvm/mvvm.js"></script>
<script type="text/javascript" src="js/mvvm/observer.js"></script>
<script type="text/javascript" src="js/mvvm/watcher.js"></script>
<script type="text/javascript">
const vm = new MVVM({
el: "#test",
data: {
name: '张三2'
}
})
console.log(vm.name) // 读取的是data中的name, vm代理对data的读操作
vm.name = '李四2' // 数据保存到data中的name上, vm代理对data的写操作
console.log(vm.name, vm._data.name)
</script>
</body>
</html>
下面代码是模拟vue源码中的vue构造函数的代码片段,这片代码就是数据代理的整个流程
// 相关于Vue的构造函数
function MVVM(options) {
// 将选项对象保存到vm
this.$options = options;
// 将data对象保存到vm和data变量中
var data = this._data = this.$options.data;
//将vm保存在me变量中
var me = this;
// 遍历data中所有属性
Object.keys(data).forEach(function (key) { // 属性名: name
// 对指定属性实现代理
me._proxy(key);
});
// 对data进行监视
observe(data, this);
// 创建一个用来编译模板的compile对象
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
$watch: function (key, cb, options) {
new Watcher(this, key, cb);
},
// 对指定属性实现代理
_proxy: function (key) {
// 保存vm
var me = this;
// 给vm添加指定属性名的属性(使用属性描述)
Object.defineProperty(me, key, {
configurable: false, // 不能再重新定义
enumerable: true, // 可以枚举
// 当通过vm.name读取属性值时自动调用
get: function proxyGetter() {
// 读取data中对应属性值返回(实现代理读操作)
return me._data[key];
},
// 当通过vm.name = 'xxx'时自动调用
set: function proxySetter(newVal) {
// 将最新的值保存到data中对应的属性上(实现代理写操作)
me._data[key] = newVal;
}
});
}
};
Vue 源码分析—模板解析
模板解析的基本流程
将 el 的所有子节点取出, 添加到一个新建的文档 fragment 对象中
对 fragment 中的所有层次子节点递归进行编译解析处理
- 对大括号表达式文本节点进行解析
- 对元素节点的指令属性进行解析 :事件指令解析 ,一般指令解析
将解析后的 fragment 添加到 el 中显示
第一步:在MVVM构造函数中创建compile实例对象,将挂在的el元素和vm对象传递给compile构造函数
第二步:在compile构造函数中判断el存在不存在后,取出el下所有的子节点,存放到fragment对象中
第三步:编译fragment中所有层次的子节点
- 第一步:将fragment传递给compileElement()方法,compileElement()方法内得到所有子节点继续遍历,判断是那种类型的节点
- 第二步:如果是元素节点(如果是,走编译节点的指令属性的方法)还是大括号表达式的文本节点(走编译大括号表达式的的方法)
第四步:将fragment添加到el中
模板解析(1): 大括号表达式解析
根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1 name
从 data 中取出表达式对应的属性值
将属性值设置为文本节点的 textContent
第一步:调用compileText()方法,然后再调用compileUtil对象下的text方法,然后再去调用compileUtil对象下的bind方法(其实就是解析v-text这个指令)
compileText: function (node, exp) {
// 调用编译工具对象解析
compileUtil.text(node, this.$vm, exp);
},
// 解析: v-text/{{}}
text: function (node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
第二步:拼接方法名,赋给变量updaterFn,然后执行这个updaterFn方法(其实就是执行了updater对象下的textUpdater方法)并且将_getVMVal方法返回的数据传给这个方法
// 得到表达式对应的value
_getVMVal: function (vm, exp) {
var val = vm._data;
exp = exp.split('.');
exp.forEach(function (k) {
val = val[k];
});
return val;
},
// 真正用于解析指令的方法
bind: function (node, vm, exp, dir) {
/*实现初始化显示*/
// 根据指令名(text)得到对应的更新节点函数
var updaterFn = updater[dir + 'Updater'];
// 如果存在调用来更新节点
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 创建表达式对应的watcher对象
new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/
// 当对应的属性值发生了变化时, 自动调用, 更新对应的节点
updaterFn && updaterFn(node, value, oldValue);
});
},
第三步:更新这个文本节点的内容
// 更新节点的textContent
textUpdater: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
第四步: