前言
谈起当前前端最热门的 js 框架,必少不了 Vue、React、Angular,对于大多数人来说,我们更多的是在使用框架,对于框架解决痛点背后使用的基本原理往往关注不多,近期在研读 Vue.js 源码,也在写源码解读的系列文章。和多数源码解读的文章不同的是,我会尝试从一个初级前端的角度入手,由浅入深去讲解源码实现思路和基本的语法知识,通过一些基础事例一步步去实现一些小功能。
本场 Chat 是系列 Chat 的开篇,我会首先讲解一下数据双向绑定的基本原理,介绍对比一下三大框架的不同实现方式,同时会一步步完成一个简单的mvvm示例。读源码不是目的,只是一种学习的方式,目的是在读源码的过程中提升自己,学习基本原理,拓展编码的思维方式。
模板引擎实现原理
对于页面渲染,一般分为服务器端渲染和浏览器端渲染。一般来说服务器端吐html页面的方式渲染速度更快、更利于SEO,但是浏览器端渲染更利于提高开发效率和减少维护成本,是一种相关舒服的前后端协作模式,后端提供接口,前端做视图和交互逻辑。前端通过Ajax请求数据然后拼接html字符串或者使用js模板引擎、数据驱动的框架如Vue进行页面渲染。
在ES6和Vue这类框架出现以前,前端绑定数据的方式是动态拼接html字符串和js模板引擎。模板引擎起到数据和视图分离的作用,模板对应视图,关注如何展示数据,在模板外头准备的数据, 关注那些数据可以被展示。模板引擎的工作原理可以简单地分成两个步骤:模板解析 / 编译(Parse / Compile)和数据渲染(Render)两部分组成,当今主流的前端模板有三种方式:
- String-based templating (基于字符串的parse和compile过程)
- Dom-based templating (基于Dom的link或compile过程)
- Living templating (基于字符串的parse 和 基于dom的compile过程)
String-based templating
基于字符串的模板引擎,本质上依然是字符串拼接的形式,只是一般的库做了封装和优化,提供了更多方便的语法简化了我们的工作。基本原理如下:
典型的库:
- art-template
- mustache.js
- doT
之前的一篇文章中我介绍了js模板引擎的实现思路,感兴趣的朋友可以看看这里:JavaScript进阶学习(一)—— 基于正则表达式的简单js模板引擎实现。这篇文章中我们利用正则表达式实现了一个简单的js模板引擎,利用正则匹配查找出模板中{{}}之间的内容,然后替换为模型中的数据,从而实现视图的渲染。
- var template = function(tpl, data) {
- var re = /{{(.+?)}}/g,
- cursor = 0,
- reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g,
- code = 'var r=[];\n';
- // 解析html
- function parsehtml(line) {
- // 单双引号转义,换行符替换为空格,去掉前后的空格
- line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,"");
- code +='r.push("' + line + '");\n';
- }
- // 解析js代码
- function parsejs(line) {
- // 去掉前后的空格
- line = line.replace(/(^\s+)|(\s+$)/g,"");
- code += line.match(reExp)? line + '\n' : 'r.push(' + 'this.' + line + ');\n';
- }
- // 编译模板
- while((match = re.exec(tpl))!== null) {
- // 开始标签 {{ 前的内容和结束标签 }} 后的内容
- parsehtml(tpl.slice(cursor, match.index));
- // 开始标签 {{ 和 结束标签 }} 之间的内容
- parsejs(match[1]);
- // 每一次匹配完成移动指针
- cursor = match.index + match[0].length;
- }
- // ***一次匹配完的内容
- parsehtml(tpl.substr(cursor, tpl.length - cursor));
- code += 'return r.join("");';
- return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
- }
源代码:http://jsfiddle.net/zhaomenghuan/bw468orv/embedded/
现在ES6支持了模板字符串,我们可以用比较简单的代码就可以实现类似的功能:
- const template = data => `
- <p>name: ${data.name}</p>
- <p>age: ${data.profile.age}</p>
- <ul>
- ${data.skills.map(skill => `
- <li>${skill}</li>
- `).join('')}
- </ul>`
- const data = {
- name: 'zhaomenghuan',
- profile: { age: 24 },
- skills: ['html5', 'javascript', 'android']
- }
- document.body.innerHTML = template(data)
Dom-based templating
Dom-based templating 则是从DOM的角度去实现数据的渲染,我们通过遍历DOM树,提取属性与DOM内容,然后将数据写入到DOM树中,从而实现页面渲染。一个简单的例子如下:
- function MVVM(opt) {
- this.dom = document.querySelector(opt.el);
- this.data = opt.data || {};
- this.renderDom(this.dom);
- }
- MVVM.prototype = {
- init: {
- sTag: '{{',
- eTag: '}}'
- },
- render: function (node) {
- var self = this;
- var sTag = self.init.sTag;
- var eTag = self.init.eTag;
- var matchs = node.textContent.split(sTag);
- if (matchs.length){
- var ret = '';
- for (var i = 0; i < matchs.length; i++) {
- var match = matchs[i].split(eTag);
- if (match.length == 1) {
- ret += matchs[i];
- } else {
- ret = self.data[match[0]];
- }
- node.textContent = ret;
- }
- }
- },
- renderDom: function(dom) {
- var self = this;
- var attrs = dom.attributes;
- var nodes = dom.childNodes;
- Array.prototype.forEach.call(attrs, function(item) {
- self.render(item);
- });
- Array.prototype.forEach.call(nodes, function(item) {
- if (item.nodeType === 1) {
- return self.renderDom(item);
- }
- self.render(item);
- });
- }
- }
- var app = new MVVM({
- el: '#app',
- data: {
- name: 'zhaomenghuan',
- age: '24',
- color: 'red'
- }
- });
源代码:http://jsfiddle.net/zhaomenghuan/6e3yg6Lq/embedded/
页面渲染的函数 renderDom 是直接遍历DOM树,而不是遍历html字符串。遍历DOM树节点属性(attributes)和子节点(childNodes),然后调用渲染函数render。当DOM树子节点的类型是元素时,递归调用遍历DOM树的方法。根据DOM树节点类型一直遍历子节点,直到文本节点。
render的函数作用是提取{{}}中的关键词,然后使用数据模型中的数据进行替换。我们通过textContent获取Node节点的nodeValue,然后使用字符串的split方法对nodeValue进行分割,提取{{}}中的关键词然后替换为数据模型中的值。
DOM 的相关基础
注:元素类型对应NodeType
元素类型 | NodeType |
---|---|
元素 | 1 |
属性 | 2 |
文本 | 3 |
注释 | 8 |
文档 | 9 |
childNodes 属性返回包含被选节点的子节点的 NodeList。childNodes包含的不仅仅只有html节点,所有属性,文本、注释等节点都包含在childNodes里面。children只返回元素如input, span, script, div等,不会返回TextNode,注释。
数据双向绑定实现原理
js模板引擎可以认为是一个基于MVC的结构,我们通过建立模板作为视图,然后通过引擎函数作为控制器实现数据和视图的绑定,从而实现实现数据在页面渲染,但是当数据模型发生变化时,视图不能自动更新;当视图数据发生变化时,模型数据不能实现更新,这个时候双向数据绑定应运而生。检测视图数据更新实现数据绑定的方法有很多种,目前主要分为三个流派,Angular使用的是脏检查,只在特定的事件下才会触发视图刷新,Vue使用的是Getter/Setter机制,而React则是通过 Virtual DOM 算法检查DOM的变动的刷新机制。
本文限于篇幅和内容在此只探讨一下 Vue.js 数据绑定的实现,对于 angular 和 react 后续再做说明,读者也可以自行阅读源码。Vue 监听数据变化的机制是把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Vue 2.x 对 Virtual DOM 进行了支持,这部分内容后续我们再做探讨。
引子
为了更好的理解Vue中视图和数据更新的机制,我们先看一个简单的例子:
- var o = {
- a: 0
- }
- Object.defineProperty(o, "b", {
- get: function () {
- return this.a + 1;
- },
- set: function (value) {
- this.a = value / 2;
- }
- });
- console.log(o.a); // "0"
- console.log(o.b); // "1"
- // 更新o.a
- o.a = 5;
- console.log(o.a); // "5"
- console.log(o.b); // "6"
- // 更新o.b
- o.b = 10;
- console.log(o.a); // "5"
- console.log(o.b); // "6"
这里我们可以看出对象o的b属性的值依赖于a属性的值,同时b属性值的变化又可以改变a属性的值,这个过程相关的属性值的变化都会影响其他相关的值进行更新。反过来我们看看如果不使用Object.defineProperty()方法,上述的问题通过直接给对象属性赋值的方法实现,代码如下
- var o = {
- a: 0
- }
- o.b = o.a + 1;
- console.log(o.a); // "0"
- console.log(o.b); // "1"
- // 更新o.a
- o.a = 5;
- o.b = o.a + 1;
- console.log(o.a); // "5"
- console.log(o.b); // "6"
- // 更新o.b
- o.b = 10;
- o.a = o.b / 2;
- o.b = o.a + 1;
- console.log(o.a); // "5"
- console.log(o.b); // "6"
很显然使用Object.defineProperty()方法可以更方便的监听一个对象的变化。当我们的视图和数据任何一方发生变化的时候,我们希望能够通知对方也更新,这就是所谓的数据双向绑定。既然明白这个道理我们就可以看看Vue源码中相关的处理细节。
Object.defineProperty()
Object.defineProperty()方法可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
语法:Object.defineProperty(obj, prop, descriptor)
参数:
- obj:需要定义属性的对象。
- prop:需被定义或修改的属性名。
- descriptor:需被定义或修改的属性的描述符。
返回值:返回传入函数的对象,即***个参数obj
该方法重点是描述,对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个拥有可写或不可写值的属性。存取描述符是由一对 getter-setter 函数功能来描述的属性。描述符必须是两种形式之一;不能同时是两者。
数据描述符和存取描述符均具有以下可选键值:
- configurable:当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false。
- enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
- value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
- writable:当且仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false。
存取描述符同时具有以下可选键值:
- get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为undefined。
- set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。
我们可以通过Object.defineProperty()方法精确添加或修改对象的属性。比如,直接赋值创建的属性默认情况是可以枚举的,但是我们可以通过Object.defineProperty()方法设置enumerable属性为false为不可枚举。
- var obj = {
- a: 0,
- b: 1
- }
- for (var prop in obj) {
- console.log(`obj.${prop} = ${obj[prop]}`);
- }
结果:
- "obj.a = 0"
- "obj.b = 1"
我们通过Object.defineProperty()修改如下:
- var obj = {
- a: 0,
- b: 1
- }
- Object.defineProperty(obj, 'b', {
- enumerable: false
- })
- for (var prop in obj) {
- console.log(`obj.${prop} = ${obj[prop]}`);
- }
结果:
- "obj.a = 0"
这里需要说明的是我们使用Object.defineProperty()默认情况下是enumerable属性为false,例如:
- var obj = {
- a: 0
- }
- Object.defineProperty(obj, 'b', {
- value: 1
- })
- for (var prop in obj) {
- console.log(`obj.${prop} = ${obj[prop]}`);
- }
结果:
- "obj.a = 0"
其他描述属性使用方法类似,不做赘述。Vue源码core/util/lang.jsS中定义了这样一个方法:
- /**
- * Define a property.
- */
- export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
- Object.defineProperty(obj, key, {
- value: val,
- enumerable: !!enumerable,
- writable: true,
- configurable: true
- })
- }
Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
语法:Object.getOwnPropertyDescriptor(obj, prop)
参数:
- obj:在该对象上查看属性
- prop:一个属性名称,该属性的属性描述符将被返回
返回值:如果指定的属性存在于对象上,则返回其属性描述符(property descriptor),否则返回 undefined。可以访问“属性描述符”内容,例如前面的例子:
- var o = {
- a: 0
- }
- Object.defineProperty(o, "b", {
- get: function () {
- return this.a + 1;
- },
- set: function (value) {
- this.a = value / 2;
- }
- });
- var des = Object.getOwnPropertyDescriptor(o,'b');
- console.log(des);
- console.log(des.get);
Vue源码分析
本次我们主要分析一下Vue 数据绑定的源码,这里我直接将 Vue.js 1.0.28 版本的代码稍作删减拿过来进行,2.x 的代码基于 flow 静态类型检查器书写的,代码除了编码风格在整体结构上基本没有太大改动,所以依然基于 1.x 进行分析,对于存在差异的部分加以说明。
监听对象变动
- // 观察者构造函数
- function Observer (value) {
- this.value = value
- this.walk(value)
- }
- // 递归调用,为对象绑定getter/setter
- Observer.prototype.walk = function (obj) {
- var keys = Object.keys(obj)
- for (var i = 0, l = keys.length; i < l; i++) {
- this.convert(keys[i], obj[keys[i]])
- }
- }
- // 将属性转换为getter/setter
- Observer.prototype.convert = function (key, val) {
- defineReactive(this.value, key, val)
- }
- // 创建数据观察者实例
- function observe (value) {
- // 当值不存在或者不是对象类型时,不需要继续深入监听
- if (!value || typeof value !== 'object') {
- return
- }
- return new Observer(value)
- }
- // 定义对象属性的getter/setter
- function defineReactive (obj, key, val) {
- var property = Object.getOwnPropertyDescriptor(obj, key)
- if (property && property.configurable === false) {
- return
- }
- // 保存对象属性预先定义的getter/setter
- var getter = property && property.get
- var setter = property && property.set
- var childOb = observe(val)
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter () {
- var value = getter ? getter.call(obj) : val
- console.log("访问:"+key)
- return value
- },
- set: function reactiveSetter (newVal) {
- var value = getter ? getter.call(obj) : val
- if (newVal === value) {
- return
- }
- if (setter) {
- setter.call(obj, newVal)
- } else {
- val = newVal
- }
- // 对新值进行监听
- childOb = observe(newVal)
- console.log('更新:' + key + ' = ' + newVal)
- }
- })
- }
定义一个对象作为数据模型,并监听这个对象。
- let data = {
- user: {
- name: 'zhaomenghuan',
- age: '24'
- },
- address: {
- city: 'beijing'
- }
- }
- observe(data)
- console.log(data.user.name)
- // 访问:user
- // 访问:name
- data.user.name = 'ZHAO MENGHUAN'
- // 访问:user
- // 更新:name = ZHAO MENGHUAN
效果如下:
监听数组变动
上面我们通过Object.defineProperty把对象的属性全部转为 getter/setter 从而实现监听对象的变动,但是对于数组对象无法通过Object.defineProperty实现监听。Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。
- const arrayProto = Array.prototype
- const arrayMethods = Object.create(arrayProto)
- function def(obj, key, val, enumerable) {
- Object.defineProperty(obj, key, {
- value: val,
- enumerable: !!enumerable,
- writable: true,
- configurable: true
- })
- }
- // 数组的变异方法
- ;[
- 'push',
- 'pop',
- 'shift',
- 'unshift',
- 'splice',
- 'sort',
- 'reverse'
- ]
- .forEach(function (method) {
- // 缓存数组原始方法
- var original = arrayProto[method]
- def(arrayMethods, method, function mutator () {
- var i = arguments.length
- var args = new Array(i)
- while (i--) {
- args[i] = arguments[i]
- }
- console.log('数组变动')
- return original.apply(this, args)
- })
- })
Vue.js 1.x 在Array.prototype原型对象上添加了$set 和 $remove方法,在2.X后移除了,使用全局 API Vue.set 和 Vue.delete代替了,后续我们再分析。
定义一个数组作为数据模型,并对这个数组调用变异的七个方法实现监听。
- let skills = ['JavaScript', 'Node.js', 'html5']
- // 原型指针指向具有变异方法的数组对象
- skills.__proto__ = arrayMethods
- skills.push('java')
- // 数组变动
- skills.pop()
- // 数组变动
效果如下:
我们将需要监听的数组的原型指针指向我们定义的数组对象,这样我们的数组在调用上面七个数组的变异方法时,能够监听到变动从而实现对数组进行跟踪。
对于__proto__属性,在ES2015中正式被加入到规范中,标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,所以 Vue 是先进行了判断,当__proto__属性存在时将原型指针__proto__指向具有变异方法的数组对象,不存在时直接将具有变异方法挂在需要追踪的对象上。
我们可以在上面Observer观察者构造函数中添加对数组的监听,源码如下:
- const hasProto = '__proto__' in {}
- const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
- // 观察者构造函数
- function Observer (value) {
- this.value = value
- if (Array.isArray(value)) {
- var augment = hasProto
- ? protoAugment
- : copyAugment
- augment(value, arrayMethods, arrayKeys)
- this.observeArray(value)
- } else {
- this.walk(value)
- }
- }
- // 观察数组的每一项
- Observer.prototype.observeArray = function (items) {
- for (var i = 0, l = items.length; i < l; i++) {
- observe(items[i])
- }
- }
- // 将目标对象/数组的原型指针__proto__指向src
- function protoAugment (target, src) {
- target.__proto__ = src
- }
- // 将具有变异方法挂在需要追踪的对象上
- function copyAugment (target, src, keys) {
- for (var i = 0, l = keys.length; i < l; i++) {
- var key = keys[i]
- def(target, key, src[key])
- }
- }
原型链
对于不了解原型链的朋友可以看一下我这里画的一个基本关系图:
- 原型对象是构造函数的prototype属性,是所有实例化对象共享属性和方法的原型对象;
- 实例化对象通过new构造函数得到,都继承了原型对象的属性和方法;
- 原型对象中有个隐式的constructor,指向了构造函数本身。
Object.create
Object.create 使用指定的原型对象和其属性创建了一个新的对象。
- const arrayProto = Array.prototype
- const arrayMethods = Object.create(arrayProto)
这一步是通过 Object.create 创建了一个原型对象为Array.prototype的空对象。然后通过Object.defineProperty方法对这个对象定义几个变异的数组方法。有些新手可能会直接修改 Array.prototype 上的方法,这是很危险的行为,这样在引入的时候会全局影响Array 对象的方法,而使用Object.create实质上是完全了一份拷贝,新生成的arrayMethods对象的原型指针__proto__指向了Array.prototype,修改arrayMethods 对象不会影响Array.prototype。
基于这种原理,我们通常会使用Object.create 实现类式继承。
- // 实现继承
- var extend = function(Child, Parent) {
- // 拷贝Parent原型对象
- Child.prototype = Object.create(Parent.prototype);
- // 将Child构造函数赋值给Child的原型对象
- Child.prototype.constructor = Child;
- }
- // 实例
- var Parent = function () {
- this.name = 'Parent';
- }
- Parent.prototype.getName = function () {
- return this.name;
- }
- var Child = function () {
- this.name = 'Child';
- }
- extend(Child, Parent);
- var child = new Child();
- console.log(child.getName())
发布-订阅模式
在上面一部分我们通过Object.defineProperty把对象的属性全部转为 getter/setter 以及 数组变异方法实现了对数据模型变动的监听,在数据变动的时候,我们通过console.log打印出来提示了,但是对于框架而言,我们相关的逻辑如果直接写在那些地方,自然是不够优雅和灵活的,这个时候就需要引入常用的设计模式去实现,vue.js采用了发布-订阅模式。发布-订阅模式主要是为了达到一种“高内聚、低耦合”的效果。
Vue的Watcher订阅者作为Observer和Compile之间通信的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
- /**
- * 观察者对象
- */
- function Watcher(vm, expOrFn, cb) {
- this.vm = vm
- this.cb = cb
- this.depIds = {}
- if (typeof expOrFn === 'function') {
- this.getter = expOrFn
- } else {
- this.getter = this.parseExpression(expOrFn)
- }
- this.value = this.get()
- }
- /**
- * 收集依赖
- */
- Watcher.prototype.get = function () {
- // 当前订阅者(Watcher)读取被订阅数据的***更新后的值时,通知订阅者管理员收集当前订阅者
- Dep.target = this
- // 触发getter,将自身添加到dep中
- const value = this.getter.call(this.vm, this.vm)
- // 依赖收集完成,置空,用于下一个Watcher使用
- Dep.target = null
- return value
- }
- Watcher.prototype.addDep = function (dep) {
- if (!this.depIds.hasOwnProperty(dep.id)) {
- dep.addSub(this)
- this.depIds[dep.id] = dep
- }
- }
- /**
- * 依赖变动更新
- *
- * @param {Boolean} shallow
- */
- Watcher.prototype.update = function () {
- this.run()
- }
- Watcher.prototype.run = function () {
- var value = this.get()
- if (value !== this.value) {
- var oldValue = this.value
- this.value = value
- // 将newVal, oldVal挂载到MVVM实例上
- this.cb.call(this.vm, value, oldValue)
- }
- }
- Watcher.prototype.parseExpression = function (exp) {
- if (/[^\w.$]/.test(exp)) {
- return
- }
- var exps = exp.split('.')
- return function(obj) {
- for (var i = 0, len = exps.length; i < len; i++) {
- if (!obj) return
- obj = obj[exps[i]]
- }
- return obj
- }
- }
Dep 是一个数据结构,其本质是维护了一个watcher队列,负责添加watcher,更新watcher,移除watcher,通知watcher更新。
- let uid = 0
- function Dep() {
- this.id = uid++
- this.subs = []
- }
- Dep.target = null
- /**
- * 添加一个订阅者
- *
- * @param {Directive} sub
- */
- Dep.prototype.addSub = function (sub) {
- this.subs.push(sub)
- }
- /**
- * 移除一个订阅者
- *
- * @param {Directive} sub
- */
- Dep.prototype.removeSub = function (sub) {
- let index = this.subs.indexOf(sub);
- if (index !== -1) {
- this.subs.splice(index, 1);
- }
- }
- /**
- * 将自身作为依赖添加到目标watcher
- */
- Dep.prototype.depend = function () {
- Dep.target.addDep(this)
- }
- /**
- * 通知数据变更
- */
- Dep.prototype.notify = function () {
- var subs = toArray(this.subs)
- // stablize the subscriber list first
- for (var i = 0, l = subs.length; i < l; i++) {
- // 执行订阅者的update更新函数
- subs[i].update()
- }
- }
模板编译
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
这种实现和我们讲到的Dom-based templating类似,只是更加完备,具有自定义指令的功能。在遍历节点属性和文本节点的时候,可以编译具备{{}}表达式或v-xxx的属性值的节点,并且通过添加 new Watcher()及绑定事件函数,监听数据的变动从而对视图实现双向绑定。
MVVM实例
在数据绑定初始化的时候,我们需要通过new Observer()来监听数据模型变化,通过new Compile()来解析编译模板指令,并利用Watcher搭起Observer和Compile之间的通信桥梁。
- /**
- * @class 双向绑定类 MVVM
- * @param {[type]} options [description]
- */
- function MVVM(options) {
- this.$options = options || {}
- // 简化了对data的处理
- let data = this._data = this.$options.data
- // 监听数据
- observe(data)
- new Compile(options.el || document.body, this)
- }
- MVVM.prototype.$watch = function (expOrFn, cb) {
- new Watcher(this, expOrFn, cb)
- }
为了能够直接通过实例化对象操作数据模型,我们需要为MVVM实例添加一个数据模型代理的方法:
- MVVM.prototype._proxy = function (key) {
- Object.defineProperty(this, key, {
- configurable: true,
- enumerable: true,
- get: () => this._data[key],
- set: (val) => {
- this._data[key] = val
- }
- })
- }
至此我们可以通过一个小例子来说明本文的内容:
- <div id="app">
- <h3>{{user.name}}</h3>
- <input type="text" v-model="modelValue">
- <p>{{modelValue}}</p>
- </div>
- <script>
- let vm = new MVVM({
- el: '#app',
- data: {
- modelValue: '',
- user: {
- name: 'zhaomenghuan',
- age: '24'
- },
- address: {
- city: 'beijing'
- },
- skills: ['JavaScript', 'Node.js', 'html5']
- }
- })
- vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`))
- </script>
本文目的不是为了造一个轮子,而是在学习优秀框架实现的过程中去提升自己,搞清楚框架发展的前因后果,由浅及深去学习基础,本文参考了网上很多优秀博主的文章,由于时间关系,有些内容没有做深入探讨,觉得还是有些遗憾,在后续的学习中会更多的独立思考,提出更多自己的想法。
参考文档
- 前端模板技术面面观
- Object.defineProperty()
- Vue.js 源码学习笔记
- vue早期源码学习系列
- 解析最简单的observer和watcher
- 剖析Vue实现原理 – 如何实现双向绑定mvvm