1.生命周期 #

Vue实例的生命周期包含一系列的初始化步骤,例如需要设置数据监听,编译模板,挂载实例到DOM,并在数据变化时更新DOM等。同时也有一些生命周期钩子函数可以在某个阶段运行自定义的函数。

以下是Vue的主要生命周期钩子:

  1. beforeCreate: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。

  2. created: 在实例创建完成后被立即调用。在这一步,实例已经完成了以下的配置:数据观测 (data observer),属性和方法的运算,watch/event事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。

  3. beforeMount: 在挂载开始之前被调用。相关的 render 函数首次被调用。

  4. mounted: el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。注意 mounted 不会保证所有的子组件也都一起被挂载。

  5. beforeUpdate: 数据更新时调用,发生在虚拟 DOM 打补丁之前。

  6. updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

  7. beforeDestroy: 实例销毁之前调用。在这一步,实例仍然完全可用。

  8. destroyed: Vue 实例销毁后调用。当这个钩子被调用时,Vue 实例指向的所有东西都会解绑,所有的事件监听器都会被移除,所有的子实例也都会被销毁。

1.1 beforeCreate #

在 Vue 中,beforeCreate 是生命周期钩子的一部分。生命周期钩子是 Vue 实例在其生命周期中的不同阶段会自动调用的特殊函数。

我们将从以下几个方面讲解 beforeCreate 钩子。

定义:

beforeCreate 是在实例初始化后,数据观测 (data observer) 和事件/观察者配置之前被调用。在这个阶段,Vue 实例的观察者 (Observer) 和事件 (Events) 还没有被初始化。

注意事项:

  1. beforeCreate 生命周期钩子在实例创建之后立即运行,这意味着此时 Vue 实例还没有完成数据观测,也就是说此时实例上的数据、计算属性和方法等都是不可用的。

  2. 如果你在 beforeCreate 钩子中执行的逻辑并不依赖于上述实例属性,那么这个钩子是非常适合的。例如,你可以在这个钩子中运行一些非异步的代码,或者进行一些初步的、不涉及实例数据和方法的配置。

  3. beforeCreate 是创建实例的最早阶段,也是所有生命周期钩子中最先执行的。因此,如果你需要在 Vue 实例创建过程中尽可能早地运行一些代码,你可以使用 beforeCreate 钩子。

示例代码:

下面的代码片段展示了如何在 Vue 实例中使用 beforeCreate 钩子:

new Vue({
  beforeCreate() {
    console.log('这是 beforeCreate 阶段');
  },
  created() {
    console.log('这是 created 阶段');
  },
  // 其他生命周期钩子...
})

在这个例子中,当 Vue 实例被创建并开始执行其生命周期钩子时,你会在控制台上看到 "这是 beforeCreate 阶段" 和 "这是 created 阶段" 的消息。注意到 beforeCreate 钩子的消息会先出现,这是因为 beforeCreate 是所有生命周期钩子中最先执行的。

1.2 created #

在 Vue 中,created 是生命周期钩子的一部分,它在 Vue 实例创建过程中的某个阶段被调用。

定义:

created 钩子是在 Vue 实例创建完毕,数据观测 (data observer) 和事件/观察者已配置完毕,但在挂载阶段 (mount phase) 之前被调用。在 created 阶段,Vue 实例的数据已经被观测,所以在这个阶段可以访问到数据、计算属性和方法。

注意事项:

  1. created 钩子中,可以执行依赖于数据的逻辑操作,如对数据的初步处理。然而要注意,此时 Vue 实例还没有被挂载到 DOM 上,因此这个阶段不能进行任何与 DOM 相关的操作。

  2. 如果你在 created 钩子中执行的逻辑需要访问或改变 Vue 实例上的数据,这个阶段是非常适合的。在这个阶段,你还可以执行一些如 AJAX 请求等异步操作。

  3. created 钩子执行完毕后,如果 eltemplate 选项都不存在,那么这个阶段将停止,并等待手动的 vm.$mount 调用。如果存在 eltemplate 选项,则进入挂载阶段。

示例代码:

以下是如何在 Vue 实例中使用 created 钩子的示例代码:

new Vue({
  data: {
    message: '你好,世界!'
  },
  created: function () {
    console.log('这是 created 阶段');
    console.log('此时的 message 是: ' + this.message); // 这里可以访问到 data 中的 message
  },
  // 其他生命周期钩子...
})

在这个示例中,当 Vue 实例创建并开始执行其生命周期钩子时,你会在控制台上看到 "这是 created 阶段" 和 "此时的 message 是: 你好,世界!" 的消息。这个示例展示了在 created 阶段可以访问和使用 Vue 实例的数据。

1.3 beforeMount #

在 Vue 生命周期中,beforeMount 生命周期钩子函数的主要作用就是在挂载之前做一些业务逻辑的处理。

在 Vue 中,生命周期钩子 beforeMount 在模板编译完成后,但在首次渲染之前被调用。这意味着在 beforeMount 钩子中,你的模板已经编译完成,转换成了 render 函数,但是这个 render 函数还没有被执行,也就是还没有生成虚拟 DOM,因此你还无法访问到渲染后的 DOM 结构。

总结一下,以下是这些过程发生的顺序:

  1. 模板编译完成,转换成 render 函数。
  2. beforeMount 钩子函数被调用。
  3. render 函数被执行,生成虚拟 DOM。
  4. 虚拟 DOM 被渲染到页面上,替换挂载点的内容。
  5. mounted 钩子函数被调用。

需要注意的是,如果你需要访问已经渲染好的 DOM 结构,你应该在 mounted 钩子中进行,而不是 beforeMount 钩子。

1.4 mounted #

在 Vue 中,mounted 是生命周期钩子的一部分,它在 Vue 实例创建过程中的特定阶段被调用。

定义:

mounted 钩子在 Vue 实例被新创建的 vm.$el 替换,并挂载到其元素上后调用。这个阶段意味着 Vue 实例已经编译完成模板,将数据和元素结合起来,并将结果渲染到 DOM 中。

注意事项:

  1. mounted 钩子中,可以执行依赖于 DOM 的操作,因为此时 Vue 实例已经被挂载到 DOM 上,你可以访问到实例的 DOM 结构。

  2. 尽管在 mounted 阶段可以访问到 DOM,但并不保证所有的子组件也都一定已经被挂载。如果你希望等到整个视图都渲染完毕,可以以 vm.$nextTickmounted 钩子中返回一个 Promise。

  3. 如果你需要在 Vue 实例创建过程中的一个稍晚的阶段运行代码,那么 mounted 钩子是一个很好的选择。

示例代码:

以下是如何在 Vue 实例中使用 mounted 钩子的示例代码:

new Vue({
  el: '#app',
  data: {
    message: '你好,世界!'
  },
  mounted: function () {
    console.log('这是 mounted 阶段');
    console.log('此时的 message 是: ' + this.message);
    console.log('此时的 DOM 结构是: ' + this.$el.outerHTML);
  },
  // 其他生命周期钩子...
})

