手撸一个简易Vue

102 阅读4分钟

1. 理解Vue的设计思想

MVVM模式

2bade2bc5fd93d43c1413da8caad30e3.png

MVVM框架的三要素:数据响应式、模板引擎及其渲染

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

  • Object.defineProperty()
  • Proxy

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

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

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

  • 模板 => vdom => dom

2. 数据响应式原理

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

简单实现

// cvue.js
const obj = {};

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key);
      return val
    },
    set(newVal) {
      if(newVal !== val) {
        console.log('set ' + key + ':' + newVal);
        val = newVal
      }
    },
  });
}


defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'fooooooooooooo'

这里值得注意的是,我们在defineReactive这个方法里面定义了key和val。但是我们在设置对象的get时将函数内部的val进行了返回,这里就形成了一个闭包。我们的val是不会被释放的。此时我们在外部引用了obj的值,那么obj中多个key和val的状态就会一直被保存。

那我们来结合视图看一下

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

  <script>
    const obj = {};

    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          console.log("get", key);
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            console.log("set " + key + ":" + newVal);
            val = newVal;
            // 更新函数
            update();
          }
        },
      });
    }

    function update() {
      app.innerText = obj.foo;
    }

    defineReactive(obj, "foo", "");
    obj.foo = new Date().toLocaleTimeString();

    setInterval(() => {
      obj.foo = new Date().toLocaleTimeString();
    }, 1000);
  </script>
</body>

因为obj的状态被保留了,当obj的数据发生变化时就会触发我们的update()方法重新渲染视图。

但是这里有个问题,有没有想过如果我们这样定义obj:const obj = { foo: "foo", bar: "bar", baz: { a: 1 } };那这个baz中的a就不会是响应式的。

我们现在只做了对象的一层响应式,我们应该去递归对象中的每一个元素都变成响应式的。

function defineReactive(obj, key, val) {
  // 递归
  observe(val)

  Object.defineProperty(obj, key, {
    ...
  });
}

function observe(obj) {
  if (typeof obj !== "object" || obj == null) {
    // 希望传入的时obj
    return;
  }

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

const obj = { foo: "foo", bar: "bar", baz: { a: 1 } };

observe(obj)

这样我们就能将obj变成一个完全的响应式对象了。

但是这里仍存在问题,当用户直接obj.baz = {a:100}赋值一个新的对象,这个对象就丢失了响应式。所以我们在赋值的监听器中应该也加上响应式的转化。

set(newVal) {
  if (newVal !== val) {
    // 如果传入的newVal依旧是Obj,需要做响应化处理
    observe(newVal)
    val = newVal;
  }
}

这时有没有想到在vue中如果我们想给对象设置一个新的属性,我们需要用到$set(),我们这里也可以这么做一下。也很简单,我们也封装一个set方法,然后将对象进行一个响应式的转化就行了。

function set(obj, key, val) {
  defineReactive(obj, key, val)
}

3. 整个过程原理分析

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

db14aff1aa3c48cb5b77c13f144f0c48.png

涉及类型介绍

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

4. CVue构造函数以及数据响应化

根据上面的分析,我们先来实现一下CVue的构造函数,并且实现一下数据响应化。

function defineReactive(obj, key, val) {
  observe(val);

  Object.defineProperty(obj, key, {
    get() {
      console.log("get", key);
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        console.log("set " + key + ":" + newVal);
        observe(newVal);
        val = newVal;
      }
    },
  });
}

function observe(obj) {
  if (typeof obj !== "object" || obj == null) {
    return;
  }

  new Observer(obj)
}

// 创建CVue构造函数
class CVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data

    // 数据响应化
    observe(this.$data)
  }
}

// 根据对象类型决定如何做响应化
class Observer {
  constructor(value) {
    this.value = value;

    // 判断数据类型
    if (typeof value === "object") {
      this.walk(value);
    }
  }

