2021-01-06 Vue的设计思想及简版实现

121 阅读3分钟

理解Vue的设计思想

将视图View的状态和行为抽象化,让我们将视图UI和业务逻辑分开

vue

Vue是MVVM框架的实现,其包括三大要素:数据响应式模板引擎渲染

数据响应式:监听数据变化并在视图中更新

  • Object.defineProperty()(Vue2)
  • Proxy(Vue3)

模板引擎:提供描述视图的模板语法

  • 插值:{{}}
  • 指令:v-bind, v-on, v-model, v-for, v-if

渲染:如何将模板转换为html

  • 模板=>vdom=>dom

数据响应式原理

数据变更能够响应在视图中,就是数据响应式,Vue2中利用Object.defineProperty()实现变更检测

简单实现,01-reactive.js

// Object.defineProperty()
// 将传入的obj,动态的设置一个key,它的值为val

// /* eslint-disable-next-line */
function defineReactive(obj, key, val) {
  // 对val判断是否为对象,递归
  observe(val);
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", key);
      return val;
    },
    set(v) {
      if (val !== v) {
        console.log("set", key);
        // 如果新传入的v是个对象
        observe(v);
        val = v;
      }
    }
  });
}

// 递归遍历对象,对每个key属性进行响应式拦截getter,setter
function observe(obj) {
  // 判断是否为对象,不是跳出
  if (typeof obj !== "object" || obj === null) return;

  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

// 类似于vue中的$set
function set(obj, key, val) {
  defineReactive(obj, key, val);
}

const obj = {
  foo: "foo",
  bar: "bar",
  baz: {
    a: 1
  }
};
// defineReactive(obj, "foo", "foo");
observe(obj); // 递归遍历
// obj.foo;
// obj.foo = "fooooo";
// obj.baz.a;
// obj.baz = { b: 1 };
// console.log(obj.baz.b);
set(obj, "dong", "dong");
obj.dong;

结合视图,02-reactive.html

<div id="app"></div>

<script>
  function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
      get() {
        return val;
      },
      set(v) {
        if (val !== v) {
          val = v; // val更新,下次触发get为新的值v
          updater();
        }
      }
    });
  }

  function updater() {
    app.innerText = obj.foo; // 触发get
  }

  const obj = {};
  defineReactive(obj, "foo", "foo");

  // 设置定时器
  setInterval(() => {
    obj.foo = new Date().toLocaleTimeString(); // 触发set
  }, 1000);
</script>

递归遍历需要响应化的对象

// 递归遍历对象,对每个key属性进行响应式拦截getter,setter
function observe(obj) {
  // 判断是否为对象,不是跳出
  if (typeof obj !== "object" || obj === null) return;

  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

解决嵌套对象的问题

function defineReactive(obj, key, val) {
  // 对val判断是否为对象,递归
  observe(val);
  Object.defineProperty(obj, key, {
    get() {
      console.log("get", key);
      return val;
    },
    set(v) {
      if (val !== v) {
        console.log("set", key);
        // 如果新传入的v是个对象
        observe(v);
        val = v;
      }
    }
  });
}

如果赋值是对象

obj.baz = {a:1}

// 在set操作函数里
set(v) {
   if (val !== v) {
     console.log("set", key);
     // 如果新传入的v是个对象
     observe(v);
     val = v;
   }
 }

添加新属性无法检测

// 类似于vue中的$set
function set(obj, key, val) {
  defineReactive(obj, key, val);
}

Vue中的数据响应化

目标代码,kvue.html,手动写一个vue实现响应式,包括编译模板,让其正常运行

<div id="app">
  <p>{{counter}}</p>
</div>

<script src="./kvue.js"></script>

<script>
  const app = new Vue({
    el: "#app",
    data: {
      counter: 1
    }
  });

  setInterval(() => {
    app.counter++;
  }, 1000);
</script>

原理分析

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer类中
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile类中
  3. 同时定义个更新函数和Watcher类,将来对映数据变化时Watcher会调用更新函数
  4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher,key与Dep是一对一关系,Dep与Watcher是一对多关系
  5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

vue2

涉及类及方法介绍

  • KVue:框架构造函数
  • Observer:执行数据响应化(分辨数据是对象还是数组)
  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
  • Dep:管理多个Watcher,批量更新

KVue-数据响应式

框架构造函数初始化,对data执行响应化处理

// 递归遍历对象,对每个key属性进行响应式拦截getter,setter
function observe(obj) {
  // 判断是否为对象,不是跳出
  if (typeof obj !== "object" || obj === null) return;

  // 每出现一个对象,创建一个Ob实例
  new Observer(obj); // __ob__
}

// Observer:判断传入obj类型,做对应的响应式处理
class Observer {
  constructor(obj) {
    this.value = obj;

    // 判断对象类型
    if (Array.isArray(obj)) {
      // todo待定
    } else {
      this.walk(obj);
    }
  }

  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

为$data做代理

class KVue {
  constructor(options) {
    // 1.保存选项
    this.$options = options;
    this.$data = options.data;

    // 2.响应式处理
    observe(this.$data);

    // 3.代理data到KVue实例上
    proxy(this);
  }
}

function proxy(vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key];
      },
      set(newValue) {
        vm.$data[key] = newValue;
      }
    });
  });
}

