1.计算属性 #

在 Vue.js 中,计算属性是一种更加高效的方法来处理数据变化的监听和处理。与 methods 方法不同,计算属性是基于它们的依赖关系缓存的。只有在它们的相关依赖发生改变时,才会重新进行计算。这就意味着只要依赖属性还没有变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。这使得计算属性比 methods 更高效。

以下是一个在 Vue 中使用计算属性的基本示例:

<body>
  <div id="app">
    <input v-model="firstName">
    <input v-model="lastName">
    <p>你的全名是: {{ fullName }}</p>
  </div>
  <script>
    new Vue({
      el: '#app',
      data: function
        () {
        return {
          firstName: '',
          lastName: ''
        }
      },
      computed: {
        fullName: function () {
          return this.firstName + ' ' + this.lastName
        }
      }
    })
  </script>
</body>

在这个例子中,fullName是一个计算属性。当firstNamelastName任一变化时,fullName会被重新计算并更新到视图上。

计算属性不仅可以用于读取操作,还可以用于设置值。这需要为计算属性提供一个 setter 函数。以下是带有 setter 的计算属性示例:

<body>
  <div id="app">
    <input v-model="firstName">
    <input v-model="lastName">
    <input v-model="fullName">
    <p>你的全名是: {{ fullName }}</p>
  </div>
  <script>
    new Vue({
      el: '#app',
      data: function
        () {
        return {
          firstName: '',
          lastName: ''
        }
      },
      computed: {
        fullName: {
          get: function () {
            return this.firstName + ' ' + this.lastName
          },
          set: function (newValue) {
            var names = newValue.split(' ')
            this.firstName = names[0]
            this.lastName = names[names.length - 1]
          }
        }
      }
    })
  </script>
</body>

在这个例子中,我们为 fullName 添加了 setter,所以现在你可以修改 fullName,它会自动更新 firstNamelastName

2.watch #

watch 是 Vue.js 中的一个选项,用于在某个数据变化时执行特定的行为。在某些情况下,它提供了计算属性不能做到的服务。你可以看看下面这个基础的例子:

<body>
  <div id="app">
    <input v-model="msg">
    <p>{{ log }}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        msg: '',
        log: '请在输入框输入文字'
      },
      watch: {
        // 当 msg 改变时,这个函数会运行
        msg: function (newValue, oldValue) {
          this.log = `msg 已经改变,新值为 ${newValue}, 旧值为 ${oldValue}`
        }
      }
    })
  </script>
</body>

在这个例子中,当 msg 改变时,我们记录这个变化到 log 里。

接下来,我们来看看 watchimmediate 属性。immediate 属性会在 Vue 实例创建后立即执行 watch 函数,而不是等待数据变化时才执行。

下面是一个 immediate 的使用示例:

<body>
  <div id="app">
    <input v-model="msg">
    <p>{{ log }}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        msg: 'hello',
        log: '请在输入框输入文字'
      },
      watch: {
        msg: {
          immediate: true,
          handler: function (newValue, oldValue) {
            this.log = `msg 已经改变,新值为 ${newValue}, 旧值为 ${oldValue}`
          }
        }
      }
    })
  </script>
</body>

在这个例子中,即使 msg 没有改变,当 Vue 实例创建后,watch 函数会立即执行一次。

最后,我们来看看 deep 属性。当你监视的数据是对象时,watch 默认只能监视到数据(对象)的引用地址变化,无法检测到对象内部属性值的变化。这时候,你可以使用 deep 选项来告诉 Vue 递归地监视这个对象的内部变化。

以下是 deep 的使用示例:

<body>
  <div id="app">
    <button @click="msg.count++">增加</button>
    <p>{{ log }}</p>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        msg: {
          count: 0
        },
        log: '请点击按钮'
      },
      watch: {
        msg: {
          deep: true,
          handler: function (newValue, oldValue) {
            this.log = `msg 的 count 属性已经改变,新值为 ${newValue.count}, 旧值为 ${oldValue.count}`
          }
        }
      }
    })
  </script>
</body>

在这个例子中,我们监视了一个对象 msg。虽然我们更改的是 msgcount 属性,但通过设置 deep: truewatch 能够检测到对象内部属性的变化。

