博客原文html
本文经过仿照 Vue ,简单实现一个的 MVVM,但愿对你们学习和理解 Vue 的原理有所帮助。vue
nodeType 为 HTML 原生节点的一个属性,用于表示节点的类型。node
Vue 中经过每一个节点的 nodeType 属性是1仍是3判断是元素节点仍是文本节点,针对不一样类型节点作不一样的处理。git
DocumentFragment是一个能够被 js 操做但不会直接出发渲染的文档对象,Vue 中编译模板时是现将全部节点存到 DocumentFragment 中,操做完后再统一插入到 html 中,这样就避免了屡次修改 Dom 出发渲染致使的性能问题。github
Object.defineProperty接收三个参数 Object.defineProperty(obj, prop, descriptor)
, 能够为一个对象的属性 obj.prop t经过 descriptor 定义 get 和 set 方法进行拦截,定义以后该属性的取值和修改时会自动触发其 get 和 set 方法。数组
如下代码的 git 地址: 如下代码的 git 地址
├── vue │ ├── index.js │ ├── obsever.js │ ├── compile.js │ └── watcher.js └── index.html
实现的这个 类 Vue 包含了4个主要模块:app
{{}}
的语法;在 index.html 中是经过 new Vue() 来使用的:frontend
<div id="app"> <input type="text" v-model="msg"> {{ msg }} {{ user.name }} </div> <script> const vm = new Vue({ el: '#app', data: { msg: 'hello', user: { name: 'pan' } } }) </script>
所以入口文件需提供这个 Vue 的类并进行一些初始化操做:dom
class Vue { constructor(options) { // 参数挂载到实例 this.$el = document.querySelector(options.el); this.$data = options.data; if (this.$el) { // 数据劫持 new Observer(this.$data); // 编译模板 new Compile(this.$el, this); } } }
index.js 中调用了 new Compile()
进行模板编译,所以这里须要提供一个 Compile 类:mvvm
class Compile { constructor(el, vm) { this.el = el; this.vm = vm; if (this.el) { // 将 dom 转入 fragment 内存中 const fragment = this.node2fragment(this.el); // 编译 提取须要的节点并替换为对应数据 this.compile(fragment); // 插回页面中去 this.el.appendChild(fragment); } } // 编译元素节点 获取 Vue 指令并执行对应的编译函数(取值并更新 dom) compileElement(node) { const attrs = node.attributes; Array.from(attrs).forEach(attr => { const attrName = attr.name; if (this.isDirective(attrName)) { const expr = attr.value; let [, ...type] = attrName.split('-'); type = type.join(''); // 调用指令对应的方法更新 dom CompileUtil[type](node, this.vm, expr); } }) } // 编译文本节点 判断文本内容包含 {{}} 则执行文本节点编译函数(取值并更新 dom) compileText(node) { const expr = node.textContent; const reg = /\{\{\s*([^}\s]+)\s*\}\}/; if (reg.test(expr)) { // 调用文本节点对应的方法更新 dom CompileUtil['text'](node, this.vm, expr); } } // 递归遍历 fragment 中全部节点判断节点类型并编译 compile(fragment) { const childNodes = fragment.childNodes; Array.from(childNodes).forEach(node => { if (this.isElementNode(node)) { // 元素节点 编译并递归 this.compileElement(node); this.compile(node); } else { // 文本节点 this.compileText(node); } }) } // 循环将 el 中每一个节点插入 fragment 中 node2fragment(el) { const fragment = document.createDocumentFragment(); let firstChild; while (firstChild = el.firstChild) { fragment.appendChild(firstChild); } return fragment; } isElementNode(node) { return node.nodeType === 1; } isDirective(name) { return name.startsWith('v-'); } }
这里利用了 nodeType 区分 元素节点 仍是 文本节点,分别调用了 compileElement 和 compileText。
compileElement 及 compileText 中最终调用了 CompileUtil 的方法更新 dom。
CompileUtil = { // 获取实例上对应数据 getVal(vm, expr) { expr = expr.split('.'); return expr.reduce((prev, next) => { return prev[next]; }, vm.$data); }, // 文本节点需先去除 {{}} 并利用正则匹配多组 getTextVal(vm, expr) { return expr.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (...arguments) => { return this.getVal(vm, arguments[1]); }) }, // 从 vm.$data 上取值并更新节点的文本内容 text(node, vm, expr) { expr.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (...arguments) => { // 添加数据监听,数据变化时调用回调函数 new Watcher(vm, arguments[1], () => { this.updater.textUpdater(node, this.getTextVal(vm, expr)); }) }) this.updater.textUpdater(node, this.getTextVal(vm, expr)); }, // 从 vm.$data 上取值并更新输入框内容 model(node, vm, expr) { // 添加数据监听,数据变化时调用回调函数 new Watcher(vm, expr, () => { this.updater.modelUpdater(node, this.getVal(vm, expr)); }) // 输入框输入时修改 data 中对应数据 node.addEventListener('input', e => { const newValue = e.target.value; this.setVal(vm, expr, newValue); }) this.updater.modelUpdater(node, this.getVal(vm, expr)); }, updater: { textUpdater(node, value) { node.textContent = value; }, modelUpdater(node, value) { node.value = value; } } }
getVal 方法用于处理嵌套对象的属性,如传入表达式 expr 为 user.name
的状况,利用 reduce 从 vm.$data 上拿到。
index.js 中调用了 new Observer()
进行数据劫持,Vue 实例 data 属性的每项数据都经过 defineProperty 方法添加 getter setter 拦截数据操做将其定义为响应式数据,所以这里首先须要提供一个 Observer 类:
class Observer { constructor(data) { // 遍历 data 将每一个属性定义为响应式 this.observer(data); } observer(data) { if (!data || typeof data !== 'object') { return; } for (const [key, value] of Object.entries(data)) { this.defineReactive(data, key, value); // 当属性为对象则需递归遍历 this.observer(value); } } // 定义响应式属性 defineReactive(obj, key, value) { const that = this; const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: false, // 获取数据时调用 get() { // 将 Watcher 实例存入依赖 Dep.target && dep.addSub(Dep.target); return value; }, // 设置数据时调用 set(newVal) { if (newVal !== value) { // 当新值为对象时,需遍历并定义对象内属性为响应式 that.observer(newVal); value = newVal; // 通知依赖更新 dep.notify(); } } }) } }
定义为响应式数据后再对其取值和修改是会触发对应的 get 和 set 方法。
取值时将改值自己返回,并先判断是否有依赖目标 Dep.target,若是有则保存起来。
修改值时先手动将原值修改并通知保存的全部依赖目标进行更新操做。
这里对每项数据都经过建立一个 Dep 类实例进行保存依赖和通知更新的操做,所以须要写一个 Dep 类:
class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify() { this.subs.forEach(watcher => watcher.update()); } }
Dep 中有一个数组,用于保存数据的依赖目标(watcher),notify 遍历全部依赖并调用其 update 方法进行更新。
经过上面的 Observer 能够知道,每项数据在被调用时可能会有依赖目标,依赖目标须要被保存并在取值时调用 notify 通知更新,且经过 Dep 能够知道依赖目标是一个有 update 方法的对象实例。
所以须要建立一个 Watcher 类:
class Watcher { constructor(vm, expr, cb) { this.vm = vm; this.expr = expr; this.cb = cb; // 记录旧值 this.value = this.get(); } getVal(vm, expr) { expr = expr.split('.'); return expr.reduce((prev, next) => { return prev[next]; }, vm.$data); } get() { Dep.target = this; // 获取 data 会触发对应数据的 get 方法,get 方法中从 Dep.target 拿到 Watcher 实例 let value = this.getVal(this.vm, this.expr); Dep.target = null; return value; } // 对外暴露的方法,获取新值与旧值对比后若不一样则触发回调函数 update() { let newValue = this.getVal(this.vm, this.expr); let oldValue = this.value; if (newValue !== oldValue) { this.cb(newValue); } } }
依赖目标就是 Watcher 的实例,对外提供了 update 方法,调用 update 时会从新根据表达式 expr 取值与老值对比并调用回调函数。
这里的回调函数就是对应的更新 dom 的方法,在 compile.js 中的 model 及 text 方法中有执行 new Watcher()
,在模板解析时就为每项数据添加了监听:
model(node, vm, expr) { // 添加数据监听,数据变化时调用回调函数 new Watcher(vm, expr, () => { this.updater.modelUpdater(node, this.getVal(vm, expr)); }) this.updater.modelUpdater(node, this.getVal(vm, expr)); },
Watcher 中很巧妙的一点就是,模板编译以前已经将全部添加了数据拦截,在 Watcher 的 get 方法中调用 getVal 取值时会触发该数据的 getter 方法,所以这里在取值前经过 Dep.target = this;
将该 Watcher 实例暂存,对应数据的 getter 方法中又将该实例做为依赖目标保存到了自身对应的 Dep 实例中。
这样就实现了一个简易的 MVVM 原理,里面的一些思路仍是很是值得反复体会学习的。