1. MVVM
angular - 脏值检测
vue - 数据劫持+发布订阅模式(不兼容低版本:因为其依赖于Object.defineProperty)
总体流程图:
2. Object.defineProperty()
1.1 概念
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。定义的这个属性具有使用 Object.defineProperty()
为其附上的特性。
语法:
1 | Object.defineProperty(obj, prop, descriptor) |
obj
:要在其上定义属性的对象。
prop
:要定义或修改的属性的名称。
descriptor
:将被定义或修改的属性描述符。
示例:
1 | var obj = { age: 18 } |
1 | > obj |
但是当我们使用 delete obj.school;
是无法删除属性的,为了实现删除 obj
的 school
属性,我们需要去使用属性修饰符:
1 | let obj = {}; |
但是不是使用 Object.defineProperty()
方法定义的对象属性,可以不受限制任意读写,如:
1 | > obj.age = 19; |
1.2 属性修饰符
在上面的代码中,value
、configurable
都属于属性修饰符,使用 Object.defineProperty
时,我们要对每一个值都独立配置这些属性修饰符。
数据描述符和存取描述符均具有以下可选键值:
configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符
才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
enumerable
当且仅当该属性的enumerable
为true
时,该属性才能够出现在对象的枚举属性中。默认为 false。(默认不可使用 for..in
循环)
数据描述符同时具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
。
writable
当且仅当该属性的writable
为true
时,value
才能被赋值运算符改变。默认为 false。
存取描述符同时具有以下可选键值:
1.3 get()与set()
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined
。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。
默认为 undefined
。
存在
get()
时,不能存在value
属性
示例:
1 | let obj = {}; |
1 | > obj.name |
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined
。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
默认为 [undefined
]。
示例:
1 | let obj = {}; |
1 | > obj.name = "xiaoming" |
3. 数据劫持
在使用vue时,我们通常将这样定义一个vm实例:
1 | let vm = new Vue({ |
实际上,Vue在其内部代码中进行了一些操作:
- 将所有vm实例的配置项都转入到变量
$options
中 - 将配置项
data
中的数据进行劫持,存放到vm实例上的_data
变量中
那么进行数据劫持的这一步就是为了将用户由 data
传入的数据使用 Object.defineProperty()
方法为其每一项数据挂载一个 get()
和 set()
方法,同时如果 data
传入的某一项数据也是一个对象,那么也要在这个对象上面挂载 get()
和 set()
方法。
我们来实现Mvvm对象:
1 | function Mvvm(option = {}) { |
实例化一个vm对象:
1 | let vm = new Vue({ |
可以看出其数据上都挂载了一个 get()
方法和 set()
方法:
4. 数据代理
在Vue中,我们通过 data
添加的数据不仅挂载到了vm实例的 _data
变量中,同时还挂载到了vm实例本身上,并且在我们正常的使用过程中,更多是去调用vm实例本身来获取数据,而并非 _data
,这时候我们就需要通过数据代理,将 _data
中的数据代理到vm实例上。
我们新增原有的核心代码:
1 | function Mvvm(option = {}) { |
实现了Vue的两个特点:
不能新增不存在的属性,因为新增的属性没有get和set
深度相应,每次赋予一个新对象时会给这个新对象增加数据劫持
5. 模板编译
在Vue中,我们在文档节点中使用 `{{}}` 来将vm中的数据渲染到文档中,这就需要有一个模板编译方法来处理文档节点中的文本,来解析并且读取数据
新增一个Compile对象来执行编译,其包含两个参数,一个el为MVVM模式下的文档范围,vm为MVVM实例:
1 | function Compile(el, vm) { |
在核心代码中启用:
1 | function Mvvm(options = {}) { |
6. 数据更新
在Vue中,当vm实例上挂载的数据发生更新时,视图也会随之刷新,他们之间存在着发布订阅关系。
6.1 发布订阅模式
我们再模拟Vue数据更新机制的时候,需要设计一个发布者的构造函数(Dep)和订阅者的构造函数(Watcher)。
发布者内部存放着一个订阅者队列 subArr
,同时其原型上挂载了一个 addSub()
方法用来向订阅者队列中添加订阅者,还有一个 carry()
方法,执行该方法后,会遍历订阅者队列,执行每个订阅者身上挂载的 update()
方法。
每个订阅者内部都传入了一个 fn
,是一个方法函数。同时其原型上挂载了一个 update()
方法,在其方法内部执行了实例化订阅者时传入的方法函数 fn
。
当发布者发布事件时,只需要调用挂载在其身上的 carry()
方法,就可以将所有订阅者的 update()
方法执行。
发布订阅模式的构造如下:
1 | // 1. 构造发布者 |
6.2 模拟Vue中的发布订阅模式
在Vue中创建一个发布订阅机制我们需要考虑以下几个问题:
- 在哪里创建订阅者 (实例化一个Watcher对象)
- 在哪里创建发布者 (实例化一个Dep对象)
- 在哪里添加订阅 (执行发布者的
addSub()
方法) - 在哪里发布事件 (执行发布者的
carry()
方法)
每一个渲染出的文本节点对应一个订阅者,一旦发生了数据更新,所有的订阅者的update方法都会被执行,也就是说所有需要解析的文本节点都会被渲染。
Vue数据更新机制的订阅者是 Compile
编译器,当数据发生了变更时,编译器需要对模板重新编译渲染。在编译器中,执行了模板替换的方法语句是 node.textContent = text.replace(/\{\{(.*)\}\}/, val);
,那么我们再创建订阅者时,传入其内部的方法就是这条语句:
1 | function Compile(el, vm) { |
这样就达成了一个目的:在页面加载完成后实例化 Compile
时,在执行模板编译的过程中,为每个文本节点对象都渲染出一个订阅者实例,去观察其对应的数据是否变动,如果数据变动,就触发当前文本节点的重新渲染。
我们先不讨论实例化的订阅者何时被调用挂载于其身上的 update()
方法,先假设一旦数据发生了变化,传入订阅者实例的方法就会被执行,即 node.textContent = text.replace(/\{\{(.*)\}\}/, val)
被执行。但我们会发现,内部参数 val
仍是一个旧值(因为Compile只执行一次,在其内部的变量val肯定是不会动态变更的)。我们在重新渲染文本节点时,需要去将旧文本替换成新文本。
那么问题就是如何获取更新后的新值?
我们需要改动代码,在实例化订阅者对象的时候传入三个值,vm
为Mvvm实例,RegExp.$1
是当前文本节点中匹配的原始待编译字符(也就是 `{{}}`
包裹的内容),第三个参数时传入的执行函数:
1 | - new Watcher(function () { |
那么传入的这些参数在构造对象 Watcher
中如何使用?
首先我们要接受传入的参数
1 | function Watcher(vm, exp, fn) { |
这时候就可以考虑如何将订阅者添加到发布者的 subArr
中了。
首先我们要清楚实例化发布者的位置应该是在 Observe
中,因为其负责了构建每一个数据。所以我们可以去尝试通过访问数据对象上的 get()
方法,来将订阅者添加到其数据上的发布者。
1 | function Watcher(vm, exp, fn) { |
其中 Dep.target
是为了存放当前的订阅者对象,在数据的 get()
方法中将订阅者添加到发布者的 subArr
中。 forEach
是为了深度遍历,因为如果当前的数据值是一个对象,那么需要去深度查找这个值中对象的 get()
和 set()
方法。
同样,当数据被重新赋值时,会调用其 set()
方法,所以最终我们在 Observe
中为数据添加 get()
和 set()
方法的代码中要加上如下额外步骤:
1 | function Observe(data) { |
但是正如最初我们提到的,执行订阅者的 update()
方法去执行传入订阅者内部的函数时,需要获取新值 newVal
,那么我们需要去更改一下 update()
方法,由于其执行前已经对数据进行了重新赋值,所以只要查找该订阅者对应的值就可以获取 newVal
了。
1 | Watcher.prototype.update = function () { |
7. 数据的双向绑定
为了实现数据的双向绑定,要点在编译模板时,去审查每个Document节点元素身上有没有挂载 v-model
属性,如果有,就获取其 value
,为其添加一个订阅,来当数据更新时连带更新输入框的内容,同时添加一个监听方法,当在其内部输入时,触发绑定数据的 set()
方法来变更数据的值:
1 | function Compile(el, vm) { |
8. 计算属性
在Vue中,计算属性可以被缓存到vm实例上:
1 | function initComputed() { // 具有缓存功能 |