概念
Vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层,不仅易于上手,还方便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue也完全能够为复杂的单页面应用提供驱动。
当你把一个普通的JavaScript对象传入Vue实例作为data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。Object.defineProperty是ES5中一个无法shim的特性,这也就是Vue不支持IE8以及更低版本浏览器的原因。
这些getter/setter对用户来说是不可见的,但是在内部它们让Vue能够追踪依赖,在属性被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对getter/setter的格式化不同,所以建议安装vue-devtools来获取对检查数据更加友好的用户界面。
每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。
主要模块:
依赖Dep、观察者Observer、监视者Watcher、编译Compile、Vue
1、定义Vue对象,声明data里面的属性值,准备初始化observe实例方法。
2、在Observe定义响应式方法Object.defineProperty()的属性,在初始化的时候,通过Watcher对象进行addDep的操作。即每定义一个data属性值,会添加到一个Watcher对象到订阅者里面。
3、每当形成一个Watcher对象的时候,定义它的响应式。即Object.defineProperty()定义。这导致了一个Observer里面的getter/setter方法与订阅者形成一种依赖关系。
4、由于依赖关系的存在,每当数据的变化后,会导致setter方法,从而触发notify通知方法,通知订阅者数据改变了,需要更新。
5、订阅者会触发内部的update方法,从而改变vm实例的值,以及每个Watcher里面对应node的nodeValue,即视图上面显示的值。
6、Watcher里面接收到了消息后,会触发改变对应对象里面的node的视图的value值,而改变视图上面的值。视图的值改变了,形成了双向绑定MVVM的效果。
1、Dep和Observer代码,即发布订阅模式
class Dep{ constructor(){ this.subs = []; } addSub(sub){ this.subs.push(sub); } notify(){ this.subs.forEach(function(sub){ sub.update(); }) }}class Observer{ constructor(data){ this.data = data; this.walk(data); } walk(data){ let _this = this; Object.keys(data).forEach(function(key){ _this.defineReactive(data, key, data[key]); }); } defineReactive(data, key, val){ let dep = new Dep(); let childObj = observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get(){ if(Dep.target){ dep.addSub(Dep.target); } return val; }, set(newVal){ if(val === newVal){ return; } val = newVal; dep.notify(); } }) }}function observe(value, vm){ if(!value || typeof value !== 'object'){ return; } return new Observer(value);}Dep.target = null;
2、Watcher监视者
class Watcher{ constructor(vm, exp, cb){ this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); } update(){ this.run(); } run(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if(value!==oldVal){ this.value = value; this.cb.call(this.vm, value, oldVal); } } get(){ Dep.target = this; let value = this.vm.data[this.exp]; Dep.target = null; return value; }}
3、Compile编译
class Compile{ constructor(el, vm){ this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init(); } init(){ if(this.el){ this.fragment = this.nodeToFragment(this.el); this.compileElement(this.fragment); this.el.appendChild(this.fragment); }else{ console.log(`Dom元素不存在`); } } nodeToFragment(el){ let fragment = document.createDocumentFragment(); let child = el.firstChild; while(child){ fragment.appendChild(child); child = el.firstChild; } return fragment; } compileElement(el){ let childNodes = el.childNodes; let _this = this; [].slice.call(childNodes).forEach(function(node){ let reg = /\{\{(.*)\}\}/; let text = node.textContent; if(_this.isElementNode(node)){ _this.compile(node); }else if(_this.isTextNode(node) && reg.test(text)){ _this.compileText(node, reg.exec(text)[1]); } if(node.childNodes && node.childNodes.length){ _this.compileElement(node); } }) } compile(node){ let nodeAttr = node.attributes; let _this = this; Array.prototype.forEach.call(nodeAttr,function(attr){ let attrName = attr.name; if(_this.isDirective(attrName)){ let exp = attr.value; let dir = attrName.substring(2); if(_this.isEventDirective(dir)){ _this.compileEvent(node, _this.vm, exp, dir); }else{ _this.compileModel(node, _this.vm, exp, dir); } node.removeAttribute(attrName); } }) } compileText(node, exp){ let _this = this; let initText = this.vm[exp]; this.updateText(node, initText); new Watcher(this.vm, exp, function(value){ _this.updateText(node, value); }) } compileEvent(node, vm, exp, dir){ let eventType = dir.split(':')[1]; let cb = vm.methods && vm.methods[exp]; if(eventType && cb){ node.addEventListener(eventType, cb.bind(vm), false); } } compileModel(node, vm, exp, dir){ let _this = this; let val = this.vm[exp]; this.modelUpdater(node, val); new Watcher(this.vm, exp, function(value){ _this.modelUpdater(node, value); }) node.addEventListener('input', function(e){ let newVal = e.target.value; if(val === newVal){ return; } _this.vm[exp] = newVal; val = newVal; }) } updateText(node, value){ node.textContent = typeof value == 'undefined'?'':value; } modelUpdater(node, value, oldVal){ node.value = typeof value == 'undefined'?'':value; } isDirective(attr){ return attr.indexOf('v-') == 0; } isEventDirective(dir){ return dir.indexOf('on:') == 0; } isElementNode(node){ return node.nodeType == 1; } isTextNode(node){ return node.nodeType == 3; }}
4、Vue
class Vue{ constructor(options){ this.data = options.data; this.methods = options.methods; Object.keys(this.data).forEach(key=>{ this.proxy(key); }) observe(this.data); new Compile(options.el, this); options.mounted.call(this); } proxy(key){ Object.defineProperty(this, key, { enumerable: true, configurable: true, get(){ return this.data[key]; }, set(newVal){ this.data[key] = newVal; } }) }}
5、测试例子
<html lang="cn"><head> <meta charset="UTF-8"> <title>前端咖 Vue设计原理title>head><body> <div id="app"> <h2>{{title}}h2> <input v-model="name"> <h1>{{name}}h1> <button v-on:click="clickMe">click me!button> div> <script> new Vue({ el: '#app', data: { title: 'hello world', name: 'canfoo' }, methods: { clickMe: function () { this.title = 'hello world'; } }, mounted: function () { window.setTimeout(() => { this.title = '你好'; }, 1000); } }); script>body>html>