vue数据响应式的实现(附图)

根据对vue源码的理解,对vue的数据响应式做一个简单的实现。
定义myvue,使用方式仿造vue,简单实现插值表达式、数据双向绑定、事件及指令。
直接上代码
创建index.html,代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title></title>
</head>

<body>
    <div id="app">

        <!-- 插值 -->
        <p>{{title}}</p>

        <!-- 双向绑定 -->
        <input type="text" my-model="name" />

        <!-- 指令 -->
        <p my-text="name"></p>

        <!-- 事件 -->
        <button @click="clear">清空</button>

        <!-- html -->
        <div my-html="html"></div>
    </div>

    <script src='./myvue.js'></script>
    <script src='./compile.js'></script>
    <script>
        const myvue = new MyVue({
            el: '#app',
            data: {
                title: "vue响应式",
                name: "_BuzzLy",
                html: "<button>按钮</button>"
            },
            methods: {
                clear() {
                    this.name = "";
                }
            },
        })
    </script>
</body>

</html>

创建myvue.js,主要作用是数据的响应化,代码如下:

class MyVue {
    constructor(options) {
        this.$options = options;
        this.$data = options.data;

        this.observe(this.$data);
        
        // 在compile.js中实现
        new Compile(options.el, this);
    }

    observe(data) {
        if (!data || typeof data !== 'object')
            return;

        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]);
            this.proxyData(key);
        })
    }

    // 数据响应化
    defineReactive(data, key, value) {
        this.observe(value);

        var dep = new Dep();
        Object.defineProperty(data, key, {
            get() {
                Dep.target && dep.addDep(Dep.target);
                return value;
            },
            set(newValue) {
                if (value === newValue) return;
                value = newValue;
                dep.notify();
            }
        })
    }

    // 代理data中的属性到vue实例上
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key];
            },
            set(newValue) {
                this.$data[key] = newValue;
            }
        })
    }

}

// 依赖收集器,管理Watcher
class Dep {
    constructor() {
        // 用来存放watcher
        this.deps = [];
    }

    addDep(dep) {
        this.deps.push(dep);
    }

    notify() {
        this.deps.forEach(dep => dep.update())
    }
}

// Watcher 订阅者
class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm;
        this.key = key;
        this.callback = callback;

        // 将当前实例指向Dep类的静态属性target
        Dep.target = this;
        this.vm[this.key];
        Dep.target = null;
    }

    update() {
        this.callback.call(this.vm, this.vm[this.key]);
    }
}

创建compile.js,主要作用是编译,代码如下:

class Compile {
    constructor(el, vm) {
        this.$el = document.querySelector(el);
        this.$vm = vm;

        if (this.$el) {
            this.$fragment = this.node2Fragment(this.$el);
            // 核心——编译(处理html模板,解析指令事件等,以及收集相关依赖)
            this.compile(this.$fragment);
            this.$el.appendChild(this.$fragment);
        }
    }

    // 将dom结构转换为fragment片段进行操作,提升性能
    node2Fragment(el) {
        const frag = document.createDocumentFragment();

        let child;
        while (child = el.firstChild) {
            frag.appendChild(child);
        }
        return frag;
    }

    compile(el) {
        const childNodes = el.childNodes;
        // 遍历所有节点
        Array.from(childNodes).forEach(node => {
            // 元素节点
            if (this.isElement(node)) {
                const nodeAttrs = node.attributes;
                // 遍历所有属性
                Array.from(nodeAttrs).forEach(attr => {
                    const attrName = attr.name;
                    const attrVal = attr.value;
                    // 指令
                    if (this.isDirective(attrName)) {
                        const dirName = attrName.substring(3);
                        this[dirName] && this[dirName](node, this.$vm, attrVal);
                    } else if (this.isEvent(attrName)) { // 事件
                        const eventName = attrName.substring(1);
                        this.eventHandler(node, this.$vm, attrVal, eventName);
                    }
                })
            } else if (this.isText(node)) { // 文本节点
                this.compileText(node);
            }

            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node);
            }
        })
    }

    // 调用指令方式初始化,并收集依赖
    update(node, vm, val, dir) {
        const updateFn = this[dir + 'Updater'];
        updateFn && updateFn(node, vm[val]);

        new Watcher(vm, val, function (value) {
            updateFn && updateFn(node, value);
        });
    }

    // 处理插值表达式
    compileText(node) {
        this.update(node, this.$vm, RegExp.$1, "text");
    }

    // text指令方法
    text(node, vm, attrVal) {
        this.update(node, vm, attrVal, "text");
    }

    // text指令更新函数
    textUpdater(node, value) {
        console.log('set:' + value)
        node.textContent = value;
    }
    
    // model指令方法
    model(node, vm, attrVal) {
        // 指定input的value属性,模型对视图的响应
        this.update(node, vm, attrVal, "model");

        // 视图对模型响应
        node.addEventListener('input', function (e) {
            vm[attrVal] = e.target.value;
        })
    }

    // 双向绑定更新函数
    modelUpdater(node, value) {
        node.value = value;
    }
    
    // html指令方法
    html(node, vm, attrVal) {
        this.update(node, vm, attrVal, "html");
    }

    // html更新函数
    htmlUpdater(node, value) {
        node.innerHTML = value;
    }

    // 事件处理函数
    eventHandler(node, vm, val, event) {
        // 在vue实例的methods中找到对应的方法
        let fn = vm.$options.methods && vm.$options.methods[val];
        if (event && fn)
            node.addEventListener(event, fn.bind(vm))
    }

    // 判断属性是否为指令
    isDirective(attr) {
        return attr.indexOf('my-') === 0;
    }

    // 判断属性是否为自定义事件
    isEvent(attr) {
        return attr.indexOf('@') === 0;
    }

    // 判断节点为元素节点
    isElement(node) {
        return node.nodeType === 1;
    }
    
    // 判断节点是文本节点并且为插值表达式
    isText(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
}

代码到这就结束了,代码都有简单的注释,就不再用文字说明了,看一张图就好。(本来想着用文字描述的,但是文字不太容易理解,索性就画了个图,觉得比文字容易理解)

image

内容有点多,字有点小,可以保存下来看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    NARUTO_86阅读 37,376评论 8 86
  • 前言 Vue.js 的核心包括一套“响应式系统”。 “响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码...
    world_7735阅读 950评论 0 2
  • vue.js简介 Vue.js读音 /vjuː/, 类似于 viewVue.js是前端三大新框架:Angular....
    LiWei_9e4b阅读 488评论 0 0
  • Vue.js是什么 Vue.js(读音 /vjuː/, 类似于 view) 是一套构建用户界面的 渐进式框架。与其...
    鱼鱼吃猫猫阅读 3,227评论 1 12
  • 最近买了几本台湾书,当时是冲李欣频写的书去的。看到了几个题材的,一直想解惑。 顺手也挑了几本(台湾书单价高,比较坑...
    夏阳777阅读 1,446评论 0 0