  // 对象数据响应化
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

这里新加了一个Observer类专门用来执行数据响应化。

但是我们这里想访问$data还得专门去实例.data访问,这里我们可以做个代理,省去这一步。

// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
  Object.keys(vm[sourceKey]).forEach(key => {
    Object.defineProperty(vm, key, {
      get() {
        return vm[sourceKey][key]
      },
      set(newVal) {
        vm[sourceKey][key] = newVal
      }
    })
  })
}

class CVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data

    // 数据响应化
    observe(this.$data)

    // 代理
    proxy(this, '$data')
  }
}

这样就能直接通过实例.XXX来访问数据了。


5. Compile编译器原理以及实现

响应式数据处理完成之后我们就来实现编译模板语法

287a933a57a4c162cddafec0a4b08715.png

上面就是编译的大致流程,我们就来一个个实现

// c-compile.js
class Compiler {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
  }
}

在创建vue实例的时候创建编译器,传入组件实例与根元素

// cvue.js
class CVue {
  constructor(options) {

    // 创建编译器
    new Compiler(this.$options.el, this)
  }
}

于是我们就可以根据根元素来遍历el树

// c-compile.js
constructor(el, vm) {
  if (this.$el) {
    // 执行编译
    this.compile(this.$el);
  }
}

// 编译函数
compile(el) {
  // 遍历el树
  const childNodes = el.childNodes;
  // childNodes只是具有可迭代性并不是数组,用from转成真的数组
  Array.from(childNodes).forEach((node) => {
    // 判断是否是元素
    if (this.isElement(node)) {
      console.log("编译元素" + node.nodeName);
      // 渲染元素
      this.compileElement(node);
    } else if (this.isInter(node)) {
      console.log("编译插值绑定" + node.textContent);
      // 添加内容
      this.compileText(node);
    }

    // 递归子节点
    if (node.childNodes && node.childNodes.length > 0) {
      this.compile(node);
    }
  });
}

上面我们使用到了isElementisInter来判断是否是元素(html标签)与插值(模板语法的内容),然后又用到两个方法来渲染和添加内容。我们就来依次实现一下。

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

isInter(node) {
  // 首先是文本标签,其次内容是{{XXX}}
  return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}

compileText(node) {
  // 因为我们在判断是否为插值之后直接调用当前方法,
  // RegExp.$1一定会拿到匹配的内容不会被后面匹配的覆盖
  node.textContent = this.$vm[RegExp.$1]; // {{ abc }} -> abc
}

compileElement(node) {
  // 节点是元素
  // 遍历其属性列表
  const nodeAttrs = node.attributes;
  Array.from(nodeAttrs).forEach((attr) => {
    // 规定:指定以c-xx="oo"定义 c-text="counter"
    const attrName = attr.name; // c-xx c-text
    const exp = attr.value; // oo counter
    // 判断是否为指令
    if (this.isDirective(attrName)) {
      const dir = attrName.substring(2); // xx text
      // 如果存在指令,执行指定
      this[dir] && this[dir](node, exp);
    }
    // 判断是否为事件
    if (this.isEvent(attrName)) {
      // 获取绑定的事件名
      const eventName = attrName.substring(1)
      // 获取绑定的回调函数
      const Fn = this.getFn(exp)
      // 获取参数的具体值
      const args = []
      Fn.args.forEach(arg => {
        if(this.$vm[arg]) {
          args.push(this.$vm[arg])
        } else {
          args.push(arg)
        }
      })
      // 添加事件监听
      node.addEventListener(eventName, () => {
        this.$vm.$options.methods[Fn.fnName].call(this.$vm, ...args)
      })
    }
  });
}

isDirective(attr) {
  return attr.indexOf("c-") === 0;
}

isEvent(attr) {
  return attr.indexOf("@") === 0;
}