Compile-编译

编译模板中Vue特殊语法,初始化视图、更新视图

编译

class KVue {
  constructor(options) {
    // 1.保存选项
    this.$options = options;
    this.$data = options.data;

    // 2.响应式处理
    observe(this.$data);

    // 3.代理data到KVue实例上
    proxy(this);

    // 4.编译
    new Compile(options.el, this);
  }
}

class Compile {
  constructor(el, vm) {
    // el-宿主,vm-KVue实例
    this.$vm = vm;
    this.$el = document.querySelector(el);

    this.compile(this.$el);
  }

  compile(el) {
    // 遍历el dom树
    el.childNodes.forEach(node => {
      if (this.isElement(node)) {
        // element
        // 需要处理属性和子节点
        // console.log("编译元素", node.nodeName);
        this.compileElement(node);

        // 递归子节点
        if (node.childNodes && node.childNodes.length > 0) {
          this.compile(node);
        }
      } else if (this.isInter(node)) {
        // console.log("编译插值表达式", node.textContent);
        // 获取表达式的值并赋值给node
        this.compileText(node);
      }
    });
  }

  isElement(node) {
    return node.nodeType === 1;
  }

  // 插值{{xxx}}
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }

  // 指令
  isDir(attrName) {
    // 是否以k-开头的
    return attrName.startsWith("k-");
  }

  // 编译文本,将{{ooxx}}
  compileText(node) {
    node.textContent = this.$vm[RegExp.$1];
  }

  // 处理元素所有动态属性
  compileElement(node) {
    Array.from(node.attributes).forEach(attr => {
      console.log(attr);
      const attrName = attr.name; // 属性名
      const exp = attr.value; // 表达式

      // pd是否是一个指令
      if (this.isDir(attrName)) {
        // 执行指令处理函数
        // k-text,关心text
        const dir = attrName.substring(2);
        this[dir] && this[dir](node, exp);
      }
    });
  }

  // k-text处理函数
  text(node, exp) {
    node.textContent = this.$vm[exp];
  }

  // k-html
  html(node, exp) {
    node.innerHTML = this.$vm[exp];
  }
}

依赖收集

视图中会用到data中某key,这称为依赖。同一个key可能会出现多次,每次都需要收集出来用一个Watcher来维护它们,此过程称为依赖收集

多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知

如下图: 1

依赖

实现思路

  1. defineReactive时为每个key创建一个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建一个watcher1
  3. 由于触发name1的getter方法,便将watcher1添加到name1对应的Dep中
  4. 当name1更新,setter触发时,便可通过对应的Dep通知管理所有Watcher更新

watcher

创建Watcher

// 小秘书:做dom更新
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;

    // 读取以下key的值,触发其get,从而收集依赖
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }

  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}

编写更新函数、创建watcher

// 更新函数
  update(node, exp, dir) {
    const fn = this[dir + "Updater"];
    fn && fn(node, this.$vm[exp]);

    // update:创建Watcher
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val);
    });
  }

  // 编译文本,将{{ooxx}}
  compileText(node) {
    this.update(node, RegExp.$1, "text");
  }

  // 实操函数
  textUpdater(node, val) {
    node.textContent = val;
  }

// k-text处理函数
  text(node, exp) {
    this.update(node, exp, "text");
  }

  // k-html
  html(node, exp) {
    this.update(node, exp, "html");
  }

  htmlUpdater(node, val) {
    node.innerHTML = val;
  }

声明Dep

/ 依赖:和响应式对象的每个key一一对应
class Dep {
  constructor() {
    this.deps = [];
  }

  addDep(dep) {
    this.deps.push(dep);
  }

  notify() {
    this.deps.forEach(dep => dep.update());
  }
}

创建watcher时触发get

class Watcher{
	constructor(vm,key,updateFn){
		// 读取以下key的值,触发其get,从而收集依赖
	    Dep.target = this;
	    this.vm[this.key];
	    Dep.target = null;
	}
}

依赖收集,创建Dep实例

function defineReactive(obj, key, val) {
  // 对val判断是否为对象,递归
  observe(val);

  // Dep在这创建
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    get() {
      console.log("get", key);

      // 依赖收集
      Dep.target && dep.addDep(Dep.target);
      return val;
    },
    set(v) {
      if (val !== v) {
        console.log("set", key);
        // 如果新传入的v是个对象
        observe(v);
        val = v;

        // 通知更新
        dep.notify();
      }
    }
  });
}