[vue面试题]2、什么是双向绑定?如何实现

1、阶段一:数据劫持

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>1、双向绑定</title>
</head>
<body>
    <div id="app">
      <span>作者:{{ name }}</span>
      <input type="text" v-model="name">
      <span>更多:{{ more.like }}</span>
      <input type="text" v-model="more.like">
    </div>
  
    <script src="./vue.js"></script>
  
    <script>
      const vm = new Vue({
        el: '#app',
        data: {
          name: 'Ethan',
          more: {
            like: '一键三连'
          }
        }
      });
  
      console.log(vm);
    </script>
  </body>
  
</html>

vue.js

class Vue{
    constructor(obj_instance){
        this.$data = obj_instance.data;
        Observer(this.$data);
    }

}

//数据劫持 -- 监听实例里的数据
function Observer(data_instance){
    Object.keys(data_instance).forEach(key => {
        let value = data_instance[key];
        Object.defineProperty(data_instance,key,{
            configurable:true,
            enumerable:true,
            get(){
                console.log(`访问了属性:${key}->值:${value}`);
                return value;
            },
            set(newValue){
                console.log(`属性${key}的值${value}修改为->${newValue}`);
                value = newValue;
            }
        })
    });
}

2、阶段二:子属性劫持


//数据劫持 -- 监听实例里的数据
function Observer(data_instance){
    //递归出口
    if(!data_instance || typeof data_instance !=="object"){
        return;
    }
    Object.keys(data_instance).forEach(key => {
        let value = data_instance[key];
        Observer(value);//递归 -- 子属性数据劫持
        Object.defineProperty(data_instance,key,{
            configurable:true,
            enumerable:true,
            get(){
                console.log(`访问了属性:${key}->值:${value}`);
                return value;
            },
            set(newValue){
                console.log(`属性${key}的值${value}修改为->${newValue}`);
                value = newValue;
            }
        })
    });
}

3、阶段三:ui处理–fragment

这时我们需要处理ui部分,首先要做的就是将ui缓存到内存中

class Vue{
    constructor(obj_instance){
        this.$data = obj_instance.data;
        Observer(this.$data);
        Compile(obj_instance.el,this);
    }

}

.........


function Compile(element,vm){
    vm.$el = document.querySelector(element);
    const fragment = document.createDocumentFragment();
    let child;
    while(child = vm.$el.firstChild){
        fragment.append(child);
    }
    console.log(fragment);
    console.log(fragment.childNodes)
}

将ui转换为fragment(dom片段内存缓存)后,就可以方便的在内存中对dom进行操作而不担心回流等副作用。

4、阶段四:ui处理–正则匹配{{}}中的内容

function Compile(element,vm){
    vm.$el = document.querySelector(element);
    const fragment = document.createDocumentFragment();
    let child;
    while(child = vm.$el.firstChild){
        fragment.append(child);
    }

    fragment_compile(fragment)
    function fragment_compile(node){
        const pattern = /\{\{\s*(\S+)\s*\}\}/
        if(node.nodeType ===3){
            const result_regex = pattern.exec(node.nodeValue);
            if(result_regex){
                console.log(result_regex);
            }
            return;
        }
        node.childNodes.forEach(child=>fragment_compile(child))
    }
}
  function fragment_compile(node) {
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        //当需要处理嵌套的属性时,使用reducer进行处理:more.like
        const arr = result_regex[1].split(".");
        const value = arr.reduce((total, current) => total[current], vm.$data);
        console.log(value)  
      }
      return;
    }
    node.childNodes.forEach((child) => fragment_compile(child));
  }

当需要处理嵌套的属性时,使用reducer处理引用链。

  function fragment_compile(node) {
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        const arr = result_regex[1].split(".");
        const value = arr.reduce((total, current) => total[current], vm.$data);
        //这里巧妙的复用了pattern进行了{{}}的替换操作
        node.nodeValue = node.nodeValue.replace(pattern,value);
        console.log(node.nodeValue);
    }
      return;
    }
    node.childNodes.forEach((child) => fragment_compile(child));
  }

然后复用pattern使用$data中对应数据替换掉{{more.like}}占位符

function Compile(element, vm) {
  vm.$el = document.querySelector(element);
  const fragment = document.createDocumentFragment();
  let child;
  while ((child = vm.$el.firstChild)) {
    fragment.append(child);
  }

  fragment_compile(fragment);
  function fragment_compile(node) {
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        const arr = result_regex[1].split(".");
        const value = arr.reduce((total, current) => total[current], vm.$data);
        node.nodeValue = node.nodeValue.replace(pattern,value);
        console.log(node.nodeValue);
    }
      return;
    }
    node.childNodes.forEach((child) => fragment_compile(child));
  }

  //将文档碎片应用到页面
  vm.$el.appendChild(fragment);
}

最终将文档碎片应用到页面后,可以看到占位符已经被data中的数据替换掉了,至此我们解决了”数据->ui“的流程。

示例:http://alaya.zone:50001/vue/%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A/

5、阶段五:数据更新后对{{}}也进行更新

class Vue {
  constructor(obj_instance) {
    this.$data = obj_instance.data;
    Observer(this.$data);
    Compile(obj_instance.el, this);
  }
}

