vue 基础


官方文档:vue 介绍

1. 创建一个 vue

官方推荐初学者先使用单个 html 引入 js 的方式来使用 vue,如创建 vue01-base.html:

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app">
    {{ message }}
  </div>
  <div id="app-2">
    <span v-bind:title="message">
      鼠标悬停几秒钟查看此处动态绑定的提示信息!
    </span>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    })
  </script>
</body>

</html>

例子解析:

  • script 标签内: new Vue: 创建一个 vue 实例,实例挂载在 el 指定的 DOM 元素上,data 内的为实例数据对象,关于挂载和实例数据的知识见后续笔记,初期这么使用就好;
  • div 内: {{ message }} 模板插值语法,将当前 DOM 挂载的实例数据作为文本渲染在页面上。

关于模板插值语法:

  • 使用双大括号包裹。
  • 支持:
    • 文本,html;
    • JavaScript 表达式,如 {{ "abcd".split('').reverse().join('') }},但这些计算不适合出现在模板中。

2. 常用指令

指令

  • 释义:Directives,是带有 v- 前缀的特殊 attribute;
  • 语法:指令="表达式"
  • 作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。

2.1 判断 v-if

v-if:

  • 语法:v-if="expr/value"
  • 值为 Truthy 时渲染元素,除 false0""nullundefinedNaN 之外都是真值,包括空数组 []、空对象 {}
<p v-if="seen1">{{seen1}}</p>
<p v-if="seen2">{{seen2}}</p>
<p v-if="seen3">{{seen3}}</p>
<script>
  var app = new Vue({
    el: '#app-4',
    data: {
      seen1: [],
      seen2: {},
      seen3: 0,
    }
  })
</script>

2.2 循环 v-for

v-for:

  • 语法: v-for="item in Obj"
  • 遍历数组,位置参数: (item, index),第一个为取出的项,第二个为其索引,第二个非必须。
    <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
    </ol>
    <script>
    var app = new Vue({
      el: '#app-4',
      data: {
        todos: [
          { text: '1'},
          { text: '2'},
          { text: '3'}
        ]
      }
    })
    </script>
    
  • 遍历对象的属性,位置参数: (value, name),第一个为属性值,第二个为属性名,第二个非必须。
    <ul>
    <li v-for="(value, name) in object">
      {{name}} : {{value}}
    </li>
    </ul>
    <script>
    var app = new Vue({
      el: '#app-4',
      data: {
        object: {
          favorite: "Python",
          using: "Java"
        }
      }
    })
    </script>
    

2.3 表单输入绑定 v-model

实际中组件的 data 必须是一个函数,否则将会在此组件的所有实例中共享,官方文档 组件数据。故下面例子全部将 data 定义为函数,在返回即可。关于组件,见下面笔记。

v-model:

  • 语法:v-model="value"
  • 在 html 的 input, select 等等元素上创建双向数据绑定,即输入和元素的数据保持同步;
  • 在 data 中声明初始值
  • 对于不同的 html 元素使用不同的 property 并抛出不同事件,后续使用 v-on 监听事件:
    • text 和 textarea 元素使用 value property 和 input 事件;
    • checkbox 和 radio 使用 checked property 和 change 事件;
    • select 字段将 value 作为 prop 并将 change 作为事件。

如,radio 的 change 事件:

<div id="app-vmodel">
  <el-radio-group size='mini' v-model="selectRegion" @change="getInfo">
    <el-radio-button v-for="region in regionMap" :key="region.name" :label="region.name" :value="region.value">
    </el-radio-button>
  </el-radio-group>
  <span id="message"></span>
</div>

<script>
  var app = new Vue({
    el: '#app-vmodel',
    data() {
      return {
        selectRegion: null,
        regionMap: [{
          name: 'r1',
          value: 'r1-val'
        }, {
          name: 'r2',
          value: 'r2-val'
        }, ]
      }
    },
    methods: {
      getInfo() {
        document.getElementById("message").innerHTML = '已选择的 region: ' + this.selectRegion;
      }
    }
  })
</script>

2.4 动态绑定属性 v-bind

v-bind:

  • 语法: v-bind:attr="value"
  • 简写:v-bind 可省略,如上面 v-model 例子中 :key 实际上为 v-bind:key
  • 动态绑定 attribute,可以使用修饰符 .prop 绑定为 DOM property,关于 HTML property 与 attribute
  • 也可以绑定 prop,关于 prop 见下面笔记;
  • 绑定的 attribute 也可以动态传入,v-bind:[key]="value"