总的来说,watch 提供了一种通用的观察和响应 Vue 实例数据变化的方法。而 immediatedeep 参数则分别提供了立即执行观察函数以及深度观察数据变化的能力。

3.filter #

在Vue中,过滤器(filter)是一种在模板插值和绑定表达式中改变输出格式的机制。过滤器并不改变原数据,只提供一个视图级别的转换。

首先,需要在Vue实例或全局范围内定义过滤器。 以下是一个全局过滤器的示例,该过滤器将输入文本的首字母大写:

<body>
  <div id="app">
    <div>{{ message | capitalize }}</div>
  </div>
  <script>
    Vue.filter('capitalize', function (value) {
      if (!value) return ''
      value = value.toString()
      return value.charAt(0).toUpperCase() + value.slice(1)
    })
    new Vue({
      el: '#app',
      data: {
        message: 'hello'
      }
    })
  </script>
</body>

全局过滤器可以在任何Vue实例的模板中使用。使用过滤器的语法是在双花括号插值或v-bind表达式后加上一个管道符(|)和过滤器的名称: 在这个示例中,页面将显示 "Hello",其中 "hello" 是原始消息,经过 "capitalize" 过滤器后首字母被大写。

在Vue.js中,过滤器不仅可以在全局范围定义,也可以在单个Vue实例(局部)中定义。局部过滤器只在定义它的那个Vue实例中可用。 过滤器可以串联使用,即在一个表达式中使用多个过滤器,像管道一样,从左到右依次处理数据: 过滤器也可以接受参数,这就像一个函数,第一个参数是管道符前的表达式,后续的参数可以在过滤器名称后的括号中指定:

<body>
  <div id="app">
    <div>{{ message | capitalize | appendSuffix('Vue.js') }}</div>
  </div>
  <script>
    Vue.filter('capitalize', function (value) {
      if (!value) return ''
      value = value.toString()
      return value.charAt(0).toUpperCase() + value.slice(1)
    })
    new Vue({
      el: '#app',
      data: {
        message: 'hello'
      },
      filters: {
        appendSuffix: function (value, suffix = '') {
          return value + ' ' + suffix
        }
      }
    })
  </script>
</body>

总结一下,Vue过滤器提供了一种灵活的方式来处理视图层的文本格式化,它不会改变原始数据,只在视图层面提供格式化功能。

4.购物车 #

<body>
<div id="app" style="width:30%;margin:auto;">
    <table>
        <thead>
            <tr>
                <th>名称</th>
                <th>数量</th>
                <th>单价</th>
                <th>小计</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            <tr v-for="(product,index) in products">
                <td>{{product.name}}</td>
                <td>
                    <button @click="increaseQuantity(index)">+</button>
                    <input v-model="product.quantity" style="width:80px"/>
                    <button @click="decreaseQuantity(index)">-</button>
                </td>
                <td>{{product.price}}</td>
                <td>{{product.price*product.quantity}}</td>
                <td><button @click="removeProduct(index)">删除</button></td>
            </tr>
        </tbody>
    </table>
    <div>
        <p>总数量:{{totalQuantity}}</p>
        <p>总金额:{{totalPrice|currency}}</p>
        <p>最高单价商品:{{maxPrice|currency}}</p>
    </div>
</div>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
  var vm = new Vue({
    el:'#app',
    data:{
        products: [
          { name: 'iphone5', price: 500, quantity: 1 },
          { name: 'iphone6', price: 600, quantity: 1 },
          { name: 'iphone7', price: 700, quantity: 1 },
        ]
    },
    methods:{
        increaseQuantity(index){//增加索引对应的商品的数量
            this.products[index].quantity++;
        },
        decreaseQuantity(index){//减少索引对应商品的数量
            //如果现在商品的数量大于1,就减少1个
            if(this.products[index].quantity>1){
                this.products[index].quantity--;
            }else{//否则就把当前的商品删除
                this.removeProduct(index);
            }
        },
        removeProduct(index){
            //从索引入删除一个商品
            this.products.splice(index,1);
        }
    },
    filters:{
        currency(value){
            return '$'+value;
        }
    },
    computed:{
        totalQuantity(){
            return this.products.reduce((result,product)=>{
                return result+product.quantity;
            },0);
        },
        totalPrice(){
            return this.products.reduce((result,product)=>{
                //每项商品的价格乘以数量得到了小计,把每个商品的小计加在一起就是最后的总价
                return result+product.quantity*product.price;
            },0);
        },
        maxPrice(){
            //循环迭代每一个商品,判断
            return this.products.reduce((result,product)=>{
                //如果当前商品价格高于result,那就返回当前的商品的价格
                if(product.price>result){
                    return product.price
                }else{//还使用老价格
                    return result;
                }
            },0);
        }
    }
  });