在这个示例中,当 Vue 实例创建并开始执行其生命周期钩子时,你会在控制台上看到 "这是 mounted 阶段","此时的 message 是: 你好,世界!" 和对应的 DOM 结构的消息。这个示例展示了在 mounted 阶段可以访问和使用 Vue 实例的数据和 DOM。

1.5 beforeUpdate #

在 Vue 中,beforeUpdate 是生命周期钩子的一部分,它在 Vue 实例创建过程中的某个阶段被调用。

定义:

beforeUpdate 钩子在数据改变之后,但是在 DOM 重新渲染之前被调用。此阶段可以在数据更新之后执行一些操作,但是此时新的数据尚未渲染到模板中。

注意事项:

  1. beforeUpdate 钩子中,可以执行依赖于数据变化的操作,如数据预处理等。然而要注意,此时虽然数据已经更新,但是 DOM 还未更新,所以不能依赖于新的 DOM 结构。

  2. 如果你需要在数据更新后、重新渲染前执行一些操作,那么 beforeUpdate 钩子是适合的。

  3. beforeUpdate 阶段,你可以访问更新后的数据,但是 DOM 仍然是旧的。新的 DOM 会在 updated 钩子中生成。

示例代码:

以下是如何在 Vue 实例中使用 beforeUpdate 钩子的示例代码:

new Vue({
  el: '#app',
  data: {
    message: '你好,世界!'
  },
  methods: {
    updateMessage: function () {
      this.message = 'Hello Vue!';
      console.log('更新后的 message 是: ' + this.message);
    }
  },
  beforeUpdate: function () {
    console.log('这是 beforeUpdate 阶段');
    console.log('此时的 message 是: ' + this.message);
  },
  // 其他生命周期钩子...
})

在这个示例中,当 updateMessage 方法被调用并更新 message 数据后,你会在控制台上看到 "这是 beforeUpdate 阶段" 和 "此时的 message 是: Hello Vue!" 的消息。这个示例展示了在 beforeUpdate 阶段可以访问到更新后的数据,但此时新的数据尚未被渲染到 DOM 中。

1.6 updated #

在 Vue 中,updated 是一个生命周期钩子函数,它在数据改变后,虚拟 DOM 重新渲染并更新 DOM 之后被调用。也就是说,当你的组件的数据发生变化,视图被重新渲染和更新后,updated 钩子函数将被执行。

请注意以下的事项:

以下是使用 updated 的一个基本示例:

new Vue({
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    updateMessage() {
      this.message = 'Hello Vue! afterUpdate';
    }
  },
  beforeUpdate() {
    // 在数据变化,虚拟 DOM 重新渲染之前,我们可以在这里进行数据处理
    console.log('beforeUpdate 执行了');
  },
  updated() {
    // 在数据变化,虚拟 DOM 重新渲染并更新 DOM 之后,我们可以在这里进行 DOM 操作
    console.log('updated 执行了');
    console.log('新的 message 是:' + this.message);
  },
  el: '#app'
});

在上面的代码中,updateMessage 方法会修改 message 数据,导致 beforeUpdateupdated 钩子函数的调用。当 Vue 实例的数据改变并完成 DOM 的更新时,updated 钩子函数将被调用,然后它会打印出新的 message 数据。

1.7 beforeDestroy #

在 Vue 中,beforeDestroy 是一个生命周期钩子函数,它在实例销毁之前被调用。在这一阶段,实例仍然完全可用,所有的数据、属性和方法都可以被访问和使用。

请注意以下的事项:

以下是使用 beforeDestroy 的一个基本示例:

new Vue({
  data: {
    message: 'Hello Vue!'
  },
  beforeDestroy() {
    // 在实例销毁之前,我们可以在这里进行一些清理工作
    console.log('beforeDestroy 执行了');
  },
  destroyed() {
    // 在实例销毁之后,我们可以在这里进行一些后续处理
    console.log('destroyed 执行了');
  },
  methods: {
    destroyInstance() {
      this.$destroy();
    },
  },
  el: '#app'
});

在上面的代码中,destroyInstance 方法会触发实例的销毁,导致 beforeDestroydestroyed 钩子函数的调用。当 Vue 实例销毁之前,beforeDestroy 钩子函数将被调用;然后,当 Vue 实例完全销毁之后,destroyed 钩子函数将被调用。

1.8 destroyed #

在 Vue 中,destroyed 是生命周期钩子的一部分,它在 Vue 实例销毁过程中的某个阶段被调用。

定义:

destroyed 钩子在 Vue 实例销毁完成之后被调用。此阶段,Vue 实例已经解除了事件监听以及和子组件的关联,其指令也已经被解除绑定。

注意事项:

  1. destroyed 钩子中,可以执行一些清理操作,如取消定时器,取消事件监听等。此时,Vue 实例已经完全被销毁,你无法再访问实例上的任何属性或执行任何方法。

  2. destroyed 钩子主要用来执行清理操作,而非在销毁后改变组件状态,因为这个阶段,Vue 实例已经不存在了。

  3. 如果你的 Vue 实例在销毁前需要执行一些特定的任务,那么你可以在 beforeDestroy 钩子中执行这些任务,而非 destroyed 钩子。

示例代码:

以下是如何在 Vue 实例中使用 destroyed 钩子的示例代码:

new Vue({
  el: '#app',
  data: {
    message: '你好,世界!'
  },
  methods: {
    destroy: function () {
      this.$destroy();
    }
  },
  destroyed: function () {
    console.log('这是 destroyed 阶段');
  },
  // 其他生命周期钩子...
})

在这个示例中,当 destroy 方法被调用并销毁 Vue 实例后,你会在控制台上看到 "这是 destroyed 阶段" 的消息。这个示例展示了在 destroyed 阶段,Vue 实例已经被销毁,你无法再访问实例上的任何属性或执行任何方法。

1.9 life.html #

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Vue Lifecycle Example</title>
    <script src="vue2.js"></script>
</head>

<body>
    <div id="app">
        <p>{{message}}</p>
    </div>
    <script>
        const vm = new Vue({
            el: '#app',
            data: { message: 'Hello, world!' },
            beforeCreate() {
                console.log('beforeCreate');
            },
            created() {
                console.log('created');
            },
            beforeMount() {
                console.log('beforeMount');
            },
            mounted() {
                console.log('mounted');
            },
            beforeUpdate() {
                console.log('beforeUpdate');
            },
            updated() {
                console.log('updated');
            },
            beforeDestroy() {
                console.log('beforeDestroy');
            },
            destroyed() {
                console.log('destroyed');
            }
        });
        setTimeout(() => {
            vm.message = 'Hello, Vue!';
        }, 1000);
        setTimeout(() => {
            vm.$destroy();
        }, 2000);
    </script>
</body>

</html>

1.10 vue.js #

