# 前端面试---Vue双向绑定原理(3.0和2.0优缺点对比)

Vue2.0实现双向绑定

所谓的双向数据绑定主要是mvvm设计模式中数据层(m)和视图层(v)的同步应用,在写入数据的同时,视图层也会自动同步更新,我们可以通过下面一张图来进行观察这种绑定原理:

image.png

经过查资料我们可以看到网上的各种原理,最多的答案就是下面这样的解释:

vue.js是采用的数据劫持结合发布者-订阅者模式的方式,通过object.defineProperty()来劫持各个属性的setter/getter
在数据变动时,发布消息给订阅者,触发相应的监听回调

具体步骤:
    1)需要observe(观察者)的数据对象进行遍历,包括子属性对象的属性,都加上setter和getter,这样的话,
    给这个对象的某个值赋值,就会触发setter,那么就能监听到数据的变化
    2)compile(解析)解析模版指令,将模版中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,
    添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
    3)watcher(订阅者)是observer和compile之间通信的桥梁,主要做的事情是
        1>在实例化时往属性订阅器(dep)里面添加自己
        2>自身必须有一个update()方法
        3>待属性变动dep.notice()通知时,能够调用自身的update()方法,并触发compile中绑定的回调,
    4)mvvm作为数据绑定的入口,整合observer,compile和watcher来监听自己的model数据变化,通过compile来解析编译模版,
    最终利用watcher搭起observer和compile之间的通信桥梁,达到数据变化->更新视图:视图交互变化->数据model变更的双向绑定效果
复制代码

补充:

ECMAScript中有两种属性: 数据属性和访问器属性, 数据属性一般用于存储数据数值, 访问器属性对应的是set/get操作, 不能直接存储数据值, 存储的一般是一个函数形式的数据

Object.defineProperty(), 这个方法接收三个参数:

   属性所在对象,

   属性的名字,

   描述符对象; 
复制代码

为对象定义多个属性的话,就用函数的复数写法:Object.defineProperties();

实现代码步骤如下: 第一步(observer实现对vue各个属性进行监听):

function observer(obj, vm){
    //通过Object.key对属性进行遍历
      Object.keys(obj).forEach(function(key){
          defineReactive(vm, key, obj[key])
  })
}
// Object.defineProperty改写各个属性
function defineReactive( obj, key, val ) {
    // 每个属性建立个依赖收集对象,get中收集依赖,set中触发依赖,调用更新函数 
    var dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          // 收集依赖  Dep.target标志
          Dep.target && dep.addSub(Dep.target)
          return val
        },
        set: function(newVal){
            if(newVal === val) return
            // 触发依赖
            dep.notify()
            val = newVal
      }
  })
}
复制代码

第二步(dep实现):

function Dep(){
    this.subs = []
}
Dep.prototype = {
    constructor: Dep,
    addSub: function(sub){
        this.subs.push(sub)
    },
    notify: function(){
        this.subs.forEach(function(sub){
            sub.update() // 调用的Watcher的update方法
      })
    }
}
复制代码

第三步compiler实现对vue各个指令模板的解析器,生成抽象语法树,编译成Virtual Dom,渲染视图

// 编译器
function compiler(node, vm){
    var reg = /\{\{(.*)\}\}/;
    // 节点类型为元素
    if(node.nodeType ===1){
        var attr = node.attributes;
        // 解析属性
        for(var i=0; i< attr.length;i++){
                if(attr[i].nodeName == 'v-model'){
                var  _value = attr[i].nodeValue
                 node.addEventListener('input', function(e){
                    //给相应的data属性赋值,触发修改属性的setter
                    vm[_value] = e.target.value
                })
                node.value = vm[_value] // 将data的值赋值给node
                node.removeAttribute('v-model')
            }
        }
        new Watcher(vm,node,_value,'input')
    }
    // 节点类型为text
    if(node.nodeType ===3){
       if(reg.test(node.nodeValue)){
           var name = RegExp.$1; 
            name = name.trim()
            new Watcher(vm,node,name,'input')
       }
    }
复制代码

第四步:Watcher 连接observer和compiler,接受每个属性变动的通知,绑定更新函数,更新视图

function Watcher(vm,node,name, nodeType){
    Dep.target = this; // this为watcher实例
    this.name = name
    this.node = node 
    this.vm = vm
    this.nodeType = nodeType
    this.update() // 绑定更新函数
    Dep.target = null //绑定完后注销 标志
}
Watcher.prototype = {
    get: function(){
        this.value = this.vm[this.name] //触发observer中的getter监听
  },
   update: function(){
      this.get()
      if(this.nodeType == 'text'){
        this.node.nodeValue = this.value
      }   
      if(this.nodeType == 'input') {
          this.node.value = this.value
    }
  }
}
复制代码

完整实现代码:

function Vue(options){
    this.date = options.data
    var data = this.data
    observer(data, this) // 监测
    var id = options.el
    var dom = nodeToFragment(document.getElmentById(id),this) //生成Virtual Dom
  // 编译完成后,生成视图
    document.getElementById(id).appendChild(dom)    
 }
function nodeToFragment(node, vm){
    var flag = document.createDocumentFragment()
    var child
    while(child = node.firstChild){
        compiler(cild, vm)
        flag.appendChild(child)
  }
  return flag
}

// 调用
  var vm = new Vue({
    el: "app",
    data: {
        msg: "hello word"
  }
})
复制代码

Vue3.0实现双向绑定

但是上述的解释只是在Vue2.0的时候进行的检测,由于Object.defineProperty本身存在一定的缺陷,比如说是不支持:

Object.defineProperty的缺陷:
1)无法检测到对象属性的新增或删除
    由于js的动态性,可以为对象追加新的属性或者删除其中某个属性,
    这点对经过Object.defineProperty方法建立的响应式对象来说,
    只能追踪对象已有数据是否被修改,无法追踪新增属性和删除属性,
    这就需要另外处理。