例子结合上面 v-model 例子。

2.5 绑定事件监听器 v-on

v-on:

  • 语法:v-on:click="expr"
  • 简写:v-on: 缩为 @,如 @click="expr"
  • 事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,方法可以传入多个参数;
  • 支持众多修饰符,如 stop, prevent 等等,如果没有修饰符也可以省略;
  • 用在普通元素上只能监听原生 DOM 事件;用在自定义元素组件上,也可以监听子组件触发的自定义事件;
  • 监听原生事件时,可以访问 $event,如 v-on:click="handle('ok', $event)"

例子一:radio 的监听,见上面 v-model 的例子;

例子二:将 table 当前行的值作为参数传入事件触发方法,需要配合 slot-scope(弃用,高版本使用v-slot)。

<div id="app-von">
  <el-table :data="tableData" style="width: 40%">
    <el-table-column prop="name" label="LastName">
    </el-table-column>
    <el-table-column prop="age" label="Age">
    </el-table-column>
    <el-table-column fixed="right" label="Update">
      <template slot-scope="scope">
        <el-button @click="handleClick(scope.row.name)" type="text" size="small">Update</el-button>
        <el-button type="text" size="small">delete</el-button>
      </template>
    </el-table-column>
  </el-table>
  <span id="message"></span>
</div>

<script>
  var app = new Vue({
    el: '#app-von',
    data() {
      return {
        tableData: [{
          name: "Morty",
          age: 12
        }, {
          name: "Rick",
          age: 70
        }]
      }
    },
    methods: {
      handleClick(param) {
        document.getElementById("message").innerHTML = '已选择的 row name: ' + param;
      }
    }
  })
</script>

2.6 插槽 v-slot

见组件。

3. 计算属性与侦听器

虽然模板中支持 JavaScript 表达式,但实际中任何复杂逻辑都不应该放在模板中。可以使用计算属性或侦听器。

3.1 计算属性 computed

computed:

  • 声明:在组件 computed 中声明;
  • 作用:当数据改变时,计算属性也会被自动执行;
  • 所有 getter/setter 的 this 上下文自动绑定为 Vue 实例,即通过 this 访问当前实际的属性和方法,如: this.tableData
  • 如果使用了箭头函数,则 this 不会绑定为当前 Vue 实例,可以将实例作为第一个参数传入。

例:

<div id="app-computed">
  <p>origin message: {{message}}</p>
  <p>Computed objUpdated: {{objUpdated}}</p>
  <p>Computed message: {{messageUpdated}}</p>
  <button @click="changeOrigin">Update</button>
  <button @click="testComputerSetter">testComputerSetter</button>
</div>

<script>
  var app = new Vue({
    el: '#app-computed',
    data() {
      return {
        message: 'origin message.',
        obj: {
          prop1: 2121,
          prop2: 'sasas'
        }
      }
    },
    computed: {
      // 1. 普通声明,默认只有  getter,this 指向实例
      // messageUpdated: function () {
      //   return this.message + '- computed 1 in computed.'
      // }
      // 2. 箭头函数声明,默认只有  getter,使用的实例需要传入
      messageUpdated: vm => {
        return vm.message + '- computed in computed.'
      },
      messageUpdater2: {
        get: function () {
          return this.message + '- computed in computed.'
        },
        set: function (v) {
          this.message = v
        }
      },
      now: function () {
        return Date.now()
      },
      objUpdated: function () {
        console.log(this.obj.prop1) // 监听对象的一个属性
        return "obj.prop1 changed to " + this.obj.prop1
      }
    },
    methods: {
      changeOrigin() {
        this.message += '- change in methods.'
        this.obj.prop1 += 100;

        this.messageUpdated = '100' // messageUpdated 无 setter,所以此处赋值并没有任何效果
      },
      testComputerSetter() {
        this.messageUpdater2 = '100' // messageUpdater2 有 setter
      },
    }
  })
</script>

可以看到:

  1. 实例中 message 变化时,messageUpdated 也会随之变化;
  2. 这样的 computed 声明,默认只有 getter,无 setter,可以声明 setter。

3.2 侦听器

