写vue也有一段时间了,对vue的底层原理虽然有一些了解,这里总结一下。

vue.js中有两个核心功能:响应式数据绑定,组件系统。主流的mvc框架都实现了单向数据绑定,而双向绑定无非是在单向绑定基础上给可输入元素添加了change事件,从而动态地修改model和view。

1. MVC,MVP,MVVM

1.1 MVC

MVC模式将软件分为下面三个部分

1.视图(View):用户界面
2.控制器(Controller):业务逻辑
3.模型(Model):数据保存

MVC各个部分之间通信的方式如下:

1.视图传送指令到控制器
2.控制器完成业务逻辑后要求模型改变状态
3.模型将新的数据发送到视图,用户得到反馈

示意图如下:

以上所有通信都是单向的。接受用户指令的时候,MVC有两种方式,一种是通过视图接受指令,然后传递给控制器。另一种是用户直接给控制器发送指令。

实际使用中可能更加灵活,下面是以Backbone.js为例说明。

1.用户可以向视图(View)发送指令(DOM事件),再由View直接要求Model改变状态。
2.用户也可以向Controller发送指令(改变URL触发hashChange事件),再由Controller发送给View。
3.Controller很薄只起到路由作用而View非常厚业务逻辑都放在View。所以Backbone索性取消了Controller,只保留了Router(路由器)

MVC模式体现了“关注点分离”这一设计原则,将一个人机交互应用涉及到的功能分为三部分,Model对应应用状态和业务功能的封装,可以将它理解为同时包含数据和行为的领域模型,Model接受Controller的请求并完成相应的业务处理,在应用状态改变的时候可以向View发出通知。View实现可视化界面的呈现和用户的交互操作,VIew层可以直接调用Model查询状态,Model也可以在自己状态发生变化的时候主动通知VIew。Controller是Model和View之间的连接器,用于控制应用程序的流程。View捕获用户交互操作后直接发送给Controller,完成相应的UI逻辑,如果涉及业务功能调用Controller会调用Model,修改Model状态。Controller也可以主动控制原View或者创建新的View对用户交互予以响应。

1.2 MVP

MVP模式将Controller改名为Presenter,同时改变了通信方向,如下图:

1.各部分之间的通信都是双向的。
2.视图(View)和模型(Model)不发生联系,都是通过表现Presenter)传递
3.View非常薄,不部署任何业务逻辑,称为被动视图(Passive View),即没有任何主动性,而Presenter非常厚,所有逻辑都这里

MVP适用于事件驱动的应用架构中,如asp.net web form,windows forms应用。

1.3 MVVM

MVVM模式将Presenter层替换为ViewModel,其他与MVP模式基本一致,示意图如下:

它和MVP的区别是,采用双向绑定视图层(View)的变动,自动反映在ViewModel,反之亦然。Angular和Vue,React采用这种方式。

MVVM的提出源于WPF,主要是用于分离应用界面层和业务逻辑层,WPF,Siverlight都基于数据驱动开发。

MVVM模式中,一个ViewModel和一个View匹配,完全和View绑定,所有View中的修改变化,都会更新到ViewModel中,同时VewModel的任何变化都会同步到View上显示。之所以自动同步是ViewModel中的属性都实现了observable这样的接口,也就是说当使用属性的set方法,会同时触发属性修改的事件,使绑定的UI自动刷新。

2. 访问器属性

访问器属性是一种特殊的属性,不能再对象中直接定义访问器属性,必须通过defineProperty()方法定义访问器属

Object.defineProperty()方法直接在对象上定义一个新属性,或修改一个对象现有的属性,并返回这个对象。该方法允许精确添加或者修改对象的属性。通过赋值操作(例如object.name = xxx)添加的普通属性是可枚举的,可枚举(for ... in或Object.keys方法),这些属性的值可以被修改或删除。默认情况下,使用Object.defineProperty()添加的属性值是不可修改的。 方法的原型如下:

Object.defineProperty(obj, prop, descriptor)
obj:要在其上定义属性的对象
prop: 要定义或者修改的属性名字
descriptor: 将被定义或修改的属性描述符

对象里目前存在的属性描述符可以归纳为两类:数据描述符存取描述符。数据描述符是一个具有值的属性,configurable为true时,这个值可是可写的,否则不可写。存取描述符是由getter,setter函数描述的属性。描述符必须是这两种类型(数据描述符读取描述符)之一,不可能同时是这两者。

数据描述符和存取描述符必须有下面可选键值:

1. configurable:当且仅当改属性的configurable为true的时候,该属性描述符才能被修改,同时该属性也能从对应的对象上被删除。默认为false。
2. enumerable:当且仅当改属性的enmerable为true的时候,改属性才能出现在对象的枚举属性中,默认为false。

数据描述符同时具有以下可选键值:

1. value:该属性对应的值。可以是任何JavaScript有效值,数值,对象,函数等,默认为undefined。
2. writable:当且仅当改属性的writable为true时,value才能被赋值运算符改变,就是用“=”赋值。默认为false。

存取描述符同时具有以下可选键值:

1. get:一个给属性提供getter的方法,如果没有getter则为undefined。访问这个属性的时候,该方法会被执行,没有参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为undefined。
2. set:一个给属性提供setter的方法,如果没有setter则为undefined。当属性值被修改的时候,触发这个setter方法。这个方法接受唯一参数,即改属性新的参数值。默认为undefined。

如果一个描述符没有value,writable,get,set任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,会产生一个异常。

这些选项不一定是自身属性,如果是继承来的也要考虑。为了确认保留这些默认值是自己定义的,可能要在这之前冻结Object.property,明确指定所有的选项,或者通过Object.create(null)将__proto__属性指向null,要不然使用起来就有些混乱。下面使用Object.create(null)方法来给对象obj定义一个“干净”的属性。

  1. // 使用__prop__定义
  2. var obj = {}
  3. var descriptor = Object.create(null);
  4. // 默认没有enumberable,configurable,writable
  5. descriptor.value = 'static';
  6. Object.defineProperty(obj, 'key', descriptor);
  7. console.log(obj); 

