2. vue基础 ①

245 阅读18分钟

一、认识Mustache、实例挂载及Vue编译渲染视图

  • 只要挂载到 vm 实例上的内容都可以“直接”在视图中使用!
  • 极少数window全局对象中的内容,也可以在视图中直接使用,例如:JSON可以,但是eval不可以...但是可以把不能直接使用的东西,赋值给实例的状态,视图中使用状态即可

1.1 Mustache库:vue中没有使用这个库

import Mustache from 'mustache';

var data = {
    title: 'This is my TITLE for MUSTACHE'
}

var html = Mustache.render(
    `<h1>{{ title }}</h1>`,
    data
);

document.getElementById('app').innerHTMl = html;

image.png

1.2 Mustache小胡子语法 {{ ... }}

+ Vue`<template>`模板语法是特殊的数据绑定方法
    + 因为vue的模板都是基于HTML的,所以模板中直接写HTML都是能够被HTML解析器解析的
+ vue有自己的表达式、自定义属性、指令,这些都不能编译,Vue提供一套模板**编译系统**
    + 开发者写的template,通过分析 HTML字符串转换成AST树,这些表达式、自定义属性、指令会变成虚拟DOM树,虚拟DOM最后变为真实DOM,最后render渲染

+ 小胡子语法中写的是“数据/状态”或者“JS表达式(代码运行有结果的)”
    + 判断:三元运算符
    + 循环:数组的map/filter/find/reduce/some/every...方法
+ 不同类型的值,最后渲染的结果是不一样的
    + number/string/boolean直接渲染(写啥就渲染啥)
    + null/undefined渲染的是空
    + Symbol/BigInt都是先变为字符串,再进行渲染的
        + 但是小胡子语法中不能直接出现Symbol函数,需要指定一个状态存储Symbol类型的值
    + 对象类型会默认装换为字符串进行渲染
        + 普通对象/数组对象:是基于JSON.strngify(...)把其变为JSON字符串
        + 正则、日期等剩下的对象,基本上都是基于String(...)处理的

1.2.1 插值表达式的使用

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

<body>
  <div id="app">
    <!-- 1.基本应用 -->
    <h2>当前计数:{{ counter }}</h2>
    <h2>message:{{ message }}</h2>

    <!-- 2.表达式 & JS API-->
    <h2>计数的双倍:{{ counter*2 }}</h2>
    <h2>显示的信息:{{ info.split(" ")}}</h2>

    <!-- 3.三元运算符 -->
    <h2>{{ age >= 18 ? "成年人" : "未成年人" }}</h2>

    <!-- 4.注意:这里不能定义语句,不能绑定多个表达式 -->
    <!-- <h2>{{ const name = "hhh"}}</h2> -->

  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          counter: 1,
          message: "hello world",
          info: "my name is hhh",
          age: 22
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

1.2.2 插值表达式中的属性绑定

# 属性:
- attribute:HTML的扩展 比如:title、src、href,缩写attr
- property:在对象内部存储数据,通常用来描述数据结构,缩写prop

# vue中的属性
- Mustache中是不支持在HTML属性中插值
- Vue中因为用底层的模板编译系统,支持Vue内置的属性
- 想要在html中插入JS的表达式,可以用v-bind,v-bind:href="url"

# disabled="true"
- 对于模板解析,true是个字符串,并不是逻辑真
- :disabled="true" 逻辑真

# truthy falsy
- falsy: false 0 "" null undefined NaN
- truthy: 除了falsy以外的值

# 对于disabled逻辑真假的属性来说
- :disabled="null/undefined" 不会渲染在标签上
- 只有true false "" 和 truthy会在解析过程中将disabled属性包含在元素上
- truthy::disbled="123"像这样的 => :disabled="true"

1.3 Vue框架:Vue自定义构造函数,每一次使用Vue都是创造这个类的一个实例(vm)

// vm.__proto__ -> Vue.prototype -> Object.prototype
let vm = new Vue({
  data(){
    return {
      msg: '你好世界',
      score: {
        x: 10,
        y: 20
      },
      arr: [10, 20, 30],
      flag: new Error()
    }
  }
});
vm.$mount('#app');//基于querySelector获取视图容器:指定的容器不能是HTML和BODY

setTimeout(() => {
  vm.msg = 'hello world';
}, 2000);

1.4 vue实例挂载与指定视图的底层机制:

vm.$mount(selector/DOM)
+ Vue.prototype.$mount
    + 在没有基于 template/render 指定单独视图的情况下, #app容器就是我们需要渲染的视图;如果指定了单独视图,则$el存储的是我们指定的视图!!
    + vm.$el 存储的就是我们需要渲染的视图

1.5 指定视图有多种方式,但是vue底层是按照如下的逻辑进行处理的:

@1 首先看是否存在el配置项
  + 有:继续下一步
  +  没有:则后期在执行$mount的时候,把里面指定的容器作为el
@2 再看是否有 render/template 配置项
  + 有:把配置项指定的内容作为视图渲染
  + 没有:把el指定的容器作为视图进行渲染
  视图渲染完毕,会创建一个“虚拟DOM对象”,挂载到VM实例的$el属性上!!
@3 把渲染完毕的结果,替换el/$mount指定的容器(把虚拟DOM变为真实DOM的过程)!!

1.6 Vue2数据驱动视图渲染(也就是更改数据后,视图会自动更新)的底层机制:

基于Object.defineProperty对data中的数据/状态,进行监听劫持「get/set」

  • 当修改状态值,会触发set函数
  • 在set函数中,除了修改了状态值,而且还会通知视图重新渲染!!

1.7 defineProperty语法

1.7.1 对象“成员”的规则:

@1 可枚举性 enumerable
    + 能够被for/in(或Object.keys)迭代到的属性是“可枚举属性”,反之则是不可枚举的  
    + 一般来讲,自定义的属性是可枚举的、内置属性是不可枚举的;这不是绝对的概念,因为枚举性是可以被更改的!
@2 可编辑性 writable  
    + 也就是成员的值是否可以被更改  
@3 可删除性 configurable  
    + 也就是成员是否可以被delete移除  
@4 成员的值 value  
    Object.getOwnPropertyDescriptor(obj,key) 获取对象某个的成员的规则  
    Object.getOwnPropertyDescriptors(obj) 获取对象所有成员的规则
    
// 基于传统方案(对象.xxx/对象[xxx])设置的成员,规则都是true「可删除、可编辑、可枚举」
let obj = {};
obj.x = 10; 

1.7.2 我们可以基于Object.defineProperty(obj,key,config)新增/设置成员的规则!

1.默认规则值都是false
    configurable:false
    enumerable:false
    writable:false
2.但是自己可以改
    // 手动设置属性
    let obj = {};
    Object.defineProperty(obj, 'x', {
      value: 10,
      enumerable: false,
      writable: true,
      configurable: true
    });     
    
    // 给数组原型扩展方法
    Object.defineProperty(Array.prototype, 'unique', {
      writable: true,
      configurable: true,
      enumerable: false,
      value: function unique() {
          // ...
      }
    });  

1.7.3 defineProperty除了可以设置规则,还可以对某个成员进行监听/劫持

  • get 获取成员值的时候触发,函数的返回值就是我们获取的成员值
  • set 设置/修改成员值的时候触发,val就是要新设置的值,如果set中啥都不处理,成员值是没办法修改的
  • 设置劫持后,和原本的value/writable规则冲突「设置劫持,就不能设置这两个规则了」!!
let obj = {
  x: 10
};
let proxy = { ...obj }; // 代理对象(默认和obj初始值是一样的)
Object.defineProperty(obj, 'x', {
  get(){
    // console.log('getter');
    return proxy.x;
  },
  set(val){
    // console.log('setter', val);
    proxy.x = val;
  }
});

1.8 挂载源码

//给Vue增加init方法
Vue.prototype._init = function (options) {
  //初始化数据
  const vm = this;
  // vm.$options 就是获取用户的配置,将用户的选项挂载到实例上
  vm.$options = options;
  
  // 初始化状态
  /*
    判断是否有vm.$options
    + 有:initData(vm)
        1. 拿到data,判断是否是函数,是函数就改变this是vm,不是就是对象data
        2. vm._data = data; 将返回的对象放到了_data上
        3. 对数据进行劫持 vue2里采用了一个api defineProperty=> observe(data);
        4. 将vm._data 用vm来代理就可以了 vm.name => vm._data.name
     + 没有不做操作      
  */ 
  initState(vm);
  
  if (options.el) {
    // 实现数据的挂载
    vm.$mount(options.el);
  }
}

Vue.prototype.$mount = function (el) {
  const vm = this;
  el = document.querySelector(el);
  let ops = vm.$options;
  // 先进行查找有没有render函数
  if (!ops.render) {
    let template;
    // 没有render看一下是否写了template
    if (!ops.template && el) {
      // 没写template采用外部的template
      template = el.outerHTML;
    } else {
      // 写了template就用ops中的
      if (el) {
        template = ops.template;
      }
    }

    if (template) {
      // 这里需要对模板进行编译
      const render = compileToFunction(template);
      ops.render = render;// jsx 最终会被编译成h('xxx')
    }
  }
  // 最终就可以获取render方法
  // script 标签引用的vue.global.js 这个编译过程是在浏览器运行的
  // runtime 是不包含模板编译的,整个编译时打包的时候通过loader来转义的.vue文件的,用runtime的时候不能使用template
  ops.render;
}

1.9 Vue框架是如何去编译和渲染视图的?

@1 基于相关插件,把template视图,编译为虚拟DOM「vnode」
  第一次渲染和每一次视图更新,不论修改了几个状态,所有视图都要重新编译(全量编译)!