getFn(func) {
  // 先用正则匹配,取得符合参数模式的字符串.
  // 第一个分组是这个:  ([^)]*) 非右括号的任意字符
  // console.log(func.toString().match(/(.*)\((.*)\)/));
  var fnName = func.toString().match(/(.*)\((.*)\)/)[1];
  var Args = func.toString().match(/(.*)\((.*)\)/)[2];

  // 用逗号来分隔参数(arguments string).
  return {
    fnName,
    args: Args.split(",").map(function(arg) {
      // 去除注释(inline comments)以及空格
      return arg.replace(/\/\*.*\*\//, "").trim();
    }).filter(function(arg) {
      // 确保没有 undefined.
      return arg;
    })
  }
}

// c-text 渲染内容
text(node, exp) {
  node.textContent = this.$vm[exp];
}

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

// c-model
model(node, exp) {
  // 完成赋值和更新
  node.value = this.$vm[exp]

  // 事件监听
  node.addEventListener("input", e => {
    // 新值赋值给数据即可
    this.$vm[exp] = e.target.value
  });
}
<body>
  <div id="app">
    <p @click="add(num)">{{counter}}</p>
    <p c-text="counter"></p>
    <p c-html="desc"></p>
    <input type="text" c-model="desc">
  </div>

  <script src="cvue.js"></script>
  <script src="c-compile.js"></script>
  <script>

    const app = new CVue({
      el:'#app',
      data: {
        counter: 1,
        desc: '<span style="color: red;">CVue</span>'
      },
      methods: {
        add(val) { 
          this.counter += val
        },
      },
    })
    setInterval(() => {
      app.counter++
    }, 1000);
    
  </script>
</body>

GIF 2023-4-10 18-27-12.gif

正常显示


6. 视图更新原理以及实现

依赖收集

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

ff650a41d72caae85da7f19ec9601ee1.png

实现思路

  1. defineReactive为每一个key创建一个Dep实例
  2. 初始化视图时读取某个key,创建一个watcher
  3. 触发对应key的getter方法时,就将对于的watcher添加到这个key的Dep上
  4. 当这个值发生变化时,setter触发,就从它的Dep中通知所有的watcher更新

所以又回到了这个图的来。

db14aff1aa3c48cb5b77c13f144f0c48.png

我们先来实现watcher,监听数据的变化并通知更新。

我们现在还没有Dep来进行管理,我们就简单一点,创建一个全局的watcers来监听所有的参数。

// cvue.js

const watchers = [] // 临时保存watcher
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    // 当前key对于的更新方法
    this.updateFn = updateFn

    watchers.push(this)
  }

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

// 在setter中统一将所有watcher进行更新
set(newVal) {
  if (newVal !== val) {
    ...
    // 通知更新
    watchers.forEach(w => w.update())
  }
},

这里我们在创建watcher时传入了一个更新方法,用来更新页面中的数据。

// compile.js
compileText(node) {
  node.textContent = this.$vm[RegExp.$1]; // {{ abc }} -> abc
}

isDirective(attr) {
  return attr.indexOf("c-") === 0;
}

// c-text 渲染内容
text(node, exp) {
  node.textContent = this.$vm[exp];
}

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

// c-model
model(node, exp) {
  node.value = this.$vm[exp]
  node.addEventListener("input", e => {
    this.$vm[exp] = e.target.value
  });
}

这些是我们之前的渲染方法,我们要写更新方法的话需要在每个里面都写上它对应的更新方法。这样代码就会变的臃肿且可读性差。于是乎,我们就创建一个update方法,为每个指令再创建一个对应的更新方法,这样我们就只需要统一调用update方法,传入对应的更新名,再调用对应的方法就可以了。

// c-compile.js
update(node, exp, dir) {
  // 初始化
  // 指令对应的更新函数xxUpdater
  const fn = this[dir + 'Updater']
  fn && fn(node, this.$vm[exp])

  // 更新处理,封装一个更新函数,可以更新对应的dom元素
  new Watcher(this.$vm, exp, function(val) {
    fn && fn(node, val)
  })
}