</script>
</body>

5.事件 #

在Vue.js中,$on$emit$off是Vue实例的方法,它们用于处理自定义事件。以下是这三个方法的详细说明:

  1. $on(eventName, eventHandler): 这个方法用于在Vue实例上监听自定义事件。eventName参数是你想要监听的事件名称,而eventHandler参数是事件触发时要执行的函数。eventHandler函数将接收所有由$emit传递的附加参数。

    示例:

     // 为自定义事件 'myEvent' 定义一个处理器
     vm.$on('myEvent', function (message) {
         console.log(message);
     });
    
  2. $emit(eventName, [...args]): 这个方法用于在Vue实例上触发一个事件。eventName参数是你想要触发的事件名称。你可以传递额外的参数到事件处理器,这些参数会在$emit之后的参数中传递。

    示例:

     // 触发 'myEvent' 事件并传递一条消息
     vm.$emit('myEvent', 'Hello, Vue!');
     // 此时,上面的 'myEvent' 处理器会打印: 'Hello, Vue!'
    
  3. $off(eventName?, eventHandler?): 这个方法用于移除自定义的事件监听器。如果没有提供参数,所有的事件监听器都会被移除。如果只提供了事件名,那么该事件的所有监听器都会被移除。如果同时提供了事件名和处理器,则只移除这个具体的事件监听器。

    示例:

     // 为 'myEvent' 事件定义一个处理器
     function eventHandler(message) {
         console.log(message);
     }
     vm.$on('myEvent', eventHandler);
    
     // 移除 'myEvent' 的特定处理器
     vm.$off('myEvent', eventHandler);
    

这三个方法常用于组件间的通信,尤其在父子组件间。对于非父子组件,可以使用事件总线(Event Bus)或者Vuex进行状态管理。在使用这些方法时,应当注意防止内存泄漏。当组件销毁或者不再需要监听某个事件时,应当使用$off方法移除不再需要的事件监听器。

<body>
    <script src="vue.js"></script>
    <script>
        // 创建一个新的 Vue 实例
        var vm = new Vue();
        // 定义一个函数作为事件处理器
        function eventHandler(message) {
            console.log(message);
        }
        // 使用 $on 监听一个名为 "hello" 的事件
        vm.$on('hello', eventHandler);
        // 使用 $emit 触发 "hello" 事件,并传递一条消息
        vm.$emit('hello', 'Hello, Vue.js!');
        // 控制台将打印: 'Hello, Vue.js!'
        // 使用 $off 移除 "hello" 事件的监听器
        vm.$off('hello', eventHandler);
        // 再次尝试触发 "hello" 事件
        vm.$emit('hello', 'Hello, Vue.js!');
        // 这次,控制台没有任何输出
    </script>
</body>

6.实例属性和方法 #

6.1 $mount #

在 Vue 中,$mount 是一个非常重要的方法。这个方法用于手动启动 Vue.js 应用程序。$mount 将实例挂载到 DOM 上,在挂载之前会执行 beforeMountcreated 生命周期钩子,在挂载完成后会执行 mounted 生命周期钩子。

在一些情况下,你可能需要延迟挂载,直到需要时才手动调用 $mount 方法。为此,可以在不传入参数的情况下调用 $mount 方法,例如 vm.$mount(),然后在需要的时候,将 Vue 实例挂载到某个 DOM 元素上。

下面是一个 $mount 的示例:

// 创建 Vue 实例
var app = new Vue({
  data: {
    message: "Hello Vue!"
  },
  template: '<div>{{ message }}</div>'
})

