Vue双向数据绑定原理和实现

在这个美好又遗憾的世界里,你我皆是自远方而来的独行者

微信图片_20200506232313.jpg

Vue的核心思想就是数据驱动以及组件化,本文就来讲述一下Vue里面双向数据绑定的原理的两种简单实现。

Object.defineProperty()

在vue3.x之前的版本,vue的双向绑定核心是通过Object.defineProperty()来实现属性的劫持,然后就可以监听数据到数据的变化。它的原理是:

  • 数据监听器Observe,能够对数据对象的所有属性进行监听
  • 指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更改函数
  • Watcher,连接Observe和Compile,订阅并收到每个属性变动,执行相应的回调函数,进而更新视图。

先来一个流程图:

image.png

Observe的实现

我们既然可以利用Object.defineProperty()来监听属性变动,那就给Observe的数据所有对象进行遍历,并且添加上setter和getter。

let fans = {name: 'bryant'}
observe(fans)
fans.name = 'chris'

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

function defineReactive(data, key, val) {
    observe(val)
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再定义
        get: function() {
            return val
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了', val, '--->', newVal)
            val = newVal
        }
    })
}
image.png

嘿嘿嘿,这样就监听到每个数据的变化,监听到之后告诉订阅者,我们可以维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的更新方法update。

function defineReactive(data, key, val) {
    let dep = new Dep();
    observe(val)
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再定义
        get: function() {
            return val
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了', val, '--->', newVal)
            val = newVal
            // 来了,老铁,大喇叭通知所有订阅者
            dep.notify()
        }
    })
}

function dep() {
    this.subs = []
}
dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub)
    },
    notify: function () {
        this.subs.forEach((sub) => {
            sub.update()
        }) 
    }
}

订阅者其实就是Watcher,我们需要在defineReactive的getter里面添加dep订阅者

function defineReactive(data, key, val) {
    observe(val)
    let dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再定义
        get: function() {
            // 通过Dep定义一个全局的target,暂存watcher
            Dep.target && dep.addSub(Dep.target);
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了', val, '--->', newVal)
            val = newVal
            // 来了,老铁,大喇叭通知所有订阅者
            Dep.notify()
        }
    })
}

Observe修改后的代码

function Observer(data) {
    this.data = data;
    this.walk(data);
}

Observer.prototype = {
    constructor: Observer,
    walk: function(data) {
        var me = this;
        Object.keys(data).forEach(function(key) {
            me.convert(key, data[key]);
        });
    },
    convert: function(key, val) {
        this.defineReactive(this.data, key, val);
    },

    defineReactive: function(data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);

        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get: function() {
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是object的话,进行监听
                childObj = observe(newVal);
                // 通知订阅者
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }

    return new Observer(value);
};


var uid = 0;

function Dep() {
    this.id = uid++;
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },

    depend: function() {
        Dep.target.addDep(this);
    },

    removeSub: function(sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },

    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

Dep.target = null;

Compile的实现

Compile干的活是解析模板指令,将模板中的变量替换成数据,然后初始化渲染视图,并将每个指令对应的节点绑定更新函数,监听数据的订阅者,一旦有异动,就去更新视图


image.png

因为遍历解析过程中有多次操作dom节点,为了提高效率,会先将根节点el转换成文档碎片fragment进行编译解析,解析完成,再将fragment添加到真实的dom中。

function Compile(el) {
    this.$el = this.isElementNode(el) ? el: document.querySelector(el)
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el)
        this.init()
        this.$el.appendChild(this.$fragment)
    }
}
Compile.prototype = {
    init: function () {
        this.compileElement(this.$fragment)
    },
    node2Fragment: function(el) {
        let fragment = document.createDocumentFragment()
        let child = el.firstChild
        // 将原生节点copy到fragment
        fragment.appendChild(child)
        return fragment
    }
}

这里的compileElement会遍历所有的节点以及子其节点,进行扫描解析编译,调用对应的渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定

