vue的MVVM
Posted pengdt
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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了
以上是关于vue的MVVM的主要内容,如果未能解决你的问题,请参考以下文章