//观察者(对属性的监听)
function Observer(data_instance) {
  if (!data_instance || typeof data_instance !== "object") {
    return;
  }
  //TODO 难点:需要对data_instance的属性进行监听。
  const dependency = new Dependency();
  Object.keys(data_instance).forEach((key) => {
    let value = data_instance[key];
    Observer(value);
    Object.defineProperty(data_instance, key, {
      configurable: true,
      enumerable: true,
      get() {
        // TODO 难点:需要对属性进行依赖收集。(需要注意TODO 【1】的触发行为触发get方法的流程是同步的)
        Dependency.temp && dependency.addSub(Dependency.temp);
        return value;
      },
      set(newValue) {
        value = newValue;
        Observer(newValue); //当现有属性的值被重新赋值时,对新赋值进行监听
        // TODO 难点:需要对属性进行依赖通知。
        dependency.notify();
      },
    });
  });
}

function Compile(element, vm) {
  vm.$el = document.querySelector(element);
  const fragment = document.createDocumentFragment();
  let child;
  while ((child = vm.$el.firstChild)) {
    fragment.append(child);
  }

  fragment_compile(fragment);
  function fragment_compile(node) {
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      //TODO 难点:需要将插值暂存。
      const xxx = node.nodeValue;
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        const arr = result_regex[1].split(".");
        const value = arr.reduce((total, current) => total[current], vm.$data);
        node.nodeValue = xxx.replace(pattern, value);
        // TODO 难点:需要对插值进行监听。
        new Watcher(vm, result_regex[1], (newValue) => {
          node.nodeValue = xxx.replace(pattern, newValue);
        });
      }
      return;
    }

    //对带有v-model属性的input进行绑定操作
    if (node.nodeType === 1 && node.nodeName) {
      const attrs = [...node.attributes];
      attrs.forEach((attr) => {
        if (attr.nodeName === "v-model") {
          const key = attr.nodeValue;
          const value = key
            .split(".")
            .reduce((total, current) => total[current], vm.$data);

          node.value = value;
          new Watcher(vm, key, (newValue) => {
            node.value = newValue;
          });

          node.addEventListener("input", (e) => {
            const newValue = e.target.value;
            const keys = key.split(".");
            const lastKey = keys.pop();
            const data = keys.reduce(
              (total, current) => total[current],
              vm.$data
            );
            data[lastKey] = newValue; // 更新 Vue 实例的数据
          });
        }
      });
    }

    node.childNodes.forEach((child) => fragment_compile(child));
  }

  vm.$el.appendChild(fragment);
}

//发布者
class Dependency {
  constructor() {
    this.subscribers = [];
  }
  addSub(sub) {
    this.subscribers.push(sub);
  }
  notify() {
    this.subscribers.forEach((sub) => {
      sub.update();
    });
  }
}

//订阅者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    Dependency.temp = this;
    // TODO 【1】难点:需要对属性进行依赖收集。(在初始化订阅者的时候进行一次主动get操作触发搜集操作)
    key.split(".").reduce((total, current) => total[current], vm.$data);
    Dependency.temp = null;
  }

  update() {
    const value = this.key
      .split(".")
      .reduce((total, current) => total[current], this.vm.$data);
    console.log("#### Watcher.update()");
    this.callback(value);
  }
}

关键代码:

//发布者
class Dependency {
  constructor() {
    this.subscribers = [];
  }
  addSub(sub) {
    this.subscribers.push(sub);
  }
  notify() {
    this.subscribers.forEach((sub) => {
      sub.update();
    });
  }
}

//订阅者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    Dependency.temp = this;
    // TODO 【1】难点:需要对属性进行依赖收集。(在初始化订阅者的时候进行一次主动get操作触发搜集操作)
    key.split(".").reduce((total, current) => total[current], vm.$data);
    Dependency.temp = null;
  }

  update() {
    const value = this.key
      .split(".")
      .reduce((total, current) => total[current], this.vm.$data);
    console.log("#### Watcher.update()");
    this.callback(value);
  }
}

设置一个订阅/观察者,旨在对数据进行监听。

  • watcher:
    • 初始化:watcher在fragment_compile中初始化,表明:”对于某些dom进行某个字段xxxx的监听“,这里是dom和属性字段产生交集的地方
    • nodeType===3: 对文本dom上的插值进行更新(当值变化时,更新插值占位的dom)
    • nodeType===1:对绑定了v-model的指令的input组件的value属性进行更新(当值变化时,更新input的value属性)
  • dependency:
    • dependency是在Observer中进行初始化的,一个observer对应一个dependency,表达了”每个object下的属性都由一个发布者进行监听“的意图
    • 在get中:进行watcher注册,利用了Dependency的静态属性进行watcher的暂存,这里比较巧妙的是watcher初始化的时候会触发一下get方法将自己注册到Dependency上去,避免了复杂的寻找和调用。
    • 在set中:进行notify响应。目的是通知对应的字段监听watcher
  • input:
    • 在fragment_compile中对input的”input“事件进行监听,newValue将在这时被赋值给v-model的属性值对应的属性。这也会导致触发属性变化及后续的一系列行为。
  • fragment_compile:这是一个处理Vue模板的函数。它的主要目标是将模板编译成一颗反映数据变化的动态DOM树。具体的功能主要分为两个部分:对文本节点进行插值绑定和对元素节点(具有v-model属性的输入元素)进行双向数据绑定。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部