说说你对双向绑定的理解?

    |     2021年4月10日   |   web前端技术, 面试题   |     0 条评论   |    917

单向绑定很简单:把 Model 绑到 View,JS 改数据,界面自动更新。双向绑定再走一步:用户改了 View(比如填表),Model 也跟着变。Vue 把这套封装在 MVVM 的 ViewModel 里。

本文从关系图讲到 Vue2 时代的 Observer / Compile / Watcher / Dep,并给出一份可运行的精简实现,方便面试时把链路说完整。

角色 职责 对应代码
Model 数据和业务 options.data
View UI / 模板 el 上的 DOM
Observer 劫持属性 defineReactive
Compiler 解析指令和插值 Compile
Watcher + Dep 依赖收集与通知 Dep.target / notify

一、什么是双向绑定

先看单向:Model 变,View 变。

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f64363537333864302d336163392d313165622d616239302d6439616538313462323430642e706e67

用户填表时 View 状态更新;若能自动写回 Model,就是双向绑定。

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f64636331643461302d336163392d313165622d616239302d6439616538313462323430642e706e67

二、双向绑定的原理

Vue 双向绑定由三层构成,合称 MVVM。控制层的核心就是数据双向绑定:

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):展示效果,各类 UI 组件
  • 业务逻辑层(ViewModel):把数据和视图关联起来

理解 ViewModel

两件主责:数据变化后更新视图;视图变化后更新数据。内部两块:

  • 监听器(Observer):对所有数据属性监听
  • 解析器(Compiler):扫描指令,按模板替换数据,绑定更新函数

三、实现双向绑定

以 Vue 为例,流程是:

  • new Vue() 初始化,对 data 做响应化,发生在 Observe 中
  • 同时编译模板,找到动态绑定,从 data 取值初始化视图,发生在 Compile 中
  • 定义更新函数和 Watcher,数据变了由 Watcher 调更新函数
  • 同一个 key 可能出现多次,所以每个 key 需要一个管家 Dep 管理多个 Watcher
  • data 变化先找对应 Dep,通知所有 Watcher 执行更新

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f65353336393835302d336163392d313165622d383566362d3666616337376330633962332e706e67

响应化 data

构造函数:执行初始化,对 data 响应化、代理到 vm,再编译。

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

扫描每个元素节点的指令,按模板替换数据,绑定更新函数。

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f66323765313963302d336163392d313165622d383566362d3666616337376330633962332e706e67

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,更新时统一通知。

68747470733a2f2f7374617469632e7675652d6a732e636f6d2f66613139316634302d336163392d313165622d616239302d6439616538313462323430642e706e67

  • defineReactive 时为每一个 key 创建一个 Dep 实例
  • 初始化视图时读取某个 key(例如 name1),创建一个 watcher1
  • 触发 name1 的 getter,把 watcher1 加到 name1 对应的 Dep
  • 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执行更新方法
    },
  });
}

四、参考文献

一句话总结:双向绑定 = Observer 劫持 data + Compile 解析模板 + Watcher 订阅 + Dep 通知;读属性收集依赖,写属性触发更新。

转载请注明来源:说说你对双向绑定的理解?
本文链接地址:https://ai.zhousir.top/?p=3214
回复 取消