代码输出结果如下:

1. 定义一个空对象obj
2. 定义一个属性描述符descriptor,使用Object.Create方法继承null对象,这样没有继承属性
3. 设置数据描述符value,值为‘static’
4. 使用Object.defineProperty方法给obj对象定义key属性,使用descriptor描述符,描述符中只有一个数据描述符value,其他的都是默认值

上面的语句和下面的效果是一样的,就是使用Object.defineProperty方法给obj对象设置一个key属性,属性的属性描述符都是默认值:

  1. // 显示定义
  2. var obj = {}
  3. Object.defineProperty(obj, 'key', {
  4. enumerable: false,
  5. configurable: false,
  6. writable: false,
  7. value: "statics"
  8. });
  9. console.log(obj); 

输出如下:

还可以循环使用同一对象最为对象描述符使用,代码如下:

  1. // 循环使用统一对象
  2. function withValue (value) {
  3. var d = withValue.d || (
  4. withValue.d = {
  5. enumerable: false,
  6. writable: false,
  7. configurable: false,
  8. value: null
  9. }
  10. );
  11. d.value = value;
  12. return d;
  13. }
  14.  
  15. var obj = {}
  16. Object.defineProperty(obj, 'key', withValue('static'));
  17. console.log(obj); 

输出结果如下:

如果对象中不存在指定的属性,Object.defineProperty()就创建这个属性。当描述符中省略某些字段时,这些字段将使用它们的默认值。拥有布尔值的字段的默认值都是false,value,get,set字段默认值是undefined。一个没有get,set,value,writable定义的属性被称为“通用的”,并被键入为一个数据描述符。

  1. // 在对象中添加一个属性与数据描述符的实例, 对象o拥有了属性a,值为37
  2. var obj = {};
  3. Object.defineProperty(obj, "a", {
  4. value: 37,
  5. writable: false,
  6. enumerable: false,
  7. configurable: true
  8. });
  9. console.log(obj); 

输出结果如下:

  1. // 在对象中添加一个属性与数据描述符的实例, 对象o拥有了属性a,值为37
  2. var obj = {};
  3. Object.defineProperty(obj, "a", {
  4. value: 37,
  5. writable: false,
  6. enumerable: false,
  7. configurable: true
  8. });
  9. console.log(obj);

输出结果如下:

  1. // 在对象中添加一个属性与存取描述符,对象o拥有了属性b,值为38
  2. var bValue;
  3. Object.defineProperty(obj, 'b', {
  4. get: function () {
  5. return bValue;
  6. },
  7. set: function (newValue) {
  8. bValue = newValue;
  9. },
  10. enumerable: true,
  11. configurable: true
  12. });
  13. // o.b的值现在总是与bValue相同,除非重新定义o.b
  14. bValue = 200;
  15. console.log(obj.b); 

输出结果如下:

  1. // 数据描述符和存取描述符不能混合使用,否则会报错
  2. var obj = {};
  3. Object.defineProperty(obj, 'confict', {
  4. value: '0x9f91102',
  5. get: function () {
  6. return 0xdeadbeef;
  7. }
  8. });
  9. // 报错:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object> 

2.1 修改属性

如果属性已经存在,Object.defineProperty()方法将尝试根据描述符中的值以及对象当前的配置来修改这个属性。如果旧对象描述符configurable为false,则属性被认为是“不可配置的”,并且没有属性可以改变(除了单向改变writable为false)。当属性不可配置时,不能再数据和访问器属性类型之间切换。当时图改变不可配置属性(除了writable属性之外)的值时会抛出TypeError,除非当前值和心智相同。

2.2 Writable属性

当writable属性设置为false时,改属性称为“不可写”。它不能被重新分配。

  1. // 创建一个新对象
  2. var o = {};
  3. Object.defineProperty(o, 'a', {
  4. value: 37,
  5. configurable: false,
  6. writable: false
  7. });
  8. console.log(o.a); // 输出37
  9. o.a = 38;
  10. console.log(o.a); // writable属性为false,o.a的值仍然是37,如果是严格模式,这里会抛错:"a" is read-only

2.3 Enumerable特性

enumerable定义了对象的属性是否可以在for...in循环和Object.keys()中枚举。

  1. var o = {};
  2. Object.defineProperty(o, 'a', {value: 1, enumerable: true});
  3. Object.defineProperty(o, 'b', {value: 2, enumerable: false});
  4. // 没有设置 enumberable属性默认值是false
  5. Object.defineProperty(o, 'c', {value: 3});
  6. // 如果使用直接赋值的方式创建对象属性,则这个属性的enumerable为true
  7. o.d = 4;
  8. console.log(Object.keys(o)); // 输出["a", "d"]
  9. console.log(o.propertyIsEnumerable('a')); // 输出true
  10. console.log(o.propertyIsEnumerable('b')); // 输出false
  11. console.log(o.propertyIsEnumerable('c')); // 输出false
  12. console.log(o.propertyIsEnumerable('d')); // 输出true 

2.4 Configurable特性

configurable特性表示对象的属性是否可以被删除,以及除writable特性外的其他特性是否可以被修改

  1. var o = {};
  2. Object.defineProperty(o, 'a', {
  3. get: function () {
  4. return 1;
  5. },
  6. configurable: false
  7. });
  8. console.log(o.a); // 输出1
  9.  
  10. Object.defineProperty(o, 'a', {configurable: true}); // Uncaught TypeError: Cannot redefine property: a
  11. Object.defineProperty(o, "a", {enumerable: true}); // Uncaught TypeError: Cannot redefine property: a
  12. Object.defineProperty(o, "a", {
  13. set: function () {
  14. }
  15. }); // Uncaught TypeError: Cannot redefine property: a
  16. Object.defineProperty(o, "a", {
  17. get: function () {
  18. }
  19. }); // Uncaught TypeError: Cannot redefine property: a
  20. delete o.a;
  21. console.log(o.a); // 对染delete语句没有报错,但是没有真正删除a属性,输出1

