Skip to content

nextTick异步更新队列🔥🔥🔥

文档 - 链接

Vue.nextTick - 链接

异步更新队列 - 链接

javascript
// 1. $nextTick
// 作用:获取异步更新的DOM元素,
// 我们虽然说:数据变化,视图自动更新 --> 但是Vue的视图更新是异步的(攒着不慌)
this.$nextTick(() => {
  // 当dom元素内容更新之后,执行回调
})

// 2. $nextTick实现原理 ---> 理解记忆,面试题!
// 当Vue监听到数据变化,会开启一个队列(js中数组实现),并缓冲在同一事件循环中
// 发生的所有数据变更。等同一事件循环中所有数据变化完成后,才会异步更新视图。
// 并触发nextTick中的回调函数。

//(异步的理解:也就是说,并不是数据变化一次,就更新一次dom(视图))

// nextTick本质上就是宏微任务的一个降级处理,  nextTick,下一个Tick
// promise.then ===> MutationObserver ==> setImmediate ==> setTimeout


----碾压面试官
// 3. src/core/global-api/index.ts ==> 45-line
Vue.nextTick = nextTick

// scor/instance/init.ts --> initRender(vm) 
// src/core/instance/render.ts -- 100-line  
Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};
不论是Vue.nextTick 还是 vm.$nextTick实例方法  内部都是调用的nextTick函数

// nextTick源码位置
// src/util/next-tick.ts 源码不就110多行吗

Eg.练习

vue
<template>
  <div>
    <!-- 首先页面中要准备两个元素,一个input,一个button -->
    <input v-if="isSearch" class="ipt" type="text" ref="ipt">
    <!-- 点击按钮修改状态, 所以要@click -->
    <button v-else @click="changeOther">点击搜索</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      isSearch: false
    }
  },
  methods: {
    change () {
      // 想要,一点击搜索按钮,输入框切换,并且输入框获取到焦点
      this.isSearch = true // 按理说,input框应该渲染 ,但是因为Vue的dom更新是异步的
      
      this.$nextTick(() => {
          this.$refs.ipt.focus()
      })
      // setTimeout(() => {
      //   this.$refs.ipt.focus()
      // }, 0);
    },
    async changeOther(){
        this.isSearch = true   
        await this.$nextTick()   
        this.$refs.ipt.focus()
    }
  }
}
</script>

动态组件

javascript
npm i less less-loader
javascript
<template>
  <div id="app">
    <h1>动态组件dynamic的学习</h1>
    <button class="my-btn-primary" @click="handleClick('UserAccount')">
      账号密码填写
    </button>
    <!-- <button class="my-btn-success" @click="handleClick('UserInfo')">
      个人信息填写
    </button> -->
   //  或者
    <button class="my-btn-success" @click="compName = 'UserInfo'">
      个人信息填写
    </button>

    <!--
      动态组件的使用 ( component 组件 + is 属性 )
      (1) 设置挂载点<component>, (在哪显示)
      (2) 使用is属性来设置要显示哪个组件 (显示哪一个组件)
    -->
    <component :is="compName"></component>
  </div>
</template>
<script>
import UserAccount from './components/UserAccount.vue'
import UserInfo from './components/UserInfo.vue'
export default {
  components: {
    UserAccount,
    UserInfo
  },
  data () {
    return {
      compName: 'UserAccount'
    }
  },
  methods:{
    handleClick(comp){
      this.compName = comp 
    }
  }
}
</script>

自定义指令

image.png

vue
<template>
  <div id="app">
    <h3>自定义指令</h3>
    <input ref="ipt" type="text" v-focus>
  </div>
</template>

<script>
// 需求,当页面加载时,元素获取焦点
export default {
  name: 'App',
  // 补充代码
  // mounted(){
  //   this.$refs.ipt.focus()
  // }
  directives:{
    focus:{
      inserted(el){
        el.focus()
      }
    }
  }
}
</script>

自定义指令的值

image.png

vue
<template>
  <div id="app">
    <h3 v-color="color1">自定义指令值-color-skyblue</h3>
    <h3 v-color="color2">自定义指令值-color-orange</h3>
    <hr>
    <button @click="changeColor">点击修改颜色</button>
  </div>
