推广

实现 Vue 的响应式系统

iseeyu3年前 (2024-02-21)推广126

Vue 响应式原理

我们先看 vue 官网的图,其实不太清晰,我初看的时候也是一脸懵逼的.:

再看下面这张图,响应式原理涵盖在里面了(图片来源于网络):

梳理一下流程:

    1. Vue 初始化 => 劫持 data 设置 get、set (拦截数据读写)
    1. Compile 解析模板 => 生成 watcher => 读取 data,触发 get 方法 => Dep 收集依赖(watcher)
    1. 数据变化 => 触发 set方法 => 通知 Dep 中的所有 watcher => 视图更新

对于 Observer、Dep 和 Watcher 这三大金刚 ,我初学的时候也是傻傻的分不清楚很懵,我的理解是:

Dep(dependence) 即依赖收集器,收集 Watcher 即观察者。

Watcher 即观察者,观察数据,数据变化时更新对应的视图(dom)。

Observer 即劫持者,通过 Object.defineProperty() 给数据设置 get 和 set 方法:

  • get: 当某个地方用到数据时,如下 h1、h2 标签都用到了 msg 数据,即观察 msg 数据 的两个 watcher 将被放入 msg 数据的依赖收集器 Dep 中。
data() {
  return {
    msg: 'hello vue',
  }
},

<h1>{{msg}}</h1>
<h2>{{msg}}</h2>
  • set:当 msg 数据改变的时候,遍历 Dep 依赖收集器,通知所有 Watcher 更新视图,即更新 h1、h2 标签内的文本内容

实现 Vue 的响应式系统

通过上面分析,可知每一个数据有一个依赖收集器 Dep,Dep 里面存放用到该数据的 Watcher,如下图所示(图片来源于网络):

1. Dep

我们先实现 Dep,Dep 我们可以用数组来模拟,它应该有两个方法:

  • add,收集 Watcher
  • notify,数据变化的时候通知 Watcher 更新视图
# 依赖收集器
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(watcher) {
    # 添加观察者
    this.subs.push(watcher);
  }
  notify() {
    # 通知每一个观察者更新视图
    this.subs.forEach(watcher => watcher.update());
  }
}

2. Watcher

Watcher 实现如下,其中 cb 是更新视图的方法,关键点在于 oldVal,它有两个用处:

  • Dep 触发 update 方法时,比对新旧值,若有变化才更新,避免不必要的视图更新
  • 初始化的时候,会获取旧值,会触发数据的 get 方法,在此时可以把依赖注入到 Dep 中(即依赖收集)
# 观察者,用于更新视图
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    # 视图更新函数
    this.cb = cb;
    # 旧值
    this.oldVal = this.getOldVal();
  }
  getOldVal() {
    # 传递watch自己
    Dep.target = this;
    # 获取值的时候会触发 get 方法,把自己 push 进 deps[] 里
    const oldVal = compileUtils.getVal(this.expr, this.vm);
    Dep.target = null;
    return oldVal;
  }
  update() {
    # 获取新值
    const newVal = compileUtils.getVal(this.expr, this.vm);
    if (newVal !== this.oldVal) {
      this.cb(newVal);
    }
  }
}

Dep.target = this 的用处是相当于设置了一个全局变量让 Dep 能收集到 watcher 自己,后面 Dep.target = null 用处是销毁全局变量

3. Observer

Observer 实现如下,通过 Object.defineProperty 拦截数据的读写操作:

  • get 收集依赖,注意判断 Dep.target 是否有值,因为模板解析的时候也会读取数据触发 get 方法
  • set 通知依赖收集器,更新视图
// 数据劫持
class Observer {
  constructor(data) {
    this.observer(data, key, data[key]);
  }
  observer(obj, key, value) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: false,
      get() {
        # 防止视图初始化的时候也被收集到Dep中
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set: newVal => {
        this.observer(newVal);
        if (newVal !== value) {
          value = newVal;
          # 通知依赖收集器,有变化
          dep.notify();
        }
      },
    });
  }
}

4. Compile

到这里我们已经实现了 Observer、Dep 和 Watcher,实现了数据的响应式追踪,可是还有一个点没打通,那就是依赖收集,那么依赖什么时候收集呢?换言之我们怎么知道哪些数据依赖了哪些视图呢?

在 Vue 解析模板的时候,实际上我们已经知道了哪些 Dom 依赖了哪些数据,所以是在 compile 的时候完成了模板解析并完成了依赖收集。

Compile 实现如下,省略大部分 dom 操作相关代码,可以用 DocumentFragment 文档碎片提升性能,逻辑比较简单,我们在 dom 解析数据的时候生成了对应的 watcher,并完成了依赖收集:

# 编译类,输出真实Dom
class Compile {
  constructor(el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    # 获取文档对象
    const fragment = this.nodeFragment(this.el);
    # 编译
    this.compile(fragment);
    # 挂载回app
    this.el.appendChild(fragment);
  }
  # 是否元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
  # 获取文档碎片
  nodeFragment(el) {
    # do something
  }
  compile(fragment) {
    const childNodes = fragment.childNodes;
    [...childNodes].forEach(node => {
      if (this.isElementNode(node)) {
        # 元素节点
        # do something
      } else {
        # 文本节点
        # do something
      }
    })
  }
}

# 根据不同指令 执行不同的编译操作
const compileUtils = {
  # v-text
  text(node, expr, vm) {
    const value = vm.$data[expr];
    # 创建观察者 完成依赖收集
    new Watcher(vm, expr, newVal => {
      node.textContent = value;
    });
    node.textContent = value;
  },
};

至此一个响应式的系统就已经完了

双向数据绑定

什么是双向数据绑定

上面我们实现了响应式的系统,但只是单向的,即数据驱动视图,什么是双向数据绑定呢?如下图:

我们常见的 v-model, 就是双向数据绑定,其实它是一个语法糖:

<input v-model="msg" />

等价于 =>

<input :value="msg" @input="msg = $event.target.value" />

实现

双向数据绑定即:

  • 数据改变 => 视图更新
  • 视图改变 => 数据改变 => 视图更新

比如最简单的 input,我们只需要监听 input 事件,文本发生变化时更新数据,触发数据的 set 方法,通知所有的 watcher 更新视图

我们在模板编译的时候,给 dom 元素绑定相应的事件,如 input 标签绑定 input 事件并指定更新数据的回调函数:

const compileUtils = {
  # v-model
  model(node, expr, vm) {
    const value = vm.$data[expr];
    # 创建观察者 完成依赖收集
    new Watcher(vm, expr, newVal => {
      node.value = value;
    });
    node.addEventListener('input', (e) => {
      # 更新数据,触发数据的 set 方法
      vm.$data[expr] = newVal;
    });
    node.value = value;
  },
};

至此大功告成

源码

源码


END

扫描二维码推送至手机访问。

版权声明:本文由西安泽虎代运营发布,如需转载请注明出处。

转载请注明出处https://www.0291.com.cn/post/57424.html

相关文章

西安企业做百度推广,一个月花三千和花三万到底差在哪?

4|最近跟几个西安的老板聊天,发现一个挺有意思的现象:同样做百度推广,有人一个月花三千块,询盘不断;有人一个月砸三万,水花都没见着。问他们区别在哪,大多数人说不清楚,只知道'钱花了,没效果'。 5| 6|百度推广这东西,说复杂也复杂,说简单也简单。核心就一件事:让你的潜在客户搜某个词的时候,你的信息...

我想当策划,大学应该选什么专业?

我想当策划,大学应该选什么专业?

学,广告,传播,公关,这些专业其实都可以。另外也可以学像汉语言文学,以及一些跟文学相关的一些专业,会对你找工作有帮助。但是,你想从事这个这份职业,除了专业的学习之外,更多的是,你要在上学的时候积极参与一些更营销策划相关的社会实践。比如说你可以做一些兼职,比如说帮别人去写稿,...

服装市场营销策略分析,“定位不慎满盘皆输”

服装市场营销策略分析,“定位不慎满盘皆输”

不同的区域需要不同的策略,而不同的营销策略需要不同的销售思路来配合。因此,在研究女装产品销售思路之前,最好先对于市场与营销策略做一次深入地了解。1.市场区域化(market segmentation):目标营销的第一步,是将整个市场拆分成若干个同性质的小市场。2.目标市场的...

晚间悦读丨你的思维方式,决定你的人生高度!

晚间悦读丨你的思维方式,决定你的人生高度!

越长大,越发现,能够决定你人生高度的,往往是你的认知层次。 你的认知在哪个层级,你的人生就处在什么状态。你认知的高度,决定了你人生的高度。 那如何才能升级方式、提升认知能力呢?可以从这三个方面做出改变。 NO. 1 改变态度 有人...

编写项目营销策划方案六大要点

编写项目营销策划方案六大要点

现在商业发展非常迅速,的需求非常的大,但是要做好营销策划是有一定难度的,我把自己做营销策划的一些体会在这里分享给朋友们。营销策划方案六大要点我们先说项目营销策划方案这个话题。大部分的营销策划人都是在企业里工作,大部分项目之初都是来源于一个想法,创始人、老板或者创始团队的一个...

【商场营销策划】如何进行商场营销策划?

【商场营销策划】如何进行商场营销策划?

商场是策划者们关注的永恒话题,那么究竟该如何作好商场营销策划呢?掌握以下这几点营销策略设计原则,至关重要!1.义利兼顾的原则。商场在获取利润的同时要充分考虑到消费者的合法利益和商场的长远利益。在作商场活动营销策略方案的时候应该将这一点作为首要的原则。对商场有利而对顾客不利的...

现在,非常期待与您的又一次邂逅

我们努力让每一部企业宣传片和抖音短视频成为商业大片