febobo / web-interview

语音打卡社群维护的前端面试题库,包含不限于Vue面试题,React面试题,JS面试题,HTTP面试题,工程化面试题,CSS面试题,算法面试题,大厂面试题,高频面试题

Home Page:https://vue3js.cn/interview

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

面试官:说说你对双向绑定的理解?

febobo opened this issue · comments

面试官:说说你对双向绑定的理解?

一、什么是双向绑定

我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了ViewModel的数据也自动被更新了,这种情况就是双向绑定举个栗子

当用户填写表单时,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把ModelView做了双向绑定关系图如下

二、双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

三、实现双向绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  5. 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

流程图如下:

实现

先来一个构造函数:执行初始化,对data执行响应化处理

class Vue {  
  constructor(options) {  
    this.$options = options;  
    this.$data = options.data;  
        
    // 对data选项做响应式处理  
    observe(this.$data);  
        
    // 代理data到vm上  
    proxy(this);  
        
    // 执行编译  
    new Compile(options.el, this);  
  }  
}  

data选项执行响应化具体操作

function observe(obj) {  
  if (typeof obj !== "object" || obj == null) {  
    return;  
  }  
  new Observer(obj);  
}  
  
class Observer {  
  constructor(value) {  
    this.value = value;  
    this.walk(value);  
  }  
  walk(obj) {  
    Object.keys(obj).forEach((key) => {  
      defineReactive(obj, key, obj[key]);  
    });  
  }  
}  

编译Compile

对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

class Compile {  
  constructor(el, vm) {  
    this.$vm = vm;  
    this.$el = document.querySelector(el);  // 获取dom  
    if (this.$el) {  
      this.compile(this.$el);  
    }  
  }  
  compile(el) {  
    const childNodes = el.childNodes;   
    Array.from(childNodes).forEach((node) => { // 遍历子元素  
      if (this.isElement(node)) {   // 判断是否为节点  
        console.log("编译元素" + node.nodeName);  
      } else if (this.isInterpolation(node)) {  
        console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}  
      }  
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  
        this.compile(node);  // 对子元素进行递归遍历  
      }  
    });  
  }  
  isElement(node) {  
    return node.nodeType == 1;  
  }  
  isInterpolation(node) {  
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);  
  }  
}  
  

依赖收集

视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知

实现思路

  1. defineReactive时为每⼀个key创建⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  3. 由于触发name1getter方法,便将watcher1添加到name1对应的Dep中
  4. name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
// 负责更新视图  
class Watcher {  
  constructor(vm, key, updater) {  
    this.vm = vm  
    this.key = key  
    this.updaterFn = updater  
  
    // 创建实例时,把当前实例指定到Dep.target静态属性上  
    Dep.target = this  
    // 读一下key,触发get  
    vm[key]  
    // 置空  
    Dep.target = null  
  }  
  
  // 未来执行dom更新函数,由dep调用的  
  update() {  
    this.updaterFn.call(this.vm, this.vm[this.key])  
  }  
}  

声明Dep

class Dep {  
  constructor() {  
    this.deps = [];  // 依赖管理  
  }  
  addDep(dep) {  
    this.deps.push(dep);  
  }  
  notify() {   
    this.deps.forEach((dep) => dep.update());  
  }  
}  

创建watcher时触发getter

class Watcher {  
  constructor(vm, key, updateFn) {  
    Dep.target = this;  
    this.vm[this.key];  
    Dep.target = null;  
  }  
}  
  

依赖收集,创建Dep实例

function defineReactive(obj, key, val) {  
  this.observe(val);  
  const dep = new Dep();  
  Object.defineProperty(obj, key, {  
    get() {  
      Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例  
      return val;  
    },  
    set(newVal) {  
      if (newVal === val) return;  
      dep.notify(); // 通知dep执行更新方法  
    },  
  });  
}  

参考文献

面试官VUE系列总进度:3/33

面试官:说说你对vue的理解?
面试官:说说你对SPA(单页应用)的理解?

commented

为什么叫双向绑定,前面的双向绑定和后面的原理不是一个东西啊,而且原理和双向也不搭边。官网的双向绑定指的是v-model,而这只是v-bind和input的语法糖,本质还是单向。上面的原理是响应式原理,和双向绑定没什么关系。

请你说下响应式和双向绑定的关系

为什么叫双向绑定,前面的双向绑定和后面的原理不是一个东西啊,而且原理和双向也不搭边。官网的双向绑定指的是v-model,而这只是v-bind和input的语法糖,本质还是单向。上面的原理是响应式原理,和双向绑定没什么关系。

请你解释下单向和双向绑定本质上如何定义?

这个原理用代码解释还存在问题,Watcher这个类没有说在哪里实例化,代码中没有体现出来

commented

我理解的双向数据绑定指的是数据和视图的绑定;响应式是指的数据的改变可以通知到全部的依赖,这种通知是单向的

在mvc的时代里,我们更改了一个状态,需要手动操作dom界面,把状态更新上去,在接受输入时,也要通过监听的方式,去更新状态,我理解的mvvm,把这两步揉到了一起,也即是vue官方的语法糖

为什么叫双向绑定,前面的双向绑定和后面的原理不是一个东西啊,而且原理和双向也不搭边。官网的双向绑定指的是v-model,而这只是v-bind和input的语法糖,本质还是单向。上面的原理是响应式原理,和双向绑定没什么关系。

因为数据响应式,所以数据和模板双向绑定。
双向绑定的原理就是数据响应式,https://cn.vuejs.org/v2/guide/reactivity.html 看下官网的这张图,是不是就是数据和页面的双向绑定