</template>
<script>
// 需求:不同的指令值,显示不同的字体颜色,点击按钮修改,更新颜色
export default {
  name: 'App',
  data(){
    return {
      color1:'pink',
      color2:'orange'
    }
  },
  methods:{
    changeColor(){
      this.color1 = 'purple'
    }
  },
  directives:{
    color:{
      // 当指令所在的元素挂载到页面上时触发
      // eslint-disable-next-line
      inserted(el, {value}){
        // console.log(binding)
        el.style.color = value
      },
      // 当我们修改指令的值的时候,触发
      update(el, binding){
        el.style.color = binding.value
      }
    }
  }
}
</script>

slot插槽

基本使用

  1. 先在组件内用 slot 占位
  2. 使用组件时, 传入具体标签结构内容插入(父组件中使用组件)

image.png

插槽后备内容

后备内容就是默认内容,当使用组件标签时,不写结构,就默认展示的内容

具名插槽

image.png

作用域插槽

image.png App.vue

vue
<template>
  <div>
    <my-user>
      <template v-slot:header>
        <h2>我是标题渲染的</h2>
      </template>
    
      <!--  子组件slot上传过来的值,还可以解构 -->

      <!-- 给v-slot:插槽名 = "喜欢的变量名", slot标签上的属性都存到了这个对象中 -->
      <template #default="{user, age}">
        <!-- <div>{{slotProps}}</div> -->
        <div>{{age}}</div>
        <div>{{user.lastName}}</div>
        <div>{{user.firstName}}</div>
      <!-- // 补充代码 -->
      </template>
      
      <template v-slot:footer="obj">
        <div>{{obj}}</div>
        <div>{{obj.msg}}</div>
        <p>我是底部footer渲染</p>
      </template>
    </my-user>
    
  </div>
</template>

<script>
import MyUser from './components/MyUser.vue'
export default {
  components:{
    // MyDialog,
    MyUser
  }
}
</script>

<style>
.main{
  width: 200px;
  height: 200px;
  border:1px solid red;
}
</style>
vue
<template>
  <div>
      <header>
          <slot name="header"></slot> 
      </header>
      <main>
          <!-- slot不加name属性,默认name为default -->
        
          <!-- 可以将user变量作为slot标签的属性绑定
           => 父组件可以使用-->
          <slot :user="user" :age="age">
              <!-- 插槽的默认内容 -->
              {{ user.lastName }}
          </slot>
      </main>
      <footer>
          <slot name="footer" msg="我是footer属性传的值"></slot>
      </footer>
  </div>
  </template>
<script>
  export default {
      data(){
          return {
              user:{
                  lastName:'王',
                  firstName:'大锤'
              },
              age: 18
          }
      }
  }
  </script>

作用域插槽-表格练习

image.png

vue
<template>
  <div>
    <MyTable :data="list">
      <template v-slot:default="{id}">
          <!-- {{slotProps}} -->
          <button @click="del(id)">删除</button>
      </template>
    </MyTable>
    
    <MyTable :data="list2">
      <template #default="{item}">
        <button @click="showItem(item)">查看</button>
      </template>
    </MyTable>
  </div>
</template>

<script>
import MyTable from './components/MyTable.vue'
export default {
  data () {
    return {
      list: [
        { id: 1, name: '张小花', age: 18 },
        { id: 2, name: '孙大明', age: 19 },
        { id: 3, name: '刘德忠', age: 17 },
      ],
      list2: [
        { id: 1, name: '赵小云', age: 18 },
        { id: 2, name: '刘蓓蓓', age: 19 },
        { id: 3, name: '姜肖泰', age: 17 },
      ]
    }
  },
  methods:{
    del(id){
      console.log(id)
      // filter过滤,留下el.id不等于传过来的id的元素
      this.list = this.list.filter(el =>el.id !==id)
    },
    showItem(item){
      alert(JSON.stringify(item))
    }
  },
  components: {
    MyTable
  }
}
</script>

MyTable.vue

vue
<template>
  <table class="my-table">
    <thead>
      <tr>
        <th>序号</th>
        <th>姓名</th>
        <th>年纪</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="item in data" :key="item.id">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
        <td>{{item.age}}</td>
        <td>
          <!-- <button>删除</button> -->
          <slot :id="item.id" :item="item"></slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  props:{
    data:{
      type:Array,
      // 如果是引用类型,需要用一个工厂
      default:() => []
    }
  }
}
</script>