@2 进行DOM-DIFF「是有视图更新的时候会进行」
  DOM-DIFF是两次虚拟DOM对象比较的算法
@3 把虚拟DOM(或者补丁包)变为真实DOM
  框架底层操作真实DOM的过程「开发者只需要操作数据,不需要操作DOM;框架对于DOM操作,做了大量优化(例如回流重绘的减少...)以此来提高性能」

image.png

二、 认识Vue的13个内置指令

  • 所有在Vue中,模板上属性的v-*都是指令

  • 为什么叫指令? 模板应该按照怎样的逻辑进行渲染或绑定行为

  • Vue提供了大量的内置指令 v-if v-else v-else-if v-for..... 开发者也可以给Vue扩展指令,也就是自定义指令

三个关于视图编译优化的指令:v-pre、v-once、v-cloak

2.1 v-once:只编译一次

  • 在视图第一次渲染的时候,会进行编译;但是后期状态改变,视图更新的时候,不再进行编译「展示的都是第一次渲染后的内容」!!
  • 适用于:已经明确知道,编译一次后不需要再进行改变的内容,可以保证视图“更新”的时候,跳过v-once指定的内容,加快更新的速度!!

2.1.1 v-once

const App = {
  data() {
    return {
      title: 'This is my TITLE',
      author: 'Xiaohe'
    }
  },
  template: `
    <div>
      <h1 v-once>{{ title }} - <span> {{ author }}</span></h1>
      <button @click="change">CHANGE</button>
    </div>
  `,
  methods: {
    change() {
      this.title = 'This is your TITLE';
      this.author = 'Hezi';
    }
  }
}

Vue.createApp(App).mount('#app');

当点击按钮修改状态的时候,这个组件及里面的子组件只渲染一次,里面的值不会改变,但是实例上的值已经改变,所以外面的组件可以正常渲染 image.png

2.1.2 {{ 绑定变量 }}

const TITLE = 'This is my TITLE';

const App = {
  data() {
    return {
      title: 'This is my TITLE',
      author: 'Xiaohe'
    }
  },
  // 视图上Vue指定的插入方式的数据变量必须声明在实例上
  template: `
    <div>
      <h1>{{ TITLE }} - <span> {{ author }}</span></h1>
      <button @click="change">CHANGE</button>
    </div>
  `,
  methods: {
    change() {
      this.title = 'This is your TITLE';
      this.author = 'Hezi';
    }
  }
}

Vue.createApp(App).mount('#app');
image.png

2.1.3 ES6模板语法

标签内没有其他的标签用v-once,反之用ES6模板语法只渲染一次这个变量,其他内部标签变量或状态不影响

const TITLE = 'This is my TITLE';

const App = {
  data() {
    return {
      title: 'This is my TITLE',
      author: 'Xiaohe'
    }
  },
  template: `
    <div>
      <h1>${TITLE} - <span> {{ author }}</span></h1>
      <button @click="change">CHANGE</button>
    </div>
  `,
  methods: {
    change() {
      this.title = 'This is your TITLE';
      this.author = 'Hezi';
    }
  }
}

Vue.createApp(App).mount('#app');
image.png

2.2 v-pre

v-pre:不编译此内容(包含其后代内容也不编译)

  • 真实项目中,视图中的内容,大部分是需要基于状态/数据动态绑定处理的,这部分内容肯定需要Vue编译;但是还有一小部分内容,就是写死的静态页面「我们写啥,最后就渲染为啥」,此部分内容我们可以设置v-pre,来加快整个页面的编译速度!!
  • 但是在Vue3中,会默认跳过静态节点的编译,不需要我们自己设置v-pre了!!
<!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>指令</title>
</head>

<body>
  <div id="app">
    <div v-pre>
      <h2>{{message}}</h2>
      <p>当前计数:{{counter}}</p>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          message: "hello world",
          counter: 100
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.3 v-cloak

  • 确保在JS已经获取到,并且视图编译后,再显示编译后视图的内容,在此之前,让编写的视图模板不呈现
  • 在后期工程化开发中,不再需要v-cloak了!!
<!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>指令</title>
  <style>
    [v-clock] {
      display: none;
    }
  </style>
</head>

<body>
  <div id="app">
    <h2 v-cloak>{{ message }}</h2>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          message: "hello world"
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.4 v-html

可以识别字符串中的html标签,当做正常的标签进行渲染,类似于innerHTML(使用的时候,要注意可能会出现XSS攻击)

2.4.1 在模板中加标签

const App = {
  data() {
    return {
      title: 'This is my TITLE'
    }
  },
  template: `
    <div >{{'<h1>' + title + '</h1>' }}</div>
  `
}

Vue.createApp(App).mount('#app');

插值不会解析HTML,因为插值是JS表达式,没有对DOM的操作,返回纯HTML( rawHTML)字符串 image.png

2.4.2 加v-html的效果

const App = {
  data() {
    return {
      title: '<h1>This is my TITLE</h1>'
    }
  },
  template: `
    <div v-html="title"></div>
  `
}

Vue.createApp(App).mount('#app');

不要试图用v-html做子模板,vue本身有一个底层的模板编译系统,而不是直接使用字符串来渲染的
- 这样仅仅只是解决了解析成HTML,再嵌套其他逻辑就没办法做到
- 应该把子模板放到子组件中,让模板的重用和组合更强大
- 不要把用户提供的内容作为v-html的插值,这种插值容易导致XSS的攻击

image.png

2.4.3 XSS的攻击

v-html动态的渲染HTML,使用基本是innerHTML
- innerHTML有时候会容易导致XSS攻击,原因是标签使用不当

var text = '<img src="123" onerror="alert(123)" />';
document.getElementById('app').innerHTML = text;

image.png

2.5 v-text

  • v-text:把所有的内容(包含HTML标签字符串),都当做普通文本进行渲染,类似于innerText或者小胡子语法
<!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>指令</title>
</head>

<body>
  <div id="app">
    <!--v-text-->
    <h2>{{ message }}</h2>
    <h2 v-text="message"></h2>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          message: "hello world"
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.6 v-memo vue3新增

memorize 动词-记住

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

