资讯专栏INFORMATION COLUMN

Vue面试题精选:Vue原理以及双向数据绑定的实战过程

malakashi / 1411人阅读

摘要:双向数据绑定指的是,将对象属性变化与视图的变化相互绑定。数据双向绑定已经了解到是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过来实现对属性的劫持,达到监听数据变动的目的。和允许观察数据的更改并触发更新。

1 MVVM

双向数据绑定指的是,将对象属性变化与视图的变化相互绑定。换句话说,如果有一个拥有name属性的user对象,与元素的内容绑定,当给user.name赋予一个新值,页面元素节点也会相应的显示新的数据。同样的,如果页面元素(通常是input)上的数据改变,输入一个新的值会导致user对象中的name属性发生变化。

MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。
把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。

总之一句话,数据与表现分离,当某一个数据改变时,页面上所有使用这个数据的元素的内容都会改变。下面是一个最简单的数据绑定的例子,来自Vue2.0源码阅读笔记–双向绑定实现原理,这个例子十分简单粗暴,就做了三件事:

创建 obj 对象,用来保存数据
监听 keyup 事件,当事件触发时,把选定的 input 标签的值赋给 obj 对象的 hello 属性。
改变 obj 对象 的 hello 属性的 set 方法,当 hell 被赋值时,将这个值同时赋值给选中的两个元素。







1.1 实现数据双向绑定的方式

双向数据绑定底层的思想非常的基本,它可以被压缩成为三个步骤:

我们需要一个方法来识别哪个UI元素被绑定了相应的属性(上面的例子里直接选中了元素,而没有提供对外的函数)
我们需要监视属性和UI元素的变化
我们需要将所有变化传播到绑定的对象和元素
常见的实现数据绑定的方法,有大致如下几种:

发布者-订阅者模式
脏值检查
数据劫持
其中最简单也是最有效的途径,是使用发布者-订阅者模式。上面的例子就使用到了。

发布者-订阅者模式的思想很简单:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候,如果某一个被绑定的内容(如JavaScript对象或者一个HTML输入字段)被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。下面是一个来自谈谈JavaScript中的双向数据绑定的例子,我在注释里添加了一些我的理解。

function DataBinder(object_id){

//创建一个简单地PubSub对象   
var pubSub = { // 一个pubSub 对象,内部有一个 callbacks 对象,保存回调函数
    callbacks: {}, // 键名为触发回调函数的自定义事件名称,值为一个数组,每一项都是一个回调函数
    on: function(msg,callback){ // on 方法 传入参数,一个字符串(就是自定义事件的名称),一个回调函数
        this.callbacks[msg] = this.callbacks[msg] || []; // 以 msg 作为键名,创建数组(如果存在,等于原数组)
        this.callbacks[msg].push(callback); // 将新的回调函数加入数组
    },
    publish: function(msg){ // publish 方法
        this.callbacks[msg] = this.callbacks[msg] || []; // 根据 msg 传入的参数,调用 this.callbacks 对象 的 msg 属性保存的数组,如果没有,等于新建的空数组
        for(var i = 0, len = this.callbacks[msg].length; i

}

//在model的设置器中
function User(uid){

var binder = new DataBinder(uid), // 返回一个 pubSub 对象,其上保存了由传入参数 uid 确定的元素所有绑定的回调函数
user = {
    attributes: {}, // 保存需要同步的数据
    set: function(attr_name,val){ // 调用 set 方法,将需要同步的数据通过 publish 方法传给监听的元素
        this.attributes[attr_name] = val;
        //使用“publish”方法  
        binder.publish(uid+ ":change", attr_name, val,this);
    },
    get: function(attr_name){
        return this.attributes[attr_name];
    }
}
return user; // 函数作为一个构造函数时,返回一个对象,作为这个构造函数的实例

}

var user = new User(123); // 返回一个 user 对象,对象有一个 attributes 属性指向一个对象,这个对象保存这需要同步的数据
user.set("name","Wolfgang"); // 所有带有 data-bind-123="name" 属性的 html 标签都会被监听,它们的值会同步改变,保持相同
然后说脏检查,脏检查是一种不关心你如何以及何时改变的数据,只关心在特定的检查阶段数据是否改变的数据监听技术。简单来说,脏检查是直接检测数据是否改变,如果某一个被监听的数据改变,就将这个值传给所有被被监听者。

而数据劫持,就是通过对属性的 set get 方法进行改造,来监测数据的改变,发布消息给订阅者,触发相应的监听回调。

2 vue 数据双向绑定

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的。

要实现mvvm的双向绑定,主要进行了:

实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
mvvm入口函数,整合以上三者
例子大体来自这篇文章的,我根据自己的理解做了些修改,添加了一些注释

为了便于理解,首先,来实现一个消息的储存中转的构造函数:

var uid = 0; // 通过全局的 uid 给 Dep 实例增加唯一 id,以区分不同实例

function Dep() {

this.id = uid++; // 给 Dep 实例添加 id,并将全局的 uid 加1
this.subs = [];

}
Dep.prototype = {

addSub: function(sub) { // 增加 sub
    this.subs.push(sub);
},

depend: function() {
    Dep.target.addDep(this); // 将全局对象 Dep 的 target 属性指向的对象(这个函数的调用者 this)添加的 subs 里
},

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

notify: function() { // 通知所有 subs 数据已更新
    this.subs.forEach(function(sub) {
        sub.update();
    });
}

};
通过修改对象的属性,每一个绑定的属性都会有一个 Dep 实例。每一个 Dep 实例都会有一个 subs 属性,用来存储需要通知的对象,当对象属性改变时,通过 set 方法,调用这个属性的 Dep 实例的原型的 notify 方法,根据 subs 数组保存的内容,通知绑定了这个属性值的数据修改内容。

function Observer(data) {

this.data = data;
this.walk(data); // 调用原型的方法,处理对象

}

Observer.prototype = {

walk: function(data) {
    var me = this;
    Object.keys(data).forEach(function(key) { // 遍历 data 的属性,修改属性的 get / set
        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(); // 将全局的 Dep.target 添加到 dep 实例的 subs 数组里
            }
            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);

};
然后对 html 模板进行编译,根据每个节点及其的属性,判断是否包含 ‘{{}}’,’v-‘,’on’ 等特殊字符串,判断是否进行了绑定,将绑定了的属性个 get set 进行处理,

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 = {

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

    // 将原生节点拷贝到fragment
    while (child = el.firstChild) { // 如果 el 有资源素,就将其赋值给 child,返回 true
        fragment.appendChild(child); // 将 child 从 el 转移到 fragment 下,el 会少一个资源素,进行下一轮循环
    }

    return fragment; // 返回 fragment
},

init: function() {
    this.compileElement(this.$fragment); // 对 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);
        }

        if (node.childNodes && node.childNodes.length) {
            me.compileElement(node); // 递归调用,处理子元素
        }
    });
},

compile: function(node) {
    var nodeAttrs = node.attributes, // 获得 dom 节点在 html 代码里设置的属性
        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;
}

}
最后,实现 watch,监视属性的变化。watch 的每个实例,会添加到希望监听的属性的 dep.subs 数组中,当监听的数据发生变化,调用 notify 函数,然后函数内部调用 subs 中所以 watch 实例的 updata 方法,通知监听这个数据的对象。受到通知后,对象判断值是否改变,如果改变,调用回调函数,更改视图

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() {
    var value = this.get(); // 取到最新值
    var oldVal = this.value;
    if (value !== oldVal) { // 判断值是否改变
        this.value = value;
        this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
    }
},
get: function() {
    Dep.target = this;  // 将当前订阅者指向自己
    var value = this.vm[exp];   // 触发getter,添加自己到属性订阅器中
    Dep.target = null;  // 添加完毕,重置
    return value;
}

};
最后,通过 MVVM 构造器,将上面及部分整合起来,实现数据绑定。

