小彭的Vue双向绑定原理实现

167 阅读3分钟

几种实现双向绑定的做法

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(vue.js)

Vue 响应式原理最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的。BTW:Vue3则是使用proxy方法来劫持。

必须实现以下几点技术:

  • 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
  • 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  • 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

先看完整代码(复制粘贴则可以实现功能)

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Vue 双向绑定源码实现</title>
    <style>
        .text{
            display: block;
            padding: 10px;
            margin-bottom: 20px;
            text-align: left;
            width: 100%;
        }
        .cell{
            margin-top: 10px;
            background-color: #f2f2f2;
            padding: 20px;
            width: 200px;
            color: #424242;
        }
        .cell input{
            padding: 8px;
        }
    </style>
</head>
<body>
<div id="app">
    <h2>
        Vue 双向绑定源码实现
    </h2>

    <div class="cell">
        <div class="text" v-text="msg"></div>
        <!-- 输入值将发生改变-->
        <input type="text" v-model="msg" >
    </div>

</div>
<script>
class Vue{
    // 构造函数
    constructor(options) {
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.options = options;
        // 保存数据model与view相关的指令、当model改变时、会触发指令更新、保证view实时更新
        this.directives = {};

        if (this.$el){
            // 观察者
            this.Observer(this.$data);
            // 解析器
            this.Compile(this.$el);
        }

    }
    /**
     * 编译模板
     * 例如: <div v-text="msg"> </div> => <div> msg的值 </div>
     * @method compile
     * @param el 根元素. 深度遍历
     */
    Compile(el){
        const {children: nodes} = el;
        [...nodes].forEach(node => {
            if (node.childNodes && node.childNodes.length){
                this.Compile(node);
            }
            if (node.hasAttribute('v-text')){
                const property = node.getAttribute('v-text');
                const value = this.$data[property];
                this.directives[property].push(new Watch( node, this, property));
                // node.removeAttribute(`v-text`);
            } else if (node.hasAttribute('v-model')){
               const property = node.getAttribute('v-model');
               const value = this.$data[property];
               node.value = value;
               // 监听输入框的变化
               node.oninput = () => {
                   // node.value = 当前输入框的值
                   // 改动data值. 去通知观察者
                   this.$data[property] = node.value;
               }

            }
        })
    }
    /**
     * 观察者
     * 对data的属性进行监听
     * @method Observer
     * @param data newVue里的data数据
     */
    Observer(data) {
        if (data && typeof data === 'object'){
        Object.keys(data).forEach(key => {
            //  如果属性是对象,进项向下监听
            if (typeof data[key] === 'object'){
                this.Observer(data[key]);
            }
            let value = data[key];
            // 每个属性做响应式
            this.directives[key] = [];
            const updateAction = this.directives[key];
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return value;
                },
                set(newValue) {
                    // 发生改动
                    if (value !== newValue){
                        value = newValue;
                        // 通知 Watch更新列表
                        updateAction.forEach(item => {
                            item.updater();
                        })
                    }
                }
            })
        })
    }
}


/**
 * Watch
 * 数据改动 => 视图更新
 * @param node 当前节点
 * @param vm newVue的this
 * @param property data里的属性
 */
class Watch {
    constructor(node, vm, property) {
        this.node = node;
        this.vm = vm;
        this.property = property;
        // 更新操作
        this.updater()
    }
    // 更新者
    updater(){
        if (this.node.hasAttribute('v-text')){
            this.node.innerText = this.vm.$data[this.property]
        }
    }
}

const app  = new Vue({
    el: '#app',
    data: {
        msg: '输入后将发生改变',
        person: {
            age: 18,
            name: 'pro'
        },
    }
})
</script>
</body>
</html>

初始化

const app  = new Vue({
    el: '#app',
    data: {
        msg: '输入后将发生改变',
        person: {
            age: 18,
            name: 'pro'
        },
    }
})

F12控制台,输入app,将输出app相关数据

实现入口

this.$el 传入第一个根元素: #App

this.$data new Vue里的data

this.directives = {} 响应式方法集合

class Vue{
    // 构造函数
    constructor(options) {
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.options = options;
        // 保存数据model与view相关的指令、当model改变时、会触发指令更新、保证view实时更新
        this.directives = {};

        if (this.$el){
            // 观察者
            this.Observer(this.$data);
            // 解析器
            this.Compile(this.$el);
        }

    }
}

实现第一步骤:建立观察者Observer

使用Object.defineProperty对对象的属性进行劫持

注意重点: 如果对象的属性是对象的话,我们就进行递归劫持,俗称深度遍历

/**
     * 观察者
     * 对data的属性进行监听
     * @method Observer
     * @param data newVue里的data数据
     */
    Observer(data) {
        if (data && typeof data === 'object'){
        Object.keys(data).forEach(key => {
            //  如果属性是对象,进项向下监听
            if (typeof data[key] === 'object'){
                this.Observer(data[key]);
            }
            let value = data[key];
            // 每个属性做响应式
            this.directives[key] = [];
            const updateAction = this.directives[key];
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return value;
                },
                set(newValue) {
                    // 发生改动
                    if (value !== newValue){
                        value = newValue;
                        // 通知 Watch更新列表
                        updateAction.forEach(item => {
                            item.updater();
                        })
                    }
                }
            })
        })
    }

实现第二步骤:建立解析器Compile

从根元素#App开始深度遍历,只要符合带‘v-’开头的指令就进行渲染

/**
     * 编译模板
     * 例如: <div v-text="msg"> </div> => <div> msg的值 </div>
     * @method compile
     * @param el 根元素. 深度遍历
     */
    Compile(el){
        const {children: nodes} = el;
        [...nodes].forEach(node => {
            if (node.childNodes && node.childNodes.length){
                this.Compile(node);
            }
            if (node.hasAttribute('v-text')){
                const property = node.getAttribute('v-text');
                const value = this.$data[property];
                // 添加响应式
                this.directives[property].push(new Watch( node, this, property));
                // node.removeAttribute(`v-text`);
            } else if (node.hasAttribute('v-model')){
               const property = node.getAttribute('v-model');
               const value = this.$data[property];
               node.value = value;
               // 监听输入框的变化
               node.oninput = () => {
                   // node.value = 当前输入框的值
                   // 改动data值. 去通知观察者
                   this.$data[property] = node.value;
               }

            }
        })
    }

第三步骤实现一个Watcher

有值改动时,观察者observer通知watcher,watcher进行视图更新

/**
 * Watch
 * 数据改动 => 视图更新
 * @param node 当前节点
 * @param vm newVue的this
 * @param property data里的属性
 */
class Watch {
    constructor(node, vm, property) {
        this.node = node;
        this.vm = vm;
        this.property = property;
        // 更新操作
        this.updater()
    }
    // 更新者
    updater(){
        if (this.node.hasAttribute('v-text')){
            this.node.innerText = this.vm.$data[this.property]
        }
    }
}

下一章实现VueDiff算法、下一章见