<body>
  <div id="app">
    <!-- 记住一个模板的子树,元素和组件都可以使用,该指令接收一个固定长度的数组作为依赖值进行记忆比对,如果数组中的值都和上次渲染的时候相同,则整个子树都不会渲染,直接跳过 即当数组中的内容发生改变,里面的标签元素才会更新,反之直接跳过-->
    <div v-memo="[name]">
      <h2>姓名: {{ name }}</h2>
      <h2>年龄: {{ age }}</h2>
      <h2>身高: {{ height }}</h2>
    </div>
    <button @click="updateInfo">改变信息</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
            name: 'zs',
            age: 18,
            height: 1.8
        }
      },
      methods: {
        updateInfo() {
          this.name = 'kobe'
          this.age = 20
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>
image.png

2.7 v-bind

v-bind:简写为“:”,给元素的属性(不论是内置还是自定义)动态绑定值

  • 改变属性值的类型
<div index="10">:给元素设置index属性,属性值是“10”(字符串类型);正常情况下,属性值都是字符串类型的,但是我们有时候需要属性值是其它类型的,此时就需要v-bind指令了!!

<div v-bind:index="10"> 或者 <div :index="10">:index属性值是数字10

<Vote :data="{x:10,y:20}"/>:data属性值是一个对象类型
  • 把状态/JS表达式的结果作为属性值
<div index="flag">:index的属性值是"flag"
<div :index="flag">:把flag状态存储的值(例如:true)作为index属性的值

2.7.1 动态绑定class

<!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>v-bind动态绑定class</title>
  <style>
    .active {
      color: red;
    }
  </style>
</head>

<body>
  <div id="app">
    <!-- v-bind的绑定属性 -->
    在组件中,父组件传递给子组件的class会默认和子组件的根元素的class进行合并,显示合并的class
    在vue3中允许多个根元素,会报错,这事我们可以手动给子元素加$attrs.xx来实现增加,$attrs是父组件传递给子组件的属性集合
    <!-- 1.绑定基本的属性 -->

    <!-- 1.1 绑定img的src属性 -->
    <img v-bind:src="imgUrl" alt="">

    <!-- 1.2 绑定a的href属性 -->
    <a v-bind:href="href">百度一下</a>
    <button @click="switchImage">切换图片</button><br/>
    <img :src="showImgUrl" alt="">

    <!-- 2.v-bind绑定class属性 -->

    <!-- 2.1 基本绑定class -->
    <h2 :class="classes">hello world</h2>

    <!-- 2.2 动态绑定class支持对象语法 -->
    <button :class="isActive ? 'active': '' " @click="btnClick">我是按钮</button>

    <!-- 对象语法的基本使用 className: boolean-->
    <button :class="{ active: isActive }" @click="btnClick">我是按钮</button>

    <!-- 对象语法的多个键值对 -->
    <button :class="{ active: isActive, wx: true, qq: fase}" @click="btnClick">我是按钮</button>

    <!-- 动态绑定的class是可以和普通的class同时使用 -->
    <button class="abc nba" :class="{ active: isActive, wx: true, qq: fase}" @click="btnClick">我是按钮</button>

    <!-- 动态绑定的class可以是一个函数 -->
    <button class="abc nba" :class="getDynamicClasses()" @click="btnClick">我是按钮</button>

    <!-- 2.3 数组语法 -->
    <h2 :class="['abc','cba']"> Hello Array</h2>
    <h2 :class="['abc', className]"> Hello Array</h2>
    <h2 :class="['abc', className, isActive ? 'active': '']" @click="btnClick"> Hello Array</h2>
    <h2 :class="['abc', className, {active: isActive}]" @click="btnClick"> Hello Array</h2>

  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          imgUrl1: 'https://img1.baidu.com/it/u=1765908055,4034886621&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
          imgUrl2: 'https://img2.baidu.com/it/u=2988589017,2923917558&fm=253&fmt=auto&app=120&f=JPEG?w=1280&h=800',
          showImgUrl: 'https://img1.baidu.com/it/u=1765908055,4034886621&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
          href: "http://www.baidu.com",
          classes: "abc cba nba",
          isActive: false,
          className: 'wx'
        }
      },
      methods: {
        switchImage() {
          this.showImgUrl = this.showImgUrl === this.imgUrl1 ? this.imgUrl2 : this.imgUrl1
        },
        btnClick() {
          this.isActive = !this.isActive;
        },
        getDynamicClasses(){
          return { active: this.isActive, wx: true, qq: false}
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

2.7.2 v-bind绑定style属性

<!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>v-bind绑定style属性</title>
</head>

<body>
  <div id="app">
    <!-- 
        camelCase:小驼峰命名法  thisIsMyVariable
            + 一般都是变量名、方法名
        kebab-Case:短横线命名法 this-is-my-varible
            + 脊柱命名法 spinal-case、train-case
            + 对象的属性名、CSS常规的类名(BEM规范)
        snake_case:蛇形命名法   this_is_my_variable
            + 大写的常量 const ERROR_TYPE = {}
        PascalCase: 大驼峰命名法 ThisIsMyVariable
        匈牙利命名法:变量=属性 + 类型(描述)
    -->
    <!-- 1.普通的html语法 -->
    <h2 style="color: red; font-size: 30px;">哈哈哈</h2>

    <!-- 2.style中的某些值,来自data中 -->

    <!-- 2.1动态绑定style,在后面跟上 对象类型,要加引号 =重要-->
    <h2 :style="{color: fontColor, 'font-size': '30px'}">哈哈哈</h2>
    <h2 :style="{color: fontColor, 'font-size': fontSize}">哈哈哈</h2>

    <!-- 2.2动态的绑定属性,这个属性是一个对象,不常用 -->
    <h2 :style="objStyle">哈哈哈</h2>

    <!-- 3.style 的数组语法 了解 -->
    <h2 :style="[objStyle, { backgroundColor: 'purple'}]">嘿嘿嘿</h2>
    
    补充:
    <!-- 1. 渲染数组中最后一个被浏览器支持的值,如果浏览器本身支持不带前缀的值,那就渲染不带前缀的值-->
    <button 
       :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"
    >confirm</button> 
    
    <!-- 2. :style使用中,vue会在运行时自动检测添加相应的前缀,如果浏览器支持无前缀的值,你写了也会去掉前缀渲染-->
    <button 
       :style="[{ '-webkit-transition': 'opacity .3s }]"
    >confirm</button> 
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          fontColor: "red",
          fontSize: '30px',
          objStyle: {
            fontSize: '50px',
            color: 'green'
          }
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>
image.png

2.7.3 v-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>v-bind绑定属性名</title>
</head>

<body>
  <div id="app">
    <!-- 1.动态的属性名参数不能出现空格和引号,HTML的合法属性名不能出现空格引号 -->
    <!-- <h1 v-bind:['data-' + name]="tag">Hello World</h1> -->
    <h2 :[name]="'aaa'">Hello World</h2>
    
    <!-- 2.null作为属性是无效的,你可以利用null解除属性绑定 -->
    <h2 :[null]="'aaa'">Hello World</h2>
    <h2 :[myAttr]="'aaa'">Hello World</h2>
    <button @click="removeMyAttr">Remove my Attr</button>
    
    <!-- 3.不能访问全局变量,受限列表 -->
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          tag: 'h1',
          name: 'title',
          myAttr: 'data-attr'
        }
      },
      methods: {
        removeMyAttr () {
            this.myAttr = null;
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.7.4 v-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>v-bind直接绑定对象</title>
</head>

<body>
  <div id="app">
    <h2 :name=name :age=age :height=height>Hello World</h2>

    <!-- v-bind 绑定对象 v-bind=对象-->
    <h2 v-bind="infos">Hello World</h2>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          infos: { name: 'wx', age: 18, height: 1.8 },
          name: 'wx',
          age: 18, 
          height: 1.8
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.7.5 webpack4样式相关配置方案

  1. 全部下载依赖
yarn add sass sass-loader postcss postcss-loader autoprefixer css-loader vue-style-loader -D
  1. webpack.config.js配置
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// const VueLoaderPlugin = require('vue-loader/lib/plugin');
const { VueLoaderPlugin } = require('vue-loader');
const autoprefixer = require('autoprefixer');


/**
 * 1. webpack各种依赖自检的版本兼容问题非常大
 * 2. webpack性能优化是很困难的
 * 3. 复杂配置的上手困难
 * 
 */
/**D
 * sass less -> sass sass-loader
 * postcss postcss-loader -> autoprefixer
 * css-loader: 模块化解析
 * vue-style-loader 放入style标签中
 */

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: {
    path: resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  // 引用外部文件cdn
  externals: {
    'vue': 'Vue'
  },
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.scss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  autoprefixer({
                    overrideBrowserslist: [
                      "> 1%",
                      "last 2 versions"
                    ]
                  })
                ]
              }
            }
          },
          'sass-loader'
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: resolve(__dirname, 'public/index.html')
    })
  ]
};
  1. main.js
import './main.scss'

const App = {
  data(){
    return {
      isActive: true
    }
  },
  template: `
    <div :class="{ active: isActive }"></div>
  `,
}

// @ts-ignore
Vue.createApp(App).mount('#app');
  1. npm run dev
sass-loader会报错,退回yarn remove sass-loader
yarn add sass-loader@^10.1.1 -D

npm run dev
yarn remove postcss-loader
yarn add postcss-loader@4 -D
 
yarn remove css-loader
yarn add css-loader@^4 -D
  1. main.scss
div {
  width: 100px;
  height: 100px;
  background-color: #000;
  transition: background-color .3s;

  &.active {
    background-color: red;
  }

  :hover {
    background-color: orange;
  }
}

2.7.6 用原生实现class和style对象与数组的绑定

要实现的代码

<div id="app">
    <button id="btn">Change</button>
</div>
import Vue from '../modules/Vue';
import './main.scss';

const vm = new Vue({
  el: '#app',
  data() {
    return {
      isShow: true,
      hasError: false,
      titleStyle: {
        color: '#fff',
        fontSize: '20px'
      },
      titleShow: true,
      isContentBig: true,
      subTitleColor: 'orange'
    }
  },
  template: `
    <div
      :class="[
        'box',
        isShow ? 'show' : '',
        hasError ? 'danger' : ''
      ]"
    >
      <h1
        :style="[
          titleStyle,
          {
            display: titleShow ? 'block' : 'none'
          }
        ]"
      >This is TITLE</h1>

      <h2
        :style="{
          display: titleShow ? 'block' : 'none',
          color: subTitleColor,
          fontSize: '20px'
        }"
      >This is SUB_TITLE</h2>
      <p
        :class="{
          danger: hasError,
          big: isContentBig
        }"
      >This is CONTENT</p>
    </div>
  `
});

console.log(vm);

var oBtn = document.getElementById('btn');

oBtn.addEventListener('click', function () {
  vm.hasError = true;
  vm.subTitleColor = 'purple';
}, false);

main.scss

.box {
  display: none;
  width: 300px;
  height: 300px;
  background-color: #999;

  &.show {
    display: block;
  }

  &.danger {
    background-color: red;
  }

  .big {
    font-size: 30px;
  }
}

Vue/index.js

import { compileAttr } from "./compile";
import { reactive } from "./reactive";
import { isObject } from './utils';

class Vue {
  constructor(options) {
    const { el, data, template } = options;

    this.$data = data();
    this.$el = document.querySelector(el);
    this.$stylePool = new Map();

    this.init(this, template);
  }

  init(vm, template) {
    this.initData(vm);
    this.render(vm, template);

  }

  initData(vm) {
    const _data = vm.$data;

    if (isObject(_data)) {
      reactive(vm, _data);
    }
  }

  render(vm, template) {
    const container = document.createElement('div');
    container.innerHTML = template;

    this.compileAttrs(vm, container);
    this.$el.appendChild(container);
  }

  compileAttrs(vm, container) {
    //[div, h1, h2, p]
    const allNodes = [...container.getElementsByTagName('*')];

    allNodes.forEach(el => {

      // 节点的属性 [:class] [:style] [:style] [:class]
      const attrs = [...el.attributes];

      attrs.forEach(attr => {

        // attr=> :class=值 :style :style :class
        // name=> :class value=> 值是 [...] 或 {...}
        const { name, value } = attr;
        compileAttr(vm, el, name, value);
      });
      el.removeAttribute(':class');
      el.removeAttribute(':style');
    });
  }

}

export default Vue;

reactive.js

import { attrUpdadte } from "./update";

export function reactive(vm, target) {
  for (let key in target) {
    Object.defineProperty(vm, key, {
      get() {
        return target[key];
      },
      set(newValue) {
        target[key] = newValue;
        attrUpdadte(vm, key);
      }
    });
  }
}

compile.js

import { REG_ARR, REG_OBJ, REG_SPACE } from './regular';
import { transformToKebab } from './utils';

/**
   Map {
    el: {
      type: class/style,
      expression: value
    }
   }
 */

