virtual dom & mvvm
虚拟dom
- 用js对象来表示dom树的结构,然后用这个对象来构建一个真正的dom树插入文档中;
- 当状态有变时,重新构造一个新的对象树,然后比较新的和旧的树,记录两个数的差异;
- 把差异部分应用到真正的dom树上,更新视图。
核心算法实现(diff算法)
- 用js对象表示dom树
var e = { tagName: 'ul', props: { id: 'list' }, children: [ {tagName: 'li', props: {class: 'li1'}, children: ['item1']}, {tagName: 'li', props: {class: 'li2'}, children: ['item2']}, {tagName: 'li', props: {class: 'li3'}, children: [ {tagName: 'h2', props: {class: 'h'}, children: ['hello qq']} ]}, ] }
- 把js对象渲染成dom树
function dom(tagName, props, children){ function Element(tagName, props, children){ this.tagName = tagName; this.props = props; this.children = children; } Element.prototype.render = function(){ const el = document.createElement(this.tagName); const props = this.props; for(let key in props){ el.setAttribute(key, props[key]); } const children = this.children || []; children.forEach(child => { const c = child.tagName ? new Element(child.tagName, child.props, child.children).render() : document.createTextNode(child); el.appendChild(c); }) return el; } return new Element(tagName, props, children); }
- 比较两个虚拟dom树的差异,同层节点进行比较(时间复杂度O(n));
对每一个树在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比,把差异部分记录到一个对象里面。
// diff 函数,对比两棵树 function diff (oldTree, newTree) { var index = 0 // 当前节点的标志 var patches = {} // 用来记录每个节点差异的对象 dfsWalk(oldTree, newTree, index, patches) return patches } // 对两棵树进行深度优先遍历 function dfsWalk (oldNode, newNode, index, patches) { // 对比oldNode和newNode的不同,记录下来 patches[index] = [...] diffChildren(oldNode.children, newNode.children, index, patches) } // 遍历子节点 function diffChildren (oldChildren, newChildren, index, patches) { var leftNode = null var currentNodeIndex = index oldChildren.forEach(function (child, i) { var newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识 ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点 leftNode = child }) }
- 因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的paches对象中找出当前遍历的节点差异,然后进行 DOM 操作。
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异 var len = node.childNodes ? node.childNodes.length : 0 for (var i = 0; i < len; i++) { // 深度遍历子节点 var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } if (currentPatches) { applyPatches(node, currentPatches) // 对当前节点进行DOM操作 } } function applyPatches (node, currentPatches) { currentPatches.forEach(function (currentPatch) { switch (currentPatch.type) { case REPLACE: node.parentNode.replaceChild(currentPatch.node.render(), node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
Vue之MVVM简单实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>MVVM</title> </head> <body> <div id='app'> <h2>{{ song }}</h2> <h3>{{ obj.name }}</h3> <h3>{{ obj.a }}</h3> <h3>{{ obj.obj1.name1 }}</h3> <input type="text" v-model='msg'> <h3>{{ msg }}</h3> <h2>{{ total }}</h2> </div> <script src="index.js"></script> <script> let vm = new Vm({ el: '#app', data(){ return { song: 'Time', obj: { name: 'xxx', a: 20, b: 3, obj1: { name1: 'll' } }, msg: 'hello' } }, computed: { total() { return this.obj.a * this.obj.b; } }, mounted() { console.log(this.$el) console.log('everything is done') } }) </script> </body> </html>
function Vm(opts = {}){ this.$opts = opts; let data = this.$data = this.$opts.data(); initComputed.call(this); // 数据监测 observer(data); for (let key in data) { Object.defineProperty(this, key, { configurable: true, get() { return this.$data[key]; }, set(newVal) { this.$data[key] = newVal; } }); } // 数据编译 new Compile(opts.el, this); opts.mounted.call(this); } function initComputed(){ let vm = this; let computed = this.$opts.computed; Object.keys(computed).forEach(key => { Object.defineProperty(vm, key, { get: typeof computed[key] === 'function' ? computed[key] : computed[key].get, set(){} }) }) } function observer(data) { if(!data || typeof data !== 'object') return; return new Observer(data); } function Observer(data) { let dep = new Dep(); for (let key in data) { let val = data[key]; observer(val); Object.defineProperty(data, key, { configurable: true, get() { Dep.target && dep.addSub(Dep.target); return val; }, set(newVal) { if(val === newVal) return; val = newVal; observer(newVal); dep.notify(); } }) } } function Compile(el, vm){ vm.$el = document.querySelector(el); var fragment = document.createDocumentFragment(); var child; while(child = vm.$el.firstChild) { fragment.appendChild(child); } function replace(fragment){ Array.from(fragment.childNodes).forEach(item => { let text = item.textContent; let reg = /\{\{(.*?)\}\}/g; if(item.nodeType === 3 && reg.test(text)){ // 重点重点重点!!!!!! // 去掉空格!!!!!!!! function replaceTxt() { item.textContent = text.replace(reg, (matched, placeholder) => { console.log(placeholder); // 匹配到的分组 如:song, album.name, singer... new Watcher(vm, placeholder.trim(), replaceTxt); // 监听变化,进行匹配替换内容 return placeholder.trim().split('.').reduce((val, key) => { return val[key]; }, vm); }); }; replaceTxt(); } if(item.nodeType === 1){ let itemAttr = item.attributes; Array.from(itemAttr).forEach(attr => { let name = attr.name; let exp = attr.value; if(name.includes('v-')){ item.value = vm[exp]; } new Watcher(vm, exp, newVal => { item.value = newVal; }) item.addEventListener('input', e => { vm[exp] = e.target.value; }) }) } if(item.childNodes && item.childNodes.length){ replace(item); } }) } replace(fragment); vm.$el.appendChild(fragment); } // 发布订阅 function Dep(){ this.subs = []; } Dep.prototype = { addSub(sub){ this.subs.push(sub); }, notify(){ this.subs.forEach(sub => { sub.update() }); } } function Watcher(vm, exp, fn){ this.fn = fn; this.vm = vm; this.exp = exp; Dep.target = this; let arr = exp.split('.'); let val = vm; arr.forEach(key => { val = val[key]; }); Dep.target = null; } Watcher.prototype.update = function(){ let arr = this.exp.split('.'); let val = this.vm; arr.forEach(key => { val = val[key]; // 通过get获取到新的值 }); this.fn(val); } // let watcher = new Watcher(() => { // console.log('watch') // }) // let dep = new Dep(); // dep.addSub(watcher); // dep.addSub(watcher); // dep.notify();