class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    vm[expr];
    Dep.target = null;
  }
  update() {
    this.cb();
  }
}
class VNode {
  constructor(tag, data, children) {
    this.tag = tag;
    this.data = data;
    this.children = children;
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.initEvents();
    this.initLifeCycle();
    this.callHook('beforeCreate');
    this.initInjections();
    this.initReactivity();
  }
  initReactivity() {
    console.log(`initReactivity`);
    for (let key in this.$data) {
      let value = this.$data[key];
      let dep = new Dep();
      Object.defineProperty(this, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal) {
          value = newVal;
          dep.notify();
        }
      });
    }
  }
  $mount(el) {
    this.callHook('beforeMount');
    const element = document.querySelector(el);
    this.$el = element;
    const template = element.outerHTML;
    this.compile(template);
    this.oldVnode = this.render();
    this.patch(null, this.oldVnode);
    this.callHook('beforeMount');
  }
  $update() {
    this.callHook("beforeUpdate");
    const newVNode = this.render();
    this.patch(this.oldVnode, newVNode);
    this.callHook("updated");
  }
  $destroy(){
    this.callHook('beforeDestroy');
    this.callHook('destroyed');
  }
  patch(oldVNode, newVNode) {
    const newDOMElement = createDOMElement(newVNode);
    this.$el.parentNode.replaceChild(newDOMElement, this.$el);
    this.$el = newDOMElement;
    this.oldVnode = newVNode;
  }
  compile(template) {
    this.render = function () {
      let reg = /\{\{(.+?)\}\}/g;
      let match;
      while ((match = reg.exec(template)) !== null) {
        const key = match[1].trim();
        new Watcher(this, key, this.$update.bind(this));
      }
      return new VNode('div', { id: 'app' }, this.msg);
    }
  }
  initInjections() {
    console.log(`initInjections`);
  }
  callHook(hookName) {
    console.log(hookName);
    this.$options[hookName]?.call(this);
  }
  initLifeCycle() {
    console.log(`initLifeCycle`);
    this._isMounted = false;
  }
  initEvents() {
    console.log(`initEvents`);
    this._events = {};
  }
  $on(eventName, callback) {
    if (!this._events[eventName]) {
      this._events[eventName] = [];
    }
    this._events[eventName].push(callback);
  }
  $emit(eventName, ...args) {
    const callbacks = this._events[eventName];
    if (callbacks) {
      callbacks.forEach(callback => callback(...args));
    }
  }
  $off(eventName, callback) {
    const callbacks = this._events[eventName];
    if (callbacks) {
      if (typeof callback === 'function') {
        const index = callbacks.indexOf(callback);
        if (index !== -1) {
          callbacks.splice(index, 1);
        }
      } else {
        this._events[eventName] = [];
      }
    }
  }
}
function createDOMElement(vdom) {
  let element = document.createElement(vdom.type);
  for (let key in vdom.data) {
    element.setAttribute(key, vdom.data[key]);
  }
  element.innerHTML = vdom.children;
  return element;
}

2.实例属性 #

2.1 事件 #

$on$emit$off 是用于组件之间通信的实例方法。这些方法提供了一种自定义事件的机制,让你能够在一个组件里触发事件,并在另一个组件里监听这些事件。

$on

$on 用于监听事件。当你在一个组件中调用这个方法时,你可以指定一个事件名和一个当该事件被触发时调用的回调函数。

this.$on('event-name', callback);

例如:

export default {
  created() {
    this.$on('say-hello', (message) => {
      console.log(`Hello from event: ${message}`);
    });
  },
};

$emit

$emit 用于触发事件。你可以在一个组件内部或者从一个组件中触发一个自定义事件,并且传递可选的参数。

this.$emit('event-name', optionalPayload);

例如:

export default {
  methods: {
    sayHello() {
      this.$emit('say-hello', 'world');
    },
  },
};

在这个例子中,当 sayHello 方法被调用时,会触发一个名为 say-hello 的事件,并传递 "world" 作为负载。

$off

$off 用于停止监听一个或多个事件。你可以使用 $off 在不再需要时移除事件监听。

例如:

export default {
  beforeDestroy() {
    this.$off('say-hello');
  },
};

在这个例子中,beforeDestroy 生命周期钩子被用于移除名为 say-hello 的所有事件监听。

2.2 $mount #

在Vue 2中,$mount 是一个非常重要的实例方法,用于手动挂载一个未挂载的 Vue 实例。当你想要在代码中明确地控制实例何时挂载到DOM上时,这非常有用。

基础用法

在最基础的用法中,$mount 可以不传递任何参数,这样 Vue 实例会立即挂载:

const app = new Vue({
  template: '<div>Hello, world!</div>'
})

// 手动挂载实例
app.$mount()

挂载到指定元素

你还可以传入一个CSS选择器或DOM元素作为参数,以指定Vue实例应该挂载到哪个DOM元素上:

const app = new Vue({
  template: '<div>Hello, world!</div>'
})

// 挂载到页面中 id 为 "app" 的元素上
app.$mount('#app')

如果你先前已经有了一个DOM元素并希望Vue接管它,可以这样做:

<div id="app">
  <!-- 这里可能有一些初始的标记 -->
</div>
const app = new Vue({
  template: '<div>Hello, world!</div>'
})

app.$mount('#app')

替代 el 选项

$mount 方法通常用作 el 选项的替代,el 选项通常在Vue实例的配置对象中指定:

new Vue({
  el: '#app',
  template: '<div>Hello, world!</div>'
})

使用 $mount 的一个好处是,你可以在文档就绪之后再进行挂载,或者在某个特定的时间或条件下进行挂载。

返回实例

$mount 方法返回实例自身,因此可以链式调用其他实例方法。

const app = new Vue({
  template: '<div>Hello, world!</div>'
})

app.$mount('#app').$destroy()

在大多数情况下,你可能不需要手动使用 $mount,因为使用 el 选项或者在组件系统中使用Vue实例通常更为方便。但了解 $mount 如何工作以及何时使用它是非常有用的。

2.3 $destroy #

$destroy 是一个实例方法,用于完全销毁一个Vue实例及其所有子组件和事件监听器。一旦调用了这个方法,Vue实例将不再是响应式的,并且所有的生命周期钩子(如beforeDestroydestroyed)也会被触发。这样做会释放实例占用的所有资源,这样垃圾回收机制就能回收这些资源。

这里是一个简单的例子:

// 创建一个新的 Vue 实例
const vm = new Vue({
  data: {
    message: 'Hello, world!'
  },
  beforeDestroy() {
    console.log('Instance will be destroyed.');
  },
  destroyed() {
    console.log('Instance destroyed.');
  }
});

// 销毁这个 Vue 实例
vm.$destroy();

在这个例子中,调用$destroy会首先触发beforeDestroy钩子,然后解除所有数据绑定和事件监听器,并最终触发destroyed钩子。

请注意,手动调用$destroy通常是不必要的,因为Vue的内存管理通常足够智能,能够自动处理大多数场景。然而,在某些特殊情况下(例如动态创建和销毁组件,但不通过v-if或者<keep-alive>等机制),手动管理组件的生命周期可能是有用的。

另外,需要强调的是,销毁一个实例后再试图访问它的任何方法或数据都是无效的。

// 创建一个新的 Vue 实例
const vm = new Vue({
  data: {
    message: 'Hello, world!'
  }
});

// 销毁这个 Vue 实例
vm.$destroy();

// 无效,因为实例已经被销毁
console.log(vm.message); // undefined