2.5 添加多个属性和默认值

使用点运算符和Object.defineProperty()为对象的属性赋值,数据描述符的属性默认值是不同的。

  1. var o = {};
  2. o.a = 1;
  3. // 上面使用点语法定义属性,等同于下面代码,注意writable,configurable,enumerable的默认属性为false,但是这里使用点语法是true
  4. Object.defineProperty(o, "a", {
  5. value: 1,
  6. writable: true,
  7. configurable: true,
  8. enumerable: true
  9. });
  10.  
  11. Object.defineProperty(o, "a", {value: 1});
  12. // 上面使用Object.defineProperty()定义属性,等同于下面代码
  13. Object.defineProperty(o, "a", {
  14. value: 1,
  15. writable: false,
  16. configurable: false,
  17. enumerable: false
  18. });

2.6 一般的getter和setter

下面的例子展示如何实现一个自存档对象,当设置temperture属性时,archive数组就会获取日志

  1. function Archiver () {
  2. var temperature = null;
  3. var archiver = [];
  4. Object.defineProperty(this, 'temperature', {
  5. get: function () {
  6. console.log('get!');
  7. return temperature;
  8. },
  9. set: function (value) {
  10. temperature = value;
  11. archiver.push({val: temperature});
  12. }
  13. });
  14. this.getArchive = function () {
  15. return archiver;
  16. }
  17. }
  18.  
  19. var arc = new Archiver();
  20. console.log(arc.temperature); // 输出get,但是arc.temperature是null
  21. arc.temperature = 11; // 触发archiver.push({val: temperature})
  22. arc.temperature = 13; // 触发archiver.push({val: temperature})
  23. console.log(arc.getArchive()); // 输出[{val: 11}, {val: 13}]

1. 定义一个方法类Archive,在内部有私有变量temperature,archiver,
2. 在方法内使用Object.defineProperty()方法定义属性temperature,定义存取描述符get,放回私有变量temperature,定义存取描述符set,用传递的参数给私有变量temperature赋值
3. 定义特权方法getArchive,返回私有变量temperature
4. 使用new操作符定义类Archive实例arc
5. 输出实例arc的temperature属性,调用get方法,返回私有变量temperature的值null
6. 给实例arc的temperatur属性赋值,调用set方法,传递参数11,触发archiver.push({val: 11})
7. 给实例arc的temperatur属性赋值,调用set方法,传递参数13,触发archiver.push({val: 13})
8. 输出实例arc的temperature属性,调用get方法,返回私有变量temperature的值[{val: 11}, {val: 13}]

  1. var pattern = {
  2. get: function () {
  3. return 'I alway return this string,whatever you have assigned';
  4. },
  5. set: function () {
  6. console.log('给属性myname赋值')
  7. this.myname = 'this is my name string';
  8. }
  9. }
  10. function TestDefineSetAndGet () {
  11. Object.defineProperty(this, 'myproperty', pattern);
  12. }
  13. var instance = new TestDefineSetAndGet();
  14. instance.myproperty = 'test'; // 输出 “给属性myname赋值”
  15. console.log(instance.myproperty); // 输出 “I alway return this string,whatever you have assigned”
  16. console.log(instance.myname); // 输出 “this is my name string”

1. 定义属性描述符pattern,属性描述符上有存取描述符get,返回字符串“I alway return this string,whatever you have assigned”,存取描述符set,先输出“给属性myname赋值”,给当前对象的myname属性赋值“this is my name string”
2. 定义类方法TestDefineSetAndGet,方法内部使用Object.defineProperty()给当前对象定义一个属性“ myproperty”,使用属性描述符pattern
3. 使用new操作符定义类TestDefineSetAndGet的实例instance
4. 给实例的属性myproperty赋值“test”,因为使用Object.defineProperty给对象定义属性的时候没有指定writable,这里赋值无效。在get函数里返回的是固定值。在set函数里输出“给属性myname赋值”
5. 输出实例的属性myproperty,访问get函数,返回“I alway return this string,whatever you have assigned”
6. 输出实例的属性myname,因为访问过set函数,在setg函数中给当前对象赋过值,所以myname的值为“this is my name string”

2.7 继承属性

如果访问者的属性是被继承的,它的get和set方法会在子对象的属性被访问或者修改时调用。如果这些方法用一个变量保存,会被所有对象共享。

  1. function myClass () {
  2. }
  3. var value;
  4. Object.defineProperty(myClass.prototype, 'x', {
  5. get () {
  6. return value
  7. },
  8. set (x) {
  9. value = x;
  10. }
  11. });
  12. var a = new myClass();
  13. var b = new myClass();
  14. a.x = 1;
  15. console.log(a.x); //
  16. console.log(b.x); // 

在类myClass的原型对象上定义了x属性,这个属性会被类myClass的所有实例共享。通过将值保存在另一个属性中固定,在get,set中,this指向某个被访问和修改属性的对象。

代码如下:

  1. var obj = {}
  2. Object.defineProperty(obj, 'hello', {
  3. get: function () {
  4. console.log('get方法被调用')
  5. },
  6. set: function (v) {
  7. console.log("set方法被调用了,参数是" + v)
  8. }
  9. })
  10. obj.hello; // get方法被调用
  11. obj.hello = 'abc'; // set方法被调用了,参数是abc 