<style scoped>
.my-table {
  width: 450px;
  text-align: center;
  border: 1px solid #ccc;
  font-size: 24px;
  margin: 30px auto;
}
.my-table thead {
  background-color: #1f74ff;
  color: #fff;
}
.my-table thead th {
  font-weight: normal;
}
.my-table thead tr {
  line-height: 40px;
}
.my-table th,
.my-table td {
  border-bottom: 1px solid #ccc;
  border-right: 1px solid #ccc;
}
.my-table td:last-child {
  border-right: none;
}
.my-table tr:last-child td {
  border-bottom: none;
}
.my-table button {
  width: 65px;
  height: 35px;
  font-size: 18px;
  border: 1px solid #ccc;
  outline: none;
  border-radius: 3px;
  cursor: pointer;
  background-color: #ffffff;
  margin-left: 5px;
}
</style>

综合案例

MyTag的封装

image.png

MyTag-v-model回显数据

image.png

vue

<template>
<div class="my-tag">
    <input
    class="input"
    type="text"
    placeholder="输入标签"
    ref="ipt"
    v-if="isEdit"
    @blur="handleClose"
    @keyup.enter="$event.target.blur()"
    :value="value"
    v-focus
    />
    <div v-else class="text" @dblclick="handleClick">{{value}}</div>
</div>
</template>

<script>
export default {
    props:{
        value:[Number,String]
    },
    data(){
        return {
            isEdit:false
        }
    },
    methods:{
        // 1. 自动获取焦点,方式1  $nextTick
        async handleClick(){
            this.isEdit = true
            // await this.$nextTick()
            // 等数据更新完了,获取最新的DOM元素
            // this.$refs.ipt.focus()
        },
        handleClose(e){
            this.isEdit = false
            console.log(e.target.value)
            if(!e.target.value.trim()){
                return alert('内容不能为空哦')
            }
            // :value + @input 
            this.$emit('input', e.target.value)
            
        }
    },
    // 2. 自定义指令的方式,自动获取焦点
    directives:{
        focus:{
            // 当指令所在的元素插入到页面中时,触发
            inserted(el){
                el.focus()
            }
        }
    }
}
</script>

<style lang="less" scoped>
.my-tag {
  cursor: pointer;
  .input {
    appearance: none;
    outline: none;
    border: 1px solid #ccc;
    width: 100px;
    height: 40px;
    box-sizing: border-box;
    padding: 10px;
    color: #666;
    &::placeholder {
      color: #666;
    }
  }
}
input{
    width: 80px;
}
</style>

MyTable的封装

image.png

vue
<template>
      <table class="my-table">
      <thead>
        <tr>
          <th>编号</th>
          <th>图片</th>
          <th>名称</th>
          <th width="100px">标签</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in goods" :key="item.id">
          <td>{{item.id}}</td>
          <td><img :src="item.picture" /></td>
          <td>{{item.name}}</td>
          <td>
              <!-- v-model -->
              <my-tag v-model="item.tag"></my-tag>
          </td>
        </tr>
      </tbody>
    </table>
</template>