// 手动挂载 Vue 实例到 #app 元素
app.$mount('#app')

在这个例子中,我们首先创建一个新的 Vue 实例,然后使用 $mount 方法将其挂载到 id 为 'app' 的 DOM 元素上。请注意,在 $mount 方法被调用之前,Vue 实例不会对 DOM 进行任何更改。但是一旦 $mount 方法被调用,Vue 实例将开始更新 DOM,并响应数据的改变。

6.2 $destroy #

$destroy方法在Vue.js 2.x中是用于完全销毁一个Vue实例及其所有子组件的方法。在调用此方法后,Vue实例将停止响应数据变化,所有的事件监听器也会被移除,所有子实例也将被销毁。

这是一个使用 $destroy 方法的示例:

var vm = new Vue({
  data: {
    title: 'Hello Vue!'
  },
  template: '<div>{{ title }}</div>'
})

// 挂载实例
vm.$mount('#app')

// 在需要的时候销毁实例
vm.$destroy()

在这个例子中,我们首先创建并挂载一个Vue实例,然后在需要的时候销毁它。

值得注意的是,虽然$destroy方法在某些情况下可能会有用,但在正常的组件生命周期中,你几乎不需要自己直接调用这个方法。当一个Vue组件从其父组件的模板中被移除时,Vue会自动触发其生命周期的beforeDestroydestroyed钩子,清理它所有的事件监听器和子组件。

另外,也需要注意的是,在Vue 3.x中,$destroy方法已被移除,因为在Vue 3.x中,组件实例的生命周期完全由Vue自己管理,不再需要手动销毁。

6.3 $set #

在 Vue.js 2.x 中,$set 是一个全局方法,用于向响应式对象中添加一个属性,并确保新属性同样是响应式的,即新属性的变化也会触发视图更新。

这是因为在 Vue.js 中,当你创建一个 Vue 实例或者组件实例的时候,Vue 将会使用其响应系统使得数据模型 (data object) 是响应式的。然而,这个响应系统只能跟踪在实例创建时就已经存在的属性。如果你动态地向实例添加一个新的属性,Vue 无法使这个新属性也变成响应式的。

$set 方法的主要用法如下:

this.$set(object, propertyName, value)

例如:

var vm = new Vue({
  data: {
    message: 'hello'
  }
})

// 使用 $set 添加新属性
vm.$set(vm.$data, 'newMessage', 'world')

在这个例子中,newMessage 属性被添加到了 vm.$data 对象中,并且 newMessage 属性也是响应式的,也就是说,当 newMessage 属性的值改变的时候,视图会自动更新。

在 Vue 3.x 中,Vue 改用了 Proxy-based 的响应式系统,你可以直接添加新的属性而不需要使用 $set 方法。所以,从 Vue 3.0 开始,$set 方法已经被废弃了。

6.4 $delete #

Vue.js 中的 $delete 是一个全局方法,用于删除对象的属性。如果对象是响应式的,确保删除属性后仍保持响应性。$delete 是对 JavaScript 内置的 delete 操作符的一个封装,以确保删除属性后视图能够更新。

这是 $delete 方法的主要用法:

this.$delete(object, propertyName)

例如:

var vm = new Vue({
  data: {
    message: 'Hello Vue!'
  }
})

// 使用 $delete 删除属性
vm.$delete(vm.$data, 'message')

在这个例子中,message 属性被从 vm.$data 对象中删除。因为 vm.$data 是响应式的,所以在 message 属性被删除后,视图会自动更新。

然而,值得注意的是,Vue 3.x 使用了基于 Proxy 的响应式系统,你可以直接删除属性而不需要使用 $delete 方法,并且保证删除属性后视图能够更新。因此,从 Vue 3.0 开始,$delete 方法已经被废弃。

6.5 $forceUpdate #

在 Vue.js 2.x 中,$forceUpdate 是一个实例方法,用于强制重新渲染组件。

在 Vue 的响应式系统中,当一个组件的数据(data)发生变化时,Vue 会自动重新渲染该组件。然而,在某些罕见的情况下,你可能需要强制重新渲染一个组件,即使其数据并未发生改变。这时,你可以使用 $forceUpdate 方法。