就是有问题, 文中初始化视图时读取某个key,例如name1,创建⼀个watcher1,是错的。data的watcher是在mountComponent方法中实例化的, 这是个全局Wathcer, 读取某个key ,会走key的getter,把全局Watcher添加到key维护的dep中。 数据变化走key的setter,dep通知全局watcher调update更新视图。 data的绑定是这个流程, 但是computed和watcher的每个属性都会初始化一个wathcer实例

commented

可以具体解释下吗? 感觉上面讲到的也是响应式的原理。 个人理解数据双向绑定一般都v-model语法糖的本质,才学疏浅 请多指教

讲的很好,从很底层的原理出发的,通过observer将数据变成响应式的,然后在编译模板的时候针对每个模板所以来的对象创建一个watcher实例,这个实例会在转换成响应式的时候通过getter的方法加入到以来某个key的dep中,当这个数据发生变化的时候回触发这个setter方法,通过setter方法将dep收集到的依赖依次调用依赖中的更新方法,更新页面的视图。
而vue中的双向数据绑定就是通过这样的方式,在v-model这个指令编译的时候,给这个输入框另外添加一个监听输入的事件也就是input事件,通过input标签的value值,将这个值赋值给这个data里面的响应的值,然后会重新触发dep中的每个watch对象的更新方法,完成更新视图双向绑定的操作.其实和v-model拆分成:value和@input事件的意思是一样的,这里讲的只是把这个过程简化了,省略了,监听input事件然后emit出$emit('input',value)然后在外界截图这个事件修改值的写法,为的是更好的表达思路

commented

据我了解,双向绑定是指的数据变导致视图更新,视图更新又会导致数据更新,这点可以通过在谷歌浏览器下载的vue数据查看插件来观察到。v-model是一个语法糖,可以解析成一个value属性和一个input事件,从而实现双向数据绑定。但是,想要一个数据在视图变化后能更新到数据内部,就需要这个数据是一个响应式的数据,因为响应式会监听这个数据,并且通过setter方法来向后台更新数据。所以,在我看来两者是分不开的,响应式是实现双向绑定的基础,双向绑定建立在这个基础上的。个人见解,有错请指出,谢谢。

这里把双向绑定等同于响应式了,但是这两个东西是有区别的。

响应性是一种可以使我们声明式地处理变化的编程范式。简单来讲就是当更改响应式数据时,视图会随即自动更新。而实现这个功能的原理就是劫持数据,收集依赖,当数据发生变化时,执行相应的依赖(副作用/更新视图)。

双向绑定是数据变化驱动视图更新,视图更新触发数据变化。其实就是v-model的功能,而我们知道v-model只是一个语法糖。因此如果要问双向绑定的原理,思路应该是如何实现这个语法糖。其原理是把input的value绑定data的一个值,当原生input的事件触发时,用事件的值来更新data的值。

BTW: 其实最初并没有关注这个问题,但是碰到面试的人也是这样的想法,同时发现网上有好多类似的观点,因此觉得有必要把这些概念理清。当然这也是个人观点,如有不对,还请指出。

参考:

vue 实现的是数据的响应性,Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM
其官方文档中,唯一提到 双向绑定的仅有一个 v-model 指令。

我认为你把 双向绑定 和 响应式 这两个概念给混淆了。

这里把双向绑定等同于响应式了,但是这两个东西是有区别的。

响应性是一种可以使我们声明式地处理变化的编程范式。简单来讲就是当更改响应式数据时,视图会随即自动更新。而实现这个功能的原理就是劫持数据,收集依赖,当数据发生变化时,执行相应的依赖(副作用/更新视图)。

双向绑定是数据变化驱动视图更新,视图更新触发数据变化。其实就是v-model的功能,而我们知道v-model只是一个语法糖。因此如果要问双向绑定的原理,思路应该是如何实现这个语法糖。其原理是把input的value绑定data的一个值,当原生input的事件触发时,用事件的值来更新data的值。

BTW: 其实最初并没有关注这个问题,但是碰到面试的人也是这样的想法,同时发现网上有好多类似的观点,因此觉得有必要把这些概念理清。当然这也是个人观点,如有不对,还请指出。

参考:

感谢科普~

这是vue1的响应式实现方式了,vue1才会有Compile类对数据变化做单个回调处理来更新视图,但是这样做颗粒度太细了,当多个数据量同时变化时(比如一个较长的v-for里面绑定的数据都有变化)开销是很大的。因此vue2的响应式是通过diff dom来进行reRender实现的,哪怕你10个 100个数据同时变化也都是通过diff dom来更新,而不是执行他们各自通过compile绑定的回调。而且当再次出现多个数据量同时变化时再结合异步任务可以优化成只进行一次diff dom从而一次性更新视图。这样做虽然对于单个数据变化来说,开销肯定是比vue1大的,但单次的dom的diff其实开销也很低,所以这是一次很有意义的性能优化。

所以说到底什么是双向绑定呢?

标点符号是不是没标好,看的有点奇怪

看完了,这是我目前看到的讲的最好的内容,没有之一

Vue.js的双向绑定利用了JavaScript语言的特性,结合了响应式编程的原理。具体来说,Vue.js通过Object.defineProperty()方法对组件的数据对象进行劫持,把数据属性转换成getter/setter,并在内部跟踪依赖,在数据发生变动时通知视图进行更新。
双向绑定极大地简化了数据驱动的交互,开发者不需要直接操作DOM,只需操作数据模型即可。这样的机制不仅提高了开发效率,也让代码更容易维护。

双向绑定说的是v-model吧,这里的响应式原理watcher有点不对,vue2并不是每个key都会创建一个watcher,一个组件会创建一个watcher,watcher里使用了render函数,render函数里包含了数据的使用,也就触发了getter,从而手机依赖;所谓的依赖其实就是这个全局watcher,当数据改变时,会通知这个watcher去更新,然后就是patch,diff算法之类的