export function compileAttr(vm, el, name, value) {
  // 去掉空格 和 冒号
  value = value.replace(REG_SPACE, '');
  name = name.replace(':', '');

  vm.$stylePool.set(el, {
    type: name,
    expression: value
  });

  switch (name) {
    case 'class':

      if (REG_OBJ.test(value)) {
        //  ['danger:hasError', 'big:isContentBig']
        const keyValueArr = value.match(REG_OBJ)[1].split(',');
        let classStr = '';

        keyValueArr.forEach(item => {
          const [key, value] = item.split(":");
          if (vm[value.trim()]) {
            // 取出所有类名
            classStr += ` ${key.trim()}`;
          }
        });
        // el: 循环到的节点 相当于给p添加类
        el.setAttribute('class', classStr.trim());
      } else if (REG_ARR.test(value)) {
        // ['box', 'show', 'danger']
        const classArr = renderArr(vm, value);
        // div.box.show.danger
        el.setAttribute('class', classArr.join(' ').trim());
      }
      break;
    case 'style':
      let styleStr = '';
      if (REG_OBJ.test(value)) {
        // {display: 'block', color: 'orange', fontSize: '20px'}
        const styleObj = renderObj(vm, value);

        for (let key in styleObj) {
          styleStr += `${transformToKebab(key)}:${styleObj[key]};`;
        }
        el.setAttribute('style', styleStr);
      } else if (REG_ARR.test(value)) {
        // 数组[0:{color: '#fff', fontSize: '20px'}, 1:{display: 'block'}]
        const styleArr = renderArr(vm, value);

        styleArr.forEach(item => {
          for (let key in item) {
            styleStr += `${transformToKebab(key)}:${item[key]};`
          }
        });
        //color:#fff;font-size:20px;display:block;
        // console.log(styleStr);
      }

      el.setAttribute('style', styleStr);
      break;
    default:
      break;
  }
}

function renderArr(vm, value) {
  // 执行三元表达式
  const _arr = (new Function(
    'vm',
    `with(vm){
      return ${value}
    }`
  ))(vm);
  // 过滤掉状态为false的时候的空字符串
  return _arr.filter(item => item);
}

function renderObj(vm, value) {
  return (new Function(
    'vm',
    `with(vm){
      return ${value}
    }`
  ))(vm);
}

regular.js

// 匹配空格
export const REG_SPACE = /\s+/g;
// 匹配{}
export const REG_OBJ = /^\{(.+?)\}$/;
// 匹配[]
export const  REG_ARR = /^\[(.+?)\]$/;
// 匹配大写字母
export const REG_UPPERCASE = /([A-Z])/g;

update.js

import { compileAttr } from "./compile";

export function attrUpdadte(vm, key) {
  const _stylePool = vm.$stylePool;

  for (let [k, v] of _stylePool) {
    // 包含
    if (v.expression.indexOf(key) !== -1) {
      compileAttr(vm, k, v.type, v.expression);
    }
  }
}

utils.js

import { REG_UPPERCASE } from "./regular";

export function isObject(value) {
  return Object.prototype.toString.call(value) === '[object Object]';
}

export function transformToKebab(key) {
  return key.replace(REG_UPPERCASE, '-$1').toLowerCase();
}

渲染结果图

目录 image.png image.png image.png

2.8 v-on 事件绑定

v-on:简写“@” 实现事件绑定
原理: 给当前元素的某个事件行为,基于addEventListener实现事件绑定

div.addEventListener('click',function(ev){
    //做一些特殊的事情...
    change.call(vm,...); //保证方法执行中的this是vue的实例
});

2.8.1 绑定事件的基本使用

<!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>事件</title>
  <style>
    .box {
      width: 100px;
      height: 100px;
      background-color: orange;
      margin-top: 10px;
    }
  </style>
</head>

