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源码解读一
vue源码解读二
MVVM实现

如果实现一个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的主要内容,如果未能解决你的问题,请参考以下文章

使用 MVVM 和数据绑定更改每个片段中的工具栏标题

vue系列1:如何定义一个基本的Vue代码结构

vue.js功能学习

vue指令大全~~~

剖析Vue原理&实现双向绑定MVVM

Android MVVM:具有多个片段的活动 - 将共享 LiveData 放在哪里?