资讯专栏INFORMATION COLUMN

自己实现MVVM(Vue源码解析)

?xiaoxiao, / 2991人阅读

摘要:无论是双向绑定还是单向绑定,都是符合思想的。看了的源码后不难发现的双向绑定的实现也就是在表单元素上添加了事件,可以说双向绑定是单向绑定的一个语法糖。

前言

本文会带大家手动实现一个双向绑定过程(仅仅涵盖一些简单的指令解析,如:v-textv-model,插值),当然借鉴的是Vue1的源码,相信大家在阅读完本文后对Vue1会有一个更好的理解,源代码放到了github,由于本人水平有限,理解不到位的地方还请大家指出。

MVVM

MVVM使开发可以更加关注于数据,减少了很大的工作量,也使代码可读性,可维护性更高,MVVM核心的思想就是视图是状态的函数:View = ViewModel(Model),所以当Model发生改变时,ViewModel会来操作View来怎么做,而非是自己写代码来做。无论是双向绑定还是单向绑定,都是符合MVVM思想的。Vue提倡的是双向绑定,也就是允许View到Model的变化,其实这个场景出现在的也就是表单操作上,看个例子,例子中分别利用了Vue和React实现了一下表单value变化,影响页面与其相关的dom节点发生变化,可以发现的是双向绑定的Vue是inputvalue发生变化则h1innerText就发生了变化,变化是由View->Model,而提倡单向数据流的React需要手动监听事件,事件触发后,更改Model的值,从而使inputvalue发生了变化。看了Vue的源码后不难发现Vue的双向绑定的实现也就是在表单元素上添加了input事件,可以说双向绑定是单向绑定的一个语法糖。

实现思路

上图是一个大体的流程,下面按照流程来实现下:

利用observerdata进行了监听,并且提供订阅某个数据项的变化的能力

这点的实现,需要借助的是Object.defineProperty()来为对象的属性绑定get/set特性(由于利用了Object.defineProperty(),所以Vue不支持ie8),observer需要将data的所有属性都绑定get/set,很容易想到的就是利用递归来实现,具体代码就不贴出,请参见这里。

利用Compile对模板进行解析

这点实现的是将我们的模板转化为html,过程中会将数据与View中的节点相关联起来,最终会将编译好的html页面替换到页面上。首先来看解析,首先从根节点开始,根据不同的节点类型采用不同的解析方式:

function compileNode(node, vm) {
    const type = node.nodeType;
    if (type === 1 && !isScript(node)) {
        compileElement(node, vm);
    } else if (type === 3 && node.data.trim()) {
        compileTextNode(node, vm);
    } else {
        return null;
    }
}

对于文本节点来说,可能存在情况只有两种

与数据不相关不用操作

含有插值,需要与数据进行关联

{{}}文本插值

{{{}}}html插值

利用下面正就可以将插值找出:

/{{{(.*?)}}}|{{(.*?)}}/g

采用下面函数来对文本节点的内容解析:

function parseText(node) {
    var text = node.wholeText;
    if (!tagRE.test(text)) {
        return void 0;
    }
    const tokens = [];
    var lastIndex = tagRE.lastIndex = 0,
        match, index, html, value;
    while (match = tagRE.exec(text)) {
        index = match.index;
        if (index > lastIndex) {
            tokens.push({
                value: text.slice(lastIndex, index)
            })
        }
        html = htmlRE.test(match[0]);
        value = html ? match[1] : match[2];
        tokens.push({
            value: value,
            tag: true,
            html: html
        });
        lastIndex = index + match[0].length;
    }
    if (lastIndex < text.length) {
        tokens.push({
            value: text.slice(lastIndex)
        })
    }
    return tokens;
}

返回了tokens,里面存储了每一个块内容,一个插值or一个普通文本,tag来标记是否为插值,html来标记是否为纯html插值。遍历返回的tokens,根据不同的类型,来采用不同的方式将其添加到其父节点上:

function compileTextNode(node, vm) {
    const tokens = parseText(node);
    if (tokens == null) return void 0;
    var frag = document.createDocumentFragment();
    tokens.forEach(token => {
        var el;
        if (token.tag) {
            if (token.html) {
                el = document.createDocumentFragment();
                el.$parent = node.parentNode;
                el.$oneTime = true;
                dirCollection["html"](el, vm, token.value);
            } else {
                el = document.createTextNode(" ");
                dirCollection["text"](el, vm, token.value);
            }
        } else {
            el = document.createTextNode(token.value);
        } 
        el && frag.appendChild(el);
    });
    return replace(node, frag);
}

