vue原理简介
写 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 定义一个“干净”的属性。
// 使用 __prop__ 定义 var obj = {} var descriptor = Object.create(null); // 默认没有 enumberable,configurable,writable descriptor.value = 'static'; Object.defineProperty(obj, 'key', descriptor); console.log(obj);
代码输出结果如下:
1. 定义一个空对象 obj
2. 定义一个属性描述符 descriptor,使用 Object.Create 方法继承 null 对象,这样没有继承属性
3. 设置数据描述符 value,值为‘static’
4. 使用 Object.defineProperty 方法给 obj 对象定义 key 属性,使用 descriptor 描述符,描述符中只有一个数据描述符 value,其他的都是默认值
上面的语句和下面的效果是一样的,就是使用 Object.defineProperty 方法给 obj 对象设置一个 key 属性,属性的属性描述符都是默认值:
// 显示定义 var obj = {} Object.defineProperty(obj, 'key', { enumerable: false, configurable: false, writable: false, value: "statics" }); console.log(obj);
输出如下:
还可以循环使用同一对象最为对象描述符使用,代码如下:
// 循环使用统一对象 function withValue (value) { var d = withValue.d || ( withValue.d = { enumerable: false, writable: false, configurable: false, value: null } ); d.value = value; return d; }</span><span style="color: rgba(0, 0, 255, 1)">var</span> obj =<span style="color: rgba(0, 0, 0, 1)"> {} Object.defineProperty(obj, </span>'key', withValue('static'<span style="color: rgba(0, 0, 0, 1)">)); console.log(obj);</span> </pre>
输出结果如下:
如果对象中不存在指定的属性,Object.defineProperty() 就创建这个属性。当描述符中省略某些字段时,这些字段将使用它们的默认值。拥有布尔值的字段的默认值都是 false,value,get,set 字段默认值是 undefined。一个没有 get,set,value,writable 定义的属性被称为“通用的”,并被键入为一个数据描述符。
// 在对象中添加一个属性与数据描述符的实例, 对象 o 拥有了属性 a,值为 37 var obj = {}; Object.defineProperty(obj, "a", { value: 37, writable: false, enumerable: false, configurable: true }); console.log(obj);
输出结果如下:
// 在对象中添加一个属性与数据描述符的实例, 对象 o 拥有了属性 a,值为 37 var obj = {}; Object.defineProperty(obj, "a", { value: 37, writable: false, enumerable: false, configurable: true }); console.log(obj);
输出结果如下:
// 在对象中添加一个属性与存取描述符, 对象 o 拥有了属性 b,值为 38 var bValue; Object.defineProperty(obj, 'b', { get: function () { return bValue; }, set: function (newValue) { bValue = newValue; }, enumerable: true, configurable: true }); // o.b 的值现在总是与 bValue 相同,除非重新定义 o.b bValue = 200; console.log(obj.b);
输出结果如下:
// 数据描述符和存取描述符不能混合使用,否则会报错 var obj = {}; Object.defineProperty(obj, 'confict', { value: '0x9f91102', get: function () { return 0xdeadbeef; } }); // 报错: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 时,改属性称为“不可写”。它不能被重新分配。
// 创建一个新对象 var o = {}; Object.defineProperty(o, 'a', { value: 37, configurable: false, writable: false }); console.log(o.a); // 输出 37 o.a = 38; console.log(o.a); // writable 属性为 false,o.a 的值仍然是 37,如果是严格模式,这里会抛错:"a" is read-only
2.3 Enumerable 特性
enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中枚举。
var o = {}; Object.defineProperty(o, 'a', {value: 1, enumerable: true}); Object.defineProperty(o, 'b', {value: 2, enumerable: false}); // 没有设置 enumberable 属性默认值是 false Object.defineProperty(o, 'c', {value: 3}); // 如果使用直接赋值的方式创建对象属性,则这个属性的 enumerable 为 true o.d = 4; console.log(Object.keys(o)); // 输出 ["a", "d"] console.log(o.propertyIsEnumerable('a')); // 输出 true console.log(o.propertyIsEnumerable('b')); // 输出 false console.log(o.propertyIsEnumerable('c')); // 输出 false console.log(o.propertyIsEnumerable('d')); // 输出 true
2.4 Configurable 特性
configurable 特性表示对象的属性是否可以被删除,以及除 writable 特性外的其他特性是否可以被修改
var o = {}; Object.defineProperty(o, 'a', { get: function () { return 1; }, configurable: false }); console.log(o.a); // 输出 1 Object.defineProperty(o, 'a', {configurable: true}); // Uncaught TypeError: Cannot redefine property: a Object.defineProperty(o, "a", {enumerable: true}); // Uncaught TypeError: Cannot redefine property: a Object.defineProperty(o, "a", { set: function (){} }); // Uncaught TypeError: Cannot redefine property: a Object.defineProperty(o, "a", { get: function (){} }); // Uncaught TypeError: Cannot redefine property: a delete o.a; console.log(o.a); // 对染 delete 语句没有报错,但是没有真正删除 a 属性,输出 1
2.5 添加多个属性和默认值
使用点运算符和 Object.defineProperty() 为对象的属性赋值,数据描述符的属性默认值是不同的。
var o = {}; o.a = 1; // 上面使用点语法定义属性,等同于下面代码,注意 writable,configurable,enumerable 的默认属性为 false,但是这里使用点语法是 true Object.defineProperty(o, "a", { value: 1, writable: true, configurable: true, enumerable: true });Object.defineProperty(o, </span>"a", {value: 1<span style="color: rgba(0, 0, 0, 1)">}); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 上面使用Object.defineProperty()定义属性,等同于下面代码</span> Object.defineProperty(o, "a"<span style="color: rgba(0, 0, 0, 1)">, { value: </span>1<span style="color: rgba(0, 0, 0, 1)">, writable: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">, configurable: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">, enumerable: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)"> });</span></pre>
2.6 一般的 getter 和 setter
下面的例子展示如何实现一个自存档对象,当设置 temperture 属性时,archive 数组就会获取日志
function Archiver () { var temperature = null; var archiver = []; Object.defineProperty(this, 'temperature', { get: function () { console.log('get!'); return temperature; }, set: function (value) { temperature = value; archiver.push({val: temperature}); } }); this.getArchive = function () { return archiver; } }var arc = new Archiver();
console.log(arc.temperature); // 输出 get,但是 arc.temperature 是 null
arc.temperature = 11; // 触发 archiver.push({val: temperature})
arc.temperature = 13; // 触发 archiver.push({val: temperature})
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}]
var pattern = { get: function () { return 'I alway return this string,whatever you have assigned'; }, set: function () { console.log('给属性 myname 赋值') this.myname = 'this is my name string'; } } function TestDefineSetAndGet () { Object.defineProperty(this, 'myproperty', pattern);} var instance = new TestDefineSetAndGet(); instance.myproperty = 'test'; // 输出 “给属性 myname 赋值” console.log(instance.myproperty); // 输出 “I alway return this string,whatever you have assigned” 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 方法会在子对象的属性被访问或者修改时调用。如果这些方法用一个变量保存,会被所有对象共享。
function myClass (){} var value; Object.defineProperty(myClass.prototype, 'x', {get () { return value }, set (x) { value = x; } }); var a = new myClass(); var b = new myClass(); a.x = 1; console.log(a.x); // 1 console.log(b.x); // 1
在类 myClass 的原型对象上定义了 x 属性,这个属性会被类 myClass 的所有实例共享。通过将值保存在另一个属性中固定,在 get,set 中,this 指向某个被访问和修改属性的对象。
代码如下:
var obj = {} Object.defineProperty(obj, 'hello', { get: function () { console.log('get 方法被调用') }, set: function (v) { console.log("set 方法被调用了,参数是" + v)}}) obj.hello; // get 方法被调用 obj.hello = 'abc'; // set 方法被调用了,参数是 abc
可以像普通属性一样读取,设置访问器属性,访问器属性比较特殊,读取或设置访问器属性的值其实是调用内部 get,set 方法来操作属性。为属性赋值,就是调用 set 方法并使用参数给属性赋值。get,set 方法内部的 this 指针指向 obj,这意味着 get 和 set 方法可以操作对象内部的值。另外,访问器属性会覆盖同名的普通属性,因为访问器属性优先访问,同名的属性会被忽略。
function myClass (){} Object.defineProperty(myClass.prototype, 'x', {get () { return this.stored_x; }, set (x) { this.stored_x = x; } }); var a = new myClass(); var b = new myClass(); a.x = 1; console.log(a.x); // 1 console.log(b.x); // undefined
不像访问者属性,值属性始终在对象自身上设置,而不是一个原型。如果一个不可写的属性被继承,它仍然可以防止修改对象的属性。
function myClass (){} myClass.prototype.x = 1; Object.defineProperty(myClass.prototype, 'y', { writable: false, value: 1 }); var a = new myClass(); a.x = 2; console.log(a.x); // 2 console.log(myClass.prototype.x); // 1 a.y = 2; console.log(a.y); // 1 console.log(myClass.prototype.y); // 1
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 代码:
<input type="text" id="a"> <span id="b"></span>
JavaScript 代码:
var obj = {}; Object.defineProperty(obj, 'hello', { set: function (newVal) { document.getElementById('a').value = newVal; document.getElementById('b').innerHTML = newVal; } }) document.addEventListener('keyup', function (e) { obj.hello = e.target.value; });
效果:
这个效果就是在文本框中输入的值会显示在旁边的 <span> 标签里。这个例子就是双向绑定的实现,但是仅仅为了说明原理,这个和我们平时用的 vue.js 还有差距,下面是我们常见的 vue.js 写法
html 代码:
<input type="text" v-model="text">
{{text}}
JavaScript 代码:
var vm = new Vue({ el: 'app', data: { text: 'hello world' } })
为了实现这样的容易理解的代码 vue.js 背后做了很多工作,我们一一分解。
1. 输入框以及文本节点与 data 中的数据绑定显示
2. 输入框变化的时候,data 中的数据同步变化。即 MVVM 中 view => viewmodel 的变化
3. data 中的数据变化时,文本节点显示的内容同步变化。即 MVVM 中 viewmode => view 的变化
3.2 数据初始化绑定
介绍数据初始化绑定之前先说一下 DocumentFragment。DocumentFragment(文档片段)可以看做是节点容器,它可以包含多个子节点,可以把它插入到 DOM 中,只有它的子节点会插入目标节点,所以可以把它看做是一组节点容器。使用 DocumentFragment 处理节点速度和性能优于直接操作 DOM。Vue 进行编译的时候就是将挂载目标的所有子节点劫持到 DocumentFragment中,经过处理后再将 DocumentFragment整体返回到挂载目标。实例代码如下:
var dom = nodeToFragment(document.getElementById("app")); console.log(dom); function nodeToFragment (node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) {flag.appendChild(child); // 劫持 node 的所有节点 } return flag; } document.getElementById("app").appendChild(dom);
有了文档片段之后再看看初始化绑定。
html 代码:
<div id="app"> <input type="text" v-model="text"> {{text}} </div>
JavaScript 代码:
function compile (node, vm) { var reg = /\{\{(.*)\}\}/; // 节点类型为元素, 使用 node.nodeType 属性 if (node.nodeType === 1) { var attr = node.attributes; // 解析属性 for (var i = 0; i < attr.length; i++) { if (attr[i].nodeName === 'v-model') { var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名 node.value = vm.data[name]; // 将 data 的值赋给该 node node.removeAttribute('v-model');} } } // 节点类型为 text, 使用 node.nodeType 属性 if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; // 获取匹配到的字符串 name = name.trim() node.nodeValue = vm.data[name]; // 将该 data 的值付给该 node } } }</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> nodeToFragment (node, vm) { </span><span style="color: rgba(0, 0, 255, 1)">var</span> flag =<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment(); </span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> child; </span><span style="color: rgba(0, 0, 255, 1)">while</span> (child =<span style="color: rgba(0, 0, 0, 1)"> node.firstChild) { compile(child, vm); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将子节点劫持到文档片段中</span>
flag.appendChild(child);
}
return flag;
}</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 构造函数</span> <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> Vue (options) { </span><span style="color: rgba(0, 0, 255, 1)">this</span>.data =<span style="color: rgba(0, 0, 0, 1)"> options.data; </span><span style="color: rgba(0, 0, 255, 1)">var</span> id =<span style="color: rgba(0, 0, 0, 1)"> options.el; </span><span style="color: rgba(0, 0, 255, 1)">var</span> dom = nodeToFragment(document.getElementById(id), <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 编译完成后把dom返回到app中</span>
document.getElementById(id).appendChild(dom);
}</span><span style="color: rgba(0, 0, 255, 1)">var</span> vm = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vue({ el: </span>'app'<span style="color: rgba(0, 0, 0, 1)">, data: { text: </span>'hello world'<span style="color: rgba(0, 0, 0, 1)"> } });</span> </pre>
最终效果:
我们看到 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 代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title> 响应式数据绑定 </title> </head> <body> <div id="app"> <input type="text" v-model="text"/> {{text}} </div> <script> /** * 使用 defineProperty 将 data 中的 text 设置为 vm 的访问器属性 * @param obj 对象 * @param 属性名 * @param 属性值 * */ function defineReactive (obj, key, val) { Object.defineProperty(obj, key, { get: function () { return val }, set: function (newVal) { if (newVal === val) { return } val = newVal // 输出日志 console.log(`set 方法触发属性值变化 ${val}`)}}) } /** * 给 vue 实例定义访问器属性 * @param obj vue 实例中的数据 * @param vm vue 对象 * */ function observe (obj, vm) {Object.keys(obj).forEach(function (key) {defineReactive(vm, key, obj[key]); }) } /** * 编译过程,给子节点初始化绑定 vue 实例中的属性值 * @param node 子节点 * @param vm vue 实例 * */ function compile (node, vm) { let reg = /\{\{(.*)\}\}/ // 节点类型为元素 if (node.nodeType === 1) { let attr = node.attributes // 解析属性 for (let i = 0; i < attr.length; i++) { if (attr[i].nodeName === 'v-model') { // 获取 v-model 绑定的属性名,v-model 一般是可输入的 dom,可修改的 dom let name = attr[i].nodeValue // 添加监听事件 node.addEventListener('input', function (e) { // 给相应的 data 属性赋值,进而触发该属性的 set 方法 vm[name] = e.target.value; }); // 将 data 的值赋给该 node node.value = vm.data[name]; node.removeAttribute('v-model')}} } // 节点类型为 text,这里只是显示数据的 dom if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { // 使用震泽表达式获取匹配到的字符串 let name = RegExp.$1 name = name.trim() // 将 data 的值赋给该 node.nodeValue node.nodeValue = vm.data[name] } } } /** * DocumentFragment 文档片段,可以看作节点容器,它可以包含多个子节点,当将它插入到 dom 中时只有子节点插入到目标节点中。 * 使用 documentfragment 处理节点速度和性能要高于直接操作 dom。vue 编译的时候,就是将挂载目标的所有子节点劫持到 documentfragment * 中,经过处理后再将 documentfragment 整体返回到挂载目标中。 * @param node 节点 * @param vm vue 实例 * */ function nodeToFragment (node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) {compile(child, vm); flag.appendChild(child); } return flag; } /*vue 类*/ function Vue (options) { this.data = options.data let data = this.data // 给 vue 实例的 data 定义访问器属性,覆盖原来的同名属性 observe(data, this) let id = options.el let dom = nodeToFragment(document.getElementById(id), this) // 编译,劫持完成后将 dom 返回到 app 中 document.getElementById(id).appendChild(dom) }</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">定义一个vue实例</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> let vm </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vue({ el: </span>'app'<span style="color: rgba(0, 0, 0, 1)">, </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这里的data属性不是访问器属性</span>
data: {
text: 'hello world!'
}
})
</script>
</body>
</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 双向绑定的实现
在实现双向绑定之前要先学习一下“订阅 / 发布模式”。订阅发布模式(又称为观察者模式)定义一种一对多的关系,让多个观察者同时监听一个主题对象,主题对象状态发生改变的时候观察者都会得到通知。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作
看下面的代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title> 订阅 / 发布模式 </title> </head> <body> <script> /** * 定义一个发布者 publisher * */ var pub = { publish: function () {dep.notify(); } } /** * 三个订阅者 * */ var sub1 = { update: function () { console.log(1);} }; var sub2 = { update: function () { console.log(2);} }; var sub3 = { update: function () { console.log(3);} } /** * 一个主题对象 * */ function Dep () { this.subs = [sub1, sub2, sub3]; } Dep.prototype.notify = function () { this.subs.forEach(function (sub) {sub.update(); }) } // 发布者发布消息,主题对象执行 notifiy 方法,触发所有订阅者响应,执行 update var dep = new Dep(); pub.publish(); </script> </body> </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 的过程中,为每个与数据绑定相关的节点生成一个订阅者 watcher,watcher 会将自己添加到相应属性的 dep 中。
前面已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性 set 方法。
接下来我们要实现的是:发出通知 dep.notify() => 触发订阅者的 updata 方法 => 更新视图,实现这个目标的关键是如何将 watcher 添加到关联属性的 dep 中去。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>双向绑定的实现</title> </head> <body> <div id="app"> <input type="text" v-model="text"> {{text}} </div> <script> /** * 使用 defineProperty 将 data 中的 text 设置为 vm 的访问器属性 * @param obj 对象 * @param key 属性名 * @param val 属性值 */ function defineReactive(obj, key, val) { // 发布者对象 var dep = new Dep(); Object.defineProperty(obj, key, { get: function () { // 依赖收集,如果主题对象类的静态属性 target 有值, 此时 Watcher 方法被调用,给主题对象添加订阅者 if (Dep.target) dep.addSub(Dep.target); return val; }, set: function (newVal) { if (newVal === val) return val = newVal; // 属性被修改时通知变更,主题对象作为发布者收到通知推送给订阅者,订阅者收到消息回调 dep.notify();} }) }</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">/*</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">* * 给vue实例定义访问器属性,将Vue中的data对象中的属性转化成getter,setter * @param obj vue实例中的数据 * @param vm vue对象 </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">*/</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> observe(obj, vm) { Object.keys(obj).forEach(</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> (key) { defineReactive(vm, key, obj[key]) }) } </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">/*</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">* * DocumentFragment文档片段 * @param node 节点 * @param vm vue实例 * </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">*/</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> nodeToFragment(node, vm) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> flag </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> document.createDocumentFragment(); </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> child; </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">while</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> (child </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> node.firstChild) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 节点编译,生成Watcher</span>
compile(child, vm);
flag.appendChild(child);
}
return flag;
}</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">/*</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">* * 给子节点初始化绑定vue实例中的属性值,并为节点生成Watcher * @param node 子节点 * @param vm vue实例 </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">*/</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> compile(node, vm) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> reg </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">/</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">\{\{(.*)\}\}</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">/</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">; </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 节点类型为元素,可输入的dom</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">if</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> (node.nodeType </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">===</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">1</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> attr </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> node.attributes; </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 解析属性</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">for</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> (</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> i </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">0</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">; i </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"><</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> attr.length; i</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">++</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">if</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> (attr[i].nodeName </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">===</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">v-model</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 获取v-model绑定的属性名</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> name </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> attr[i].nodeValue; node.addEventListener(</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">input</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">, </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> (e) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 给相应的data属性赋值,触发set方法</span>
vm[name] = e.target.value
});
// 将 data 的值赋给该 node
node.value = vm[name];
node.removeAttribute('v-model');
}
}
new Watcher(vm, node, name, 'input')
}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
// 将 data 的值赋给该 node,订阅,同上
new Watcher(vm, node, name, 'text');
}
}
}</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">/*</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">* * 编译 HTML 过程中,为每个与 data 关联的节点生成一个 Watcher,收集依赖的时候会addSub到subs集合中,修改data数据的时候触发dep对象的 * notify通知所有Wathcer对象去修改对应视图 * @param vm * @param node * @param name * @param nodeType * @constructor </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">*/</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> Watcher(vm, node, name, nodeType) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 将当前对象赋值给全局变量Dep.target</span>
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
// 更新
this.update();
// 设置为空,避免重复添加订阅者
Dep.target = null;
}
Watcher.prototype = {
// 更新
update: function () {
/*调用 get, 这里 Dep.target 不为空,getter 中会将当前属性添加到订阅者集合中,update 函数执行完之后就不行了/
this.get();
if (this.nodeType === 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType === 'input') {
this.node.value = this.value;
}
},
get: function () {
// this.vm[this.name] 触发 getter
this.value = this.vm[this.name];
}
}</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">/*</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">* * 定义一个发布者 * @constructor </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">*/</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> Dep() { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 订阅者集合</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">this</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">.subs </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> []; } </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">/*</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">* * 发布者,添加订阅者和通知变化 * @type {{addSub: Dep.addSub, notify: Dep.notify}} </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">*/</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> Dep.prototype </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 添加订阅者</span>
addSub: function (sub) {
this.subs.push(sub);
},
// 轮询订阅者,通知变化,触发更新
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
};</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">/*</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">* * 定义Vue类 * @param options Vue参数选项 * @constructor </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">*/</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">function</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> Vue(options) { </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">this</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">.data </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> options.data; </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> data </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">this</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">.data; observe(data, </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">this</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">); </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> id </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> options.el; </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 编译,收集依赖</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> dom </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> nodeToFragment(document.getElementById(id), </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">this</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">); </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 编译完成后,将dom返回到app中</span>
document.getElementById(id).appendChild(dom);
}</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)">//</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 128, 0, 1)"> 定义Vue实例</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">var</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> vm </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">=</span> <span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 255, 1)">new</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> Vue({ el: </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">app</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">, data: { text: </span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">hello world</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)">'</span><span style="background-color: rgba(245, 245, 245, 1); color: rgba(0, 0, 0, 1)"> } })
</script>
</body>
</html>
最终效果如下截图:
这里不再逐句分析,只把重点说明一下
1. 定义主题对象 Dep,对象中有addSub和notify两个方法,前者负责向当前对象中添加订阅者,后者轮询订阅者,调用订阅者的更新方法 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 功能远不止这些,要深入了解,需要研究源代码,待后续博客。
- 给 Vue 定义data选项;
- 使用 Object.defineProperty 将 data 选项中的属性转化成getter,setter属性;
- getter将观察者添加到主题对象中,收集依赖;
- setter中通知变更,给 dom 元素赋值;
- 编译,将 el 节点及子节点编译到render函数;
- 编译,触发getter,将当前节点对应的data 属性添加到主题中;
- 编译,监听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