2)不能监听数组的变化(对数组基于下标的修改、对于 .length 修改的监测)
   vue在实现数组的响应式时,它使用了一些hack,
   把无法监听数组的情况通过重写数组的部分方法来实现响应式,
   这也只限制在数组的push/pop/shift/unshift/splice/sort/reverse七个方法,
   其他数组方法及数组的使用则无法检测到, 
解决方法主要是使用proxy属性,这个proxy属性是ES6中新增的一个属性,
    proxy属性也是一个构造函数,他也可以通过new的方式创建这个函数,
    表示修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程
    proxy可以理解为在目标对象之前架设一层拦截,外界对该对象的访问,都必须经过这层拦截,
    因此提出了一种机制,可以对外界的网文进行过滤和改写,proxy这个词是代理,
    用来表示由他代理某些操作,可以译为代理器

    Proxy,字面意思是代理,是ES6提供的一个新的API,用于修改某些操作的默认行为,
    可以理解为在目标对象之前做一层拦截,外部所有的访问都必须通过这层拦截,
    通过这层拦截可以做很多事情,比如对数据进行过滤、修改或者收集信息之类。
    借用proxy的巧用的一幅图,它很形象的表达了Proxy的作用。
proxy代理的特点:
    proxy直接代理的是整个对象而非对象属性,
    proxy的代理针对的是整个对象而不是像object.defineProperty针对某个属性,
    只需要做一层代理就可以监听同级结构下的所有属性变化,
    包括新增的属性和删除的属性
proxy代理身上定义的方法共有13种,其中我们最常用的就是set和get,但是他本身还有其他的13种方法

proxy的劣势:
    兼容性问题,虽然proxy相对越object.defineProperty有很有优势,但是并不是说proxy,就是完全的没有劣势,主要表现在以下的两个方面:
        1)proxy有兼容性问题,无完全的polyfill:
            proxy为ES6新出的API,浏览器对其的支持情况可在w3c规范中查到,通过查找我们可以知道,
            虽然大部分浏览器支持proxy特性,但是一些浏览器或者低版本不支持proxy,
            因此proxy有兼容性问题,那能否像ES6其他特性有polyfill解决方案呢?,
            这时我们通过查询babel文档,发现在使用babel对代码进行降级处理的时候,并没有合适的polyfill
        2)第二个问题就是性能问题,proxy的性能其实比promise还差,
        这就需要在性能和简单实用上进行权衡,例如vue3使用proxy后,
        其对对象及数组的拦截很容易实现数据的响应式,尤其是数组

        虽然proxy有性能和兼容性处理,但是proxy作为新标准将受到浏览器厂商重点持续的性能优化,
        性能这块会逐步得到改善
复制代码

Vue算法及模型比较

之前在北森面试的时候,被问到Vue的模型建立及他们是怎么实现的,问完之后真的是一脸懵,但是面试官说这里是必问的,当时也是没回答出来,以为面试要凉凉了,没想到后来又打电话,给出了几道算法题,后来查了一下,怎么实现,才知道面试官可能想问的是AST模型和diff算法,还说在Vue的官方文档上,我是真心没找到,在网上查找了好多资料,在这里总结一下

AST模型

AST就是抽象语法树,他是js代码另一种结构映射,可以将js拆解成AST,也可以把AST转成源代码。这中间的过程就是我们的用武之地。 利用 抽象语法树(AST) 可以对你的源代码进行修改、优化,甚至可以打造自己的编译工具。其实有点类似babel的功能。

AST也是一种数据结构,这种数据结构其实就是一个大的json对象,json我们都熟悉,他就像一颗枝繁叶茂的树。

[
    {
        "name": 12,
        "children": [
            {
                "name": 4,
                "children": [
                    {
                        "name": 1,
                        "children": [
                            {
                                "name": 0
                            },
                            {
                                "name": 2
                            }
                        ]
                    },
                    {
                        "name": 8,
                        "children": [
                            {
                                "name": 7
                            },
                            {
                                "name": 9
                            }
                        ]
                    }
                ]
            },
            {
                "name": 18,
                "children": [
                    {
                        "name": 16,
                       "children": [
                            { 
                                "name": 15
                            },
                            { 
                                "name": 17
                            }
                        ]
                    },
                    {
                        "name": 20,
                        "children": [
                            { 
                                "name": 19
                            },
                            { 
                                "name": 24
                            }
                        ]
                    }
                ]
            }
        ]
    }
]
复制代码

大概就是这种形式的树状结构,对AST语法树进行深度优先和广度优先遍历

 // 深度遍历, 使用递归
        function getName(data) {
            const result = [];
            data.forEach(item => {
                const map = data => {
                    result.push(data.name);
                    data.children && data.children.forEach(child => map(child));
                }
                map(item);
            })
            return result.join(',');
        }
复制代码
// 广度遍历, 创建一个执行队列, 当队列为空的时候则结束
        function getName2(data) {
            let result = [];
            let queue = data;
            while (queue.length > 0) {
                [...queue].forEach(child => {
                    queue.shift();
                    result.push(child.name);
                    child.children && (queue.push(...child.children));
                });
            }
            return result.join(',');
        }
复制代码

Vue算法diff

Diff算法的作用是用来计算出Virtual DOM中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面,diff算法其实就是深度优先算法

算法策略

diff有三大策略:

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