Compile.prototype = {
    init: function () {
        this.compileElement(this.$fragment)
    },
    node2Fragment: function(el) {
        let fragment = document.createDocumentFragment()
        let child = el.firstChild
        // 将原生节点copy到fragment
        fragment.appendChild(child)
        return fragment
    },
    compileElement: function (el) {
        let childNodes = el.childNodes
        let self = this
        [].slice.call(childNodes).forEach(node => {
            const text = node.textContent
            const reg = /\{\{(.*)\}\}/ // 表达式文本
            if (self.isElementNode(node)) {
                self.compile(node)
            } else if (self.isTextNode(node) && reg.test(text)) {
                self.compileText(node, RegExp.$1) // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
            }
            // 遍历子节点
            if (node.childNodes && node.childNodes.length) {
                // 递归
                self.compileElement(node)
            } 
        })
    },
    compile: function (node) {
        let nodeAttrs = node.attributes
        let self = this
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (self.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (self.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, self.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, self.$vm, exp);
                }
            }
        });
    }
}

let compileUtil = {
    text: function (node, vm, exp) {
        this.bind(node, vm, 'text')
    },
    bind: function (node, vm, exp, dir) {
        let updateFn = update[dir + 'Update']
        // 第一次初始化视图
        updateFn && updateFn(node, vm[exp])
        // 实例化订阅者
        new Watcher(vm, exp, function(value, oldValue) {
            updateFn && updateFn(node, value, oldValue)
        })
    }
}

let update = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined'? '': value
    }
}

这里通过递归遍历每个节点以及子节点
完整代码:

function Compile(el, vm) {
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);

    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype = {
    constructor: Compile,
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(),
            child;

        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }

        return fragment;
    },

    init: function() {
        this.compileElement(this.$fragment);
    },

    compileElement: function(el) {
        var childNodes = el.childNodes,
            me = this;

        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;

            if (me.isElementNode(node)) {
                me.compile(node);

            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1.trim());
            }

            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes,
            me = this;

        [].slice.call(nodeAttrs).forEach(function(attr) {
            var attrName = attr.name;
            if (me.isDirective(attrName)) {
                var exp = attr.value;
                var dir = attrName.substring(2);
                // 事件指令
                if (me.isEventDirective(dir)) {
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                    // 普通指令
                } else {
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }

                node.removeAttribute(attrName);
            }
        });
    },

    compileText: function(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    },

    isDirective: function(attr) {
        return attr.indexOf('v-') == 0;
    },

    isEventDirective: function(dir) {
        return dir.indexOf('on') === 0;
    },

    isElementNode: function(node) {
        return node.nodeType == 1;
    },

    isTextNode: function(node) {
        return node.nodeType == 3;
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },

    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },

    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];

        updaterFn && updaterFn(node, this._getVMVal(vm, exp));

        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },

    // 事件处理
    eventHandler: function(node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },

    _getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k) {
            val = val[k];
        });
        return val;
    },

    _setVMVal: function(vm, exp, value) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k, i) {
            // 非最后一个key,更新val的值
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                val[k] = value;
            }
        });
    }
};


var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },

    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

Watcher实现

Watcher作为订阅者,主要干的活就是

  • 在自身实例化时往属性订阅器(dep)里面添加自己
  • 自身必须有一个update()
  • 待属性变动dep.notice()通知时,调用自身的update()方法,并触发Compile中绑定回调
function Watcher(vm, exp, cb) {
    this.cb = cb
    this.vm = vm
    this.exp = exp
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更好理解
    this.value = this.get()
}

Watcher.prototype = {
    update: function () {
        this.run()
    },
    run: function () {
        let value = this.get()
        let oldValue = this.value
        if (value !== oldValue) {
            this.value = value
            this.cb.call(this.vm, value, oldValue)
        }
    },
    get: function () {
        Dep.target = this // 将当前订阅者指向自己
        let value = this.vm[exp] // 触发getter, 添加自己到属性订阅器
        Dep.target = null // 添加完毕,重置
        return value
    }
}

实例化Watcher的时候,调用get(),通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。
完整代码:

function Watcher(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.depIds = {};

    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = this.parseGetter(expOrFn.trim());
    }

    this.value = this.get();
}

Watcher.prototype = {
    constructor: Watcher,
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    addDep: function(dep) {
        // 1. 每次调用run()的时候会触发相应属性的getter
        // getter里面会触发dep.depend(),继而触发这里的addDep
        // 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
        // 则不需要将当前watcher添加到该属性的dep里
        // 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
        // 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
        // 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
        // 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
        // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
        // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
        // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
        // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
        // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
        // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    },
    get: function() {
        Dep.target = this;
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    },

    parseGetter: function(exp) {
        if (/[^\w.$]/.test(exp)) return; 

        var exps = exp.split('.');

        return function(obj) {
            for (var i = 0, len = exps.length; i < len; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }
    }
};

Object.defineProperty的缺陷

  • Object.defineProperty监听的都是对象,无法监听数组变化,虽然vue里面也是可以监听数组,但是那是做了一些特别处理的
  • Object.defineProperty只能劫持对象的属性,当属性值也是对象的时候就需要深度遍历

Proxy实现的双向绑定

Proxy是ES6的新特性,它其实就是一层拦截器,想访问目标对象,都必须过它这条道,这不是完美解决我们 Object.defineProperty的不足了吗?Vue3.x就使用这一新的特性来替代之前的Object.defineProperty

Proxy可以直接监听对象

来个栗子先

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
    <p>请输入:</p>
    <input type="text" id="input">
    <p id="p"></p>
    
    <script type="text/javascript">
        const input = document.getElementById('input')

const p = document.getElementById('p')

const obj = {}

const newObj = new Proxy(obj, {
    get: function(target, key, receiver) {
        console.log(`getting ${key}`)
        return Reflect.get(target, key, receiver) //  Reflect.get()  方法的工作方式,就像从对象 ( target[propertyKey] ) 中获取属性,但它是作为一个函数执行的。
    },
    set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver)
        if (key === 'text') {
            input.value = value
            p.innerHTML = value
        }
        return Reflect.set(target, key, value, receiver)
    }
})

input.addEventListener('keyup', function (e) {
    newObj.text = e.target.value
})
    </script>
</body>
</html>
image.png

可以监听数组

当数组进行push,shift,splice等操作和length变化的时候,我们再来一个栗子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul id="list">
    </ul>
    <button type="button" name="button" id="btn">添加列表项</button>
    <script type="text/javascript">
       const list = document.getElementById('list');
        const btn = document.getElementById('btn');

        // 渲染列表
        const Render = {
            // 初始化
            init: function(arr) {
                const fragment = document.createDocumentFragment();
                for (let i = 0; i < arr.length; i++) {
                const li = document.createElement('li');
                li.textContent = arr[i];
                fragment.appendChild(li);
                }
                list.appendChild(fragment);
            },
            // 我们只考虑了增加的情况,仅作为示例
            change: function(val) {
                const li = document.createElement('li');
                li.textContent = val;
                list.appendChild(li);
            },
        };

        // 初始数组
        const arr = [1, 2, 3, 4];

        // 监听数组
        const newArr = new Proxy(arr, {
            get: function(target, key, receiver) {
                console.log(key);
                return Reflect.get(target, key, receiver);
            },
            set: function(target, key, value, receiver) {
                console.log(target, key, value, receiver);
                if (key !== 'length') {
                Render.change(value);
                }
                return Reflect.set(target, key, value, receiver);
            },
        });

        // 初始化
        window.onload = function() {
            Render.init(arr);
        }

        // push数字
        let num = 5
        btn.addEventListener('click', function() {
        newArr.push(num++);
    });
    </script>
</body>
</html>
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,002评论 6 542
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,400评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,136评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,714评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,452评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,818评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,812评论 3 446
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,997评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,552评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,292评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,510评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,035评论 5 363
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,721评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,121评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,429评论 1 294
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,235评论 3 398
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,480评论 2 379

推荐阅读更多精彩内容