总的来说,$destroy是一个强大但需要谨慎使用的方法。在大多数情况下,Vue的正常生命周期管理应该足够满足你的需求。

2.4 $set #

$set 是一个很有用的实例方法,用于解决 Vue 的响应式系统中的一些限制。具体来说,当你在一个已经创建的 Vue 实例上动态添加一个新的根级别属性时,这个属性不会自动变为响应式的。同样,Vue 也无法检测到对象属性的添加或删除。

这时,$set 就可以派上用场。$set 方法可以接受三个参数:

  1. 目标对象(Object 或者 Array):你想要添加或更新属性的对象。
  2. 键名(String 或者 Number):要添加或更新的属性的名字。
  3. :新的值。

下面是几个示例:

添加新属性到对象

let vm = new Vue({
  data: {
    obj: {
      a: 1
    }
  }
})

// `obj.b` 不是响应式的
vm.obj.b = 2

// 使用 $set 使其变为响应式的
vm.$set(vm.obj, 'b', 2)

更新数组的索引

Vue 不能检测到以下变动:

let vm = new Vue({
  data: {
    arr: [1, 2, 3]
  }
})

vm.arr[0] = 4  // 不是响应式的

但你可以使用 $set

vm.$set(vm.arr, 0, 4)  // 现在 arr 是 [4, 2, 3],并且这个更改是响应式的。

注意事项

2.5 $delete #

在 Vue 中,Vue.$delete 方法用于删除对象或数组上的属性或项,并触发相应的视图更新。这是 Vue 的响应式系统的一部分,用于处理 Vue 无法检测到属性删除的情况。

使用场景

Vue 的响应式系统可以追踪对象和数组的变化,并相应地更新视图。然而,当你直接使用 delete 操作符删除一个对象的属性时,Vue 无法检测到这一变化。Vue.$delete 解决了这个问题。

用法

  1. 删除对象属性:
let vm = new Vue({
  data: {
    myObject: {
      a: 1,
      b: 2,
      c: 3
    }
  }
});

// 使用 $delete 删除对象属性
vm.$delete(vm.myObject, 'a');

在上面的例子中,属性 a 将从 myObject 对象中删除,并且视图将相应地更新。

  1. 删除数组项:
let vm = new Vue({
  data: {
    myArray: [1, 2, 3, 4]
  }
});

// 使用 $delete 删除数组项
vm.$delete(vm.myArray, 1);

在上面的例子中,数组的第二个元素(索引为 1 的元素)将被删除,并且视图将相应地更新。

注意事项

2.6 $forceUpdate #

$forceUpdate 是一个实例方法,用于强制重新渲染组件,跳过 Vue 的普通的依赖追踪系统。一般情况下,Vue 组件会自动检测依赖(即 datapropscomputed 等)的变化,并在需要时重新渲染。然而,在某些情况下,你可能需要手动触发组件的重新渲染。

何时使用 $forceUpdate

  1. 不可检测的数据更改: Vue 不能追踪到普通对象和数组的某些变化(例如,通过索引直接设置数组项或添加新属性到对象)。
  2. 优化: 在某些极端情况下,你可能希望避免不必要的依赖追踪和渲染,而是手动控制这一过程。

如何使用

this.$forceUpdate();

这将导致 Vue 组件实例以及所有子组件重新渲染。

注意事项

  1. 性能: 频繁使用 $forceUpdate 可能会导致性能问题,因为它跳过了 Vue 的普通优化。
  2. 代码可维护性: 过度使用 $forceUpdate 可能会使代码难以理解和维护。
  3. 避免使用: 一般情况下,如果你觉得需要使用 $forceUpdate,很可能是因为你没有正确地使用 Vue 的响应式系统。在大多数情况下,更好的方法是使用 Vue 的响应式规则(datapropscomputed 等)。

总体而言,$forceUpdate 是一个逃生舱门,仅在特殊情况下使用。大多数情况下,你应该依赖 Vue 的响应式系统来进行组件更新。

2.7 $nextTick #

$nextTick 是一个非常重要的实用函数,它用于延迟执行一段代码,直到下一个 DOM 更新周期。换句话说,如果你需要在 Vue 完成对 DOM 的更新后立即执行某个操作,那么 $nextTick 是一个非常有用的工具。

这是特别重要的,因为 Vue 更新 DOM 是异步的。当你改变一个组件的状态(例如,通过改变 data、props、或者 computed 属性等),Vue 不会立即更新 DOM,而是将这些更新放在一个队列中,然后在下一个事件循环("tick")中批量执行。

基本用法

// 在模板中改变一个消息
this.message = 'Hello, world!';

// 立即输出元素的文本,可能还是旧的值
console.log(this.$el.textContent); // 可能输出 "Old message"

// 在下一个 DOM 更新循环之后输出元素的文本
this.$nextTick(() => {
  console.log(this.$el.textContent); // 输出 "Hello, world!"
});

在组件中使用

在组件的方法中,你也可以使用 this.$nextTick 来确保某些逻辑只在 DOM 更新后执行。

methods: {
  updateSomething() {
    // 改变组件状态
    this.someData = 'new value';

    // DOM 更新后执行
    this.$nextTick(() => {
      // 执行某些 DOM 相关操作
    });
  }
}

在生命周期钩子中使用

在生命周期钩子(例如 mountedupdated)中,$nextTick 同样非常有用。

mounted() {
  this.$nextTick(() => {
    // 这里的代码将在组件完全挂载到 DOM 后执行
  });
}

返回 Promise

从 Vue 2.1.0 开始,如果没有提供回调且在支持的环境中,则 $nextTick 会返回一个 Promise

async updated() {
  await this.$nextTick();
  // 代码将在 DOM 更新后执行
}

为什么这很重要?

在不使用 $nextTick 的情况下,如果你尝试在数据改变后立即访问或操作 DOM,你可能会得到旧的或不一致的数据。$nextTick 允许你更准确地控制这些操作。

2.8 vue.js #