function MVVM(options) {

this.$options = options;
var data = this._data = this.$options.data;
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)

}
上面的内容只是实现数据绑定的大概思路,其他内容我再慢慢完善。

3 vue 数据双向绑定的缺陷

3.1 vue 实例创建后,再向其上添加属性,不能监听

当创建一个Vue实例时,将遍历所有 DOM 对象,并为每个数据属性添加了 get 和 set。 get 和 set 允许 Vue 观察数据的更改并触发更新。但是,如果你在 Vue 实例化后添加(或删除)一个属性,这个属性不会被 vue 处理,改变 get 和 set。

如果你不想创建一个新的对象,你可以使用Vue.set设置一个新的对象属性。该方法确保将属性创建为一个响应式属性,并触发视图更新:

function addToCart (id) {

var item = this.cart.findById(id);
if (item) {
    item.qty++
} else {
    // 不要直接添加一个属性,比如 item.qty = 1
    // 使用Vue.set 创建一个响应式属性
    Vue.set(item, "qty", 1)
    this.cart.push(item)
}

}
addToCart(myProduct.id);
3.2 数组

Object.defineProperty 的一个缺陷是无法监听数组变化。

当直接使用索引(index)设置数组项时,不会被 vue 检测到:

app.myArray[index] = newVal;
然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法, vm.items[indexOfItem] = newValue 这种是无法检测的。

push();
pop();
shift();
unshift();
splice();
sort();
reverse();
同样可以使用Vue.set来设置数组项:

Vue.set(app.myArray, index, newVal);
3.3 proxy 与 defineproperty

Proxy 对象在ES2015规范中被正式发布,用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

我们可以这样认为,Proxy是Object.defineProperty的全方位加强版,具体的文档可以查看此处;

Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,是Object.defineProperty不具备的。
Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。
Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。
当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。

喜欢的可以关注小编哈~

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

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

相关文章

  • 前端面试总结(js、html、小程序、React、ES6、Vue、算法、全栈热门视频资源)

    摘要:并总结经典面试题集各种算法和插件前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快速搭建项目。 本文是关注微信小程序的开发和面试问题,由基础到困难循序渐进,适合面试和开发小程序。并总结vue React html css js 经典面试题 集各种算法和插件、前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快...

    pumpkin9 评论0 收藏0
  • 前端面试总结(js、html、小程序、React、ES6、Vue、算法、全栈热门视频资源)

    摘要:并总结经典面试题集各种算法和插件前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快速搭建项目。 本文是关注微信小程序的开发和面试问题,由基础到困难循序渐进,适合面试和开发小程序。并总结vue React html css js 经典面试题 集各种算法和插件、前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快...

    Carson 评论0 收藏0
  • 前端面试总结(js、html、小程序、React、ES6、Vue、算法、全栈热门视频资源)

    摘要:并总结经典面试题集各种算法和插件前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快速搭建项目。 本文是关注微信小程序的开发和面试问题,由基础到困难循序渐进,适合面试和开发小程序。并总结vue React html css js 经典面试题 集各种算法和插件、前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快...

    muzhuyu 评论0 收藏0

发表评论

0条评论

malakashi

|高级讲师

TA的文章

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