可以像普通属性一样读取,设置访问器属性,访问器属性比较特殊,读取或设置访问器属性的值其实是调用内部get,set方法来操作属性。为属性赋值,就是调用set方法并使用参数给属性赋值。get,set方法内部的this指针指向obj,这意味着get和set方法可以操作对象内部的值。另外,访问器属性会覆盖同名的普通属性,因为访问器属性优先访问,同名的属性会被忽略。

  1. function myClass () {
  2. }
  3. Object.defineProperty(myClass.prototype, 'x', {
  4. get () {
  5. return this.stored_x;
  6. },
  7. set (x) {
  8. this.stored_x = x;
  9. }
  10. });
  11. var a = new myClass();
  12. var b = new myClass();
  13. a.x = 1;
  14. console.log(a.x); //
  15. console.log(b.x); // undefined

不像访问者属性,值属性始终在对象自身上设置,而不是一个原型。如果一个不可写的属性被继承,它仍然可以防止修改对象的属性。

  1. function myClass () {
  2. }
  3. myClass.prototype.x = 1;
  4. Object.defineProperty(myClass.prototype, 'y', {
  5. writable: false,
  6. value: 1
  7. });
  8. var a = new myClass();
  9. a.x = 2;
  10. console.log(a.x); //
  11. console.log(myClass.prototype.x); //
  12. a.y = 2;
  13. console.log(a.y); //
  14. console.log(myClass.prototype.y); // 

1. 定义方法类myClass
2. 在方法原型对象上通过点语法定义属性x,值为1,它是可写的,可配置的,可枚举的
3.  通过Object.defineProperty()方法在方法原型上定义属性y,它是可写的,不可配置的,不可枚举的
4. 定义一个myClass类的实例
5. 访问实例的属性x,赋值为2,对象本身没有这个属性,在它原型对象上有这个属性,这个属性是可写的,赋值为2
6. 输出实例x的属性为2
7. 输出方法类myClass的原型对象上的属性x是1
8. 访问实例的属性y,它是通过Object.defineProperty()方法定义的,是不可写的,赋值为2,它的值仍然是1
9. 出事方法类myClass的原型对象上的属性y,它仍然是1

介绍完访问器属性之后我们来看看vue是如何实现双向绑定的。

3. vue.js双向绑定

3.1. 极简双向绑定

vue.js最重要的概念是数据双向绑定,也是MVVM主要特点。

html代码:

  1. <input type="text" id="a">
  2. <span id="b"></span>

JavaScript代码:

  1. var obj = {};
  2. Object.defineProperty(obj, 'hello', {
  3. set: function (newVal) {
  4. document.getElementById('a').value = newVal;
  5. document.getElementById('b').innerHTML = newVal;
  6. }
  7. })
  8. document.addEventListener('keyup', function (e) {
  9. obj.hello = e.target.value;
  10. });

效果:

这个效果就是在文本框中输入的值会显示在旁边的<span>标签里。这个例子就是双向绑定的实现,但是仅仅为了说明原理,这个和我们平时用的vue.js还有差距,下面是我们常见的vue.js写法

html代码:

  1. <input type="text" v-model="text">
  2. {{ text }} 

JavaScript代码:

  1. var vm = new Vue({
  2. el: 'app',
  3. data: {
  4. text: 'hello world'
  5. }
  6. }) 

为了实现这样的容易理解的代码vue.js背后做了很多工作,我们一一分解。

1. 输入框以及文本节点与data中的数据绑定显示
2. 输入框变化的时候,data中的数据同步变化。即MVVM中 view => viewmodel的变化
3. data中的数据变化时,文本节点显示的内容同步变化。即MVVM中viewmode => view的变化

3.2 数据初始化绑定

介绍数据初始化绑定之前先说一下DocumentFragment。DocumentFragment(文档片段)可以看做是节点容器,它可以包含多个子节点,可以把它插入到DOM中,只有它的子节点会插入目标节点,所以可以把它看做是一组节点容器。使用DocumentFragment处理节点速度和性能优于直接操作DOM。Vue进行编译的时候就是将挂载目标的所有子节点劫持到DocumentFragment中,经过处理后再将DocumentFragment整体返回到挂载目标。实例代码如下:

  1. var dom = nodeToFragment(document.getElementById("app"));
  2. console.log(dom);
  3. function nodeToFragment (node, vm) {
  4. var flag = document.createDocumentFragment();
  5. var child;
  6. while (child = node.firstChild) {
  7. flag.appendChild(child); // 劫持node的所有节点
  8. }
  9. return flag;
  10. }
  11. document.getElementById("app").appendChild(dom); 

有了文档片段之后再看看初始化绑定。

html代码:

  1. <div id="app">
  2. <input type="text" v-model="text">
  3. {{text}}
  4. </div> 

JavaScript代码:

  1. function compile (node, vm) {
  2. var reg = /\{\{(.*)\}\}/;
  3. // 节点类型为元素,使用node.nodeType属性
  4. if (node.nodeType === 1) {
  5. var attr = node.attributes;
  6. // 解析属性
  7. for (var i = 0; i < attr.length; i++) {
  8. if (attr[i].nodeName === 'v-model') {
  9. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
  10. node.value = vm.data[name]; // 将data的值赋给该node
  11. node.removeAttribute('v-model');
  12. }
  13. }
  14. }
  15. // 节点类型为text,使用node.nodeType属性
  16. if (node.nodeType === 3) {
  17. if (reg.test(node.nodeValue)) {
  18. var name = RegExp.$1; // 获取匹配到的字符串
  19. name = name.trim()
  20. node.nodeValue = vm.data[name]; // 将该data的值付给该node
  21. }
  22. }
  23. }
  24.  
  25. function nodeToFragment (node, vm) {
  26. var flag = document.createDocumentFragment();
  27. var child;
  28. while (child = node.firstChild) {
  29. compile(child, vm);
  30. // 将子节点劫持到文档片段中
  31. flag.appendChild(child);
  32. }
  33. return flag;
  34. }
  35.  
  36. // 构造函数
  37. function Vue (options) {
  38. this.data = options.data;
  39. var id = options.el;
  40. var dom = nodeToFragment(document.getElementById(id), this);
  41. // 编译完成后把dom返回到app中
  42. document.getElementById(id).appendChild(dom);
  43. }
  44.  
  45. var vm = new Vue({
  46. el: 'app',
  47. data: {
  48. text: 'hello world'
  49. }
  50. }); 