const ELEMENT_NODE = 1;//元素节点
const TEXT_NODE = 3;//文本节点
const TEXT = "TEXT";//文本类型
const CHANGE_TEXT = 'CHANGE_TEXT';//文本的内容改变
//依赖类 每1个属性都会对应创建一个依赖的实例
class Dep{
    constructor(){
        this.subs = [];//保存了观察者Watcher的数组
    }
    addSub(sub){//添加观察者
        this.subs.push(sub);
    }
    notify(){//通知观察者更新
        this.subs.forEach(sub=>sub.update());
    }
}
class Watcher{
    //观察vm上的expr属性,当vm上的expr属性发生变化的时候,执行cb
    constructor(vm,expr,cb){
        this.vm = vm;//Vue实例
        this.expr = expr;//Vue实例的属性名msg
        this.cb = cb;//回调函数
        Dep.target = this;//把当前Watcher实例赋给全局变量
        vm[expr];//尝试获取vm上的expr属性 vm.msg
        Dep.target = null;
    }
    update(){
        this.cb();
    }
}
class  VNode {
    constructor(type,attrs,children=[]){
        this.type = type;//节点的类型 div p span
        this.attrs = attrs;//节点的属性 id ,style,class
        this.children = children;//子节点 hello, []
    }
}
class Vue {
    constructor(options) {
        //保存创建Vue实例的时候的选项对象
        this.$options = options;
        //保存数据对象
        this.$data = options.data;
        //初始化事件
        this.initEvents();
        //初始化生命周期
        this.initLifeCycle();
        //调用创建前的生命周期钩子
        this.callHook('beforeCreate');
        //初始化依赖注入系统
        this.initInjections();
        //初始化响应式系统
        this.initReactivity();
        //初始化计算属性
        this.initComputed();
        //初始化方法methods
        this.initMethods();
        //会在数据、计算属性、方法都初始化之后
        this.callHook('created');
        //如果你在选项中提供了el,则会立刻自动挂载节点
        if(options.el){
            this.$mount(options.el);
        }
    }
    initComputed(){
        console.log('initComputed')
    }
    initMethods(){
        console.log('initMethods')
    }
    $mount(selector){
        //获取要挂载的DOM元素
        let el = document.querySelector(selector);
        //获取模板内容,是一个字符串
        const template = this.$options.template?this.$options.template: el.outerHTML;
        //编译模板为render函数
        this.compile(template);
        //触发挂载前钩子
        this.callHook('beforeMount');
        //调用render方法获取虚拟DOM节点
        let vnode = this.vnode =  this.render();
        //根据虚拟DOM节点创建真实DOM节点 this.$el
        this.$el = this.createDOMElementFromVnode(vnode);
        //用新创建的DOM节点替换掉老的DOM节点
        el.parentNode.replaceChild(this.$el,el);
        //触发挂载成功钩子函数
        this.callHook('mounted');
    }
    $forceUpdate(){
        this.callHook('beforeUpdate');
        //重新调用render方法,得到最新的虚拟DOM
        let newVnode = this.render();
        //创建一个补丁包
        let patch = [];
        //进行DOM-DIFF,收集DOM操作补丁
        this.domDiff(this.vnode,newVnode,patch);
        //比完之后,要让newVnode成为下一次比较时的老节点
        this.vnode=newVnode;
        //执行DOM操作补丁,修改真实DOM
        patch.forEach(({type,domElement,content})=>{
            //如果要修改文本内容的话
            if(type === CHANGE_TEXT){
                //把最新的文本内容赋值的真实DOM
                domElement.nodeValue = content;
            }
        });
        this.callHook('updated');
    }
    domDiff(oldVnode,newVnode,patch){
        //如果新老虚拟DOm类型一样,则表示老的DOM节点可以复用
        if(oldVnode.type === newVnode.type){
            //如果想复用老节点
            let domElement = newVnode.domElement=oldVnode.domElement;
            //如果新的老的节点都是文本节点的话
            if(oldVnode.type ===  TEXT){
                //如果文本节点的内容不一样的话
                if(oldVnode.content !== newVnode.content){
                    //向补丁包中放入一个DOM操作补丁
                    patch.push({
                        type:CHANGE_TEXT,//修改文本的内容
                        content:newVnode.content,//需要要修改成什么内容
                        domElement//修改哪个真实的文本节点
                    });
                }
            }else{//那就是元素节点,肯定会有儿子数组
                for(let i=0;i<oldVnode.children.length;i++){
                    //把每个儿子进行比较
                    this.domDiff(oldVnode.children[i],newVnode.children[i],patch)
                } 
            }
        }
    }
    createDOMElementFromVnode(vnode){
        //根据节点类型创建真实的DOM元素
        let domElement = document.createElement(vnode.type);
        //给真实DOM元素添加属性
        for(let key in vnode.attrs){
            domElement.setAttribute(key,vnode.attrs[key]);
        }
        //把每个儿子都变成真实DOM并且插入到父节点上
        vnode.children.forEach(childNode=>{
            if(childNode.type === TEXT){
                //如果虚拟DOM是文本节点的话,创建真实的文本节点
                let childDOMElement = document.createTextNode(childNode.content);
                //让虚拟DOM的domElement属性指向childDOMElement
                childNode.domElement=childDOMElement;
                //把此文本节点添加到父亲身上
                domElement.appendChild(childDOMElement);
            }else{
                domElement.appendChild(this.createDOMElementFromVnode(childNode));
            }
        });
        //让虚拟DOM的domElement属性指向它的真实DOM
        vnode.domElement = domElement;
        return domElement;
    }
    compile(template){
        //创建DOM解析器
        let domParser = new DOMParser();
        //使用DOM解析器解析模板字符串,转成文档对象
        let doc = domParser.parseFromString(template,'text/html');
        //获取根节点
        let rootNode = doc.body.firstChild;
        //根据根节点创建render函数,render执行的时候可创建虚拟DOM节点
        this.render = this.generateRenderFn(rootNode);
    }
    generateRenderFn(node){
        //如果节点的类型是一个文本节点的话
        if(node.nodeType === TEXT_NODE){
            let text = node.nodeValue;//{{name}}
            let reg = /\{\{(.+?)\}\}/g;
            let match;
            //遍历匹配文本内容,如果找到了插件表达式 {{name}}
            while((match = reg.exec(text))!==null){
                const key = match[1];//name
                //创建一个观察,观察Vue实例上的name的变化,当发生变化后重新更新当前Vue实例
                new Watcher(this,key,this.$forceUpdate.bind(this))
            }
            return ()=>({
                type:"TEXT",//我们给文本节点定义一个类型,叫TEXT
                content:text.replace(reg,(_,key)=>this[key]),
                children:[]
            })
        //如果节点的类型是元素节点的话    
        }else if(node.nodeType === ELEMENT_NODE){
            //节点的标签名转小写就是类型   DIV=>div
            let type = node.tagName.toLowerCase();
            const attrs = Array.from(node.attributes).reduce((acc,attr)=>{
                acc[attr.name]=attr.value;///acc.id='app'
            },{});
            //获取每个子节点,把子节点也变成一个可以返回虚拟DOM的render函数
            const children = Array.from(node.childNodes).map(childNode=>this.generateRenderFn(childNode));
            return ()=>new VNode(type,attrs,children.map(childRender=>childRender()));
        }
    }
    initReactivity(){
        console.log(`initReactivity`);
        //数据代理,把$data上的属性代理给Vue实例this
        for(let key in this.$data){
            //获取data上的值
            let value = this.$data[key];
            let dep = new Dep();
            //为每个属性定义一个观察者的数组
            //给Vue实例也就是vm定义同名属性
            Object.defineProperty(this,key,{
                get(){
                    //如果当前是正在收集观察者的话,就把当前的观察者添加到依赖的观察者列表中
                    if(Dep.target){
                        dep.addSub(Dep.target);
                    }
                    return value;
                },
                set(newVal){
                    value = newVal;
                    //会通知此依赖的所有的观察者
                    dep.notify();
                }
            });
        }
    }
    update(){
        console.log('update')
    }
    initInjections(){
        console.log(`initInjections`)
    }
    //定义一个调用钩子函数的方法
    callHook(hookName){
        console.log(hookName)
        this.$options[hookName]?.call(this);
    }
    initLifeCycle(){
        console.log(`initLifeCycle`)
        //定义当前的Vue实例尚未挂载
        this._isMounted = false;
    }
    initEvents() {
        console.log(`initEvents`)
        //先初始化一下保存事件和监听的对象
        this._events = {};
    }
    //当调用Vue实例的on方法的时候,可以给某个事件添加监听函数
    $on(eventName, callback) {
        //先判断是事件没有对应的值,如果没有的话,说明没有添加过此事件的监听,则先赋为空数组
        if (!this._events[eventName]) {
            this._events[eventName] = [];
        }
        //向数组里添加一个新的监听函数
        this._events[eventName].push(callback);
    }
    $emit(eventName, ...args) {
        //获取此事件对应的监听函数数组
        const callbacks = this._events[eventName];
        if (callbacks) {
            //遍历执行所有的监听函数数组,记得透传参数
            callbacks.forEach(callback => callback(...args));
        }
    }
    //取消某个事件的监听
    $off(eventName, callback) {
        //获取此事件对应的回调函数的数组
        const callbacks = this._events[eventName];
        //如果有值的话
        if (callbacks) {
            if (typeof callback === 'function') {
                //获取要取消的监听函数在数组中的索引
                const index = callbacks.indexOf(callback);
                if (index !== -1) {
                    callbacks.splice(index, 1);
                }
            } else {
                this._events[eventName] = [];
            }
        }
    }
    $destroy(){
        //准备开始销毁Vue实例
        this.callHook('beforeDestroy');
        //清除所有的事件监听函数
        this._events = {};
        //销毁后
        this.callHook('destroyed');
    }
}

