美文网首页
vue双向数据绑定原理剖析

vue双向数据绑定原理剖析

作者: 欢欣的膜笛 | 来源:发表于2020-03-29 16:39 被阅读0次
  • 原理:
    Vue.js是通过数据劫持以及结合发布者-订阅者来实现的数据绑定,数据劫持是利用ES5Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。

  • 数据双向绑定的流程图:


    数据双向绑定的流程图.png
    • 1、实现一个数据监听器Obverser,对data中的数据进行监听,若有变化,通知相应的订阅者。
    • 2、实现一个指令解析器Compile,对于每个元素上的指令进行解析,根据指令替换数据,更新视图。
    • 3、实现一个Watcher,用来连接ObverserCompile, 并为每个属性绑定相应的订阅者,当数据发生变化时,执行相应的回调函数,从而更新视图。
    • 4、mvvm入口函数,整合以上三者,即构造函数new Vue({})
  • index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue双向数据绑定原理剖析</title>
</head>
<body>
    <div id="app">
        <div>{{name}}</div>
        <p v-text="name"></p>
        <p>{{age}}</p>
        <input type="text" v-model="name" placeholder="enter something" />
        <button @click="changeName">changeName</button>
        <div v-html="html"></div>
    </div>

    <script src="./vue.js"></script>
    <script src="./compile.js"></script>
    <script>
        new Vue({
            // 挂载视图
            el: '#app',
            // 定义数据
            data: {
                name: 'I am name',
                age: 12,
                html: '<button>我是按钮</button>'
            },
            created() {
                setTimeout(() => {
                    this.name = '我是name,我变了';
                }, 800);
            },
            methods: {
                changeName() {
                    this.name = '我是name,我又又又又又又又变了';
                    this.age = 18;
                }
            }
        })
    </script>
</body>
</html>
  • vue.js
class Vue {
    constructor(options) {
        this.$options = options;
        // 处理 data
        this.$data = options.data;
        // 添加属性观察对象(实现属性挟持)
        this.observe(this.$data);
        // 创建模板编译器,来解析视图
        new Compile(options.el, this);
        if (options.created) {
            options.created.call(this);
        }
    }

    observe(obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        Object.keys(obj).forEach(key => {
            this.defineReactive(obj, key, obj[key]);
            this.proxyData(key);
        })
    }

    // 针对当前对象属性的重新定义(挟持)
    defineReactive(obj, key, val) {
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                // 将Dep.target添加到dep中
                // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
                Dep.target && dep.addDep(Dep.target);
                return val;
            },
            set(newVal) {
                if (newVal !== val) {
                    val = newVal;
                    // 通知,触发update操作
                    dep.notify();
                }
            }
        })
        this.observe(val);
    }

    // 给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key];
            },
            set(newVal) {
                this.$data[key] = newVal;
            }
        })
    }
}

// 消息订阅器,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法
class Dep {
    constructor() {
        this.deps = [];
    }
    addDep(dep) {
        this.deps.push(dep);
    }
    notify() {
        // 调用订阅者的update方法,通知变化
        this.deps.forEach(dep => dep.update());
    }
}