最终效果:

我们看到hello word已经绑定到input标签和节点中了

先看compile方法,这个方法主要负责给node节点赋值
1. compile方法接收两个参数,第一个是DOM节点,第二个vm是当前对象
2. 判断dom节点类型,如果是1,表示元素(这里判断不太严谨,只是为了说明原理),在node节点的所有属性中查找nodeName为“v-model”的属性,找到属性值,这里是“text”。用当前对象中名字为“text”的属性值给节点赋值,最后删除这个属性,就是删除节点的v-model属性。
3. 判断dom节点类型,如果是3,表示是节点内容,用正则表达式判断是“{{text}}”这样的字符串,用当前对象中名字为“text”的属性值给节点赋值,直接覆盖掉“{{text}}”
4.这里是简单的例子,实际情况是dom结构要比这个复杂的多,可能会递归的寻找节点,判断节点类型,操作赋值。

nodeToFragment方法负责创建文档片段,并将compile处理过的子节点劫持到这个文档片段中
1. 创建一个文档片段
2. 循环查找传入的node节点,调用compile方法给节点赋值
3. 将赋值后的节点劫持到文档片段中

Vue构造函数
1. 用传入参数的data属性给当前对象的data属性赋值
2. 用传入参数的id标记查找挂载节点,调用nodeToFragment方法获取劫持后的文档片段,这个过程称为编译
3. 编译完成后,将文档片段插入到指定的当前节点中

实例化vue
1. 实例化一个vue对象,el属性为挂载节点的id,data属性为要绑定的属性及属性值

3.3 响应式数据绑定

初始化绑定只是实现了第一步,然后我们要实现的是在文本框中输入内容的时候,vue实例中的属性值也跟着变化。思路是在文本框中输入数据的时候,触发文本框的input事件(也可以是keyup,change),在相应的事件处理程序中,获取输入内容赋值给当前vue实例vm的text属性。这里利用上面介绍的Object.defeinProperty()方法来给vue实例中data中的属性重新定义为访问器属性,就是在定义这个属性的时候添加get,set这两个存取描述符,这样给vm.text赋值的时候就会触发set方法。然后在set方法中更新vue实例属性的值。看下面的html,js代码:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>响应式数据绑定</title>
  6. </head>
  7. <body>
  8. <div id="app">
  9. <input type="text" v-model="text"/>
  10. {{ text }}
  11. </div>
  12. <script>
  13. /**
  14. * 使用defineProperty将data中的text设置为vm的访问器属性
  15. * @param obj 对象
  16. * @param 属性名
  17. * @param 属性值
  18. * */
  19. function defineReactive (obj, key, val) {
  20. Object.defineProperty(obj, key, {
  21. get: function () {
  22. return val
  23. },
  24. set: function (newVal) {
  25. if (newVal === val) {
  26. return
  27. }
  28. val = newVal
  29. // 输出日志
  30. console.log(`set方法触发属性值变化${val}`)
  31. }
  32. })
  33. }
  34. /**
  35. * 给vue实例定义访问器属性
  36. * @param obj vue实例中的数据
  37. * @param vm vue对象
  38. * */
  39. function observe (obj, vm) {
  40. Object.keys(obj).forEach(function (key) {
  41. defineReactive(vm, key, obj[key]);
  42. })
  43. }
  44. /**
  45. * 编译过程,给子节点初始化绑定vue实例中的属性值
  46. * @param node 子节点
  47. * @param vm vue实例
  48. * */
  49. function compile (node, vm) {
  50. let reg = /\{\{(.*)\}\}/
  51. // 节点类型为元素
  52. if (node.nodeType === 1) {
  53. let attr = node.attributes
  54. // 解析属性
  55. for (let i = 0; i < attr.length; i++) {
  56. if (attr[i].nodeName === 'v-model') {
  57. // 获取v-model绑定的属性名,v-model一般是可输入的dom,可修改的dom
  58. let name = attr[i].nodeValue
  59. // 添加监听事件
  60. node.addEventListener('input', function (e) {
  61. // 给相应的data属性赋值,进而触发该属性的set方法
  62. vm[name] = e.target.value;
  63. });
  64. // 将data的值赋给该node
  65. node.value = vm.data[name];
  66. node.removeAttribute('v-model')
  67. }
  68. }
  69. }
  70. // 节点类型为text,这里只是显示数据的dom
  71. if (node.nodeType === 3) {
  72. if (reg.test(node.nodeValue)) {
  73. // 使用震泽表达式获取匹配到的字符串
  74. let name = RegExp.$1
  75. name = name.trim()
  76. // 将data的值赋给该node.nodeValue
  77. node.nodeValue = vm.data[name]
  78. }
  79. }
  80. }
  81. /**
  82. * DocumentFragment文档片段,可以看作节点容器,它可以包含多个子节点,当将它插入到dom中时只有子节点插入到目标节点中。
  83. * 使用documentfragment处理节点速度和性能要高于直接操作dom。vue编译的时候,就是将挂载目标的所有子节点劫持到documentfragment
  84. * 中,经过处理后再将documentfragment整体返回到挂载目标中。
  85. * @param node 节点
  86. * @param vm vue实例
  87. * */
  88. function nodeToFragment (node, vm) {
  89. var flag = document.createDocumentFragment();
  90. var child;
  91. while (child = node.firstChild) {
  92. compile(child, vm);
  93. flag.appendChild(child);
  94. }
  95. return flag;
  96. }
  97. /*vue类*/
  98. function Vue (options) {
  99. this.data = options.data
  100. let data = this.data
  101. // 给vue实例的data定义访问器属性,覆盖原来的同名属性
  102. observe(data, this)
  103. let id = options.el
  104. let dom = nodeToFragment(document.getElementById(id), this)
  105. // 编译,劫持完成后将dom返回到app中
  106. document.getElementById(id).appendChild(dom)
  107. }
  108.  
  109. /*定义一个vue实例*/
  110. let vm = new Vue({
  111. el: 'app',
  112. // 这里的data属性不是访问器属性
  113. data: {
  114. text: 'hello world!'
  115. }
  116. })
  117. </script>
  118. </body>
  119. </html> 