3.自定义指令 #

在 Vue.js 中,自定义指令是一种可以复用的功能,这个功能可以直接应用在 HTML 元素上。自定义指令提供了一种方法来封装 DOM 操作,这样你就可以在应用程序的多个组件中重用这些操作。

每个 Vue 实例在创建时都会调用 Vue.directive(name, definition) 来全局注册自定义指令。其中,name 是指令的名字,而 definition 可以是一个对象或函数。如果是对象,那么这个对象可以包含一些生命周期钩子函数,这些函数在指令的不同阶段被调用。这些阶段包括:

3.1 bind #

在 Vue 中,binding 是一个对象,传递给指令钩子函数的第二个参数。它包含了一些有用的信息,你可以在指令中使用这些信息。以下是 binding 对象的属性:

  1. name:指令的名字,不包括 v- 前缀。
  2. value:指令的绑定值,例如 v-my-directive="1 + 1" 中的 2
  3. oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
  4. expression:字符串形式的指令表达式,例如 v-my-directive="1 + 1" 中的 "1 + 1"
  5. arg:传给指令的参数,可选,例如 v-my-directive:foo 中的 "foo"
  6. modifiers:一个包含修饰符的对象,例如 v-my-directive.foo.bar 中的 { foo: true, bar: true }

因此,binding 提供了一种灵活的方式,可以用来在自定义指令中处理各种情况。例如,你可以根据 binding.valuebinding.oldValue 的不同来决定是否更新 DOM,或者根据 binding.modifiers 的存在与否来改变指令的行为。

  <body>
  <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <div id="app">
     <div v-draggable:absolute.ctrl="styleObj"></div>
    </div>
    <script>
        //定义一个指令 名字叫draggable,再定义一个对象,里面编写很多钩子函数
        //def 指的就是指令的定义对象
        //name: "draggable" 就是指令的名字,就是v-后面这个单词
        //rawName: "v-draggable" 就是加上前面的v-的原生的指令名称
        //modifiers: {disable:true} 指令修饰符
        Vue.directive('draggable',{
            //这就是一个钩子函数,你不需要自己调用此函数,只需要写好放在这就可以
            bind(el,binding){
                console.log(binding);
                //如果有disable这个修饰符,那就直接返回,不再去支持拖动
                if(binding.modifiers.disable)return;
                let isCtrl = false;
                //获取指令的参数
                el.style.position=binding.arg;
                //通过binding.value获取样式对象
                let {value:styleObj} = binding;//let styleObj=binding.value;
                //把样式对象里的样式赋给真实的DOM元素
                for(let key in styleObj){
                    el.style[key]=styleObj[key];
                }
                let isDragging = false;//当前是否正在拖拽
                let offsetX,offsetY;
                //给当前这个DIV添加鼠标按下的事件
                el.addEventListener('mousedown',(event)=>{
                    //如果需要按下ctrl键,但当前并没有按下ctrl键的话就不能拖
                    if(binding.modifiers.ctrl && !isCtrl){
                        return;
                    }
                    offsetX=event.offsetX;
                    offsetY=event.offsetY;
                    isDragging=true;
                });
                //当我们移动鼠标的时候,计算元素的最新的位置 
                el.addEventListener('mousemove',(event)=>{
                   if(isDragging){
                    el.style.left = event.clientX-offsetX +'px';
                    el.style.top = event.clientY-offsetY+'px';
                   }
                });
                //当松下的时候,停止拖动
                el.addEventListener('mouseup',(event)=>{
                    isDragging=false;
                });
                //监听按键事件,当按下ctrl键的话就把isCtrl=true
                document.addEventListener('keydown',(event)=>{
                    if(event.key==='Control')isCtrl=true;
                });
                  //监听按键事件,当松开ctrl键的话就把isCtrl=false
                document.addEventListener('keyup',(event)=>{
                    if(event.key==='Control')isCtrl=false;
                });
            }
        });
        var vm = new Vue({
            el:'#app',
            data:{
                styleObj:{width:'100px',height:'100px',backgroundColor: 'green'}
            }
        })
    </script>
  </body>

3.2 inserted #

自定义指令的使用方法非常简单,只需要在你想要添加自定义行为的 HTML 元素上添加 v-your-directive。例如,如果你的指令名字是 focus,那么你可以在任何元素上添加 v-focus,这样就能在该元素上应用这个指令的行为。

这里有一个自定义指令的简单示例:

Vue.directive('focus', {
  inserted: function(el) {
    // 聚焦元素
    el.focus();
  }
});

然后在模板中使用它:

<input v-focus>

这样,当页面加载并且该指令被插入到 DOM 中时,这个元素就会自动获取焦点。

3.3 update #

在 Vue 2 中,自定义指令的 update 钩子函数在元素的数据发生变化时会被调用。以下是一个使用 update 钩子的例子,这个自定义指令 v-color 会根据绑定的值改变元素的背景颜色:

<body>
    <script src="my-vue.js"></script>
      <div id="app">
          <div v-color="color">color</div>
      </div>
      <script>
          Vue.directive('color',{
              bind(el,binding){
                  el.style.color = binding.value;
              },
              update(el,binding){
                  el.style.color = binding.value;
              }
          });
          var vm = new Vue({
              el:'#app',
              data:{color:'green'}
          })
      </script>
</body>

3.4 componentUpdated #

componentUpdated 钩子函数会在包含组件的 VNode 及其子 VNode 全部更新后调用。