<script>
import MyTag from './MyTag.vue'
export default {
  components:{
    MyTag
  },
  data () {
    return {
      goods: [
        { id: 101, picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg', name: '梨皮朱泥三绝清代小品壶经典款紫砂壶', tag: '茶具' },
        { id: 102, picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg', name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌', tag: '男鞋' },
        { id: 103, picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png', name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm', tag: '儿童服饰' },
        { id: 104, picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg', name: '基础百搭,儿童套头针织毛衣1-9岁', tag: '儿童服饰' },
      ]
    }
  }
}
</script>

<style lang="less" scoped>
.my-table {
  width: 100%;
  border-spacing: 0;
  img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
  }
  th {
    background: #f5f5f5;
    border-bottom: 2px solid #069;
  }
  td {
    border-bottom: 1px dashed #ccc;
  }
  td,
  th {
    text-align: center;
    padding: 10px;
    transition: all .5s;
    &.red {
      color: red;
    }
  }
  .none {
    height: 100px;
    line-height: 100px;
    color: #999;
  }
}
</style>

MyTag.vue

vue

<template>
<div class="my-tag">
    <input
    class="input"
    type="text"
    placeholder="输入标签"
    ref="ipt"
    v-if="isEdit"
    @blur="handleClose"
    @keyup.enter="$event.target.blur()"
    :value="value"
    v-focus
    />
    <div v-else class="text" @dblclick="handleClick">{{value}}</div>
</div>
</template>

<script>
export default {
    props:{
        value:[Number,String]
    },
    data(){
        return {
            isEdit:false
        }
    },
    methods:{
        // 1. 自动获取焦点,方式1  $nextTick
        async handleClick(){
            this.isEdit = true
            // await this.$nextTick()
            // 等数据更新完了,获取最新的DOM元素
            // this.$refs.ipt.focus()
        },
        handleClose(e){
            this.isEdit = false
            console.log(e.target.value)
            if(!e.target.value.trim()){
                return alert('内容不能为空哦')
            }
            // :value + @input 
            this.$emit('input', e.target.value)
            
        }
    },
    // 2. 自定义指令的方式,自动获取焦点
    directives:{
        focus:{
            // 当指令所在的元素插入到页面中时,触发
            inserted(el){
                el.focus()
            }
        }
    }
}
</script>

<style lang="less" scoped>
.my-tag {
  cursor: pointer;
  .input {
    appearance: none;
    outline: none;
    border: 1px solid #ccc;
    width: 100px;
    height: 40px;
    box-sizing: border-box;
    padding: 10px;
    color: #666;
    &::placeholder {
      color: #666;
    }
  }
}
input{
    width: 80px;
}
</style>

App.vue

vue
<template>
  <div class="table-case">
    <my-table></my-table>
  </div>
</template>

<script>
import MyTable from './components/MyTable.vue'
export default {
  name: 'TableCase',
  components:{
    MyTable
  }
}
</script>

<style lang="less" scoped>
.table-case {
  width: 1000px;
  margin: 50px auto;
  img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
  }
}
</style>

自定义表头和结构

image.png

app.vue

vue
<template>
  <div class="table-case">
    <my-table>
      <template v-slot:head>
          <th>编号</th>
          <th>图片</th>
          <th>名称</th>
          <th width="100px">标签</th>
      </template>

      <template #main="{item}">
          <td>{{item.id}}</td>
          <td><img :src="item.picture" /></td>
           <td>{{item.name}}</td> 
          <td>
              <my-tag v-model="item.tag"></my-tag>
          </td> 
      </template>
    </my-table>
  </div>
</template>

<script>
import MyTable from './components/MyTable.vue'
import MyTag from './components/MyTag.vue'
export default {
  name: 'TableCase',
  components:{
    MyTable,
    MyTag
  }
}
</script>

<style lang="less" scoped>
.table-case {
  width: 1000px;
  margin: 50px auto;
  img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
  }
}
</style>

MyTable

vue
<template>
      <table class="my-table">
      <thead>
        <tr>
          <slot name="head"></slot>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in goods" :key="item.id">
            <slot name="main" :item="item"></slot>
        </tr>
      </tbody>
    </table>
</template>

<script>
/* 
    数据不能写死:动态渲染
    结构不能写死:slot插槽 ==> 多个结构要自定义 => 具名插槽
        - 表头自定义
        - 表格主体自定义

*/

export default {
  components:{
  },
  data () {
    return {
      goods: [
        { id: 101, picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg', name: '梨皮朱泥三绝清代小品壶经典款紫砂壶', tag: '茶具' },
        { id: 102, picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg', name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌', tag: '男鞋' },
        { id: 103, picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png', name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm', tag: '儿童服饰' },
        { id: 104, picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg', name: '基础百搭,儿童套头针织毛衣1-9岁', tag: '儿童服饰' },
      ]
    }
  }
}
</script>

<style lang="less" scoped>
.my-table {
  width: 100%;
  border-spacing: 0;
  img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
  }
  th {
    background: #f5f5f5;
    border-bottom: 2px solid #069;
  }
  td {
    border-bottom: 1px dashed #ccc;
  }
  td,
  th {
    text-align: center;
    padding: 10px;
    transition: all .5s;
    &.red {
      color: red;
    }
  }
  .none {
    height: 100px;
    line-height: 100px;
    color: #999;
  }
}
</style>