修改文本框中的内容,vue实例中的属性值也跟着变化,如下截图:

下面不再逐句分析,只说重点的。

1. 在defineReactive方法中,vue实例中的data的属性重新定义为访问器属性,并在set方法中将新的值更新到这个属性
2. 在observe方法中,遍历vue实例中data的属性,逐一调用defineReactive方法,把他们定义为访问器属性
3. 在compile方法中,如果是input这样的标签,给它添加事件(也可以是keyup,change),监听input值变化,并给vue实例中相应的访问器属性赋值
4. 在Vue类方法中,调用observer方法,传入当前实例对象和对象的data属性,将data属性中的子元素重新定义为当前对象的访问器属性

set方法被触发之后,vue实例的text属性跟着变化,但是<span>的内容并没有变化,下面的内容将会介绍“订阅/发布模式”来解决这个问题。

3.4 双向绑定的实现

在实现双向绑定之前要先学习一下“订阅/发布模式”。订阅发布模式(又称为观察者模式)定义一种一对多的关系,让多个观察者同时监听一个主题对象,主题对象状态发生改变的时候观察者都会得到通知

发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作

看下面的代码:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>订阅/发布模式</title>
  6. </head>
  7. <body>
  8. <script>
  9. /**
  10. * 定义一个发布者publisher
  11. * */
  12. var pub = {
  13. publish: function () {
  14. dep.notify();
  15. }
  16. }
  17. /**
  18. * 三个订阅者
  19. * */
  20. var sub1 = {
  21. update: function () {
  22. console.log(1);
  23. }
  24. };
  25. var sub2 = {
  26. update: function () {
  27. console.log(2);
  28. }
  29. };
  30. var sub3 = {
  31. update: function () {
  32. console.log(3);
  33. }
  34. }
  35. /**
  36. * 一个主题对象
  37. * */
  38. function Dep () {
  39. this.subs = [sub1, sub2, sub3];
  40. }
  41. Dep.prototype.notify = function () {
  42. this.subs.forEach(function (sub) {
  43. sub.update();
  44. })
  45. }
  46. // 发布者发布消息,主题对象执行notifiy方法,触发所有订阅者响应,执行update
  47. var dep = new Dep();
  48. pub.publish();
  49. </script>
  50. </body>
  51. </html>

运行结果如下截图:

1. 定义发布者对象pub,对象中定义publish方法,方法调用主题对象实例dep的notify()方法
2. 定义三个订阅者对象,对象中定义update方法,三个对象的update方法分别输出1,2,3
3. 定义一个主题方法类,主题对象中定义数组属性subs,包含三个订阅者对象
4. 在主题方法类的原型对象上定义通知方法notify,方法中循环调用三个订阅者对象的update()方法
5. 实例化主题方法类得到实例dep
6. 调用发布者对象的通知方法notifiy(),分别输出1,2,3