这里有一个例子,使用 componentUpdated 钩子函数在组件更新后,检查元素的内容,并根据长度改变元素的背景颜色:

<body>
<script src="vue.js"></script>
  <div id="app">
    <input v-checklength="username" v-model="username">
  </div>
  <script>
      Vue.directive('checklength',{
        bind(el){
          el.style.color ='gray';
        },
        componentUpdated(el,binding){
          let {value} = el;
          if(value.length<6){
            el.style.color ='red';
          }else{
            el.style.color ='green';
          }
        }
      });
      var vm = new Vue({
          el:'#app',
          data:{username:''}
      })
  </script>
</body>

在 Vue 的自定义指令中,updatecomponentUpdated 是两个钩子函数,它们在指令所在的元素更新时被调用,但具体的调用时间和调用条件有所不同。

  1. update 钩子:当指令所在的元素更新时调用,但并不保证其子节点也已更新。这个钩子函数会在指令所在组件的 VNode 更新时调用,但在其子 VNode 更新之前调用。

  2. componentUpdated 钩子:指令所在的元素及其子节点都更新完成后调用。这个钩子函数会在指令所在组件的 VNode 及其子 VNode 更新完成后调用。

下面是一个示例,使用 updatecomponentUpdated 钩子跟踪并打印更新的次数:

let updateCount = 0;
let componentUpdatedCount = 0;

Vue.directive('count-updates', {
  update: function(el, binding, vnode, oldVnode) {
    updateCount++;
    console.log(`Update count: ${updateCount}`);
  },
  componentUpdated: function(el, binding, vnode, oldVnode) {
    componentUpdatedCount++;
    console.log(`Component Updated count: ${componentUpdatedCount}`);
  }
});

在这个示例中,每当指令所在的元素或组件更新时,update 钩子就会增加 updateCount 并打印更新的次数。同样,每当指令所在的元素和其子节点都更新完成时,componentUpdated 钩子就会增加 componentUpdatedCount 并打印更新的次数。

3.5 unbind #

在 Vue 中,unbind 钩子函数在指令从元素上解绑时被调用。这个钩子函数通常被用来执行清理操作,比如移除事件监听器。

这是一个使用 unbind 钩子函数的示例,该指令 v-click-outside 用于点击元素外部时触发某个操作:

<body>
    <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <div id="app">
        <div v-draggable:absolute.ctrl="styleObj"></div>
    </div>
    <script>
        //定义一个指令 名字叫draggable,再定义一个对象,里面编写很多钩子函数
        //def 指的就是指令的定义对象
        //name: "draggable" 就是指令的名字,就是v-后面这个单词
        //rawName: "v-draggable" 就是加上前面的v-的原生的指令名称
        //modifiers: {disable:true} 指令修饰符
        Vue.directive('draggable', {
            //这就是一个钩子函数,你不需要自己调用此函数,只需要写好放在这就可以
            bind(el, binding) {
                console.log(binding);
                //如果有disable这个修饰符,那就直接返回,不再去支持拖动
                if (binding.modifiers.disable) return;
                let isCtrl = false;
                //获取指令的参数
                el.style.position = binding.arg;
                //通过binding.value获取样式对象
                let { value: styleObj } = binding;//let styleObj=binding.value;
                //把样式对象里的样式赋给真实的DOM元素
                for (let key in styleObj) {
                    el.style[key] = styleObj[key];
                }
                let isDragging = false;//当前是否正在拖拽
                let offsetX, offsetY;
                //给当前这个DIV添加鼠标按下的事件
                el.addEventListener('mousedown', (event) => {
                    //如果需要按下ctrl键,但当前并没有按下ctrl键的话就不能拖
                    if (binding.modifiers.ctrl && !isCtrl) {
                        return;
                    }
                    offsetX = event.offsetX;
                    offsetY = event.offsetY;
                    isDragging = true;
                });
                //当我们移动鼠标的时候,计算元素的最新的位置 
                el.addEventListener('mousemove', (event) => {
                    if (isDragging) {
                        el.style.left = event.clientX - offsetX + 'px';
                        el.style.top = event.clientY - offsetY + 'px';
                    }
                });
                //当松下的时候,停止拖动
                el.addEventListener('mouseup', (event) => {
                    isDragging = false;
                });
                el.onKeyDown = (event) => {
                    if (event.key === 'Control') isCtrl = true;
                }
                //监听按键事件,当按下ctrl键的话就把isCtrl=true
                document.addEventListener('keydown', el.onKeyDown);
                el.onKeyUp = (event) => {
                    if (event.key === 'Control') isCtrl = false;
                }
                //监听按键事件,当松开ctrl键的话就把isCtrl=false
                document.addEventListener('keyup', el.onKeyUp);
            },
            unbind(el, binding) {
                console.log('unbind')
                document.removeEventListener('keydown', el.onKeyDown);
                document.removeEventListener('keyup', el.onKeyUp);
             }

        });
        var vm = new Vue({
            el: '#app',
            data: {
                styleObj: { width: '100px', height: '100px', backgroundColor: 'green' }
            }
        })
        vm.$destroy();
    </script>
</body>

在这个例子中,当点击发生在元素外部时,会调用 handleClickOutside 方法。当 v-click-outside 指令从元素上解绑时,事件监听器会被移除,以避免不必要的内存使用。

在 Vue.js 中,unbind 钩子函数会在以下情况下被触发:

  1. 指令从元素上被解绑,即元素或组件被销毁。
  2. Vue 组件在被销毁的过程中(例如,由于 v-if 条件不再满足,或者组件不再被路由使用)。
  3. 当用于绑定指令的 Vue 实例被销毁时。

这个钩子函数常常用于执行清理操作,比如移除指令在 bind 钩子函数中绑定的事件监听器,以防止内存泄露。

3.6 v.html #

<!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>LifeCycle</title>
</head>

<body>
    <div id="app">{{msg}}</div>
    <script src="my-vue.js"></script>
    <script>
        Vue.directive('focus', {
            bind(el, value, vm) {
                console.log("bind");
            },
            inserted(el, value, vm) {
                console.log("inserted");
            },
            update(el, value, vm) {
                console.log("update");
            },
            componentUpdated(el, value, vm) {
                console.log("componentUpdated");
            },
            unbind(el, value, vm) {
                console.log("unbind");
            }
        });
        var vm = new Vue({
            el:'#app',
            data: { msg: 'hello' },
        });
    </script>
</body>

</html>