这是 $forceUpdate 方法的主要用法:

this.$forceUpdate()

例如:

var vm = new Vue({
  data: {
    message: 'hello'
  },
  methods: {
    updateMessage: function() {
      this.message = 'world'
      this.$forceUpdate()
    }
  }
})

在这个例子中,当 updateMessage 方法被调用时,首先会更新 message 属性的值,然后调用 $forceUpdate 方法强制重新渲染组件。虽然在这个特定的情况下,$forceUpdate 是不必要的(因为 message 属性的改变将自动触发组件的重新渲染),但这个例子演示了 $forceUpdate 方法的基本用法。

然而,需要注意的是,$forceUpdate 方法应当谨慎使用,它会导致整个组件树的重新渲染,可能会对性能产生影响。在大多数情况下,你应该依赖 Vue 的响应式系统来管理组件的更新和重新渲染。如果你发现自己需要使用 $forceUpdate,这可能是因为你的代码中存在某些问题,你应该尝试解决这些问题,而不是依赖 $forceUpdate 来强制更新组件。

此外,从 Vue 3.0 开始,$forceUpdate 方法已经被废弃,因为新的 Vue 版本提供了更好的重新渲染策略和更精确的依赖跟踪。

6.6 $nextTick #

在 Vue 2 中,$nextTick 是一个非常重要的实例方法。当你更改数据后,视图不会立即更新,而是等到 JavaScript 事件循环的下一个tick时,Vue.js 会更新 DOM。$nextTick 方法用于在 DOM 更新后,延迟一段代码直到下一次 DOM 更新周期。换句话说,$nextTick 允许你在 DOM 更新之后立即执行一些操作。

这是 $nextTick 方法的主要用法:

this.$nextTick([callback])

例如:

var vm = new Vue({
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    updateMessage: function() {
      this.message = 'Hello'
      this.$nextTick(function() {
        // 这个代码块将在下一次 DOM 更新周期之后执行
        console.log('DOM updated!')
      })
    }
  }
})

在这个例子中,当 updateMessage 方法被调用时,首先会更新 message 属性的值,然后在 DOM 更新后输出 'DOM updated!'。

$nextTick 在以下场景中非常有用:

  1. 当你需要在 Vue.js 重新渲染 DOM 后,基于新的 DOM 状态做一些操作。
  2. 在组件生命周期钩子中,由于 Vue.js 更新 DOM 是异步的,所以你可能需要 this.$nextTick 来确保 DOM 更新完成。

需要注意的是,在 Vue 3.x 中,$nextTick 依然存在,其用法与 Vue 2.x 中的用法相同。

6.7 响应式系统 #

这个例子中,我们定义了两个函数,defineReactiveobserve

defineReactive 函数接受一个对象、一个键和一个值。它使用 Object.defineProperty 方法将这个键定义为响应式的。当你尝试获取这个键的值时,它将在控制台输出一条日志并返回值。当你尝试设置这个键的值时,如果新值与旧值不同,它将在控制台输出一条日志并更新值。

observe 函数接受一个对象,遍历这个对象的每个键,调用 defineReactive 函数使这个键变为响应式的。

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`获取 ${key}: ${val}`);
            return val;
        },
        set(newVal) {
            if (newVal === val) return;
            console.log(`设置 ${key} 为: ${newVal}`);
            val = newVal;
        },
    });
}

function observe(obj) {
    if (typeof obj !== "object" || obj === null) return;
    Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
}

// 测试代码
let data = { message: 'hello' };
observe(data);
// 当你尝试获取 message 或设置 message 时,你会在控制台看到相关的日志信息
console.log(data.message); // 获取 message: hello
data.message = 'world'; // 设置 message 为: world
console.log(data.message); // 获取 message: world

6.8 更新逻辑 #

在 Vue.js 中,Watcher 是一个重要的概念,它是 Vue 响应式系统的核心部分。简单来说,一个 Watcher 就是一个观察者,它可以观察一个或多个属性,并在这些属性发生变化时执行相应的操作。

Watcher 在 Vue.js 中的主要功能是依赖收集和派发更新:

在这个例子中,我们添加了一个新的类 "Watcher"。每一个 "Watcher" 实例都关联了一个对象的一个属性和一个回调函数。当属性的值发生变化时,"Watcher" 将调用回调函数,并将新的值和旧的值作为参数。

defineReactive 函数现在接受一个新的参数 watchers,这是一个数组,存储了所有依赖于当前属性的 "Watcher"。当属性的值发生变化时,所有的 "Watcher" 都会收到更新通知。

let currentWatcher = null;
class Watcher {
  constructor(obj, key, callback) {
    this.obj = obj;
    this.key = key;
    this.callback = callback;
    this.value = this.get();
  }

  get() {
    currentWatcher = this;
    let value = this.obj[this.key]; // 触发 getter
    currentWatcher = null;
    return value;
  }

  update() {
    const oldVal = this.value;
    this.value = this.get();
    this.callback.call(this.obj, this.value, oldVal);
  }
}

function defineReactive(obj, key, val) {
  const watchers = [];

  Object.defineProperty(obj, key, {
    get() {
      if (currentWatcher) {
        watchers.push(currentWatcher);
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      watchers.forEach(watcher => watcher.update());
    },
  });
}

function observe(obj) {
  if (typeof obj !== "object" || obj === null) return;
  Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
}

let data = { message: 'hello' };
observe(data);

let watcher = new Watcher(data, 'message', (val, oldVal) => {
  console.log(`message 从 ${oldVal} 变为 ${val}`);
});

data.message = 'world';

7.生命周期 #

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 实例指向的所有东西都会解绑,所有的事件监听器都会被移除,所有的子实例也都会被销毁。

下面是一个包含这些生命周期钩子的Vue实例的示例:

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

7.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 是所有生命周期钩子中最先执行的。

7.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 实例的数据。

7.3 beforeMount #

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

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

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

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

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

7.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。

7.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 中。

7.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 数据。

7.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 钩子函数将被调用。

7.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 实例已经被销毁,你无法再访问实例上的任何属性或执行任何方法。

8.基本原理 #

8.1 vue.html #

<body>
  <div id="app">
    {{message}}
  </div>
  <script src="myVue.js"></script>
  <script>
    var vm = new Vue({
      el: '#app',
      data: {
        message: 'hello'
      },
      beforeCreate: function () {
        console.log('beforeCreate')
      },
      created: function () {
        console.log('created')
      },
      beforeMount: function () {
        console.log('beforeMount')
      },
      mounted: function () {
        console.log('mounted')
      },
      beforeUpdate: function () {
        console.log('beforeUpdate')
      },
      updated: function () {
        console.log('updated')
      },
      beforeDestroy: function () {
        console.log('beforeDestroy')
      },
      destroyed: function () {
        console.log('destroyed')
      }
    })
   setTimeout(()=>{
    vm.message='world';
    vm.$destroy();
   },1000)
  </script>
</body>

8.2 vue.js #

class Vue {
  constructor(options) {
    //保存创建Vue实例的时候的选项对象
    this.$options = options;
    //保存数据对象
    this.$data = options.data;
    //初始化事件
    this.initEvents();
    //初始化生命周期
    this.initLifeCycle();
    this.callHook("beforeCreate");
    //初始化provide/inject系统
    this.initInjections();
    //初始化响应式系统
    this.initReactivity();
    this.callHook('created');
    //如果用户没有提供el参数,则不会继续执行,直接返回了,等待用户手工调用$mount方法
    if(!this.$options.el){
        return;
    }
    //获取CSS选择器对应的DOM元素
    this.$el = document.querySelector(this.$options.el);
    //判断有没有template选项
    let {template} = this.$options;
    //如果没有提供,则会使用
    if(!template){
        template=this.$el.outerHTML;
    }
    //开始编译模板
    this.compile(template);
  }
  initEvents() {
    console.log("初始化事件系统"); //this.$on $emit $off
  }
  initLifeCycle() {
    this._isMounted = false; //布尔值,表示当前实例是否已经挂载成功
    console.log("初始化生命周期");
  }
  callHook(hookName) {
    //执行参数对象上对应的钩子函数
    this.$options[hookName]?.();
  }
  initInjections() {
    console.log("初始化注入系统");
  }
  initReactivity() {
    //遍历data对象的所有的属性
    for (let key in this.$data) {
      let watchers = [];  
      //获取data对象此key的值
      let value = this.$data[key];
      //把data上的属性都代理到this,也就是vm,也就是Vue实例上
      Object.defineProperty(this, key, {
        get() {
          const watcher = ()=>{
              this.callHook("beforeUpdate");
              this.update();
              this.callHook("updated");
          }
          watchers.push(watcher);
          //收集依赖
          return value;
        },
        set(newValue) {
          value = newValue;
          //派发更新
          watchers.forEach(watcher=>watcher());
        },
      });
    }
  }
  compile(template){
    //把用户提供的模板字符编译成render函数
    //render负责执行后生成虚拟DOM,虚拟DOM就是用JS描述DOM的结构
    this.render = function(vm){
        return {
            type:'div',
            props:{id:'app'},
            children:vm.message// 我们在执行render方法的时候会进行依赖收集
        }
    }
    this.callHook('beforeMount');
    this.update();
    this.callHook('mounted');
  }
  update(){
    //调用render方法生成虚拟DOM
    let vdom = this.render(this);
    //根据虚拟DOM生成真实DOM
    let newEl = createElement(vdom);
    //获取老的真实DOM
    let oldEl= this.$el;
    //用新生成的真实DOM替换掉老的真实DOM
    oldEl.parentNode.replaceChild(newEl,oldEl);
    //把this.$l赋值为新的DOM
    this.$el=newEl;
  }
  $destroy(){
    this.callHook('beforeDestroy');
    this.callHook('destroyed');
  }
}
function createElement(vdom){
   //创建给定类型的DOM节点
   let element = document.createElement(vdom.type);//div
   //遍历虚拟DOM上的属性对象,给真实DOM赋属性值
   for(let key in vdom.props){
    element.setAttribute(key,vdom.props[key]);
   }
   //给元素的儿子赋值
   element.innerHTML = vdom.children;
   return element;
}

8.3 加入响应式 #

class Dep {
  constructor() {
    this.subs = []; // 存储所有的 Watcher
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    // 通知所有的 Watcher
    this.subs.forEach(sub => {
      sub.update();
    });
  }
}

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // 将当前 Watcher 实例指定到 Dep 静态属性 target
    Dep.target = this;
    this.vm[this.key]; // 触发 getter,添加依赖
    Dep.target = null;
  }

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