每当创建一个Vue实例的时候,主要做了两件事情,第一是监听数据:observe(data),第二个是编译HTML:nodeToFragment(id)。
在监听数据过程中,为data的每一个属性生成主题对象dep
在编译HTML的过程中,为每个与数据绑定相关的节点生成一个订阅者watcherwatcher会将自己添加到相应属性的dep中
前面已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性set方法。
接下来我们要实现的是:发出通知dep.notify() => 触发订阅者的updata方法 => 更新视图,实现这个目标的关键是如何将watcher添加到关联属性的dep中去。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>双向绑定的实现</title>
  6. </head>
  7. <body>
  8. <div id="app">
  9. <input type="text" v-model="text">
  10. {{ text }}
  11. </div>
  12. <script>
  13. /**
  14. * 使用defineProperty将data中的text设置为vm的访问器属性
  15. * @param obj 对象
  16. * @param key 属性名
  17. * @param val 属性值
  18. */
  19. function defineReactive(obj, key, val) {
  20. // 发布者对象
  21. var dep = new Dep();
  22. Object.defineProperty(obj, key, {
  23. get: function () {
  24. // 依赖收集,如果主题对象类的静态属性target有值, 此时Watcher方法被调用,给主题对象添加订阅者
  25. if (Dep.target) dep.addSub(Dep.target);
  26. return val;
  27. },
  28. set: function (newVal) {
  29. if (newVal === val) return
  30. val = newVal;
  31. // 属性被修改时通知变更,主题对象作为发布者收到通知推送给订阅者,订阅者收到消息回调
  32. dep.notify();
  33. }
  34. })
  35. }
  36.  
  37. /**
  38. * 给vue实例定义访问器属性,将Vue中的data对象中的属性转化成getter,setter
  39. * @param obj vue实例中的数据
  40. * @param vm vue对象
  41. */
  42. function observe(obj, vm) {
  43. Object.keys(obj).forEach(function (key) {
  44. defineReactive(vm, key, obj[key])
  45. })
  46. }
  47.  
  48. /**
  49. * DocumentFragment文档片段
  50. * @param node 节点
  51. * @param vm vue实例
  52. * */
  53. function nodeToFragment(node, vm) {
  54. var flag = document.createDocumentFragment();
  55. var child;
  56. while (child = node.firstChild) {
  57. // 节点编译,生成Watcher
  58. compile(child, vm);
  59. flag.appendChild(child);
  60. }
  61. return flag;
  62. }
  63.  
  64. /**
  65. * 给子节点初始化绑定vue实例中的属性值,并为节点生成Watcher
  66. * @param node 子节点
  67. * @param vm vue实例
  68. */
  69. function compile(node, vm) {
  70. var reg = /\{\{(.*)\}\}/;
  71. // 节点类型为元素,可输入的dom
  72. if (node.nodeType === 1) {
  73. var attr = node.attributes;
  74. // 解析属性
  75. for (var i = 0; i < attr.length; i++) {
  76. if (attr[i].nodeName === 'v-model') {
  77. // 获取v-model绑定的属性名
  78. var name = attr[i].nodeValue;
  79. node.addEventListener('input', function (e) {
  80. // 给相应的data属性赋值,触发set方法
  81. vm[name] = e.target.value
  82. });
  83. // 将data的值赋给该node
  84. node.value = vm[name];
  85. node.removeAttribute('v-model');
  86. }
  87. }
  88. new Watcher(vm, node, name, 'input')
  89. }
  90. if (node.nodeType === 3) {
  91. if (reg.test(node.nodeValue)) {
  92. var name = RegExp.$1; // 获取匹配到的字符串
  93. name = name.trim();
  94. // 将data的值赋给该node,订阅,同上
  95. new Watcher(vm, node, name, 'text');
  96. }
  97. }
  98. }
  99.  
  100. /**
  101. * 编译 HTML 过程中,为每个与 data 关联的节点生成一个 Watcher,收集依赖的时候会addSub到subs集合中,修改data数据的时候触发dep对象的
  102.    * notify通知所有Wathcer对象去修改对应视图
  103. * @param vm
  104. * @param node
  105. * @param name
  106. * @param nodeType
  107. * @constructor
  108. */
  109. function Watcher(vm, node, name, nodeType) {
  110. // 将当前对象赋值给全局变量Dep.target
  111. Dep.target = this;
  112. this.name = name;
  113. this.node = node;
  114. this.vm = vm;
  115. this.nodeType = nodeType;
  116. // 更新
  117. this.update();
  118. // 设置为空,避免重复添加订阅者
  119. Dep.target = null;
  120. }
  121. Watcher.prototype = {
  122. // 更新
  123. update: function () {
  124. /**调用get,这里Dep.target不为空,getter中会将当前属性添加到订阅者集合中,update函数执行完之后就不行了*/
  125. this.get();
  126. if (this.nodeType === 'text') {
  127. this.node.nodeValue = this.value;
  128. }
  129. if (this.nodeType === 'input') {
  130. this.node.value = this.value;
  131. }
  132. },
  133. get: function () {
  134. // this.vm[this.name] 触发getter
  135. this.value = this.vm[this.name];
  136. }
  137. }
  138.  
  139. /**
  140. * 定义一个发布者
  141. * @constructor
  142. */
  143. function Dep() {
  144. // 订阅者集合
  145. this.subs = [];
  146. }
  147.  
  148. /**
  149. * 发布者,添加订阅者和通知变化
  150. * @type {{addSub: Dep.addSub, notify: Dep.notify}}
  151. */
  152. Dep.prototype = {
  153. // 添加订阅者
  154. addSub: function (sub) {
  155. this.subs.push(sub);
  156. },
  157. // 轮询订阅者,通知变化,触发更新
  158. notify: function () {
  159. this.subs.forEach(function (sub) {
  160. sub.update();
  161. });
  162. }
  163. };
  164.  
  165. /**
  166. * 定义Vue类
  167. * @param options Vue参数选项
  168. * @constructor
  169. */
  170. function Vue(options) {
  171. this.data = options.data;
  172. var data = this.data;
  173. observe(data, this);
  174. var id = options.el;
  175. // 编译,收集依赖
  176. var dom = nodeToFragment(document.getElementById(id), this);
  177. // 编译完成后,将dom返回到app中
  178. document.getElementById(id).appendChild(dom);
  179. }
  180.  
  181. // 定义Vue实例
  182. var vm = new Vue({
  183. el: 'app',
  184. data: {
  185. text: 'hello world'
  186. }
  187. })
  188. </script>
  189. </body>
  190. </html>

最终效果如下截图:

这里不再逐句分析,只把重点说明一下
1. 定义主题对象Dep,对象中有addSubnotify两个方法,前者负责向当前对象中添加订阅者,后者轮询订阅者,调用订阅者的更新方法update()
2. 定义观察者对象方法Watcher,在方法中先将自己赋给一个全局变量Dep.target,其实是给主题类Dep定义了一个静态属性target,可以直接使用Dep.target访问这个静态属性。然后给类定义共有属性name(vue实例中的访问器属性名“text”),node(html标签,如<input>,{{text}}),vm(当前vue实例),nodeType(html标签类型),其次执行update方法,进而执行了原型对象上的get方法,get方法中的this.vm[this.name]读取了vm中的访问器属性,从而触发了访问器属性的get方法,get方法中将wathcer添加到对应访问器属性的dep中,同时将属性值赋给临时变量value。再者,获取属性的值(保存在临时变量value中),然后更新视图。最后将Dep.target设为空。因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。
3. 在编译方法compile中,劫持子节点的时候,在节点上定义一个观察者对象Watcher
4. defineReactive方法中,定义访问器属性的时候,在存取描述符get中,如果主题对象类的静态属性target有值, 此时Watcher方法被调用,给主题对象添加订阅者。

data中的数据重新定义为访问器属性,get中将当前数据对应的节点添加到主题对象中,set方法中通知数据对应的节点更新。编译过程将data数据生成数据节点,并生成一个观察者来观察节点变化。

4. 总结