侦听器:更通用的观察和响应实例数据变化

  • 声明:在组件 watch 中声明,声明语法:
    watch: {
    要侦听的数据: function(newVal, oldVal) { // 位置参数获取新设置的值和上次的值
      操作
    }
    }
    
  • 侦听器默认不会监听对象属性的变化,如果需要监听,则需要加 deep: true,如果只需要监听某一个属性推荐写为字符串形式: "obj.prop1": function...

  • 侦听器可以通过位置参数,获取到新设置的值以及上次的值,如上;
  • 无返回值;
  • 注意:在侦听器中不能更改当前侦听的数据,否则会造成无限调用的死循环。

例:

<div id="app-watch">
  <p>origin message: {{message}}</p>
  <button @click="changeOriginMess">UpdateMessage</button>
  <div id='message'></div>
  <div id='obj'></div>
</div>

<script>
  var app = new Vue({
    el: '#app-watch',
    data() {
      return {
        message: 'origin message.',
        obj: {
          prop1: 2121,
          prop2: 'sasas'
        }
      }
    },
    watch: {
      message: function (newVal, oldVal) {
        this.printDebugInfo('message', 'newVal: ' + newVal + '--- oldVal: ' + oldVal)
      },
      'obj.prop1': function (newVal, oldVal) {
        // 监听 obj.prop1
        this.printDebugInfo('obj', 'newVal: ' + newVal + '--- oldVal: ' + oldVal)
      },
    },
    methods: {
      changeOriginMess() {
        this.message += 'changed in methods.'
        this.obj.prop1 += 100
      },
      printDebugInfo(id, info) {
        document.getElementById(id).innerText = 'change: ' + info
      }
    }
  })
</script>

3.3 计算属性、侦听器、方法区别

3.3.1 计算属性与方法

计算属性与方法:

  • 相同点:都可以完成跟随数据变化而变化;
  • 不同点 - 缓存:
    • 计算属性基于它们的响应式依赖进行缓存,对于一个响应式依赖,如果未发生变化,多次调用 computed 是不会进行重复计算,除非响应式依赖发生变化;
    • 关于响应式,目前可以理解为在组件 data 中声明的数值为响应式(数组和对象有额外要求,见后续笔记),像下面这样的并不是响应式依赖,computed 无法同步计算:
      <script>
      var app = new Vue({
        el: '#app-computed',
        data() {
          return {
            message: 'origin message.'
          }
        },
        computed: {
          now: function () {
            return Date.now()  // Date.now() 不是响应式依赖,computed 无法同步计算
          }
        },
      })
      </script>
      
    • 而方法会在每次调用时均进行计算。

3.3.2 计算属性与侦听器

计算属性与侦听器:

  • 相同点:都只能监听响应式依赖变化;
  • 不同点:
    • 声明不同,计算属性声明时需要写新创建的数据名,内部写监听的数据;而侦听器在声明时写侦听的数据;
    • 计算属性作为一个“属性”,需要在 getter 内写返回;而侦听器通常是侦听数据变化而进行某一系列操作,不会有返回值,且不能在侦听器内更改当前侦听的数据;
    • 计算属性可以一次监听多个数据;侦听器只能监听声明时写的数据。

关于计算属性监听多个数据,直接写在内部即可,例:

<div id="app-computedVSwatch">
  <p>Full Name by computed: {{fullNameComp}}</p>
  <p>Full Name by watch: {{fullNameWatch}}</p>
  <button @click="changeLastName">changeLastName</button>
</div>

<script>
  var app = new Vue({
    el: '#app-computedVSwatch',
    data() {
      return {
        firstName: 'Jerry',
        lastName: 'Smith',
        fullNameWatch: 'Jerry Smith',
      }
    },
    computed: {
      fullNameComp: function() {
        return this.firstName + ' ' + this.lastName
      }
    },
    watch: {
      firstName: function (newVal) {
        this.fullNameWatch = newVal + ' ' + this.lastName
      },
      lastName: function (newVal, oldVal) {
        this.fullNameWatch = this.firstName + ' ' + newVal
      },
    },
    methods: {
      changeLastName() {
        this.lastName = 'Foo'
      },
    }
  })
</script>

可以看到 watch 不适合处理多个关联数据变化。

vue 官方文档中说“使用 watch 选项允许我们执行异步操作,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。”,我此时不明白,试着用 computed 好像也能做到?