class Vue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.initEvents();
    this.initLifeCycle();
    this.callHook("beforeCreate");
    this.initInjections();
    this.initReactivity();
    this.callHook('created');
    if (!this.$options.el) {
      return;
    }
    this.$el = document.querySelector(this.$options.el);
    let {
      template
    } = this.$options;
    if (!template) {
      template = this.$el.outerHTML;
    }
    this.compile(template);
  }
  initEvents() {
    console.log("初始化事件系统");
  }
  initLifeCycle() {
    this._isMounted = false;
    console.log("初始化生命周期");
  }
  callHook(hookName) {
    this.$options[hookName]?.();
  }
  initInjections() {
    console.log("初始化注入系统");
  }
  initReactivity() {
    for (let key in this.$data) {
      let dep = new Dep();
      let value = this.$data[key];
      Object.defineProperty(this, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newValue) {
          value = newValue;
          dep.notify();
        }
      });
    }
  }

  compile(template) {
    this.render = function (vm) {
      return {
        type: 'div',
        props: {
          id: 'app'
        },
        children: vm.message
      };
    };
    new Watcher(this, 'message', () => {
      this.callHook('beforeUpdate');
      this.update();
      this.callHook('updated');
    });
    this.callHook('beforeMount');
    this.update();
    this.callHook('mounted');
  }
  update() {
    let vdom = this.render(this);
    let newEl = createElement(vdom);
    let oldEl = this.$el;
    oldEl.parentNode.replaceChild(newEl, oldEl);
    this.$el = newEl;
  }
  $destroy() {
    this.callHook('beforeDestroy');
    this.callHook('destroyed');
  }
}
function createElement(vdom) {
  let element = document.createElement(vdom.type);
  for (let key in vdom.props) {
    element.setAttribute(key, vdom.props[key]);
  }
  element.innerHTML = vdom.children;
  return element;
}

