前端面试题 - 5. 双向绑定的原理是什么(实现一个简版Vue)?

185 阅读2分钟

原理:通过发布订阅来修改数据的值。

响应式

使用Object.defineProperty来实现。对对象的get/set进行重写。通过重写set的时候修改元素的值实现。

import React, { useEffect, useRef } from "react";

const Component = () => {
  const input = useRef();
  const text = useRef();

  useEffect(() => {
    // 实现双向绑定
    input.current.addEventListener("keyup", function (e) {
      obj.val = e.target.value
    });

    let obj = {}
    Object.defineProperty(obj, 'val', {
      get: function () { return obj.val },
      set: function (_val) {
        input.current.value = _val
        text.current.innerHTML = _val
      }
    })
  }, []);

  return <>
    <input type="text" ref={input}/>
    你输入的文本是:
    <p ref={text} />
  </>
};

export default Component;

发布订阅

简单的发布订阅。就是收集订阅者,并执行订阅者的方法

/**
 * 订阅者
 */
class Watcher {
  callback: Function;

  constructor(callback) {
    this.callback = callback
  }

  public update() {
    this.callback();
  }
}

/**
 * 依赖和订阅者收集
 */
class Dep {
  private subs: Watcher[] = [];
  constructor() {
    this.subs = []
  }

  public add(watcher: Watcher) {
    this.subs.push(watcher)
  }

  public notify() {
    this.subs.forEach(watcher => {
      watcher.update();
    })
  }

}

// test
let w1 = new Watcher(() => { console.log('第一个订阅者') })
let w2 = new Watcher(() => { console.log('第二个订阅者') })
let dep = new Dep();
dep.add(w1)
dep.add(w2)
dep.notify(); 
// 输出
// 第一个订阅者
// 第二个订阅者

vue实现

  • 首先准备好模版和数据
<div id="app">
  演示双向绑定
  <h3>姓名:{{name}}</h3>
  <h3>年龄:{{age}}</h3>
  <h3>身份:{{info.a}}</h3>
  修改名称:<input type="text" v-model="name" />
</div>
{
  name: '大鹏',
  age: 21,
  info: {
    a: "共产主义接班人",
  }
}
  • 然后编写Vue的主程序 主要是数据的监听和模版的编译
import Observer from './Observer';
import Compier from './Compier';

class Vue {
  private $data;
  constructor(options: any) {
    this.$data = options.data;
    // 递归监听内部数据
    Observer(this.$data);

    // 对this.$data.xxx换成this.xxx
    this._()

    // 编译模版
    Compier(options.el, this);
  }

  private _() {
    // 属性代理,方便直接取值
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this.$data[key];
        },
        set(v) {
          this.$data[key] = v;
        },
      })
    });
  }

}

export default Vue;
  • 接下来看数据的响应式监听 对数据的每个key重定义get/set方法。初始get的时候添加到发布依赖,set的时候通过发布器通知所有订阅者更新。
import Dep from './Dep';

function Observer(obj: any) {
  if(!obj || typeof obj !== 'object') return;
  const dep = new Dep()
  Object.keys(obj).forEach(key => {
    let value = obj[key];
    // 递归监听
    Observer(value);

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log(`取${key}的值:${value}`)
        // 收集有哪些订阅者
        // @ts-ignore
        window._target_ && dep.add(window._target_);
        return value
      },
      set(v) {
        value = v;
        // 重新监听
        Observer(value)

        //通知每个订阅者更新自己的文本
        dep.notify()
      }
    }) 
  })
}

export default Observer
  • 接下来看下发布者如何实现
import Watcher from "./Watcher";

/**
 * 订阅者(观察者)
 */
class Dep {
  private subs: Watcher[] = [];
  constructor() {
    this.subs = []
  }
  
  public add(watcher: Watcher) {
    this.subs.push(watcher)
  }

  public notify() {
    this.subs.forEach(watcher => {
      watcher.update();
    })
  }

}

export default Dep
  • 再看订阅者如何实现 注意初始化的时候有调用get方法去添加依赖。并且使用_target_全局变量传递当前实例。
type Vue = any

/**
 * 发布者(被观察者)
 */
class Watcher {
  vm: Vue;
  cb: Function;
  key: string;

  constructor(vm: Vue, key: string, cb: Function) {
    this.vm = vm
    this.key = key
    this.cb = cb
    // 被观察者放全局对象上
    // @ts-ignore
    window._target_ = this;
    // 取值的时候回执行get,此时调用Dep添加依赖列表
    key.split(".").reduce((obj, key) => obj[key], vm);
    // @ts-ignore
    window._target_ = null
  }

  public update() {
    // 更新key指向的最细粒度的值
    const value = this.key.split(".").reduce((obj, key) => obj[key], this.vm)
    this.cb(value)
  }
}

export default Watcher
  • 最后写个组件调用试试
import { useEffect } from "react";
import Vue from './Vue';

const Component = () => {
  useEffect(() => {
    new Vue({
      el: '#app',
      data: {
        name: '大鹏',
        age: 21,
        info: {
          a: "共产主义接班人",
        }
      },
    })
  }, []);

  return null
};

export default Component;

看看效果,还不错:

参考: 双向绑定原理以及实现:blog.csdn.net/qq_41998083…