<body>
  <div id="app">
    <!-- 1.完整的写法 -->
    <div class="box" v-on:click="divClick"></div>

    <!-- 2.语法糖写法 -->
    <div class="box" @click="divClick"></div>

    <!-- 3.绑定的方法的位置可以写成一个表达式 -->
    <h2>{{ counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="counter++">+1</button>

    <!-- 4.绑定其他方法 -->
    <div class="box" @mousemove="divMousemove"></div>

    <!-- 5.元素绑定多个事件 -->
    <div class="box" @click="divClick" @mousemove="divMousemove"></div>
    <div class="box" v-on="{ click: divClick, mousemove: divMousemove}"></div>
    <div class="box" @="{ click: divClick, mousemove: divMousemove}"></div>

  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          counter: 100
        }
      },
      methods: {
        divClick() {
          console.log('divClick');
        },
        increment() {
          this.counter++;
        },
        divMousemove() {
          console.log('divMousemove');
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

2.8.2 绑定事件参数传递

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

<body>
  <div id="app">
    <!-- 1. 默认传递event对象-->
    <button @click="btn1Click">按钮1</button>

    <!-- 2. 明确参数-->
    <!-- 
        不会像原生JS一样,btn2Click没有立即执行,还是要等待点击的时候,才会把btn2Click执行,并且传递'wx', age,类似于:div.onclick=change.bind(vm,'wx', age)
    -->
    <button @click="btn2Click('wx', age)">按钮2</button>

    <!-- 3. 自己的参数和event对象-->
    <!-- 模板中想要明确的获取event对象:$event -->
    <button @click="btn3Click('wx', age, $event)">按钮3</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          age: 18
        }
      },
      methods: {
        // 1.默认参数:event对象
        // 总结:如果在绑定事件的时候,没有传递任何的参数,那么event对象会被默认传递进来
        btn1Click(event) {
          console.log("btn1click:", event);
        },
        // 2.明确参数,不传递event也可以获取到event对象
        btn2Click(name, age) {
          console.log("btn2click:", name, age);
        },
        // 3.明确参数+event对象
        btn3Click(name, age) {
          console.log("btn3click:", name, age, event);
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

2.8.3 绑定事件的修饰符

DOM4标准的事件监听与滚屏优化

addEventListener(evetType, handler, capture || options);
options是一个对象,里面默认都是false
1. once:true 只调用一次事件处理函数,会移除当前事件的监听器
    + 第二次点击不会触发事件处理函数,但是点击之后还会有事件冒泡存在,因为事件是不需要绑定的,浏览器固然存在,冒泡也是默认的行为
2. passive:true 会报错:永远不调用阻止事件默认行为的方法 preventDefault
window.addEventListener('touchstart', function(e){
    // touchstart的默认行为是 scroll滚动
    // 滚屏优化:chrome firefox中默认为true window -> touchstart passive -> true
    
    // e.preventDefault();
    // console.log(123);//123
    // console.log(e.defaultPrevented);// false 没有调用,证明passive是true
   
    touchstart执行顺序 =>
        1. 处理器程序执行 console.log(123);
          这中间有很大性能问题 -> 等待的时间 浪费 ->导致scroll的卡顿
        2. 执行默认行为 scroll
    当执行e.preventDefault(),阻止滚动,所以屏幕不会滚动了
    不执行e.preventDefault(),等待处理器函数执行完毕,再执行scroll
    e.preventDefault();
    console.log(123);//123
    console.log(e.defaultPrevented);// true
},{
    /*设置passive会提高滚屏性能的原因:
    阻止默认行为的方法不会调用
    有两个线程去处理滚动的问题,就没有等待的时间,让性能提升
        1. 处理器程序的执行
        2. 执行默认行为*/
    // passive: true
    
    passive:false
});

Event实例的只读属性 cancelable 表明该时间是否可以被取消,当事件被阻止后,该时间久好像没有被触发一样。如果事件不能被取消,则其cancelable属性的值为false,且事件发生时无法在事件监听回调中停止事件
在许多事件的监听回调中调用 preventDefault前,都需要检测cancelable属性的值

绑定事件的修饰符

# 事件修饰符 @click.prevent = 'xxx'
    .stop 阻止事件的冒泡传播
    .prevent 阻止事件的默认行为 和 .passive不能连用
    .capture 让事件在捕获阶段触发「默认都是在目标和冒泡阶段触发」
    .self 并没有阻碍冒泡传播机制,只是会判断事件源是不是这个元素本身,如果是我们把函数执行,反之不执行
    .once 当前事件只能触发一次,第一次触发完就把事件绑定移除

# 按键修饰符 @keydown.enter/13 = 'xxx'  
    .enter/tab/delete/space/esc/up/down/left/right
    .13
    .ctrl/alt/shift/meta   目的 “.alt.63 实现组合按键「ALT+C」”
    .exact 精准修饰符  例如: @keyup.ctrl 只要按下ctrl就触发,不论是否还按着其它键呢;@keyup.ctrl.exact 当前只按住ctrl键才会触发

# 鼠标修饰符
    .left/right/middle

# 自定义按键修饰符
    Vue.config.keyCodes.修饰符名字 = 对应的键盘码;
<!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>事件</title>
  <style>
    .box{
      width: 100px;
      height: 100px;
      background-color: orange;
    }
  </style>
</head>

<body>
  <div id="app">
   <div class="box" @click="divClick">
     <button @click.stop="btnClick">按钮</button>
   </div>
   <a href="" @click.prevent.stop='func'>
   <input type="text" placeholder="请输入搜索内容" v-model='text' @keydown='func'>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          
        }
      },
      methods: {
        btnClick(){
          console.log('btnClick');
        },
        divClick(){
          console.log('divClick');
        },
        func(ev){
          alert('修饰符');
          /*enter代替
          if(ev.keyCode === 13){
            alert('修饰符')
          }
          */
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

2.8.4 todoList

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

<body>
  <div id="app">
    <todo-list></todo-list>
  </div>
  <template id="todolist">
    <div>
      <div>
        <input type="text" placeholder="Typing..." v-model="inputValue">
        <button @click="addTodo">ADD</button>
      </div>

      <div>
        <ul>
          <template v-if="todoList.length > 0">
            <li v-for="todo of todoList" :key="todo.id">
              <input type="checkbox" :checked="todo.completed" @click="toggleTodo(todo.id)">
              <span :style="{
                  textDecoration: todo.completed ? 'line-through' : ''
                }">{{ todo.content }}</span>
              <button @click="deleteTodo(todo.id)">DEL</button>
            </li>
          </template>
          <template v-else>
            <li>- No data available -</li>
          </template>
        </ul>
      </div>
    </div>
  </template>
  <script src="./js/vue.js"></script>
  <script>

    const todoList = {
      data() {
        return {
          todoList: [],
          inputValue: ''
        }
      },
      template: '#todolist',
      methods: {
        addTodo() {
          const _inputValue = this.inputValue.trim();

          if (_inputValue.length === 0) return;

          const hasThisContent = this.todoList.some(todo => todo.content === _inputValue);

          if (hasThisContent) {
            alert('This content existed in todoList');
            return;
          }

          this.todoList.push({
            id: new Date().getTime(),
            content: _inputValue,
            completed: false
          });

          this.inputValue = '';
        },
        deleteTodo(id) {
          this.todoList = this.todoList.filter(todo => todo.id !== id);
        },
        toggleTodo(id) {
          this.todoList = this.todoList.map(todo => {
            if (todo.id === id) {
              todo.completed = !todo.completed;
            }
            return todo;
          });
        }
      }
    };

    const App = {
      components: {
        todoList
      }
    };

    Vue.createApp(App).mount('#app');
  </script>
</body>

</html>

image.png

2.9 v-if 条件渲染

2.9.1 需求demo

<!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>条件渲染</title>
</head>

<body>
  <div id="app">
    <ul v-if="names.length">
      <li v-for="item in names">{{item}}</li>
    </ul>
    <h2 v-else>当前names没有数据,请求获取数据后展示</h2>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data: function () {
        return {
          names: ['abc', 'cba', 'nba']
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

image.png

2.9.2 v-if的使用

<!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>条件渲染</title>
</head>

<body>
  <div id="app">
   <!-- v-if="条件" -->
    <div class="info" v-if="Object.keys(info).length">
      <h2>个人信息</h2>
      <ul>
        <li>姓名:{{ info.name }}</li>
        <li>年龄:{{ info.age }}</li>
      </ul>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          info: {name: 'wx', age: 18}
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.9.3 v-else的使用

<!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>条件渲染</title>
</head>

<body>
  <div id="app">
   <!-- v-if="条件" -->
   <!-- 此处div也可以用template代替 -->
    <div class="info" v-if="Object.keys(info).length">
      <h2>个人信息</h2>
      <ul>
        <li>姓名:{{ info.name }}</li>
        <li>年龄:{{ info.age }}</li>
      </ul>
    </div>

    <!-- v-else -->
    <div v-else>
      <h2>没有输入个人信息</h2>
      <p>请输入信息后,再进行展示</p>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          info: {name: 'wx', age: 18}
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.9.4 v-else-if的使用

<!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>条件渲染</title>
</head>

<body>
  <div id="app">
   <!--这三个必须紧挨在一起-->
   <h1 v-if="score > 90">优秀</h1>
   <h2 v-else-if="score > 80">良好</h2>
   <h3 v-else-if="score > 60">及格</h3>
   <h4 v-else>不及格</h4>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          score: 40
        }
      },
      methods: {
        
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.9.5 条件渲染-阶段案例

<!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>二维码显示隐藏</title>
  <style>
    img{
      width: 200px;
      height: 200px;
    }
  </style>
</head>

<body>
  <div id="app">
   <div>
    <button @click="toggle">切换</button>
   </div>

   <template v-if="isShowCode">
    <img src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
   </template>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          isShowCode: true
        }
      },
      methods: {
        toggle(){
          this.isShowCode = !this.isShowCode;
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>
image.png

2.10 v-if和v-show的区别

<!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>二维码显示隐藏</title>
  <style>
    img{
      width: 200px;
      height: 200px;
    }
  </style>
</head>

<body>
  <div id="app">
   <div>
    <button @click="toggle">切换</button>
   </div>
   <!-- 
    v-show和v-if的区别
      1.v-show是不支持template
      2.v-show不可以和v-else一起用

    本质区别:
      1.v-show指令值是true和false,用来控制元素的显示隐藏
          + 原理:无论值是啥,元素肯定要进行渲染,它的DOM实际都是有存在的,也就是,只是通过css的display属性来进行切换
      2.也可以控制元素的显示隐藏,只不过其原理和v-show是不同的;
          + 原理:v-if当条件为false时,会用注释节点代替,其对应的原生压根不会被渲染到DOM中

    如果我们的原生需要在显示隐藏之间频繁的切换,那么使用v-show
    如果不会频繁的发生切换,那么使用v-if
    -->
    <div v-show="isShowCode">
      <img src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
    </div>
    <div v-if="isShowCode">
      <img src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
   </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          isShowCode: true
        }
      },
      methods: {
        toggle(){
          this.isShowCode = !this.isShowCode;
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>
image.png

2.10.1 实现v-if和v-show以及生命周期函数的执行

var Vue = (function () {
  function Vue(options) {

    var recycles = {
      beforeCreate: options.beforeCreate.bind(this),
      created: options.created.bind(this),
      beforeMount: options.beforeMount.bind(this),
      mounted: options.mounted.bind(this)
    }

    recycles.beforeCreate();

    // el
    this.$el = document.querySelector(options.el);
    this.$data = options.data();

    // template 和 methods不暴露在实例上
    this._init(this, options.template, options.methods, recycles);
  }

  Vue.prototype._init = function (vm, template, methods, recycles) {
    // 实例创建完成
    recycles.created();

    var container = document.createElement('div');
    container.innerHTML = template;

    var showPool = new Map();
    var eventPool = new Map();

    initData(vm, showPool);
    initPool(container, methods, showPool, eventPool);
    bindEvent(vm, eventPool);
    render(vm, showPool, container, recycles);
  }

  function initData(vm, showPool) {
    var _data = vm.$data;

    for (var key in _data) {
      (function (key) {
        Object.defineProperty(vm, key, {
          get: function () {
            return _data[key];
          },
          set: function (newValue) {
            _data[key] = newValue;

            update(vm, key, showPool);
          }
        })
      })(key);
    }
  }

  function initPool(container, methods, showPool, eventPool) {

    var _allNodes = container.getElementsByTagName('*');
    var dom = null;

    for (var i = 0; i < _allNodes.length; i++) {
      // 每一个原生节点
      dom = _allNodes[i];

      var vIfData = dom.getAttribute('v-if');
      var vShowData = dom.getAttribute('v-show');
      var vEvent = dom.getAttribute('@click');

      if (vIfData) {
        showPool.set(
          dom,
          {
            type: 'if',
            prop: vIfData
          }
        );
        // 存储完了删除属性
        dom.removeAttribute('v-if');
      } else if (vShowData) {
        showPool.set(
          dom,
          {
            type: 'show',
            prop: vShowData
          }
        );
        dom.removeAttribute('v-show');
      }
      // console.log(showPool);

      if (vEvent) {
        // vEvent 事件名
        eventPool.set(
          dom,
          methods[vEvent]
        )
      }
      dom.removeAttribute('@click');
      // console.log(eventPool); button toggle函数
    }
    // console.log(container.innerHTML); 此时已删除所有的v-show、v-if、@click
  }

  function bindEvent(vm, eventPool) {
    for (var [dom, handler] of eventPool) {
      // 实例挂载函数名 = 函数
      vm[handler.name] = handler;
      // 绑定事件处理函数
      dom.addEventListener('click', vm[handler.name].bind(vm), false);
    }
  }

  function render(vm, showPool, container, recycles) {
    var _data = vm.$data;
    var _el = vm.$el;

    for (var [dom, info] of showPool) {
      switch (info.type) {
        case 'if':
          info.comment = document.createComment('v-if');
          !_data[info.prop] && (dom.parentNode.replaceChild(info.comment, dom));
          break;
        case 'show':
          // 如果是假就display
          !_data[info.prop] && (dom.style.display = 'none');
          break;
        default:
          break;
      }
    }

    recycles.beforeMount();

    _el.appendChild(container);

    recycles.mounted();
  }

  function update(vm, key, showPool) {
    var _data = vm.$data;

    for (var [dom, info] of showPool) {
      if (info.prop === key) {
        switch (info.type) {
          case 'if':
            !_data[key] ? (dom.parentNode.replaceChild(info.comment, dom))
                        : info.comment.parentNode.replaceChild(dom, info.comment);
            break;
          case 'show':
            // 如果是假就display
            !_data[info.prop] ? (dom.style.display = 'none')
                              : (dom.removeAttribute('style'));
            break;
          default:
            break;
        }
      }
    }
  }

  return Vue;
})();

export default Vue;
import Vue from '../modules/Vue'

const vm = new Vue({
  el: '#app',
  data() {
    return {
      isShowCode: true
    }
  },
  beforeCreate() {
    // console.log(this);// 指向Vue
    console.log('beforeCreate');
  },
  created() {
    console.log('created');
  },
  beforeMount() {
    console.log('beforeMount');
  },
  mounted() {
    console.log('mounted');
  },
  template: `
    <div>
      <button @click="toggle">切换</button>
    </div>
    <div v-show="isShowCode">
      <img width="200px" src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
    </div>
    <div v-if="isShowCode">
      <img width="200px" src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
    </div>
  `,
  methods: {
    toggle() {
      this.isShowCode = !this.isShowCode;
    }
  }
});

console.log(vm);

2.11 v-for 列表渲染

2.11.1 v-for的基本使用

<!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>vue列表渲染</title>
  <style>
    .item{
      margin-top: 10px;
      background-color: orange;
    }

    .item .title{
      color: red;
    }
  </style>
</head>

<body>
  <div id="app">
    <!-- 1.电影列表进行渲染 -->
    <h2>电影列表</h2>
    <ul>
      <li v-for="item in movies">{{ item }}</li>
    </ul>

    <!-- 2.电影列表同时展示索引 -->
    <h2>电影列表</h2>
    <ul>
      <li v-for="(item, index) in movies">{{ index+1 }}=={{ item }}</li>
    </ul>

    <!-- 3.遍历数组复杂数据 -->
    <h2>电影列表</h2>
    <div class="item" v-for="item in products">
      <h3 class="title">{{item.name}}</h3>
      <span>价格:{{item.price}}</span>
      <p>秒杀:{{item.desc}}</p>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          movies: ['星际穿越', '少年派', '大话西游', '哆啦A梦'],
          products: [
            {id: 110, name: 'Macbook', price: 9.9, desc: '9.9秒杀,快来抢购!'},
            {id: 111, name: 'iPone', price: 8.8, desc: '9.9秒杀,快来抢购!'},
            {id: 112, name: '小米电脑', price: 9.9, desc: '9.9秒杀,快来抢购!'},
          ]
        }
      },
      methods: {
        
      }
    });
    app.mount("#app");
  </script>