modelUpdater(node, value) {
  node.value = value;
}

textUpdater(node, value) {
  node.textContent = value;
}

htmlUpdater(node, value) {
  node.innerHTML = value
}

接着改造之前的渲染方法。

compileText(node) {
  this.update(node, RegExp.$1, 'text')
}

text(node, exp) {
  this.update(node, exp, 'text')
}

html(node, exp) {
  this.update(node, exp, 'html')
}

model(node, exp) {
  this.update(node, exp, "model");
  node.addEventListener("input", e => {
    this.$vm[exp] = e.target.value
  });
}

这样我们的属性监听就完成了。但是我们现在只要有一个key发生改变,所有的watcher都要重新执行更新方法。于是就需要用Dep来搜集管理watcher,让key与watcher一一对应,每次只需要更新发生变化的key的所有watcher。

现在我们梳理一下我们要做的步骤:

  1. 为每一个key创建一个Dep
  2. 一个key的Dep收集所有的watcher
  3. key的值发生变化时通知对应Dep中的所有wathcer更新。

我们先声明Dep:

// cvue.js

// Dep:依赖,管理某个key相关Watcher势力
class Dep {
  constructor() {
    // 存放watcher
    this.deps = []
  }

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

  // 通知更新 
  notify() {
    this.deps.forEach(dep => dep.update())
  }
}

创建watcher触发getter

class Watcher {
  constructor(vm, key, updateFn) {
    // Dep.target静态属性上设置为当前watcher实例
    Dep.target = this
    this.vm[this.key] // 读取触发了getter
    Dep.target = null // 收集完就置空
  }
}

收集依赖,创建Dep实例

function defineReactive(obj, key, val) {
  // 创建一个Dep和当前key一一对应
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        // 通知更新
        dep.notify()
      }
    },
  });
}

最后我们再来梳理一下我们整个CVue的执行过程

4f420351de97466a19ed13a326b27ce9.png


7. 优化——数组的响应化