dirCollection是一个指令集合,也就是决定了如何初始化以及如何更新该节点。对于nodeType1的节点来说,指令全部存储在其属性中,遍历属性,假若指令中含有v-html,v-model,v-text,则停止遍历其子树,直接将调用相应指令即可,否则,则需要遍历其子节点,对其子节点应用compileNode进行解析:

function compileNodeList(nodes, vm) {
    for (let val of nodes) {
        compileNode(val, vm);
    }
}
function compileElement(node, vm) {
    var flag = false;
    const attrs = Array.prototype.slice.call(node.attributes);
    attrs.forEach((val) => {
        const name = val.name,
            value = val.value;
        if (dirRE.test(name)) {
            var dir;
            // 事件指令
            if (
                (dir = name.match(eventRE)) && 
                (dir = dir[1])
            ) {
                dirCollection["eventDir"](node, dir, vm, value);
            } else {
                dir = name.match(dirRE)[1];
                dirCollection[dir](node, vm, value);
            }
            // 指令中为v-html or v-text or v-model终止递归
            flag = flag || 
                name === vhtml || 
                name === vtext;    
            node.removeAttribute(name);
        }    
    });
    const childs = node.childNodes;
    if (!flag && childs && childs.length) {
        compileNodeList(childs, vm);
    }
}

dirCollections中还会做的就是将数据与View的dom节点相关联,利用的就是DepWatcher,页面上每一个与数据相关联的节点都含有一个Watcher,当数据发生变化是Watcher用于计算,是否需要更新该节点;数据的每一个属性都有一个Dep,当该属性发生变化时,Dep会通知与该数据相关联的Watcher来进行计算是否需要更新对应页面。Dep代码,Watcher代码。

异步更新队列

异步更新队列,是一个优化,将更新dom的操作变为异步的,放到下一个事件循环来做,这样做可以减少不必要的dom更新,看下面情况:

vm.value++;
vm.value++;
vm.value++;

三次数据改变,假若同步更新的话,则每次数据改变会立即更新dom,而异步更新的话,可以先将更新推入一个队列中,由于是异步,也可以保证每一个Watcher只被推入到一次,这样就避免了不必要的更新,异步更新主要利用的是nextTick,这个函数会优先使用Promise,不兼容则利用MutationObserver,再不兼容的话会利用setTimeout

写在后面

看过了Vue的源码不得不感叹Vue的优美,而Vue2又增加了虚拟dom,这样就可以做到服务端渲染,给了我们更多的可能!

这篇博客最好配合着源码来看,关于源码欢迎star

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91287.html

相关文章

  • JavaScript从初级往高级走系列————MVVM-Vue

    摘要:原文博客地址如何理解如何实现是否解读过的源码与框架的区别实现实现独立初始化实例两者的区别数据和视图的分离,解耦开放封闭原则,对扩展开放,对修改封闭在中在代码中操作视图和数据,混在一块了以数据驱动视图,只关心数据变化, 原文博客地址:https://finget.github.io/2018/05/31/mvvm-vue/ MVVM 如何理解 MVVM 如何实现 MVVM 是否解读过 ...

    codercao 评论0 收藏0
  • 剖析Vue原理&实现双向绑定MVVM

    摘要:所以无需太过介怀是实现的单向或双向绑定。监听数据绑定更新函数的处理是在这个方法中,通过添加回调来接收数据变化的通知至此,一个简单的就完成了,完整代码。 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也...

    melody_lql 评论0 收藏0
  • JavaScript 进阶之深入理解数据双向绑定

    摘要:当我们的视图和数据任何一方发生变化的时候,我们希望能够通知对方也更新,这就是所谓的数据双向绑定。返回值返回传入函数的对象,即第一个参数该方法重点是描述,对象里目前存在的属性描述符有两种主要形式数据描述符和存取描述符。 前言 谈起当前前端最热门的 js 框架,必少不了 Vue、React、Angular,对于大多数人来说,我们更多的是在使用框架,对于框架解决痛点背后使用的基本原理往往关注...

    sarva 评论0 收藏0
  • 剖析Vue实现原理 - 如何实现双向绑定mvvm(转载)

    摘要:接下来要看看这个订阅者的具体实现了实现订阅者作为和之间通信的桥梁,主要做的事情是在自身实例化时往属性订阅器里面添加自己自身必须有一个方法待属性变动通知时,能调用自身的方法,并触发中绑定的回调,则功成身退。 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模块2、缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,...

    nemo 评论0 收藏0

发表评论

0条评论

?xiaoxiao,

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<