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了

原文地址:https://www.cnblogs.com/pengdt/p/12046431.html

时间: 2024-10-08 20:54:44

vue的MVVM的相关文章

vue和mvvm的一些小区别

Vue.js 和 MVVM 小细节 MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定. Vue.js 是一个提供了 MVVM 风格的双向数据绑定的 Javascript 库,专注于View 层.它的核心是 MVVM 中的 VM,也就是 ViewModel. ViewModel负责连接 View 和 Mode

对类Vue的MVVM前端库的实现

关于实现MVVM,网上实在是太多了,本文为个人总结,结合源码以及一些别人的实现 关于双向绑定 vue 数据劫持 + 订阅 - 发布 ng 脏值检查 backbone.js 订阅-发布(这个没有使用过,并不是主流的用法) 双向绑定,从最基本的实现来说,就是在defineProperty绑定的基础上在绑定input事件,达到v-model的功能 代码思路图 两个版本: 简单版本: 非常简单,但是因为是es6,并且代码极度简化,所以不谈功能,思路还是很清晰的 标准版本: 参照了Vue的部分源码,代码的

手写实现vue的MVVM响应式原理

MVVM响应式实现原理: 1.模板编译 2.数据劫持 3.watcher 文中应用到的数据名词: MVVM   ------------------        视图-----模型----视图模型                三者与 Vue 的对应:view 对应 template,vm 对应 new Vue({…}),model 对应 data nodeType       判断节点是否是元素节点 querySelector    创建一个元素节点 createDocumentFragme

VUE的MVVM框架解析

这篇文章主要介绍了MVVM模式中ViewModel和View.Model有什么区别?本文分别解释了它们的功能和作用,然后总结了它之间的区别,需要的朋友可以参考下 Model:很简单,就是业务逻辑相关的数据对象,通常从数据库映射而来,我们可以说是与数据库对应的model. View:也很简单,就是展现出来的用户界面. 基本上,绝大多数软件所做的工作无非就是从数据存储中读出数据,展现到用户界面上,然后从用户界面接收输入,写入到数据存储里面去.所以,对于数据存储(model)和界面(view)这两层,

手动实现一个vue的mvvm,思路解析

1.解析dom.fragment编译,初始化new watcher 2 ,数据劫持,Object.defineProperty(obj,key,{ configurable:true,// 可以配置 enumerable:true, // 可以枚举 get:function(){return value}, // 添加wacher,添加订阅者 set:function(newValue){ return newValue}  // noticfy:执行,更新数据 }) 3, 发布订阅模式: 什么

MVVM大比拼之vue.js源码精析

VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多来自knockout.angularjs等大牌框架影响,但作者相信 vue 在性能.易用性方面是有优势.同时也自己做了和其它框架的性能对比,在这里.今天以版本 0.10.4 为准 入口 Vue 的入口也很直白: ? 1 var demo = new Vue({ el: '#demo', data: { message: 'Hello V

1000行代码实现MVVM (类似Angular1.x.x , Vue)

最近花了近半个多月的时间, 自己纯手工写了一个很小型的类angularjs/vue的mvvm 库. 目前已经用于公司一个项目. 项目托管在github https://github.com/leonwgc/link 也许有许多人觉得如今angularjs , react , vue , knockout ,avalon 等框架/库层出不穷, 为什么还要自己造一个相同(类似)的轮子?  原因如下: 1 . 从最初knockoutjs 到现在用angularjs ,写了不少项目, 一直想自己写一个m

vue.js组件化开发实践

前言 公司以往制作一个H5活动,特别是有一定统一结构的活动都要每次用html.css.js滚一次重复的轮子,费时费力.后来接到了一个基于模板的活动发布系统的需求,于是就有了下面的内容. 开始 需求一到,接就是怎么实现,技术选型自然成为了第一个问题.鉴于目前web前端mvvm框架的流行,以及组件化开发方式的出现,决定采用vue进行开发. 这里首先简单说下web前端组件化开发方式的历程: 最早的组件化结构,或者叫做组件化1.0时代,代码结构可能如下: 1 - lib/components/calen

理解Vue

Vue.js是JavaScript MVVM(Model-View-ViewModel)库,十分简洁,Vue核心只关注视图层,相对AngularJS提供更加简洁.易于理解的API.Vue尽可能通过简单的API实现响应的数据绑定和组合的视图组件. Vue和MVVM模式 MVVM模式即Model-View-ViewModel. Vue是以数据为驱动的,Vue自身将DOM和数据进行绑定,一旦创建绑定,DOM和数据将保持同步,每当数据发生变化,DOM会跟着变化. ViewModel是Vue的核心,它是V