在 Vue 中,Watcher 通常在以下情况下创建:

  1. 当你使用 vm.$watch API 创建一个监听器时。例如,vm.$watch('a', cb) 将会创建一个 Watcher 来监听属性 a 的变化,并且当 a 变化时,回调函数 cb 将会被调用。
  2. 当你在 Vue 组件的模板中使用了某个属性时。例如,如果你的模板是 <div>{{ a }}</div>,那么 Vue 将会创建一个 Watcher 来监听属性 a 的变化,并且当 a 变化时,重新渲染这个组件。
  3. 当你在计算属性中使用了某个属性时。例如,如果你定义了一个计算属性 computedA,并且 computedA 的定义中使用了属性 a,那么 Vue 将会创建一个 Watcher 来监听属性 a 的变化,并且当 a 变化时,重新计算 computedA 的值。

一个 Watcher 实例包含以下主要属性:

其中,getter 通常是一个函数,它返回被观察的属性的当前值。这个 getter 函数在 Watcher 创建时指定。

callback 是一个函数,它在被观察的属性发生变化时将被调用。这个 callback 函数通常根据 Watcher 的用途而变化,比如,如果 Watcher 是用于监听属性,那么 callback 将是用户提供的函数;如果 Watcher 是用于渲染组件,那么 callback 将是渲染函数。

Watcher 中,当 getter 返回的值发生变化时,Watcher 将会调用它的 callback 函数,然后触发组件的重新渲染或执行用户提供的函数,这就是 Watcher 的更新逻辑。

Dep 类的 addSub 方法会添加 Watcher 实例,但并没有在此处做去重操作。其实,去重操作是在 Watcher 实例本身的 addDep 方法中完成的。

具体来说,每个 Watcher 实例都有一个 deps 数组,存放所有相关的 Dep 实例,并且有一个 newDeps 数组,每次计算时会存放当前所有相关的 Dep 实例。在一次计算开始时,会清空 newDeps 数组,并且在一次计算结束时,会比较 deps 和 newDeps,去除在 deps 中而不在 newDeps 中的 Dep 实例,并添加在 newDeps 中而不在 deps 中的 Dep 实例。

这样就做到了两个事情:

  1. 当一个属性不再依赖某个 Watcher 时,能够正确地从那个 Watcher 的依赖中移除,避免不必要的更新。
  2. 当一个属性开始依赖某个 Watcher 时,能够正确地添加到那个 Watcher 的依赖中,保证能够在该属性变化时收到更新。

在添加新的 Dep 实例时,会先检查这个 Dep 实例是否已经在 deps 中,避免重复添加。这就是 Watcher 做去重的地方。

具体代码大致如下(为了简洁,略去了部分细节):

class Watcher {
  //...
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  //...
}

在这段代码中,newDepIds 是一个 Set,用于快速查找 newDeps 中是否已经包含某个 Dep 实例。depIds 也是一个 Set,用于快速查找 deps 中是否已经包含某个 Dep 实例。当调用 addDep 方法时,会先查找 newDepIds 和 depIds,避免重复添加 Dep 实例。

Dep(依赖)和 Watcher 之间是多对多的关系。

在 Vue.js 的响应式系统中,每个可观察的数据对象都会关联一个 Dep 实例。这个 Dep 实例维护了一组 Watcher 实例,这些 Watcher 实例代表了依赖于这个数据对象的所有地方。

相反,每个 Watcher 实例也可以依赖于多个数据对象。例如,如果一个计算属性或者一个组件模板依赖于多个数据对象,那么与这个计算属性或者组件模板关联的 Watcher 实例将会加入到这些数据对象的 Dep 实例中。

因此,Dep 和 Watcher 之间是多对多的关系。每个 Dep 实例可以被多个 Watcher 实例依赖,而每个 Watcher 实例也可以依赖多个 Dep 实例。这种关系使得 Vue.js 能够在任何数据变化时,精确地找到需要更新的计算属性和组件。