学习Vue---5.Vue源码分析
分析 vue 作为一个MVVM 框架的基本实现原理
一、预备知识
1. [].slice.call(lis): 将伪数组转换为真数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //1. [].slice.call(lis): 根据伪数组生成对应的真数组 const lis = document.getElementsByTagName('li') // lis是伪数组(是一个特别的对象, length和数值下标属性) console.log(lis instanceof Object, lis instanceof Array) // 数组的slice()截取数组中指定部分的元素, 生成一个新的数组 [1, 3, 5, 7, 9], slice(0, 3) // slice2() 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() |
2. node.nodeType: 得到节点类型
节点:document(html文件节点)、Element(元素节点)、Attribute(属性节点)、Text(文本节点)
1 2 3 4 5 | //2. node.nodeType: 得到节点类型 const elementNode = document.getElementById('test') const attrNode = elementNode.getAttributeNode('id') const textNode = elementNode.firstChild console.log(elementNode.nodeType, attrNode.nodeType, textNode.nodeType) # 1 2 3 |
3. Object.defineProperty(obj, propertyName, {}): 给对象添加属性(指定描述符)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | //3. Object.defineProperty(obj, propertyName, {}): 给对象添加属性(指定描述符) const obj = { firstName: 'A', lastName: 'B' } //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 /*Object.defineProperty(obj, 'fullName2', { configurable: true, enumerable: true, value: 'G-H', writable: true })*/ |
4. Object.keys(obj): 得到对象自身可枚举属性组成的数组
1 2 3 | //4. Object.keys(obj): 得到对象自身可枚举属性组成的数组 const names = Object.keys(obj) console.log(names) |
5. obj.hasOwnProperty(prop): 判断prop是否是obj自身的属性
1 2 | //5. obj.hasOwnProperty(prop): 判断prop是否是obj自身的属性 console.log(obj.hasOwnProperty('fullName'), obj.hasOwnProperty('toString')) // true false |
6. DocumentFragment: 文档碎片(高效批量更新多个节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /6. DocumentFragment: 文档碎片(高效批量更新多个节点) // document: 对应显示的页面, 包含n个elment 一旦更新document内部的某个元素界面更新 // documentFragment: 内存中保存n个element的容器对象(不与界面关联), 如果更新fragment中的某个element, 界面不变 // DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的document一样, // 存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。 /* < 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的文本 fragment.childNodes.forEach(node => { console.log('node', node) if (node.nodeType === 1) { // 元素节点 < li > node.textContent = 'niuxiaofu hi' } }) // 4. 将fragment插入ul ul.appendChild(fragment) console.log('ul.childNodes', ul.childNodes) console.log('ul.children', ul.children) |
二、数据代理
1.数据代理
通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)
2.vue 数据代理
data 对象的所有属性的操作(读/写)由 vm 对象来代理操作
3.好处
通过 vm 对象就可以方便的操作 data 中的数据
4.基本实现流程
- 通过 Object.defineProperty() 给 vm 添加与 data 对象的属性对应的属性描述符
- 所有添加的属性都包含 getter/setter
- getter/setter 内部去操作 data 中对应的属性数据
三、模板解析
1.模板解析的基本流程
1) 将 el 的所有子节点取出,添加到一个新建的文档 fragment 对象中
2) 对 fragment 中的所有层次子节点递归进行编译解析处理
-
对大括号表达式文本节点进行解析
-
对元素节点的指令属性进行解析
-
事件指令解析
-
一般指令解析
-
3) 将解析后的 fragment 添加到 el 中显示
2.大括号表达式解析
- 根据正则对象得到匹配出的表达式字符串:子匹配/RegExp.$1 name
- 从 data 中取出表达式对应的属性值
- 将属性值设置为文本节点的 textContent
3.事件指令解析
- 从指令名中取出事件名
- 根据指令的值(表达式)从 methods 中得到对应的事件处理函数对象
- 给当前元素节点绑定指定事件名和回调函数的 dom 事件监听
- 指令解析完后,移除此指令属性
4.一般指令解析
1) 得到指令名和指令值(表达式) text/html/class msg/myClass
2) 从 data 中根据表达式得到对应的值
3) 根据指令名确定需要操作元素节点的什么属性
- v-text---textContent 属性
- v-html---innerHTML 属性
- v-class--className 属性
4) 将得到的表达式的值设置到对应的属性上
5) 移除元素的指令属性
四、数据绑定
1.数据绑定
一旦更新了 data 中的某个属性数据,所有界面上直接使用或间接使用了此属性的节点都会更新。
2.数据劫持
- 数据劫持是 vue 中用来实现数据绑定的一种技术
- 基本思想:通过 defineProperty() 来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面
3.四个重要对象
实现数据的绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer,用来监听所有属性。
如果属性发生变化了,就需要告诉订阅者 Watcher 看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅 Watcher 之间进行统一管理。
接着,我们还需要有一个指令解析器 Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数。此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
3.1 Observer(监听器)
- 用来对 data 所有属性数据进行劫持的构造函数
- 给 data 中所有属性重新定义属性描述(get/set)
- 为 data 中的每个属性创建对应的 dep 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | function Observer(data) { // 保存data对象 this.data = data; // 走起,开始对data监视 this.walk(data); } Observer.prototype = { walk: function(data) { //将observer保存在me变量中 var me = this; // 遍历data中所有属性 Object.keys(data).forEach(function(key) { // 针对指定属性进行处理 me.convert(key, data[key]); }); }, convert: function(key, val) { // 对指定属性实现响应式数据绑定 this.defineReactive(this.data, key, val); }, defineReactive: function(data, key, val) { // 创建与当前属性对应的dep对象 var dep = new Dep(); // 间接递归调用实现对data中所有层次属性的劫持 var childObj = observe(val); // 给data重新定义属性(添加set/get) Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, // 不能再define get: function() { // 建立dep与watcher的关系 if (Dep.target) { dep.depend(); } // 返回属性值 return val; }, set: function(newVal) { // 监视key属性的变化,目的是更新界面 if (newVal === val) { return; } val = newVal; // 新的值是object的话,进行监听 childObj = observe(newVal); // 通知dep dep.notify(); } }); } }; function observe(value, vm) { // value必须是对象, 因为监视的是对象内部的属性 if (!value || typeof value !== 'object') { return; } // 创建一个对应的观察者对象 return new Observer(value); }; var uid = 0; function Dep() { // 标识属性 this.id = uid++; // 相关的所有watcher的数组 this.subs = []; } Dep.prototype = { // 添加watcher到dep中 addSub: function(sub) { this.subs.push(sub); }, // 建立dep与watcher之间的关系 depend: function() { Dep.target.addDep(this); }, removeSub: function(sub) { var index = this.subs.indexOf(sub); if (index != -1) { this.subs.splice(index, 1); } }, notify: function() { // 通知所有相关的watcher(订阅者) this.subs.forEach(function(sub) { sub.update(); }); } }; Dep.target = null; |
3.2 Dep(Depend)
- data 中的每个属性(所有层次)都对应一个 dep 对象
- 创建的时机:
- 在初始化 define data 中各个属性时创建对应的 dep 对象
- 在 data 中的某个属性值被设置为新的对象时
- 对象的结构:
1234567891011
function Dep() {
// 标识属性
this.id = uid++; // 每个dep都有一个唯一的id
// 相关的所有watcher的数组
this.subs = []; //包含n个对应watcher的数组(subscribes的简写)
}
{
this.id = uid++,
this.subs = []
}
- subs 属性说明
- 当 watcher 被创建时,内部将当前 watcher 对象添加到对应的 dep 对象的subs 中
- 当此 data 属性的值发生改变时,subs 中所有的 watcher 都会收到更新的通知,从而最终更新对应的界面
3.3 Compiler(指令解析器)
-
用来解析模板页面的对象的构造函数(一个实例)
-
利用 compile 对象解析模板页面
-
每解析一个表达式(非事件指令,如{{}}或v-text,v-html)都会创建一个对应的 watcher 对象,并建立 watcher 与 dep 的关系
-
complie 与 watcher 关系:一对多的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | function Compile(el, vm) { // 保存vm this.$vm = vm; // 保存el元素 this.$el = this.isElementNode(el) ? el : document.querySelector(el); // 如果el元素存在 if (this.$el) { // 1. 取出el中所有子节点, 封装在一个framgment对象中 this.$fragment = this.node2Fragment(this.$el); // 2. 编译fragment中所有层次子节点 this.init(); // 3. 将fragment添加到el中 this.$el.appendChild(this.$fragment); } } Compile.prototype = { node2Fragment: function (el) { var fragment = document.createDocumentFragment(), child; // 将原生节点拷贝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, init: function () { // 编译fragment this.compileElement(this.$fragment); }, compileElement: function (el) { // 得到所有子节点 var childNodes = el.childNodes, // 保存compile对象 me = this; // 遍历所有子节点 [].slice.call(childNodes).forEach(function (node) { // 得到节点的文本内容 var text = node.textContent; // 正则对象(匹配大括号表达式) var reg = /\{\{(.*)\}\}/; // {{name}} // 如果是元素节点 if (me.isElementNode(node)) { // 编译元素节点的指令属性 me.compile(node); // 如果是一个大括号表达式格式的文本节点 } else if (me.isTextNode(node) && reg.test(text)) { // 编译大括号表达式格式的文本节点 me.compileText(node, RegExp.$1); // RegExp.$1: 表达式 name } // 如果子节点还有子节点 if (node.childNodes && node.childNodes.length) { // 递归调用实现所有层次节点的编译 me.compileElement(node); } }); }, compile: function (node) { // 得到所有标签属性节点 var nodeAttrs = node.attributes, me = this; // 遍历所有属性 [].slice.call(nodeAttrs).forEach(function (attr) { // 得到属性名: v-on:click var attrName = attr.name; // 判断是否是指令属性 if (me.isDirective(attrName)) { // 得到表达式(属性值): test var exp = attr.value; // 得到指令名: on:click var dir = attrName.substring(2); // 事件指令 if (me.isEventDirective(dir)) { // 解析事件指令 compileUtil.eventHandler(node, me.$vm, exp, dir); // 普通指令 } else { // 解析普通指令(v-model和v-class) compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); } // 移除指令属性 node.removeAttribute(attrName); } }); }, compileText: function (node, exp) { // 调用编译工具对象解析 compileUtil.text(node, this.$vm, exp); }, 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; } }; // 指令处理集合 var compileUtil = { // 解析: v-text/{{}} text: function (node, vm, exp) { this.bind(node, vm, exp, 'text'); }, // 解析: v-html html: function (node, vm, exp) { this.bind(node, vm, exp, 'html'); }, // 解析: v-model model: function (node, vm, exp) { // 实现数据的初始化显示和创建对应watcher this.bind(node, vm, exp, 'model'); var me = this, // 得到表达式的值 val = this._getVMVal(vm, exp); // 双向数据绑定 // 1.给节点绑定input事件监听(输入改变时触发) node.addEventListener('input', function (e) { // 得到输入的最新值 var newValue = e.target.value; // 如果没有变化直接结束 if (val === newValue) { return; } // 2.将最新的value保存给表达式对应的属性 me._setVMVal(vm, exp, newValue); // 保存最新的值 val = newValue; }); }, // 解析: v-class class: function (node, vm, exp) { this.bind(node, vm, exp, 'class'); }, // 真正用于解析指令的方法 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); }); }, // 事件处理 eventHandler: function (node, vm, exp, dir) { // 得到事件名/类型: click var eventType = dir.split(':')[1], // 根据表达式得到事件处理函数(从methods中): test(){} fn = vm.$options.methods && vm.$options.methods[exp]; // 如果都存在 if (eventType && fn) { // 绑定指定事件名和回调函数的DOM事件监听, 将回调函数中的this强制绑定为vm node.addEventListener(eventType, fn.bind(vm), false); } }, // 得到表达式对应的value _getVMVal: function (vm, exp) { var val = vm._data; exp = exp.split('.'); exp.forEach(function (k) { val = val[k]; }); return val; }, _setVMVal: function (vm, exp, value) { var val = vm._data; exp = exp.split('.'); exp.forEach(function (k, i) { // 非最后一个key,更新val的值 if (i < exp.length - 1) { val = val[k]; } else { val[k] = value; // 触发data的set(又进入了数据绑定流程) } }); } }; // 包含多个用于更新节点方法的对象 var updater = { // 更新节点的textContent textUpdater: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }, // 更新节点的innerHTML htmlUpdater: function (node, value) { node.innerHTML = typeof value == 'undefined' ? '' : value; }, // 更新节点的className classUpdater: function (node, value, oldValue) { var className = node.className; node.className = className + (className?' ':'') + value; }, // 更新节点的value modelUpdater: function (node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value; } }; |
3.4 Watcher(订阅者)
- 模板中每个非事件指令或表达式都对应一个 watcher 对象(与模板中表达式[不包括事件指令]一一对应)
- 监视当前表达式数据的变化
- 创建的时机:初始化的解析大括号表达式/一般指令时创建
- 对象的组成
1 2 3 4 5 6 7 8 9 | function Watcher(vm, exp, cb){ this.vm = vm; // vm 对象 this.exp = exp; // 对应指令的表达式 this.cb = cb; // 当表达式所对应的数据发生改变的回调函数 this.value = this.get(); // 表达式当前的值 this.depIds = {}; // 表达式中各级属性所对应的dep对象的集合对象 // 属性名为dep的id, 属性值为dep } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | function Watcher(vm, exp, cb) { this.cb = cb; // callback this.vm = vm; this.exp = exp; // 表达式 this.depIds = {}; // {0: d0, 1: d1, 2: d2}包含所有相关的dep的容器对象 this.value = this.get(); // 保存表达式的初始值 } Watcher.prototype = { update: function () { this.run(); }, run: function () { // 得到最新的值 var value = this.get(); // 得到旧值 var oldVal = this.value; // 如果不相同 if (value !== oldVal) { this.value = value; // 调用回调函数更新对应的界面 this.cb.call(this.vm, value, oldVal); } }, addDep: function (dep) { // 判断dep与watcher的关系是否已经建立 if (!this.depIds.hasOwnProperty(dep.id)) { // 将watcher添加到dep,用于更新 dep.addSub(this); // 将dep添加到watcher,用于防止重复建立关系 this.depIds[dep.id] = dep; } }, get: function () { // 给dep指定当前的watcher Dep.target = this; // 获取当前表达式的值, 内部会调用data属性的get(建立dep与watcher的关系) var value = this.getVMVal(); // 去除dep中指定的当前watcher Dep.target = null; return value; }, // 得到表达式的值 getVMVal: function () { var exp = this.exp.split('.'); var val = this.vm._data; // data对象 exp.forEach(function (k) { val = val[k]; // 触发表达式中有的属性的get(建立属性对应的dep与当前watcher的关系) }); return val; } }; /* const obj1 = {id: 1} const obj12 = {id: 2} const obj13 = {id: 3} const obj14 = {id: 4} const obj2 = {} const obj22 = {} const obj23 = {} // 双向1对1 // obj1.o2 = obj2 // obj2.o1 = obj1 // obj1: 1:n obj1.o2s = [obj2, obj22, obj23] // obj2: 1:n obj2.o1s = { 1: obj1, 2: obj12, 3: obj13 } */ |
总结:dep 与 watcher 的关系 --> 多对多
vm.name = 'abc'-->data中的name属性值变化-->name的set()调用-->dep-->相关的所有watcher-->cb()-->updater
-
data 中的一个属性对应一个 dep,一个 dep 中可能包含多个 watcher(模板中有几个表达式使用到了同一个属性)【{{name}}/v-text="name"】
-
模板中一个非事件表达式对应一个 watcher,一个 watcher 中可能包含多个dep【多层表达式:a.b.c】
-
数据绑定使用到2个核心技术
- defineProperty()
- 消息订阅与发布
4.MVVM原理图分析
4.1 初始化阶段
MVVM 中会创建 Observer(用来劫持/监听所有属性)和 Compile(解析指令/大括号表达式),
Observer:要劫持就需要对应的set()方法,所以在observer中为每一个属性创建了一个 dep 对象(与 data 中的属性一一对应)
Compile:(做了两件事)
- 目的是初始化视图(显示界面),调用 updater(有很多更新节点的方法)
- 为表达式创建对应的 Watcher ,同时指定了更新节点的函数
Watcher 和 Dep 建立关系:
- watcher 放到 dep 中(添加订阅者),dep 中有一个 subs,是用来保存 n 个 watcher 的数组容器
- dep 放到 watcher 中,watcher 中的 depIds 是用来保存 n 个 dep 的对象容器。为了判断 dep 与 watcher 的关系是否已经建立(防止重复的建立关系)
以上都是初始化阶段会经历的过程
4.2 更新阶段
vm.name = 'Tom' 导致 data 中的数据变化,会触发监视 data 属性的 observer 中的 set() 方法,然会它又会通知 dep,dep 会去通知它保存的所有相关的 watcher,watcher 收到信息后,其回调函数会去调用 updater 更新界面
如下图所示:(黑线是初始化阶段,红线是更新阶段)
五、双向数据绑定
- 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
- 双向数据绑定的实现流程:
- 在解析 v-model 指令时,给当前元素添加 input 监听
- 当 input 的 value 发生改变时,将最新的值赋值给当前表达式所对应的 data 属性
参考链接:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现