</body>

</html>
image.png

2.11.2 v-for的其他类型

<!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>vue列表渲染</title>
</head>

<body>
  <div id="app">
    <!-- 1.遍历数组 -->

    <!-- 2.遍历对象:遍历对象的顺序,会按照Object.keys()的结果进行遍历 -->
    <ul>
      <li v-for="(value, key, index) in info">{{ value }}=={{ key }}=={{index}}</li>
    </ul>

    <!-- 3.遍历字符串 每一个字母和空格都会遍历到-->
    <ul>
      <li v-for="item in message">{{ item }}</li>
    </ul>

    <!-- 4.遍历数字 遍历出1到10的数字-->
    <ul>
      <li v-for="item in 10">{{ item }}</li>
    </ul>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          message: 'hello world',
          info: {name: 'wx', age: 18, height: 1.8}
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.11.3 v-for和template

<!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>vue列表渲染</title>
</head>

<body>
  <div id="app">
    <!-- 如果div没有实际的意义,那么可以使用template替换 -->
    <div v-for="(value, key, index) in infos">
      <span>{{value}}</span>
      <strong>{{key}}</strong>
      <i>{{index}}</i>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          infos: {name: 'wx', age: 18, height: 1.8}
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.11.4 数组更新的检测

<!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>vue列表渲染</title>
</head>

