vue的MVVM
vue的相关知识有
- MVVM
- 虚拟dom和domdiff
- 字符串模板
MVVM
MVVM 设计模式,是由 MVC(最早来源于后端)、MVP 等设计模式进化而来
- M - 数据模型(Model)
- VM - 视图模型(ViewModel)
- V - 视图层(View)
在 Vue 的 MVVM 设计中,我们主要针对Compile
(模板编译),Observer
(数据劫持),Watcher
(数据监听),Dep
(发布订阅)几个部分来实现,核心逻辑流程可参照下图:
数据监听API
- vue2.0和vue2.x是用
defineProperty
- vue3.0即将使用
proxy
为什么要改用proxy,因为defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,defineProperty需要判断如果是数组,需要重写他的原型方法,而proxy就不需要
为什么还不上线,因为proxy的兼容性太差
defineProperty监听
// 监听普通属性 function isKey(obj,key){ return Object.defineProperty(obj,key,{ get: function() { console.log('get :', key); return eval(key) || ""; }, set: function(newValue) { console.log('set :', newValue); key = newValue; } }) } // 监听数组属性 function toNewArray(data,key){ // 实例具名回调函数 window.eval("var callback = function "+key+" (args,k){console.log('数组'+k+'发生变化...');}") return new NewArray(data[key],callback) // 注入回调函数 } class NewArray extends Array{ constructor(arr,callback){ if(arguments.length === 1){ return super() } // 产生中间数组会再进入构造方法 // let args = arr // 原数组 arr.length === 1 ? super(arr[0].toString()) : super(...arr) this.callback = callback // 注入回调具名函数 } push(...args){ super.push(...args) this.callback(this, this.callback.name) // 切面调用具名回调函数 } pop(){ super.pop() this.callback(this, this.callback.name) } splice(...args){ super.splice(...args) this.callback(this, this.callback.name) } } var data = { arr:[1,2,3,4], name:"pdt" } function init(data){ Object.keys(data).forEach(key => { let value = data[key] // 如果是obj就递归 if(value是对象){ init(value) }else if(Array.isArray(value)){ // 如果value是数组 data[key] = toNewArray(data,key) }else{ // 如果是普通的值 isKey(data,key) } }) } init(data)
proxy监听
var data = { arr:[1,2,3,4], name:"pdt" } function init(data){ Object.keys(data).forEach(key => { let value = data[key] if(value 是对象){ data[key] = init(value) } }) data = newData(data) } init(data) function newData(data){ return new Proxy(data, { get: function(target, key, receiver) { console.log(target, key, receiver) return Reflect.get(target, key, receiver); }, set: function(target, key, value, receiver) { console.log(target, key, value, receiver); return Reflect.set(target, key, value, receiver); } }) }
使用proxy写一个简易版的vue
<div id="app"> <input type="text" v-model='count' /> <input type="button" value="增加" @click="add" /> <input type="button" value="减少" @click="reduce" /> <div v-html="count"></div> </div> <script type="text/javascript"> class Vue { constructor(options) { this.$el = document.querySelector(options.el); this.$methods = options.methods; this._binding = {}; this._observer(options.data); this._compile(this.$el); } _pushWatcher(watcher) { if (!this._binding[watcher.key]) { this._binding[watcher.key] = []; } this._binding[watcher.key].push(watcher); } /* observer的作用是能够对所有的数据进行监听操作,通过使用Proxy对象 中的set方法来监听,如有发生变动就会拿到最新值通知订阅者。 */ _observer(datas) { const me = this; const handler = { set(target, key, value) { const rets = Reflect.set(target, key, value); me._binding[key].map(item => { item.update(); }); return rets; } }; this.$data = new Proxy(datas, handler); } /* 指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的更新函数 */ _compile(root) { const nodes = Array.prototype.slice.call(root.children); const data = this.$data; nodes.map(node => { if (node.children && node.children.length) { this._compile(node.children); } const $input = node.tagName.toLocaleUpperCase() === "INPUT"; const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA"; const $vmodel = node.hasAttribute('v-model'); // 如果是input框 或 textarea 的话,并且带有 v-model 属性的 if (($vmodel && $input) || ($vmodel && $textarea)) { const key = node.getAttribute('v-model'); this._pushWatcher(new Watcher(node, 'value', data, key)); node.addEventListener('input', () => { data[key] = node.value; }); } if (node.hasAttribute('v-html')) { const key = node.getAttribute('v-html'); this._pushWatcher(new Watcher(node, 'innerHTML', data, key)); } if (node.hasAttribute('@click')) { const methodName = node.getAttribute('@click'); const method = this.$methods[methodName].bind(data); node.addEventListener('click', method); } }); } } /* watcher的作用是 链接Observer 和 Compile的桥梁,能够订阅并收到每个属性变动的通知, 执行指令绑定的响应的回调函数,从而更新视图。 */ class Watcher { constructor(node, attr, data, key) { this.node = node; this.attr = attr; this.data = data; this.key = key; } update() { this.node[this.attr] = this.data[this.key]; } } </script> <script type="text/javascript"> new Vue({ el: '#app', data: { count: 0 }, methods: { add() { this.count++; }, reduce() { this.count--; } } }); </script>
虚拟dom和domdiff
上面的简易版代码的dom是没有被重新部署的,但是真正的vue是看不到原来写在app里的标签的,因为vue用了虚拟dom进行记录,再渲染新的dom到页面上,并且每个新dom都会有一个【data-编码】作为标识好找到虚拟dom
{ tag:"div", parend:"#app", dataId:"data123", child:[{ tag:"input-text", parend: "data123", dataId:"data6145", v-model: "name" },{ tag:"text", parend: "data123", dataId:"data112", v-text:"我的名字是{{name}}" },{ tag:"div", parend: "data123", v-for:"value,index in arr", // 这个for数组就是domDiff要对比的 for:[{ value:"tom", dataId:"data412", text:"我的名字是{{value}}" },{ value: "mary", dataId:"data162", text:"我的名字是{{value}}" }] } }
然后再根据上面的虚拟dom生成普通的dom添加到页面上去,在遍历的时候给data添加数据监听,一旦数据变化,相应的dataId就要做出对于的改变,如果是修改了数组,需要先生成一批新的虚拟dom,跟旧的虚拟dom进行对比,虚拟dom是需要算法才能理解的,上几个原理图,和链接自己去理解
Tree DIFF是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁。如图所示,左边是旧属,右边是新属,第一层是R组件,一模一样,不会发生变化;第二层进入Component DIFF,同一类型组件继续比较下去,发现A组件没有,所以直接删掉A、B、C组件;继续第三层,重新创建A、B、C组件。
Component Diff第一层遍历完,进行第二层遍历时,D和G组件是不同类型的组件,不同类型组件直接进行替换,将D删掉,再将G重建
Element DIFF紧接着以上统一类型组件继续比较下去,常见类型就是列表。同一个列表由旧变新有三种行为,插入、移动和删除,它的比较策略是对于每一个列表指定key,先将所有列表遍历一遍,确定要新增和删除的,再确定需要移动的。如图所示,第一步将D删掉,第二步增加E,再次执行时A和B只需要移动位置即可,就是说key增加了dom的复用率
domDiff第一篇
domDiff第二篇
domDiff第三篇
domDiff第四篇
domDiff第五篇
// diff算法的实现 function diff(oldTree, newTree) { // 差异收集 let pathchs = {} dfs(oldTree, newTree, 0, pathchs) return pathchs } function dfs(oldNode, newNode, index, pathchs) { let curPathchs = [] if (newNode) { // 当新旧节点的 tagName 和 key 值完全一致时 if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 继续比对属性差异 let props = diffProps(oldNode.props, newNode.props) curPathchs.push({ type: 'changeProps', props }) // 递归进入下一层级的比较 diffChildrens(oldNode.children, newNode.children, index, pathchs) } else { // 当 tagName 或者 key 修改了后,表示已经是全新节点,无需再比 curPathchs.push({ type: 'replaceNode', node: newNode }) } } // 构建出整颗差异树 if (curPathchs.length) { if(pathchs[index]){ pathchs[index] = pathchs[index].concat(curPathchs) } else { pathchs[index] = curPathchs } } } // 属性对比实现 function diffProps(oldProps, newProps) { let propsPathchs = [] // 遍历新旧属性列表 // 查找删除项 // 查找修改项 // 查找新增项 forin(olaProps, (k, v) => { if (!newProps.hasOwnProperty(k)) { propsPathchs.push({ type: 'remove', prop: k }) } else { if (v !== newProps[k]) { propsPathchs.push({ type: 'change', prop: k , value: newProps[k] }) } } }) forin(newProps, (k, v) => { if (!oldProps.hasOwnProperty(k)) { propsPathchs.push({ type: 'add', prop: k, value: v }) } }) return propsPathchs } // 对比子级差异 function diffChildrens(oldChild, newChild, index, pathchs) { // 标记子级的删除/新增/移动 let { change, list } = diffList(oldChild, newChild, index, pathchs) if (change.length) { if (pathchs[index]) { pathchs[index] = pathchs[index].concat(change) } else { pathchs[index] = change } } // 根据 key 获取原本匹配的节点,进一步递归从头开始对比 oldChild.map((item, i) => { let keyIndex = list.indexOf(item.key) if (keyIndex) { let node = newChild[keyIndex] // 进一步递归对比 dfs(item, node, index, pathchs) } }) } // 列表对比,主要也是根据 key 值查找匹配项 // 对比出新旧列表的新增/删除/移动 function diffList(oldList, newList, index, pathchs) { let change = [] let list = [] const newKeys = getKey(newList) oldList.map(v => { if (newKeys.indexOf(v.key) > -1) { list.push(v.key) } else { list.push(null) } }) // 标记删除 for (let i = list.length - 1; i>= 0; i--) { if (!list[i]) { list.splice(i, 1) change.push({ type: 'remove', index: i }) } } // 标记新增和移动 newList.map((item, i) => { const key = item.key const index = list.indexOf(key) if (index === -1 || key == null) { // 新增 change.push({ type: 'add', node: item, index: i }) list.splice(i, 0, key) } else { // 移动 if (index !== i) { change.push({ type: 'move', form: index, to: i, }) move(list, index, i) } } }) return { change, list } }
字符串模板
function render(template, data) { const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 if (reg.test(template)) { // 判断模板里是否有模板字符串 const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段 template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 return render(template, data); // 递归的渲染并返回渲染后的结构 } return template; // 如果模板没有模板字符串直接返回 } // 使用 let template = '我是{{name}},年龄{{age}},性别{{sex}}'; let data = { name: '姓名', age: 18 } render(template, data); // 我是姓名,年龄18,性别undefined
如果实现一个vue
- 把data复制一个出来叫做Deps,结构一定要一样
- data递归遍历给每个key添加监听,创建Dep更新方法存储对象,Dep对象是放在Deps对象上的,格式跟data一样,一旦数据改变,去执行Deps相同结构位置上的Dep的updata方法,Dep对象就是一个闭包的数组,数组用来存更新方法,还有个updata方法,用来遍历这个闭包的数组
data:{ name: "name", obj:{ arr: [1,2,3] age: 18 } } Deps:{ name: Dep, obj:{ arr: Dep age: Dep } }
- 解析
template
的vue指令,变成vnode,虚拟dom - 遍历虚拟dom数据,生成新的dom,再结合data数据,methods,计算属性,watch,数据绑定到新的dom上,数据更新的方法就是push到Dep对象的数组里,这个就是订阅,更新就是发布,发布订阅就是观察者也就是watcher,所以Dep对象的数组里装着很多的观察者
[watcher,watcher...]
- 结合上domDiff【如果使用proxy,就不需要domdiff了】,就是一个真正的vue了