<div id="computedVSwatch-asyncReq">
  <div id='watch'>
    <p>
      Ask a yes/no question by watch:
      <input v-model="questionForWatch">
    </p>
    <p>{{ answerByWatch }}</p>
    <button @click="cntAddOne">{{cnt}}</button>
  </div>
  <div id='computed'>
    <p>
      Ask a yes/no question by computed:
      <input v-model="questionForComp">
    </p>
    <p>{{ answerByComp }}</p>
    <button @click="cntAddOne">{{cnt}}</button>
  </div>
  <span id="message"></span><br>
  <span>{{answerByCompInfo}}</span>
</div>


<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
  var watchExampleVM = new Vue({
    el: '#computedVSwatch-asyncReq',
    data() {
      return {
        cnt: 0,
        questionForWatch: '',
        answerByWatch: 'I cannot give you an answer until you ask a question!',
        questionForComp: '',
        answerByComp: 'I cannot give you an answer until you ask a question!',
      }
    },
    computed: {
      answerByCompInfo: function () {
        this.printDebugInfo('questionForComp: ' + this.questionForComp)

        this.answerByComp = 'Waiting for you to stop typing...'
        this.debouncedGetAnswerByComp(this.questionForComp)
      }
    },
    watch: {
      questionForWatch: function (newQuestion, oldQuestion) {
        this.answerByWatch = 'Waiting for you to stop typing...'
        this.debouncedGetAnswer()
      }
    },
    created: function () {
      this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
      this.debouncedGetAnswerByComp = _.debounce(this.getAnswerByComp, 500)
    },
    methods: {
      cntAddOne: function () {
        this.cnt += 1
      },
      printDebugInfo(info) {
        document.getElementById("message").innerHTML = 'debugInfo: ' + info;
      },
      getAnswer: function () {
        if (this.questionForWatch.indexOf('?') === -1) {
          this.answerByWatch = 'Questions usually contain a question mark. ;-)'
          return
        }
        this.answerByWatch = 'Thinking...'
        var vm = this
        axios.get('https://yesno.wtf/api')
          .then(function (response) {
          vm.answerByWatch = _.capitalize(response.data.answer)
        })
          .catch(function (error) {
          vm.answerByWatch = 'Error! Could not reach the API. ' + error
        })
      },
      getAnswerByComp: function (questionForWatch) {
        if (questionForWatch.indexOf('?') === -1) {
          answerByWatch = 'Questions usually contain a question mark. ;-)'
          return
        }
        this.answerByComp = 'Thinking...'
        var vm = this
        axios.get('https://yesno.wtf/api')
          .then(function (response) {
          vm.answerByComp = _.capitalize(response.data.answer)
        })
          .catch(function (error) {
          vm.answerByComp = 'Error! Could not reach the API. ' + error
        })
      }
    }
  })
</script>

4. 挂载和响应式原理

4.1 挂载

提供的元素只能作为挂载点,且所有挂载元素会被 Vue 生成的 DOM 替换,所以父级向子组件分发内容时要使用插槽,否则开始标签和结束标签的内容都会被删除。

4.2 响应式原理

vue 会将 data 选项的所有对象的属性转为 getter/setter,例如:

console.log(Object.getOwnPropertyDescriptors(this.aObject))

image-20200903161106094

每个实例都会对应一个 watcher,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

4.2.1 对象的注意事项

Vue 对于 data 中的对象可以检测与不可检测的解决:

  • 可检测原有属性的值的变化;
  • 不可检测属性的添加 - 解决:使用 Vue.set/this.$set, Object.assign()混入原对象和新对象:
    • this.$set 为实例方法,在实例内绑定比较方便,如:
    this.$set(this.obj, "addedProp2", 1111);
    
    • Object.assign()混入原对象和新对象:
      this.obj = Object.assign({}, *this*.obj, { addedProp2: 2222 });
      
  • 不可检测添加属性的值的变化;

  • 不可检测任何属性的删除。

如下面例子(以使用 watch 为例):

<div id="app-reactive-object">
  obj 对象的:
  <div style="margin-left: 2em">obj: {{ obj }}</div>
  <button @click="addPropToObj">addPropToObj</button>
  <div id="obj"></div>

  <button @click="changeOrgProp1">changeOrgProp1</button>
  <div id="obj-prop1"></div>

  <button @click="delOrgProp1">delOrgProp1</button><br>

  <button @click="changeAddedProp1">changeAddedProp1</button>
  <div id="obj-addedProp1"></div>

  <button @click="changeAddedProp2">changeAddedProp2</button>
  <div id="obj-addedProp2"></div>