<body>
  <div id="app">
    <ul>
      <li v-for="item in names">{{item}}</li>
    </ul>
    <button @click="changeArray">修改数组</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          names: ['abc', 'cba', 'nba']
        }
      },
      methods: {
        changeArray(){
          // 1.直接将数组修改为一个新的数组
          // this.names = ['james', 'kobe']

          // 2.通过一些数组的方法(push pop shift unshift splice reverse sort),修改数字中的元素
          // this.names.push('wx');

          // 3.不修改原数组的方法是不能侦听
          const newNames = this.names.map(item => item +1);
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

2.11.5 v-for循环中的就地更新和key属性

<!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>vue列表渲染</title>
</head>

<body>
  <div id="app">
    <button @click="insertF">插入f</button>
    <ul>
      <!-- key要求是唯一的 -->
      <!-- 
        VNode(Virtual Node)虚拟节点:是一个JavaScript对象 
        虚拟DOM:多个VNode Tree,可以进行diff算法、跨平台
        template -> 虚拟DOM -> 真实DOM,渲染在浏览器上
                               移动端的button...     -> 移动原生控件在移动端显示出来
                               桌面端的一些控件
                               VR设备
      -->
      <li v-for="(item, index) in letters" :key="index">{{item}}</li>
    </ul>

    <!-- 就地更新案例1:span会替换,a是就地更新,只更新innerText复用-->
    <div v-if="isLogin">
      <span>欢迎</span>
      <a href="#">Hezi</a>
    </div>
    <div v-else>
      <a href="javascript:;" @click="isLogin = true">登录</a>
      <a href="#">注册</a>
    </div>

    <!-- 就地更新案例2:没有加key的情况 -->
    <ul>
      <li v-for="(item, index) in list">
        <!-- 点击删除第二项的li:将'item-3'移到'item-2'中的span中,然后删除item-3的li,这就是就地更新 -->
        <span>{{ item.value }}</span>
        <!-- 给三个input依次手动添加123或绑定value,vue不会跟踪input的value,已经更新的标签input值不会更新,比如删除第二项,标签已经是item-3,但是input中的值还是2 -->
        <input type="text" :value="tempArr[index]" />
        <!-- 子组件也不会更新 -->
        <my-component :num="tempArr[index]"></my-component>
        <button @click="deleteItem(index)">删除</button>
      </li>
    </ul>

    <!-- 就地更新案例2:加key的情况input和子组件index改变,值也会改变,key尽可能不使用index,如果操作当前列表,index会随时更新-->
    <ul>
      <li v-for="(item, index) in list" :key="item.id" :index="index">
        <span>{{ item.value }}</span>
        <input type="text" :value="tempArr[index]" />
        <my-component :num="tempArr[index]"></my-component>
        <button @click="deleteItem(index)">删除</button>
      </li>
    </ul>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const MyComponent = {
      props: {
        num: Number,
      },
      template: `
        <span>{{num}}</span>
      `
    };

    const app = Vue.createApp({
      components: {
        MyComponent
      },
      data() {
        return {
          letters: ['a', 'b', 'c', 'd', 'e'],
          list: [
            {
              id: 1,
              value: 'item-1'
            },
            {
              id: 2,
              value: 'item-2'
            },
            {
              id: 3,
              value: 'item-3'
            }
          ],
          isLogin: false,
          tempArr: [1, 2, 3]
        }
      },
      methods: {
        /**
         *  『在中间插入f』
         *    + 在vue中,对于相同父元素节点并不会重新渲染整个列表,因为对于列表中'a', 'b', 'c', 'd', 'e'是没有变化的,在操作真实DOM的时候,只需要在中间插入一个f的li即可
         *    + Vue事实上会对于有key和没有key调用两个不用的方法:
         *      + 有key:使用patchKeyedChildren方法
         *        1.如果旧节点遍历完毕,有新节点插入,那么就新增节点即可,其他的都复用
         *        2.移除也会进行复用
         *        3.乱序的也会进行复用原来的节点
         *      + 没有key:patchUnkeyedChildren方法
         *        + f复用c的位置,依次往后,最后一个新增
         *        1. 从头部开始遍历,遇到相同的节点就继续,遇到不同的节点就跳出循环
         *        2. 从尾部开始遍历,遇到相同的节点就继续,遇到不同的节点就跳出循环
         *        3. 如果最后新节点更多,最后就添加新节点
         *        4. 如果旧节点跟多就移除旧节点
         *        5. 如果中间操作不知道如何排列的位置序列,那么就使用可以建立索引最大限度的使用旧节点
         */
        insertF() {
          this.letters.splice(2, 0, 'f');
        },
        deleteItem(index) {
          this.list.splice(index, 1);
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

2.11.6 v-if与v-for的联合使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <ul>
        <!--
        不推荐“在同一元素上”使用v-if和v-for,放在一起存在优先级问题,从实例中render函数中可以看出来优先级问题
            + vue2中:v-for的优先级比v-if要高「先把v-for都循环了,循环结束后,再逐一判断v-if,把条件是false的移除掉,这样比较浪费性能」

            + vue3中:v-if的优先级要高于v-for「渲染的时候会优先渲染v-if,此时v-for还没有处理,我们没有办法拿到循环的item和index」
            + 所以,会报属性todo渲染了但是没有定义在实例上
        -->
        <!--<li v-for="todo of todoList" :key="todo.id" v-if="!todo.completed">
            {{ todo.content }}
        </li>-->

        <!--选出todolist中为false的-->

        <!--解决方法1:在template上写v-for包裹住li,在li写v-if-->
        <!--<template v-for="todo of todoList" :key="todo.id">
            <li v-if="!todo.completed">{{ todo.content }}</li>
        </template>-->

        <!--解决方法2:先用”计算属性“过滤符合条件的元素,然后v-for遍历这个计算属性即可-->
        <!--<li v-for="todo of NotCompletedTodoList" :key="todo.id">
            {{ todo.content }}
        </li>-->

        <!-- todoList有值就显示,没有值就不显示  -->
        <!--解决方法1:如果没有值,即使写在一起也不会报错,但是不推荐这样写-->
        <!--<li v-for="todo of todoList" v-if="todoList.length > 0">
            {{ todo.content }}
        </li>-->

        <!--解决方法2:v-if用template-->
        <!--<template v-if="todoList.length > 0">
            <li v-for="todo of todoList">
                {{ todo.content }}
            </li>
        </template>-->
    </ul>
    <!--解决方法3:v-if加在ul上(列表不存在,ul也没必要存在)-->
    <ul v-if="todoList.length > 0">
        <li v-for="todo of todoList">
            {{ todo.content }}
        </li>
    </ul>
</div>

<script src="./js/vue.js"></script>
<script>
    const App = {
        data() {
            return {
                todoList: [
                    // {
                    //     id: 1,
                    //     content: 'CONTENT 1',
                    //     completed: true
                    // },
                    // {
                    //     id: 2,
                    //     content: 'CONTENT 2',
                    //     completed: false
                    // },
                    // {
                    //     id: 3,
                    //     content: 'CONTENT 3',
                    //     completed: true
                    // }
                ]
            }
        },
        computed: {
            NotCompletedTodoList() {
                return this.todoList.filter(item => !item.completed);
            }
        }
    };

    Vue.createApp(App).mount('#app');
</script>
</body>
</html>

2.11.7 实现v-for的响应式和模板编译

1.我们需要实现v-for的数据响应式和模板编译 首先请看需要实现的代码

// app.js

const app = createApp({});

app.mount('#app');

2.因为我们需要用到createApp函数,所以下一步编写此函数,然后导入到app.js中

// application.js
export function createApp(options){
  return {
    mount
  }
}

// Hezi/index 
export { createApp } from './application';

3.创建模块testA和testB

//testA
import { createReactive } from '../../Hezi';

const template = `
  <ul class="list">
    <h1>{{ title }}</h1>
    {{ dataTime }}
    <for data="list" tag="li" class="item">
      <span>姓名:{ name }</span>
      <span>年龄:{ age }</span>
    </for>
  </ul>
`

function testA() {
  const state = createReactive({
    title: '学生列表',
    dataTime: '2023-02-03 09:05',
    list: [
      {
        id: 1,
        name: '张三',
        age: 18
      },
      {
        id: 2,
        name: '李四',
        age: 20
      },
      {
        id: 3,
        name: '王五',
        age: 22
      }
    ]
  });

  return [template, state];
}

export default testA;
//testB
import { createReactive } from '../../Hezi';

const template = `
  <ul class="list">
    <h1>{{ title }}</h1>
    {{ dataTime }}
    <for data="list" tag="li" class="item">
      <span>Name:{ name }</span>
      <span>Age:{ age }</span>
    </for>
  </ul>
`

function testB() {
  const state = createReactive({
    title: '老师信息列表',
    dataTime: '2023-02-03 09:05',
    list: [
      {
        id: 1,
        name: '小明',
        age: 26
      },
      {
        id: 2,
        name: '小红',
        age: 28
      },
      {
        id: 3,
        name: '小李',
        age: 30
      }
    ]
  });

  return [template, state];
}

export default testB;

4.导入并注册到createApp中

//app.js
import { createApp } from './Hezi';

import testA from './components/testA';
import testB from './components/testB';

const app = createApp({
  components: [
    testA,
    testB
  ]
});

app.mount('#app');

5.实现数据响应式函数createReactive

// reactive.js

import { isObject } from "./utils";
import { proxyHandler } from "./handler";

// 数据响应式
export function createReactive(data){
  return createReactiveData(data, proxyHandler);
}

// get set
function createReactiveData(data, proxyHandler){
  if(!isObject(data)) return data;

  return new Proxy(data, proxyHandler);
}

// Hezi/index.js
export { createApp } from './application';
export { createReactive } from './reactive';

5.1用到了两个函数isObject、proxyHandler

//utils.js
export function isObject(data) {
  return typeof data === 'object' && data !== null;
}
// handler.js
import { createReactive } from "./reactive";
import { isObject } from "./utils";

const get = createGetter();
const set = createSetter();

function createGetter() {
  return function (target, key, receiver) {
    const res = Reflect.get(target, key, receiver);

    if (isObject(res)) {
      return createReactive(res);
    }

    return res;
  }
}

function createSetter() {
  return function (target, key, value, receiver) {
    // @ts-ignore
    return Reflect(target, key, value, receiver);
  }
}

export const proxyHandler = {
  get,
  set
}

6.把注册的函数执行,编译模板及挂载到app上

// application.js

import { compileTemplate } from "./compile";

const domNodePool = [];

export function createApp(options) {

  for (let option in options) {
    switch (option) {
      case 'components':
        initComponents(options[option]);
        break;
      default:
        break;
    }
  }

  return {
    mount
  }
}

function initComponents(components) {

  for (let component of components) {
    let [template, state] = component();
    const node = compileTemplate(template, state);
    // 保存两个ul
    domNodePool.push(node);
  }
}

function mount(el) {
  const app = document.querySelector(el);
  const oFrag = document.createDocumentFragment();

  // [ul.list, ul.list]
  domNodePool.forEach(node => {
    oFrag.appendChild(node);
  });

  app.appendChild(oFrag);
}
// compile.js

const customTags = ['if', 'for'];
const reg_sigle_bracket = /\{(.+?)\}/g;
const reg__double_bracket = /\{\{(.+?)\}\}/g;

// 编译模板
export function compileTemplate(template, data) {
  template = replaceVar(template, data, reg__double_bracket);

  const _node = document.createElement('div');
  _node.innerHTML = template;

  return compileNode(_node, data);
}
// 编译不是html标签的node
function compileNode(node, data) {
  const allNodes = node.querySelectorAll('*');

  allNodes.forEach(item => {
    const tagName = item.tagName.toLowerCase();

    if (customTags.includes(tagName)) {
      replaceNode(item, tagName, data);
    }
  });
  // console.log(allNodes);
  return [...node.childNodes].find(item => item.nodeType === 1);

}
// 处理for标签
function replaceNode(node, tag, data) {
  // list
  const dataKey = node.getAttribute('data');
  // item
  const className = node.className;
  // li
  const realTag = node.getAttribute('tag');

  switch (tag) {
    case 'for':
      vFor(node, data, dataKey, className, realTag);
      break;
    default:
      break;
  }
}

// 把for变为html标签
function vFor(node, data, dataKey, className, realTag) {

  const oFrag = document.createDocumentFragment();

  data[dataKey].forEach(item => {
    // item是每一个数据 => Proxy {id: 1, name: '张三', age: 18}
    const el = document.createElement(realTag);
    el.className = className ? className : '';
    // 替换单大括号
    el.innerHTML = replaceVar(node.innerHTML, item, reg_sigle_bracket);

    oFrag.appendChild(el);
  });

  // 循环出来的li替换原有的for
  node.parentNode.replaceChild(oFrag, node);
}

// for标签数据替换
function replaceVar(html, data, reg) {
  return html.replace(reg, (node, key) => {
    const obj = {};
    key = key.trim();
    return obj[key] = data[key];
  });
}

image.png

image.png

2.12 v-model

2.12.1 v-model的基本使用

<!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>vue v-model</title>
</head>

<body>
  <div id="app">
    <!-- 1.手动实现双向绑定 -->
    <!-- 
      v-model原理:v-bind 绑定value属性的值,v-on绑定input事件监听到函数中,函数会获取最新的值赋值到绑定的属性中 
      当type为text或textarea时,使用value属性和input事件;
      当type为checkbox或radio时,使用value属性和change事件;
      select框,使用value属性和change事件。
    -->
    <!-- <input type="text" :value="message" @input="changeInput" /> -->

    <!-- 2.v-model实现双向绑定 -->
    <!-- <input type="text" v-model="message" /> -->

    <!-- 3.登录功能 -->
    <label for="account">
      账号:<input type="text" id="account" v-model="account" />
    </label>
    <label for="password">
      密码:<input type="password" id="password" v-model="password" />
    </label>
    <button @click="loginClick">登录</button>

    <h2>{{message}}</h2>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
         message: 'Hello Vue',
         account: '',
         password: ''
        }
      },
      methods: {
        changeInput(event){
          this.message = event.target.value
        },
        loginClick(){
          const account = this.account
          const password = this.password
          console.log(account, password);
          // todo...
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.12.2 v-model绑定textarea

<!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>vue v-model</title>
</head>

<body>
  <div id="app">
    <textarea cols="30" rows="10" v-model="content"></textarea>
    <p>输入的内容:{{content}}</p>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
         content: ''
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.12.3 v-model绑定checkbox

<!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>vue v-model</title>
</head>

<body>
  <div id="app">
      <!-- 
          复选框的处理操作:
            + 给每个复选框设置value
            + 创建一个状态,值是一个数组,数组中包含选中复选框的value值
            + 给复选框的v-model绑定相同的状态
          原理:
            + 拿现有的状态值「数组」和每个复选框进行匹配,如果数组中包含了某个复选框的value值,则此复选框默认选中即可!!
            + 给每个复选框监听change事件,在事件触发后,判断复选框的选中状态,根据是否选中,从状态数组中新增或者移除对应的value值!!
        -->
    <!-- 1.checkbox单选框:绑定到属性中的值是一个boolean -->
    <label for="agree">
      <input type="checkbox" id="agree" v-model="isAgree">同意协议
    </label>
    <h2>单选框的值:{{isAgree}}</h2>

    <!-- 2.checkbox多选框:绑定到属性中的值是一个数组 -->
    <!-- 注意:多选框当中,必须明确的绑定一个value值 -->
    <!-- 绑定同一个hobbies,可以不同name分组 -->
    <div class="hobbies">
      <h2>请选择你的爱好:</h2>
      <label for="sing">
        <input id="sing" value="sing" type="checkbox" v-model="hobbies"></label>
      <label for="dance">
        <input id="dance" value="dance" type="checkbox" v-model="hobbies"></label>
      <label for="rap">
        <input id="rap" value="rap" type="checkbox" v-model="hobbies">rap
      </label>
      <label for="baskteball">
        <input id="basketball" value="basketball" type="checkbox" v-model="hobbies">篮球
      </label>
      <h2>爱好:{{hobbies}}</h2>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
         isAgree: false,
         hobbies: []
        }
      },
      methods: {
        
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.12.4 v-model绑定radio

<!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>vue v-model</title>
</head>

<body>
  <div id="app">
    <!-- 绑定同一个v-model,可以不同name区分 -->
    <div class="gender">
      <label for="male">
        <input type="radio" id="male" v-model="gender" value="male"></label>
      <label for="female">
        <input type="radio" id="female" v-model="gender" value="female"></label>
      <h2>性别:{{gender}}</h2>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
         gender: 'female'
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.12.5 v-model绑定select

<!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>vue v-model</title>
</head>

<body>
  <div id="app">
    <!-- select单选 -->
    <select v-model="fruit">
      <option value="apple">苹果</option>
      <option value="orange">橘子</option>
      <option value="banana">香蕉</option>
    </select>
    <h2>单选:{{fruit}}</h2>

    <!-- select多选 -->
    <select multiple size="3" v-model="fruits">
      <option value="apple">苹果</option>
      <option value="orange">橘子</option>
      <option value="banana">香蕉</option>
    </select>
    <h2>多选:{{fruits}}</h2>

    <!-- 
      下拉框的使用:
        + 指定好对应的option选项和每个选项的value
        + 创建一个状态,如果是单选,状态值是一个值,如果是多选,状态值是一个数组「值都是每个选项的value值」
        + 给select设置v-model等于状态值即可
      原理:
        + 把状态值赋值给select的value,这样就可以让指定value的配置项默认选中
        + 监听change事件,在选择改变的时候,获取最新选择项的value值,去修改对应的状态
    -->
    <select @change="selectValue" :value="fruit">
      <option value="apple">苹果</option>
      <option value="orange">橘子</option>
      <option value="banana">香蕉</option>
    </select>
    <h2>单选:{{fruit}}</h2>

  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          fruit: 'orange',
          fruits: []
        }
      },
      methods: {
        selectValue(e) {
          this.fruit = e.target.value;
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>
image.png

2.12.6 v-model的值绑定

<!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>vue v-model</title>
</head>

<body>
  <div id="app">
    <!-- 1.select的值绑定 -->
    <select multiple size="3" v-model="fruits">
      <option v-for="item in allFruies" 
              :key="item.value" 
              :value="item.value"
      >{{item.text}}</option>
    </select>
    <h2>多选:{{fruits}}</h2>

    <!-- 2.checkbox的值绑定 -->
    <div class="hobbies">
      <h2>请选择你的爱好:</h2>
      <template v-for="item in allHobbies" :key="item.value">
        <label :for="item.value">
          <input :id="item.value" :value="item.value" type="checkbox" v-model="hobbies">{{item.text}}
        </label>
      </template>
      <h2>爱好:{{hobbies}}</h2>
    </div>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          // 水果
          allFruies: [
            {value: 'apple', text: '苹果'},
            {value: 'orange', text: '橘子'},
            {value: 'banana', text: '香蕉'}
          ],
          fruits: [],
          // 爱好
          allHobbies: [
            {value: 'sing', text:"唱"},
            {value: 'dance', text:"跳"},
            {value: 'rap', text:"rap"},
            {value: 'basketball', text:"篮球"}
          ],
          hobbies: []
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.12.7 v-model的修饰符

<!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>vue v-model</title>
</head>

<body>
  <div id="app">
    <!-- 1.lazy修饰符:默认v-model绑定input事件,输入后就会把最新的值和属性绑定同步,如果加上lazy,会绑定change事件,只有提交或者回车才会触发 -->
    <input type="text" v-model.lazy="message">
    <h2>message:{{message}}</h2>

    <!-- 2.number:自动将内容转化为数字 -->
    <input type="text" v-model.number="counter">
    <h2>counter: {{counter }}-{{typeof counter}}</h2>

    <input type="number" v-model="counter2">
    <h2>counter2: {{counter2 }}-{{typeof counter2}}</h2>

    <!-- 3.trim:去除首位的空格 -->
    <input type="text" v-model.trim="content">
    <h2>content:{{content}}</h2>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          message: 'xxx',
          counter: 0,
          counter2: 0,
          content: ''
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

image.png

2.13 v-slot

后续介绍......

2.14 自定义指令

三、JS铺垫

3.1 ES5-ES6贯穿对象深拷贝问题

ES5深拷贝

// ES5深拷贝
/**
 * 对象深拷贝
 * @param {拷贝对象} origin 
 * @param {目标对象} target 
 * @returns target
 */
function deepClone(origin, target){
      // target是否存在 如果不存在创建空对象
  var tar = target || {},
      // 判断是否为引用数据类型
      toStr = Object.prototype.toString,
      arrType = '[object Array]';
  
  // 循环获取对象的key
  for(var k in origin){
    // 排除origin的公有属性
    if (origin.hasOwnProperty(k)){
      // 判断是一个对象
      if (typeof origin[k] === 'object' && origin[k] !== null) {
        tar[k] = toStr.call(origin[k]) === arrType ? [] : {};
        // 再次深克隆
        deepClone(origin[k], tar[k]);
      } else {
        // 如果不是对象就直接复制
        tar[k] = origin[k];
      }
    }
  }
  return tar;
}

var obj = {
  name: '小王',
  age: 18,
  info: {
    hobby: ['travel', 'piano', { a: 1 }],
    career: {
      teacher: 4,
      engineer: 9
    }
  }
};

const newObj = deepClone(obj, {});

console.log(obj === newObj);// false
console.log(obj, newObj);
newObj.info.hobby[2].a = 123;

WeakMap 的作用

// Map 键名:任意类型,一般的对象,键名只能是字符串、数字
// WeakMap 键名:只能是对象

案例:
const oBtn1 = document.querySelector('#btn1');
const oBtn2 = document.querySelector('#btn2');

oBtn1.addEventListener('click', handleBtn1Click, false);
oBtn2.addEventListener('click', handleBtn2Click, false);

function handleBtn1Click (){}
function handleBtn2Click (){}

// 需要移除节点
oBtn1.remove();
oBtn2.remove();
// 手动垃圾回收,weakmap就是解决这个事情的
handleBtn1Click = null;
handleBtn2Click = null;
//========================
const oBtnMap = new WeakMap();

// 键名是弱引用,如果键名和外部断开连接了,则自动回收键名和键值
oBtnMap.set(oBtn1, handleBtn1Click);
oBtnMap.set(oBtn2, handleBtn2Click);

oBtn1.addEventListener('click', oBtnMap.get(oBtn1), false);
oBtn2.addEventListener('click', oBtnMap.get(oBtn2), false);

oBtn1.remove();
oBtn2.remove();

ES6深拷贝

image.png

image.png

/**
 * ES6深拷贝
 * function(){} 静态的不拷贝
 */
function deepClone(origin){
  // 基本类型值
  if (origin == undefined || typeof origin !== 'object') {
    return origin;
  }

  if (origin instanceof Date){
    return new Date(origin);
  }

  if (origin instanceof RegExp){
    return new RegExp(origin);
  }

  // origin的构造器会创建一个新的数组或对象,当前的target就是新的对象 
  const target = new origin.constructor();

  for(let k in origin){
    if (origin.hasOwnProperty(k)){
      target[k] = deepClone(origin[k]);
    }
  }

  return target;
}

const newObj = deepClone(obj);
newObj.info.hobby[2].a = 123;

console.log(obj, newObj);

存在循环调用的情况造成死循环,用weakmap记录防止死循环 image.png

function deepClone(origin, hashMap = new WeakMap()){
  // 基本类型值
  if (origin == undefined || typeof origin !== 'object') {
    return origin;
  }

  if (origin instanceof Date){
    return new Date(origin);
  }

  if (origin instanceof RegExp){
    return new RegExp(origin);
  }
  // 在每次进来的时候获取origin
  const hashKey = hashMap.get(origin);
  // 判断存不存在,存在返回就行
  if (hashKey){
    return hashKey;
  }

  // origin的构造器会创建一个新的数组或对象,当前的target就是新的对象 
  const target = new origin.constructor();
  // 记录origin target
  hashMap.set(origin, target);
  for(let k in origin){
    if (origin.hasOwnProperty(k)){
      target[k] = deepClone(origin[k], hashMap);
    }
  }

  return target;
}

const newObj = deepClone(obj);
newObj.info.hobby[2].a = 123;

console.log(obj, newObj);

let test1 = {};
let test2 = {};
test2.test1 = test1;
test1.test2 = test2;
console.log(deepClone(test2));

image.png

3.2 JS 中循环的6种方式

JS中循环的几种方式:for循环、while循环、do while循环 | for循环 | forin循环| for of循环
let arr = [10, 20, 30, 40],
    obj = {name: 'wx',year: 10, 1: 100};
Object.prototype.AA = 12;

for(let key in obj){
  // key遍历的属性名
  // obj[key]属性值
  // 优先遍历属性名为数字的
  // if(!obj.hasOwnProperty(key)) break;会把所属类原型上自定义的属性方法也遍历到
}

for( let key in arr){
  if(!obj.hasOwnProperty(key)) break;
  console.log(key, arr[key]);
}

ES6新增for of循环
  + 获取的不是属性名是属性值
  + 不会遍历原型上公有的属性方法(哪怕是自定义的)
  + 只能遍历可被迭代的数据类型值(Symbol.iterator):Array/Sting/Arguments/NodeList/Set/Map等,但是普通对象是不可被迭代的,所以不能用for of循环
for(let item of arr){
  console.log(item);
}
for(let item of obj){
  console.log(item);
}