我们比较耳熟能详的mvc模式,用户action触发view,在通知到control,control再修改到model,model收到修改数据,通知到control,再反馈到view上。
mvvm即model view viewModel
model是数据,view是视图,viewmodel是视图模型,model改变能够反馈到view上,实现方式是数据绑定,view动作能够监听并反馈到model上,实现方式是dom事件监听。那上述两种方式都实现就是双向数据绑定,比如vue,react通过state改变能够触发view更新,实现单向数据绑定。
下面我们来总结一下,在mvvm模式下视图模型是不能直接通信的,通过viewmodel连接起来,通常情况下会通过对象劫持方式实现model改变,触发视图修改,通过节点事件监听方式实现视图修改同时修改model。
对象劫持(观察者-订阅)
普通的data对象传入双向数据绑定架构中比如vue,vue将data中对象遍历并通过object.defineproperties,转换为setter和getter方法。当访问data属性时会触发getter方法,当修改data属性时会触发setter方法。
vue的数据双向绑定是通过数据劫持和发布-订阅者功能来实现的
实现步骤:1.实现一个监听者Oberver来劫持并监听所有的属性,一旦有属性发生变化就通知订阅者
2.实现一个订阅者watcher来接受属性变化的通知并执行相应的方法,从而更新视图
3.实现一个解析器compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相对应的订阅
下面通过一个例子说明下
(function(){
const data = {
display: '',
}
Object.defineProperties(data, {
display: {
set: function(v) {
console.log('set display value')
// 事件分发
publishDisplay(v);
}
}
})
document.querySelector('#bindingInput').addEventListener('keyup', function(e) {
data.display = this.value;
});
function publishDisplay(v) {
document.querySelector('#bindingDisplay').innerHTML = v;
}
})();
或者使用es6 proxy对象完成
(function(){
const data = {
display: '',
}
const handler = {
get: function(target, key, proxy){
console.log('get display value');
return target[key];
},
set: function(target, key, value, proxy) {
console.log('set display value');
target[key] = value;
publishDisplay();
}
}
const proxy = new Proxy(data, handler);
document.querySelector('#bindingInputES6').addEventListener('keyup', function(e) {
proxy.display = this.value;
});
function publishDisplay(v) {
document.querySelector('#bindingDisplayES6').innerHTML = proxy.display;
}
})();
下面讲述MVVM中虚拟节点——visual dom
在前面文章中,我们讲过操作节点是非常耗费性能的,频繁的操作节点导致用户的体验性非常差,所以我们有各种各样的优化措施,下面要讲述的visual dom也是其中一种,而且这种方式效率更高。
首先,我们要知道,网页上的元素和层级关系,可以通过树形的结构展示出来,比如:
const element = {
tagName:'body',
props:{
style:{
width: '200px',
}
},
children: [
{
tagName:'div',
props:{
height: '100%'
},
children :[{...}]
}
]
}
这种js展示出来的方式非常简洁,方便,而且效率特别高,关于效率这个,我们可以对比一下document.createElement这个方法,这里面做了很多事情,包括节点原型、事件绑定等等。所以效率都是相对而言,是非常高。
react在初始化的时候,会建立一个基于js树形结构的虚拟节点树,之后一旦修改数据,频发操作节点的时候,react在每次更新节点的时候,会新建一颗虚拟节点树,然后对新旧节点树进行diff比较,把差异部分找出来,然后交给render方法,渲染到实际的页面上去。
diff算法:传统的diff算法是O(n^3),这个显然满足不了当前的效率要求,所以react对diff进行优化,只做同层比较,也就是说,只对两棵树的同级进行比较,比较类型,如果是字符串,则直接比较是否全等,否则如果是标签,先比较标签名称,然后是树形比较,然后在递归遍历子级节点,如果节点名称或者key都不相同,则直接替换本级以及下级节点。
function dfsWalk (oldNode, newNode, index, patches) {
var currentPatch = []
// Node is removed.
if (newNode === null) {
// Real DOM node will be removed when perform reordering, so has no needs to do anthings in here
// TextNode content replacing
} else if (_.isString(oldNode) && _.isString(newNode)) {
if (newNode !== oldNode) {
currentPatch.push({ type: patch.TEXT, content: newNode })
}
// Nodes are the same, diff old node's props and children
} else if (
oldNode.tagName === newNode.tagName &&
oldNode.key === newNode.key
) {
// Diff props
var propsPatches = diffProps(oldNode, newNode)
if (propsPatches) {
currentPatch.push({ type: patch.PROPS, props: propsPatches })
}
// Diff children. If the node has a `ignore` property, do not diff children
if (!isIgnoreChildren(newNode)) {
diffChildren(
oldNode.children,
newNode.children,
index,
patches,
currentPatch
)
}
// Nodes are not the same, replace the old node with new node
} else {
currentPatch.push({ type: patch.REPLACE, node: newNode })
}
网友评论