上面我们的CVue通过Object.defineProperty``的方法实现了对象的数据响应式,但是我们在修改数组的时候通常是用push、shift、pop...`来修改数组。这些方法对数组的修改是无法被监听到的。

这些方法就是我们来进行响应化的切入点。而有人说直接赋值呢,直接赋值修改整个数组,会被响应化后的data对象监听,所以不需要考虑。

Vue官方对此的做法是重写Array的这七种方法,在重写的方法里我们就可以做我们想做的事。

// 获取array的原型
const orginalProto = Array.prototype;
// 我们不能直接改array的原型,我们创建一个array原型的备份
const arrayProto = Object.create(orginalProto);

// 重写的array的7大方法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 重写原型的方法
methodsToPatch.forEach((method) => {
  // 获取原生方法
  const original = orginalProto[method];
  // 执行原方法
  original()
  // 我们可以进行的后续操作
});

这里我们需要做的后续方法就是对数组执行收集到的依赖,执行更新方法。

对于对象来说,对象不会有很多的属性,可以为每个属性设置gettersetter,但是数组可以有很多的元素,给每个元素都添加会极大消耗性能。

这里官方的做法又给我秀到了。我们在对象进行defineReactive来创建Dep存储依赖,数组用不了。那干脆直接在Observer里创建一个Dep来存储数组的依赖,直接通过def函数来为数据创建一个__ob__属性存放当前的Observer实例。这样数组就能查看自己的依赖,然后创建了一个dependArray方法,从__ob__拿到Dep实例,在编译器中进行触发依赖收集。在getter中判别是否为数组,是数组就调用dependArray

// 添加属性值
function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}
// 根据类型决定如何做响应化
class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();

    def(value, "__ob__", this);

    // 判断数据类型
    if (typeof value === "object") {
      ...
    }

    if (Array.isArray(value)) {
      this.changeArray(value);
    }
  }

  // 数组数据响应化。。。
  changeArray(arr) {
    arr.__proto__ = arrayProto;
    const keys = Object.keys(arr);
    for (let i = 0; i < keys.length; i++) {
      observe(arr[i]);
    }
  }
}
// 递归转化所有数组元素
function dependArray (value) {
  value.__ob__.dep.addDep(e.__ob__.dep.target)
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    // 对象元素也有__ob__也要搜集依赖
    e.__ob__.dep.addDep(e.__ob__.dep.target)
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

这里我们的e.__ob__.dep.addDep(e.__ob__.dep.target)通过__ob__实例添加依赖的写法太臃肿了。我们来改写一下依赖收集的方法。

class Watcher {
  addDep(dep) {
    dep.addDeps(this);
  }
}

class Dep {
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

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

dep为Dep实例,Dep.target又为Watcher实例,这样互相引用也是一一对应的一种体现我们data中的每个属性key与dep是一一对应的,这样我们就能串起来。

这样 e.__ob__.dep.addDep(e.__ob__.dep.target)就可以改写成e.__ob__.dep.depend()

最后在调用7大方法的时候我们可以通知依赖进行更新。

methodsToPatch.forEach((method) => {
  // 获取原生方法
  const original = orginalProto[method];
  // 重写原型上的方法
  def(arrayProto, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    // 通知更新
    ob.dep.notify();
    return result;
  });
});

这样就实现了一个简易的CVue了。

8. 最后附上完整代码

cvue.js

// 获取array的原型
const orginalProto = Array.prototype;
// 我们不能直接改array的原型,我们创建一个array原型的备份
const arrayProto = Object.create(orginalProto);

// 重写的array的7大方法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

methodsToPatch.forEach((method) => {
  // 获取原生方法
  const original = orginalProto[method];
  // 重写原型上的方法
  def(arrayProto, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    ob.dep.notify();
    return result;
  });
});

// 添加属性值
function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}

// 递归转化所有数组元素
function dependArray (value) {
  value.__ob__.dep.depend();
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

function defineReactive(obj, key, val) {
  // 递归
  observe(val);

  // 创建一个Dep和当前key一一对应
  const dep = new Dep();
  // let childOb = observe(val)
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集
      if (Dep.target) {
        dep.depend();
        if (Array.isArray(val)) {
          dependArray(val)
        }
      }
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        // console.log("set " + key + ":" + newVal);
        // 如果传入的newVal依旧是Obj,需要做响应化处理
        observe(newVal);
        val = newVal;

        // 通知更新
        // watchers.forEach(w => w.update())
        dep.notify();
      }
    },
  });
}

function observe(obj) {
  if (typeof obj !== "object" || obj == null) {
    // 希望传入的是obj
    return;
  }

  // 创建Observer实例
  new Observer(obj);
}

// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
  Object.keys(vm[sourceKey]).forEach((key) => {
    Object.defineProperty(vm, key, {
      get() {
        return vm[sourceKey][key];
      },
      set(newVal) {
        vm[sourceKey][key] = newVal;
      },
    });
  });
}

// 创建CVue构造函数
class CVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;

    // 数据响应化
    observe(this.$data);

    // 代理
    proxy(this, "$data");

    // 创建编译器
    new Compiler(this.$options.el, this);
  }
}

// 根据对象类型决定如何做响应化
class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();

    def(value, "__ob__", this);

    // 判断数据类型
    if (typeof value === "object") {
      this.walk(value);
    }

    if (Array.isArray(value)) {
      this.changeArray(value);
    }
  }

  // 对象数据响应化
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }

  // 数组数据响应化。。。
  changeArray(arr) {
    arr.__proto__ = arrayProto;
    const keys = Object.keys(arr);
    for (let i = 0; i < keys.length; i++) {
      observe(arr[i]);
    }
  }
}

// 观察者:保存更新函数,值发生变化调用更新函数
// const watchers = []
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;

    // watchers.push(this)

    // Dep.target静态属性上设置为当前watcher实例
    Dep.target = this;
    this.vm[this.key]; // 读取触发了getter
    Dep.target = null; // 收集完就置空
  }

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

  addDep(dep) {
    dep.addDeps(this);
  }
}

// Dep:依赖,管理某个key相关Watcher势力
class Dep {
  constructor() {
    this.deps = [];
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

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

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

c-compile.js

// 编译器
// 递归遍历dom树
// 判断节点类型,如果是文本,则判断是否插值绑定
// 如果是元素,则遍历属性判断是否是指令或事件,然后递归子元素
class Compiler {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);

    if (this.$el) {
      // 执行编译
      this.compile(this.$el);
    }
  }

  compile(el) {
    // 遍历el树
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      // 判断是否是元素
      if (this.isElement(node)) {
        // console.log("编译元素" + node.nodeName);
        this.compileElement(node);
      } else if (this.isInter(node)) {
        // console.log("编译插值绑定" + node.textContent);
        this.compileText(node);
      }

      // 递归子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    });
  }

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

  isInter(node) {
    // 首先是文本标签,其次内容是{{XXX}}
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }

  compileText(node) {
    this.update(node, RegExp.$1, "text");
  }

  compileElement(node) {
    // 节点是元素
    // 遍历其属性列表
    const nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach((attr) => {
      // 规定:指定以c-xx="oo"定义 c-text="counter"
      const attrName = attr.name; // c-xx c-text
      const exp = attr.value; // oo counter
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2); // xx text
        // 如果存在指令,执行指定
        this[dir] && this[dir](node, exp);
      }
      // 判断是否为事件
      if (this.isEvent(attrName)) {
        const eventName = attrName.substring(1);
        const Fn = this.getFn(exp);
        // const method = this.$vm.$options.methods[Fn.fnName]
        const args = [];
        Fn.args.forEach((arg) => {
          if (this.$vm[arg]) {
            args.push(this.$vm[arg]);
          } else {
            args.push(arg);
          }
        });
        node.addEventListener(eventName, () => {
          this.$vm.$options.methods[Fn.fnName].call(this.$vm, ...args);
        });
      }
    });
  }

  isDirective(attr) {
    return attr.indexOf("c-") === 0;
  }

  isEvent(attr) {
    return attr.indexOf("@") === 0;
  }

  getFn(func) {
    // 先用正则匹配,取得符合参数模式的字符串.
    // 第一个分组是这个:  ([^)]*) 非右括号的任意字符
    // console.log(func.toString().match(/(.*)\((.*)\)/));
    var fnName = func.toString().match(/(.*)\((.*)\)/)[1];
    var Args = func.toString().match(/(.*)\((.*)\)/)[2];

    // 用逗号来分隔参数(arguments string).
    return {
      fnName,
      args: Args.split(",")
        .map(function (arg) {
          // 去除注释(inline comments)以及空格
          return arg.replace(/\/\*.*\*\//, "").trim();
        })
        .filter(function (arg) {
          // 确保没有 undefined.
          return arg;
        }),
    };
  }

  update(node, exp, dir) {
    // 初始化
    // 指令对应的更新函数xxUpdater
    const fn = this[dir + "Updater"];
    fn && fn(node, this.$vm[exp]);

    // 更新处理,封装一个更新函数,可以更新对应的dom元素
    new Watcher(this.$vm, exp, function (val) {
      fn && fn(node, val);
    });
  }

  // c-text
  text(node, exp) {
    this.update(node, exp, "text");
  }

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

  // c-model
  model(node, exp) {
    // update方法只完成赋值和更新
    this.update(node, exp, "model");
    // node.value = this.$vm[exp]

    // 事件监听
    node.addEventListener("input", e => {
      // 新值赋值给数据即可
      this.$vm[exp] = e.target.value
    });
  }

  modelUpdater(node, value) {
    // 表单元素赋值
    node.value = value;
  }

  textUpdater(node, value) {
    node.textContent = value;
  }

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