// Observer和Compile之间通信的桥梁
class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm;
        this.key = key;
        this.callback = callback;
        // 将当前订阅者指向自己
        Dep.target = this;
        // 为了触发属性的getter,从而在dep添加watcher
        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);
        // vm为创建的vue实例
        this.$vm = vm;
        // 判断视图是否存在
        if (this.$el) {
            // 提取宿主中模板内容到Fragment(文档片段)标签,dom操作会提高效率
            this.$fragment = this.node2Fragment(this.$el);
            // 编译模板内容,同时进行依赖收集
            this.compileNode(this.$fragment);
            this.$el.appendChild(this.$fragment);
        }
    }

    // 核心方法(节省内存)把模板放入内存,等待解析
    node2Fragment(el) {
        // 创建内存片段
        const fragment = document.createDocumentFragment();
        let child;
        // 模板内容放到内存
        // appendChild会把原来的child给移动到新的文档中,当el.firstChild为空时,
        // while也会结束 a = undefined  => 返回 undefined
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }

    compileNode(el) {
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
            // 判断节点类型
            if (node.nodeType === 1) {
                // element 节点
                this.compileElement(node);
            } else if (this.isInterpolation(node)) {
                // 插值表达式
                this.compileText(node);
            }
            // 递归子节点
            if (node.childNodes && node.childNodes.length) {
                this.compileNode(node)
            }
        })
    }

    compileElement(node) {
        // <div k-model="foo" k-text="test" @click="onClick">
        let nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(attr => {
            const attrName = attr.name;
            const attrValue = attr.value;
            if (this.isDirective(attrName)) {
                const dir = attrName.substring(2);
                this[dir] && this[dir](node, attrValue);
            }
            if (this.isEvent(attrName)) {
                const dir = attrName.substring(1);
                this.eventHandle(node, attrValue, dir);
            }
        })
    }

    // 指令
    isDirective(attrName) {
        return attrName.includes('v-');
    }

    // 双向绑定
    model(node, key) {
        // data -> view
        this.update(node, key, 'model');
        // view -> data
        node.addEventListener('input', event => {
            this.$vm.$data[key] = event.target.value;
        })
    }

    text(node, key) {
        this.update(node, key, 'text');
    }

    html(node, key) {
        this.update(node, key, 'html');
    }

    isInterpolation(node) {
        // 文本 && {{}}
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }

    compileText(node) {
        // RegExp.$1, 缓存最近一次的正则里面的值
        this.update(node, RegExp.$1, 'text');
    }

    update(node, key, dir) {
        // 找到更新规则对象的更新方法
        let updatrFn = this[dir + 'Updater'];
        // 第一次初始化视图
        updatrFn && updatrFn(node, this.$vm.$data[key]);
        // 每解析一个表达式(非事件指令)都会创建一个对应的watcher对象, 并建立watcher与dep的关系
        new Watcher(this.$vm, key, (value) => {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updatrFn && updatrFn(node, value);
        })
    }

    textUpdater(node, value) {
        node.textContent = value;
    }

    htmlUpdater(node, value) {
        node.innerHTML = value;
    }

    modelUpdater(node, value) {
        node.value = value;
    }

    // 事件
    isEvent(attrName) {
        return attrName.includes('@');
    }

    // 事件绑定
    eventHandle(node, key, dir) {
        const fn = this.$vm.$options.methods && this.$vm.$options.methods[key];
        if (dir && fn) {
            node.addEventListener(dir, () => { fn.call(this.$vm) });
        }
    }
}

相关文章

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • Vue原理研究之双向数据绑定

    前言 本篇文章主要研究Vue的双向数据绑定的学习笔记。具体细节请参考《剖析Vue原理&实现双向绑定MVVM》。 原...

  • vue双向数据绑定

    剖析Vue原理、实现双向绑定MVVM 几种实现双向绑定的做法 目前几种主流的mvc(vm)框架都实现了单向数据绑定...

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • vue 双向数据绑定

    Vue实现数据双向绑定的原理:Object.defineProperty()vue实现数据双向绑定主要是:采用数据...

  • Vue2.0原理与MVVM的实现

    剖析Vue原理&实现双向绑定MVVM vue源码 双向绑定 -- MVVM 目前几种主流的MVC框架都实现了单向数...

  • 前端理论面试--VUE

    vue双向绑定的原理(详细链接) VUE实现双向数据绑定的原理就是利用了 Object.definePropert...

  • Vue实现数据双向绑定的原理

    Vue实现数据双向绑定的原理:Object.defineProperty() vue实现数据双向绑定主要是:采用数...

  • 【转】JavaScript的观察者模式(Vue双向绑定原理)

    关于Vue实现数据双向绑定的原理,请点击:Vue实现数据双向绑定的原理原文链接:JavaScript设计模式之观察...

网友评论

      本文标题:vue双向数据绑定原理剖析

      本文链接:https://www.haomeiwen.com/subject/aaluuhtx.html