</div>

<script>
  var app = new Vue({
    el: "#app-reactive-object",
    name: "App-11",
    data: function () {
      return {
        obj: {
          prop1: 1,
        },
      };
    },
    watch: {
      obj: function (newVal, oldVal) {
        this.printDebugInfo(
          "obj",
          "newVal: " + newVal + "--- oldVal: " + oldVal
        );
      },
      "obj.prop1": function (newVal, oldVal) {
        this.printDebugInfo(
          "obj-prop1",
          "newVal: " + newVal + "--- oldVal: " + oldVal
        );
      },
      "obj.addedProp1": function (newVal, oldVal) {
        this.printDebugInfo(
          "obj-addedProp1",
          "newVal: " + newVal + "--- oldVal: " + oldVal
        );
      },
      "obj.addedProp2": function (newVal, oldVal) {
        this.printDebugInfo(
          "obj-addedProp2",
          "newVal: " + newVal + "--- oldVal: " + oldVal
        );
      },
    },
    methods: {
      addPropToObj() {
        this.obj.addedProp1 = "1111"; // 这样添加的无 getter 和 setter
        // 1. set 添加的有 getter 和 setter
        // this.set(this.obj, "addedProp2", 2222);
        // 2. Object.assign()混入原对象和新对象,会将所有属性都添加 getter/setter
        //        上面的 addedProp1 本没有 getter/setter,经过下面 assign 后也会有getter/setter
        this.obj = Object.assign({}, this.obj, { addedProp2: 2222 });
        console.log(this.obj);
      },
      changeOrgProp1() {
        this.obj.prop1 += 1111;
      },
      delOrgProp1() {
        delete this.obj.prop1;
      },
      changeAddedProp1() {
        this.obj.addedProp1 += "1111";
      },
      changeAddedProp2() {
        this.obj.addedProp2 += 2222;
      },
      printDebugInfo(id, info) {
        document.getElementById(id).innerText = "change: " + info;
      },
    },
  });
</script>

4.2.2 数组的注意事项

Vue 对于 data 中的数组可以检测与不可检测的解决:

  • 可检测数组新添加或者删除元素;
  • 不可检测索引方式的添加或修改 - 解决:使用 Vue.set/this.$set:
    this.set(this.arr, 1, 654)  // 指定的位置存在,则更改
    this.set(this.arr, 10, 654)  // 指定的位置不存在,则添加,且中间的会添加为 null
    
  • 不可检测直接修改数组长度,如 this.arr.length = newLength 不可被检测 - 解决:使用 splice:
    this.arr.splice(1);
    

如下面例子(以使用 watch 为例):

<div id="app-reactive-array">
  arr 数组的:
  <div style="margin-left: 2em">obj: {{ arr }}</div>
  <button @click="addEle">addEle</button>
  <div id="arr"></div>

  <button @click="changeEleByIndex">changeEleByIndex</button><br />
  <button @click="popEle">popEle</button><br />
  <button @click="changeOrAddEleBySet">changeOrAddEleBySet</button><br />
  <button @click="changeLenByDirect">changeLenByDirect</button><br />
  <button @click="changeLenBySplice">changeLenBySplice</button><br />
</div>

<script>
  var app = new Vue({
    el: "#app-reactive-array",
    data: function () {
      return {
        arr: [1, 2, 3],
      };
    },
    watch: {
      arr: function (newVal, oldVal) {
        this.printDebugInfo(
          "arr",
          "newVal: " + newVal + "--- oldVal: " + oldVal
        );
      },
    },
    methods: {
      addEle() {
        this.arr.push(9);
      },
      changeEleByIndex() {
        this.arr[0] = 123;
      },
      popEle() {
        this.arr.pop();
      },
      changeOrAddEleBySet() {
        this.set(this.arr, 1, 654); // 指定的位置存在,则更改
        this.set(this.arr, 10, 654); // 指定的位置不存在,则添加,且中间的会添加为 null
      },
      changeLenByDirect() {
        this.arr.length = 1;
      },
      changeLenBySplice() {
        this.arr.splice(1);
      },
      printDebugInfo(id, info) {
        document.getElementById(id).innerText = "change: " + info;
      },
    },
  });
</script>
评论
还没有评论
    发表评论 说点什么