面试官:请手写一个双向绑定? 我:...

152 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

前言

关于双向绑定,这个词出现的频率太高了。有的人也会称他为vue reactive也就是vue响应式,其实他们的底层都是一个东西。相信大家对这个东西都不陌生。

要实现一个源码,首先我们要先弄懂 原理流程,这样子我们才能快速实现

提出几个问题?

  • vue是双向数据绑定,react是单向数据流,为什么这样设计,有什么优缺点?
  • 热更新原理和双向数据绑定相同点

原理

双向绑定的原理可以理解为副作用+桶+数据劫持

副作用函数

是的,因为他要改变dom,那肯定是有副作用的了

function effect(){
    document.body.innerText = 'hello world';
}

但是这样写太简单了,因为我们的副作用函数多种多样,有可能是匿名函数,有可能使用名称的,所以我们写一个副作用函数注册的函数

function effect(fn){
    fn();
}

现在我们想添加什么副作用函数,直接添加到effect中就行了

桶可以理解为存储数据的地方

const data = {text: 'hello world'}

我们要将这段数据与副作用函数建立绑定,对应的数据关系可以理解为 target---->key---->effect

为什么要建立数据对应关系呢?

因为只有这样,你才知道哪一个key值改变了,要更新哪一个副作用

target就是这一段数据,因为我们要以这一段数据做数据绑定
key就是text,因为这个副作用是和这个key建立的联系
effect那就是副作用了

这是一种属性结构,所以我们使用map来进行存储

为了能让表达式执行完毕以后,副作用函数和变量能够被垃圾回收,所以我们使用weakMap,而keyeffect的对应关系是一对多的关系,所以使用set进行存储

const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap

数据劫持

数据劫持我们使用proxy实现

他主要分为两个步骤

  • 当读取操作发生时,将副作用函数收集到桶中
  • 当设置操作发生时,从桶中取出副作用函数并执行
  
  const obj = new Proxy(data, {
      get: (target, key) => {
        let depsMap = bucket.get(target)
        if (!depsMap) { // 不存在,则创建一个Map
            bucket.set(target, depsMap = new Map())
        }
        let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
        if (!deps) { // 不存在,则创建一个Set
            depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect) // 将副作用函数加进去
        return target[key];
      },
      set: (target, key, value) => {
        // 设置属性值
        target[key] = value;
        const depsMap = bucket.get(target) // target Map
        if (!depsMap) return;
        const effects = depsMap.get(key) // effectFn Set
        effects && effects.forEach(fn => fn())
      }
  })

先监听,再改变

此时我们可以往副作用函数中添加监听的数据

   effect(() => {
        console.log('effect run')
        document.body.innerHTML = obj.text
    })

然后一秒钟以后改变数据

    setTimeout(() => {
        obj.text = 'heeeeeeeeee'
    }, 1000)

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
<script>
  // 要建立绑定的数据
  const data = {text: 'hello world'};
  // 当前的副作用, 建立对应关系的时候要用
  let activeEffect;
  // 桶
  const bucket = new WeakMap();
  // 建立响应式注册函数
  function effect(fn){
    activeEffect = fn;
    fn();
  }

  // 建立数据劫持
  const obj = new Proxy(data, {
    get: (target, key) => {
      // 把数据放进桶中
      let depsMap = bucket.get(target);
      if(!depsMap){
        bucket.set(target, depsMap = new Map());
      }
      let deps = depsMap.get(key);
      if(!deps){
        depsMap.set(key, deps = new Set());
      }
      deps.add(activeEffect)
      // 返回要访问的值
      return target[key];
    },
    set: (target, key, value)=>{
      // 设置值
      target[key] = value;
      // 执行对应的副作用函数
      const depsMap = bucket.get(target);
      if(!depsMap) return;
      const deps = depsMap.get(key);
      if(!deps) return;
      deps.forEach((dep)=> dep());
    }
  })

  // 监听
  effect(()=>{
    document.body.innerHTML = obj.text;
  })

  // 改变
  setTimeout(() => {
        obj.text = 'heeeeeeeeee'
  }, 1000)


</script>
</html>

参考

  • vue.js设计与实现