介绍了这么多,最后vue的原理总结如下。vue功能远不止这些,要深入了解,需要研究源代码,待后续博客。

  1. 给Vue定义data选项;
  2. 使用Object.defineProperty将data选项中的属性转化成gettersetter属性;
  3. getter将观察者添加到主题对象中,收集依赖;
  4. setter中通知变更,给dom元素赋值;
  5. 编译,将el节点及子节点编译到render函数;
  6. 编译,触发getter,将当前节点对应的data属性添加到主题中;
  7. 编译,监听input节点的change事件,修改当前节点对应的data属性值,触发setter,通知变更,更新dom节点值,反馈给用户;

本文介绍了vue.js的简单实现以及相关的知识,包含MVC,MVP,MVVM的原理,对象的访问器属性,html的文档片段(DocumentFragment),观察者模式。vue.js的实现主要介绍数据编译(compile),通过文档片段实现数据劫持挂载,通过观察者模式(订阅发布模式)的实现数据双向绑定等内容。

参考:

https://www.cnblogs.com/icebutterfly/p/7977033.html
http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension#!comments

vue原理简介的更多相关文章

  1. [转]vue原理简介

    写vue也有一段时间了,对vue的底层原理虽然有一些了解,这里总结一下. vue.js中有两个核心功能:响应式数据绑定,组件系统.主流的mvc框架都实现了单向数据绑定,而双向绑定无非是在单向绑定基础上 ...

  2. 06 Vue路由简介,原理,实现及嵌套路由,动态路由

    路由概念 路由的本质就是一种对应关系,比如说我们在url地址中输入我们要访问的url地址之后,浏览器要去请求这个url地址对应的资源. 那么url地址和真实的资源之间就有一种对应的关系,就是路由. 路 ...

  3. storm 原理简介及单机版安装指南——详细版【转】

    storm 原理简介及单机版安装指南 本文翻译自: https://github.com/nathanmarz/storm/wiki/Tutorial 原文链接自:http://www.open-op ...

  4. Vue.js简介

    Vue.js简介 Vue.js的作者为Evan You(尤雨溪),任职于Google Creative Lab,虽然是Vue是一个个人项目,但在发展前景上个人认为绝不输于Google的AngularJ ...

  5. Java进阶(二十四)Java List集合add与set方法原理简介

    Java List集合add与set方法原理简介 add方法 add方法用于向集合列表中添加对象. 语法1 用于在列表的尾部插入指定元素.如果List集合对象由于调用add方法而发生更改,则返回 tr ...

  6. kafka原理简介并且与RabbitMQ的选择

    kafka原理简介并且与RabbitMQ的选择 kafka原理简介,rabbitMQ介绍,大致说一下区别 Kafka是由LinkedIn开发的一个分布式的消息系统,使用Scala编写,它以可水平扩展和 ...

  7. InheritableThreadLocal类原理简介使用 父子线程传递数据详解 多线程中篇(十八)

      上一篇文章中对ThreadLocal进行了详尽的介绍,另外还有一个类: InheritableThreadLocal 他是ThreadLocal的子类,那么这个类又有什么作用呢?   测试代码 p ...

  8. Nginx 负载均衡原理简介与负载均衡配置详解

    Nginx负载均衡原理简介与负载均衡配置详解   by:授客  QQ:1033553122   测试环境 nginx-1.10.0 负载均衡原理 客户端向反向代理发送请求,接着反向代理根据某种负载机制 ...

  9. Nginx 反向代理工作原理简介与配置详解

    Nginx反向代理工作原理简介与配置详解   by:授客  QQ:1033553122   测试环境 CentOS 6.5-x86_64 nginx-1.10.0 下载地址:http://nginx. ...

随机推荐

  1. Django 系列博客(四)

    Django 系列博客(四) 前言 本篇博客介绍 django 如何和数据库进行交互并且通过 model 进行数据的增删查改 ORM简介 ORM全称是:Object Relational Mappin ...

  2. SQL Server2008附加数据库出现错误

    在用SQL Server 2008附加数据库时,出现了如下错误: 解决方法如下: 一. 检查对数据库文件及其所在文件夹是否有操作权限,右键文件属性,将权限修改为完全控制: 二. 权限改了之后,发现还是 ...

  3. Laravel日志

    大家在使用 Log::info() 会让日志全部记录默认在 storage/logs/laravel.log 文件里,文件大了查找起来就比较麻烦.那么我可不可以单独记录在一个日志文件里呢? 只需在你的 ...

  4. 细说flush、ob_flush的区别

    ob_flush/flush在手册中的描述, 都是刷新输出缓冲区, 并且还需要配套使用, 所以会导致很多人迷惑… 其实, 他们俩的操作对象不同, 有些情况下, flush根本不做什么事情.. ob_* ...

  5. SpringBoot数据库集成-Mybatis

    一.java web开发环境搭建 网上有很多教程,参考教程:http://www.cnblogs.com/Leo_wl/p/4752875.html 二.Spring boot搭建 1.Intelli ...

  6. eclipse编写js代码没有提示

    安装插件 点击Help,选择Eclipse Marketplace... 搜索js,安装AngularJS Eclipse 重启eclipse,右键项目,选择Configure(配置),选择Conve ...

  7. 汇编语言--微机CPU的指令系统(五)(移位操作指令)

    (5) 移位操作指令 移位操作指令是一组经常使用的指令,它包括算术移位.逻辑移位.双精度移位.循环移位和带进位的循环移位等五大类. 移位指令都有指定移动二进制位数的操作数,该操作数可以是立即数或CL的 ...

  8. redux 入门

    背景: 在react中使用redux 重点:不要滥用redux,如果你的页面非常简单,没有 那么多的互动,那么就不要使用redux,反而会增加项目的复杂性. 如果你有以下情况,则可以考虑使用redux ...

  9. react学习(一)

    组件和属性(props) 函数式组件: function Welcome(props) { return <h1>Hello, {props.name}</h1>; } 渲染一 ...

  10. element-ui 时间日期选择器格式化后台需要的格式

    <el-date-picker v-model="startTime" type="datetime" format="yyyy-MM-dd H ...