Vue实例的生命周期包含一系列的初始化步骤,例如需要设置数据监听,编译模板,挂载实例到DOM,并在数据变化时更新DOM等。同时也有一些生命周期钩子函数可以在某个阶段运行自定义的函数。
以下是Vue的主要生命周期钩子:
beforeCreate
: 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
created
: 在实例创建完成后被立即调用。在这一步,实例已经完成了以下的配置:数据观测 (data observer),属性和方法的运算,watch/event事件回调。然而,挂载阶段还没开始,$el
属性目前不可见。
beforeMount
: 在挂载开始之前被调用。相关的 render
函数首次被调用。
mounted
: el
被新创建的 vm.$el
替换,并挂载到实例上去之后调用。注意 mounted
不会保证所有的子组件也都一起被挂载。
beforeUpdate
: 数据更新时调用,发生在虚拟 DOM 打补丁之前。
updated
: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
beforeDestroy
: 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed
: Vue 实例销毁后调用。当这个钩子被调用时,Vue 实例指向的所有东西都会解绑,所有的事件监听器都会被移除,所有的子实例也都会被销毁。
在 Vue 中,beforeCreate
是生命周期钩子的一部分。生命周期钩子是 Vue 实例在其生命周期中的不同阶段会自动调用的特殊函数。
我们将从以下几个方面讲解 beforeCreate
钩子。
定义:
beforeCreate
是在实例初始化后,数据观测 (data observer) 和事件/观察者配置之前被调用。在这个阶段,Vue 实例的观察者 (Observer) 和事件 (Events) 还没有被初始化。
注意事项:
beforeCreate
生命周期钩子在实例创建之后立即运行,这意味着此时 Vue 实例还没有完成数据观测,也就是说此时实例上的数据、计算属性和方法等都是不可用的。
如果你在 beforeCreate
钩子中执行的逻辑并不依赖于上述实例属性,那么这个钩子是非常适合的。例如,你可以在这个钩子中运行一些非异步的代码,或者进行一些初步的、不涉及实例数据和方法的配置。
beforeCreate
是创建实例的最早阶段,也是所有生命周期钩子中最先执行的。因此,如果你需要在 Vue 实例创建过程中尽可能早地运行一些代码,你可以使用 beforeCreate
钩子。
示例代码:
下面的代码片段展示了如何在 Vue 实例中使用 beforeCreate
钩子:
new Vue({
beforeCreate() {
console.log('这是 beforeCreate 阶段');
},
created() {
console.log('这是 created 阶段');
},
// 其他生命周期钩子...
})
在这个例子中,当 Vue 实例被创建并开始执行其生命周期钩子时,你会在控制台上看到 "这是 beforeCreate 阶段" 和 "这是 created 阶段" 的消息。注意到 beforeCreate
钩子的消息会先出现,这是因为 beforeCreate
是所有生命周期钩子中最先执行的。
在 Vue 中,created
是生命周期钩子的一部分,它在 Vue 实例创建过程中的某个阶段被调用。
定义:
created
钩子是在 Vue 实例创建完毕,数据观测 (data observer) 和事件/观察者已配置完毕,但在挂载阶段 (mount phase) 之前被调用。在 created
阶段,Vue 实例的数据已经被观测,所以在这个阶段可以访问到数据、计算属性和方法。
注意事项:
在 created
钩子中,可以执行依赖于数据的逻辑操作,如对数据的初步处理。然而要注意,此时 Vue 实例还没有被挂载到 DOM 上,因此这个阶段不能进行任何与 DOM 相关的操作。
如果你在 created
钩子中执行的逻辑需要访问或改变 Vue 实例上的数据,这个阶段是非常适合的。在这个阶段,你还可以执行一些如 AJAX 请求等异步操作。
在 created
钩子执行完毕后,如果 el
和 template
选项都不存在,那么这个阶段将停止,并等待手动的 vm.$mount
调用。如果存在 el
或 template
选项,则进入挂载阶段。
示例代码:
以下是如何在 Vue 实例中使用 created
钩子的示例代码:
new Vue({
data: {
message: '你好,世界!'
},
created: function () {
console.log('这是 created 阶段');
console.log('此时的 message 是: ' + this.message); // 这里可以访问到 data 中的 message
},
// 其他生命周期钩子...
})
在这个示例中,当 Vue 实例创建并开始执行其生命周期钩子时,你会在控制台上看到 "这是 created 阶段" 和 "此时的 message 是: 你好,世界!" 的消息。这个示例展示了在 created
阶段可以访问和使用 Vue 实例的数据。
在 Vue 生命周期中,beforeMount
生命周期钩子函数的主要作用就是在挂载之前做一些业务逻辑的处理。
你可以在 beforeMount
阶段进行的操作有:数据计算,数据转换等。例如,你可能需要在此阶段对数据进行初步处理或转换,为接下来的渲染做好准备。
beforeMount
是在实例被挂载到 DOM 之前同步调用的,它的调用时机是在 beforeCreate
和 created
之后,mounted
之前。
在 Vue 中,生命周期钩子 beforeMount
在模板编译完成后,但在首次渲染之前被调用。这意味着在 beforeMount
钩子中,你的模板已经编译完成,转换成了 render 函数,但是这个 render 函数还没有被执行,也就是还没有生成虚拟 DOM,因此你还无法访问到渲染后的 DOM 结构。
总结一下,以下是这些过程发生的顺序:
beforeMount
钩子函数被调用。mounted
钩子函数被调用。需要注意的是,如果你需要访问已经渲染好的 DOM 结构,你应该在 mounted
钩子中进行,而不是 beforeMount
钩子。
在 Vue 中,mounted
是生命周期钩子的一部分,它在 Vue 实例创建过程中的特定阶段被调用。
定义:
mounted
钩子在 Vue 实例被新创建的 vm.$el
替换,并挂载到其元素上后调用。这个阶段意味着 Vue 实例已经编译完成模板,将数据和元素结合起来,并将结果渲染到 DOM 中。
注意事项:
在 mounted
钩子中,可以执行依赖于 DOM 的操作,因为此时 Vue 实例已经被挂载到 DOM 上,你可以访问到实例的 DOM 结构。
尽管在 mounted
阶段可以访问到 DOM,但并不保证所有的子组件也都一定已经被挂载。如果你希望等到整个视图都渲染完毕,可以以 vm.$nextTick
从 mounted
钩子中返回一个 Promise。
如果你需要在 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。
在 Vue 中,beforeUpdate
是生命周期钩子的一部分,它在 Vue 实例创建过程中的某个阶段被调用。
定义:
beforeUpdate
钩子在数据改变之后,但是在 DOM 重新渲染之前被调用。此阶段可以在数据更新之后执行一些操作,但是此时新的数据尚未渲染到模板中。
注意事项:
在 beforeUpdate
钩子中,可以执行依赖于数据变化的操作,如数据预处理等。然而要注意,此时虽然数据已经更新,但是 DOM 还未更新,所以不能依赖于新的 DOM 结构。
如果你需要在数据更新后、重新渲染前执行一些操作,那么 beforeUpdate
钩子是适合的。
在 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 中。
在 Vue 中,updated
是一个生命周期钩子函数,它在数据改变后,虚拟 DOM 重新渲染并更新 DOM 之后被调用。也就是说,当你的组件的数据发生变化,视图被重新渲染和更新后,updated
钩子函数将被执行。
请注意以下的事项:
updated
是在数据改变后,虚拟 DOM 重新渲染并更新 DOM 之后执行。这意味着你在这个阶段可以访问到新的更新后的 DOM。如果你需要在数据和视图更新完成后进行某些操作,那么 updated
是合适的钩子函数。
你可以在 updated
阶段进行的操作有:DOM 操作,获取新状态等。例如,你可能需要在此阶段根据新的 DOM 状态,进行一些 DOM 操作或动画。
updated
是在实例的数据变化后,重新渲染和打补丁之后异步调用的,它的调用时机是在数据变化,beforeUpdate
之后。
记住,当 updated
钩子函数被调用时,说明组件的 DOM 已经更新完毕,但是要注意不要在此期间更改数据,因为这可能会导致无限循环的更新。如果你需要基于新的 DOM 状态更改数据,那么最好使用计算属性或者 $nextTick
函数。
以下是使用 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
数据,导致 beforeUpdate
和 updated
钩子函数的调用。当 Vue 实例的数据改变并完成 DOM 的更新时,updated
钩子函数将被调用,然后它会打印出新的 message
数据。
在 Vue 中,beforeDestroy
是一个生命周期钩子函数,它在实例销毁之前被调用。在这一阶段,实例仍然完全可用,所有的数据、属性和方法都可以被访问和使用。
请注意以下的事项:
beforeDestroy
是在 Vue 实例销毁之前执行的。这意味着你在这个阶段还可以访问实例的所有数据、属性和方法。如果你需要在实例销毁之前执行一些清理工作或保存一些状态,那么 beforeDestroy
是适合的钩子函数。
你可以在 beforeDestroy
阶段进行的操作有:清理定时器,取消网络请求,解绑事件,保存数据等。例如,你可能需要在此阶段取消尚未完成的网络请求,或者清理定时器以防止内存泄漏。
beforeDestroy
是在实例销毁之前同步调用的,它的调用时机是在实例销毁之前,destroyed
之前。
注意,在 beforeDestroy
钩子函数中,不应再去改变数据或触发视图更新,因为此时的实例已经处于被销毁的过程中。
以下是使用 beforeDestroy
的一个基本示例:
new Vue({
data: {
message: 'Hello Vue!'
},
beforeDestroy() {
// 在实例销毁之前,我们可以在这里进行一些清理工作
console.log('beforeDestroy 执行了');
},
destroyed() {
// 在实例销毁之后,我们可以在这里进行一些后续处理
console.log('destroyed 执行了');
},
methods: {
destroyInstance() {
this.$destroy();
},
},
el: '#app'
});
在上面的代码中,destroyInstance
方法会触发实例的销毁,导致 beforeDestroy
和 destroyed
钩子函数的调用。当 Vue 实例销毁之前,beforeDestroy
钩子函数将被调用;然后,当 Vue 实例完全销毁之后,destroyed
钩子函数将被调用。
在 Vue 中,destroyed
是生命周期钩子的一部分,它在 Vue 实例销毁过程中的某个阶段被调用。
定义:
destroyed
钩子在 Vue 实例销毁完成之后被调用。此阶段,Vue 实例已经解除了事件监听以及和子组件的关联,其指令也已经被解除绑定。
注意事项:
在 destroyed
钩子中,可以执行一些清理操作,如取消定时器,取消事件监听等。此时,Vue 实例已经完全被销毁,你无法再访问实例上的任何属性或执行任何方法。
destroyed
钩子主要用来执行清理操作,而非在销毁后改变组件状态,因为这个阶段,Vue 实例已经不存在了。
如果你的 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 实例已经被销毁,你无法再访问实例上的任何属性或执行任何方法。
<!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>
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;
}
$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
在不再需要时移除事件监听。
移除所有事件监听:
this.$off();
移除特定事件的所有监听:
this.$off('event-name');
移除特定事件的特定监听:
this.$off('event-name', callback);
例如:
export default {
beforeDestroy() {
this.$off('say-hello');
},
};
在这个例子中,beforeDestroy
生命周期钩子被用于移除名为 say-hello
的所有事件监听。
在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
如何工作以及何时使用它是非常有用的。
$destroy
是一个实例方法,用于完全销毁一个Vue实例及其所有子组件和事件监听器。一旦调用了这个方法,Vue实例将不再是响应式的,并且所有的生命周期钩子(如beforeDestroy
和destroyed
)也会被触发。这样做会释放实例占用的所有资源,这样垃圾回收机制就能回收这些资源。
这里是一个简单的例子:
// 创建一个新的 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的正常生命周期管理应该足够满足你的需求。
$set
是一个很有用的实例方法,用于解决 Vue 的响应式系统中的一些限制。具体来说,当你在一个已经创建的 Vue 实例上动态添加一个新的根级别属性时,这个属性不会自动变为响应式的。同样,Vue 也无法检测到对象属性的添加或删除。
这时,$set
就可以派上用场。$set
方法可以接受三个参数:
下面是几个示例:
添加新属性到对象
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],并且这个更改是响应式的。
注意事项
$set
无法使一个没有声明的根级别响应式数据变成响应式的。你需要在 data
选项中预先声明所有根级别响应式属性。$set
在嵌套对象上作用可能有限。如果一个对象已经是响应式的,那么其嵌套对象也是响应式的。但如果你有一个非响应式的对象并想要使其整体成为响应式的,仅使用 $set
是不够的。在 Vue 中,Vue.$delete
方法用于删除对象或数组上的属性或项,并触发相应的视图更新。这是 Vue 的响应式系统的一部分,用于处理 Vue 无法检测到属性删除的情况。
使用场景
Vue 的响应式系统可以追踪对象和数组的变化,并相应地更新视图。然而,当你直接使用 delete
操作符删除一个对象的属性时,Vue 无法检测到这一变化。Vue.$delete
解决了这个问题。
用法
let vm = new Vue({
data: {
myObject: {
a: 1,
b: 2,
c: 3
}
}
});
// 使用 $delete 删除对象属性
vm.$delete(vm.myObject, 'a');
在上面的例子中,属性 a
将从 myObject
对象中删除,并且视图将相应地更新。
let vm = new Vue({
data: {
myArray: [1, 2, 3, 4]
}
});
// 使用 $delete 删除数组项
vm.$delete(vm.myArray, 1);
在上面的例子中,数组的第二个元素(索引为 1 的元素)将被删除,并且视图将相应地更新。
注意事项
Vue.$delete
只能用于可变的数据类型,如对象和数组。对于基本数据类型(如字符串、数字、布尔值等)或不可变的数据结构(如 Map
和 Set
)则无效。Vue.$delete
删除数组项会导致数组长度的改变。如果你不希望数组长度发生变化,可以用其他值(如 null
)替代该项,但这不会触发视图的更新。$forceUpdate
是一个实例方法,用于强制重新渲染组件,跳过 Vue 的普通的依赖追踪系统。一般情况下,Vue 组件会自动检测依赖(即 data
、props
、computed
等)的变化,并在需要时重新渲染。然而,在某些情况下,你可能需要手动触发组件的重新渲染。
何时使用 $forceUpdate
如何使用
this.$forceUpdate();
这将导致 Vue 组件实例以及所有子组件重新渲染。
注意事项
$forceUpdate
可能会导致性能问题,因为它跳过了 Vue 的普通优化。$forceUpdate
可能会使代码难以理解和维护。$forceUpdate
,很可能是因为你没有正确地使用 Vue 的响应式系统。在大多数情况下,更好的方法是使用 Vue 的响应式规则(data
、props
、computed
等)。总体而言,$forceUpdate
是一个逃生舱门,仅在特殊情况下使用。大多数情况下,你应该依赖 Vue 的响应式系统来进行组件更新。
$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 相关操作
});
}
}
在生命周期钩子中使用
在生命周期钩子(例如 mounted
、updated
)中,$nextTick
同样非常有用。
mounted() {
this.$nextTick(() => {
// 这里的代码将在组件完全挂载到 DOM 后执行
});
}
返回 Promise
从 Vue 2.1.0 开始,如果没有提供回调且在支持的环境中,则 $nextTick
会返回一个 Promise
。
async updated() {
await this.$nextTick();
// 代码将在 DOM 更新后执行
}
为什么这很重要?
在不使用 $nextTick
的情况下,如果你尝试在数据改变后立即访问或操作 DOM,你可能会得到旧的或不一致的数据。$nextTick
允许你更准确地控制这些操作。
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');
}
}
在 Vue.js 中,自定义指令是一种可以复用的功能,这个功能可以直接应用在 HTML 元素上。自定义指令提供了一种方法来封装 DOM 操作,这样你就可以在应用程序的多个组件中重用这些操作。
每个 Vue 实例在创建时都会调用 Vue.directive(name, definition)
来全局注册自定义指令。其中,name
是指令的名字,而 definition
可以是一个对象或函数。如果是对象,那么这个对象可以包含一些生命周期钩子函数,这些函数在指令的不同阶段被调用。这些阶段包括:
bind
: 只调用一次,指令第一次绑定到元素时调用。这里可以进行一次性的初始化设置。
inserted
: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
update
: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
componentUpdated
: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind
: 只调用一次,指令与元素解绑时调用。
bind
: 只调用一次,指令第一次绑定到元素时调用。这里可以进行一次性的初始化设置。在 Vue 中,binding
是一个对象,传递给指令钩子函数的第二个参数。它包含了一些有用的信息,你可以在指令中使用这些信息。以下是 binding
对象的属性:
name
:指令的名字,不包括 v-
前缀。value
:指令的绑定值,例如 v-my-directive="1 + 1"
中的 2
。oldValue
:指令绑定的前一个值,仅在 update
和 componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式,例如 v-my-directive="1 + 1"
中的 "1 + 1"
。arg
:传给指令的参数,可选,例如 v-my-directive:foo
中的 "foo"
。modifiers
:一个包含修饰符的对象,例如 v-my-directive.foo.bar
中的 { foo: true, bar: true }
。因此,binding
提供了一种灵活的方式,可以用来在自定义指令中处理各种情况。例如,你可以根据 binding.value
和 binding.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>
inserted
: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。自定义指令的使用方法非常简单,只需要在你想要添加自定义行为的 HTML 元素上添加 v-your-directive
。例如,如果你的指令名字是 focus
,那么你可以在任何元素上添加 v-focus
,这样就能在该元素上应用这个指令的行为。
这里有一个自定义指令的简单示例:
Vue.directive('focus', {
inserted: function(el) {
// 聚焦元素
el.focus();
}
});
然后在模板中使用它:
<input v-focus>
这样,当页面加载并且该指令被插入到 DOM 中时,这个元素就会自动获取焦点。
在 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>
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 的自定义指令中,update
和 componentUpdated
是两个钩子函数,它们在指令所在的元素更新时被调用,但具体的调用时间和调用条件有所不同。
update
钩子:当指令所在的元素更新时调用,但并不保证其子节点也已更新。这个钩子函数会在指令所在组件的 VNode 更新时调用,但在其子 VNode 更新之前调用。
componentUpdated
钩子:指令所在的元素及其子节点都更新完成后调用。这个钩子函数会在指令所在组件的 VNode 及其子 VNode 更新完成后调用。
下面是一个示例,使用 update
和 componentUpdated
钩子跟踪并打印更新的次数:
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
并打印更新的次数。
在 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
钩子函数会在以下情况下被触发:
这个钩子函数常常用于执行清理操作,比如移除指令在 bind
钩子函数中绑定的事件监听器,以防止内存泄露。
<!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>
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);
}
}