3.6 vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const TEXT = "TEXT";
const CHANGE_TEXT = 'CHANGE_TEXT';
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    vm[expr];
    Dep.target = null;
  }
  update() {
    this.cb();
  }
}
class VNode {
  constructor(type, attrs, children = []) {
    this.type = type;
    this.attrs = attrs;
    this.children = children;
  }
}
class Vue {
  static directives = {};
  static directive(name, definition) {
    Vue.directives[name] = definition;
  }
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.initEvents();
    this.initLifeCycle();
    this.callHook('beforeCreate');
    this.initInjections();
    this.initReactivity();
    this.initComputed();
    this.initMethods();
    this.callHook('created');
    if (options.el) {
      this.$mount(options.el);
    }
  }
  initComputed() {
    console.log('initComputed');
  }
  initMethods() {
    console.log('initMethods');
  }
  $mount(selector) {
    let el = document.querySelector(selector);
    const template = this.$options.template ? this.$options.template : el.outerHTML;
    this.compile(template);
    this.callHook('beforeMount');
    let vnode = this.vnode = this.render();
    this.$el = this.createDOMElementFromVnode(vnode);
    el.parentNode.replaceChild(this.$el, el);
    this.callDirectiveHook(this.$el, 'inserted');
    this.callHook('mounted');
  }
  $forceUpdate() {
    this.callHook('beforeUpdate');
    let newVnode = this.render();
    let patch = [];
    this.domDiff(this.vnode, newVnode, patch);
    this.vnode = newVnode;
    patch.forEach(({
      type,
      domElement,
      content
    }) => {
      if (type === CHANGE_TEXT) {
        domElement.nodeValue = content;
      }
    });
    this.callHook('updated');
    this.callComponentUpdatedHooks(this.$el);
  }
  callComponentUpdatedHooks(rootNode) {
    const elements = rootNode.querySelectorAll('*');
    elements.forEach(element => {
      this.callDirectiveHook(element, 'componentUpdated');
    });
  }
  domDiff(oldVnode, newVnode, patch) {
    if (oldVnode.type === newVnode.type) {
      let domElement = newVnode.domElement = oldVnode.domElement;
      if (oldVnode.type === TEXT) {
        if (oldVnode.content !== newVnode.content) {
          patch.push({
            type: CHANGE_TEXT,
            content: newVnode.content,
            domElement
          });
        }
      } else {
        for (let i = 0; i < oldVnode.children.length; i++) {
          this.domDiff(oldVnode.children[i], newVnode.children[i], patch);
        }
      }
    }
  }
  createDOMElementFromVnode(vnode) {
    let domElement = document.createElement(vnode.type);
    let bindings = [];
    for (let attrName in vnode.attrs) {
      if (attrName.startsWith('v-')) {
        const rawName = attrName;
        const name = attrName.slice(2).split(':')[0];
        const arg = attrName.split(':')[1]?.split('.')[0];
        const modifiers = attrName.split(':')[1]?.split('.').slice(1).reduce((acc, modifier) => {
          acc[modifier] = true;
          return acc;
        }, {});
        const def = Vue.directives[name];
        if (def) {
          const expression = vnode.attrs[attrName];
          bindings.push({
            arg,
            def,
            expression,
            modifiers,
            name,
            rawName,
            value: this[expression]
          });
        }
      } else {
        domElement.setAttribute(attrName, vnode.attrs[attrName]);
      }
    }
    domElement.bindings = bindings;
    bindings.forEach(binding => {
      new Watcher(this, binding.expression, () => {
        this.callDirectiveHook(domElement, 'update');
      });
    });
    this.callDirectiveHook(domElement, 'bind');
    vnode.children.forEach(childNode => {
      if (childNode.type === TEXT) {
        let childDOMElement = document.createTextNode(childNode.content);
        childNode.domElement = childDOMElement;
        domElement.appendChild(childDOMElement);
      } else {
        let childDOMElement = this.createDOMElementFromVnode(childNode);
        domElement.appendChild(childDOMElement);
        this.callDirectiveHook(childDOMElement, 'inserted');
      }
    });
    vnode.domElement = domElement;
    return domElement;
  }
  callDirectiveHook(domElement, hookName) {
    domElement.bindings.forEach(binding => {
      if (hookName === 'update' || hookName === 'componentUpdated') {
        binding.oldValue = binding.value;
        binding.value = this[binding.expression];
      }
      binding.def[hookName]?.(domElement, binding);
    });
  }
  collectWatchingKeys(node) {
    let watchingKeys = new Set();
    const reg = /\{\{(.+?)\}\}/g;
    function traverse(node) {
      if (node.nodeType === TEXT_NODE) {
        let text = node.nodeValue;
        let match;
        while ((match = reg.exec(text)) !== null) {
          watchingKeys.add(match[1]);
        }
      } else if (node.nodeType === ELEMENT_NODE) {
        Array.from(node.childNodes).forEach(childNode => traverse(childNode));
      }
    }
    traverse(node);
    return watchingKeys;
  }
  compile(template) {
    let domParser = new DOMParser();
    let doc = domParser.parseFromString(template, 'text/html');
    let rootNode = doc.body.firstChild;
    let watchingKeys = this.collectWatchingKeys(rootNode);
    watchingKeys.forEach(key => {
      new Watcher(this, key, () => this.$nextTick(this.$forceUpdate.bind(this)));
    });
    this.render = this.generateRenderFn(rootNode);
  }
  generateRenderFn(node) {
    if (node.nodeType === TEXT_NODE) {
      let text = node.nodeValue;
      const reg = /\{\{(.+?)\}\}/g;
      return () => ({
        type: "TEXT",
        content: text.replace(reg, (_, key) => this[key]),
        children: []
      });
    } else if (node.nodeType === ELEMENT_NODE) {
      let type = node.tagName.toLowerCase();
      const attrs = Array.from(node.attributes).reduce((acc, attr) => {
        acc[attr.name] = attr.value;
        return acc;
      }, {});
      const children = Array.from(node.childNodes).map(childNode => this.generateRenderFn(childNode));
      return () => new VNode(type, attrs, children.map(childRender => childRender()));
    }
  }
  initReactivity() {
    console.log(`initReactivity`);
    for (let key in this.$data) {
      let value = this.$data[key];
      let dep = new Dep();
      Object.defineProperty(this, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal) {
          value = newVal;
          dep.notify();
        }
      });
    }
  }
  initInjections() {
    console.log(`initInjections`);
  }
  callHook(hookName) {
    this.$options[hookName]?.call(this);
  }
  initLifeCycle() {
    console.log(`initLifeCycle`);
    this._isMounted = false;
  }
  initEvents() {
    console.log(`initEvents`);
    this._events = {};
  }
  $on(eventName, callback) {
    if (!this._events[eventName]) {
      this._events[eventName] = [];
    }
    this._events[eventName].push(callback);
  }
  $emit(eventName, ...args) {
    const callbacks = this._events[eventName];
    if (callbacks) {
      callbacks.forEach(callback => callback(...args));
    }
  }
  $off(eventName, callback) {
    const callbacks = this._events[eventName];
    if (callbacks) {
      if (typeof callback === 'function') {
        const index = callbacks.indexOf(callback);
        if (index !== -1) {
          callbacks.splice(index, 1);
        }
      } else {
        this._events[eventName] = [];
      }
    }
  }
  callUnbindHooks(rootNode) {
    const elements = rootNode.querySelectorAll('*');
    elements.forEach(element => {
      this.callDirectiveHook(element, 'unbind');
    });
  }
  $destroy() {
    this.callHook('beforeDestroy');
    this._events = {};
    this.callUnbindHooks(this.$el);
    this.callHook('destroyed');
  }
  $nextTick(callback) {
    setTimeout(callback, 0);
  }
}