Vue2(十一):全局事件总线、消息订阅与发布pubsub、TodoList的编辑功能、$nextTick、过渡与动画

一、全局事件总线

1、思路解析

一种组件间通信的方式,适用于任意组件间通信。通俗理解就是一个定义在所有组件之外的公共x,这个x可以有vm或vc上的同款$on、$off、$emit,也可以让所有组件都访问到。

第一个问题:那怎样添加这个x才能让所有组件都看到呢?要想实现这个事情,只能在Vue的原型对象上去添加了!就是在Vue.prototype上添加一个属性,值是vm或vc.

那么Vue.prototype应该放在那里写?应该在main.js中写,因为你在main.js中引入的vue

第二个问题,怎样才能访问到 $on,$off,$emit这些呢?直接去输出x的$on这些,你是找不到的,因为他只是个对象object。其实vue的原型上都有,输出vue.prototype就会发现这些属性全都有。

接下来我们看看如何使用?

 2、安装全局事件总线

安装的话用vc也行用vm也行,用vc的话可以在main.js中这么写:

const Demo = Vue.extend({});
const d = new Demo();
Vue.prototype.$bus = d;//这个d其实就是我们的vc

但其实标准的写法不是这样繁琐的,应该是用vm下面这样:

new Vue({
	......
	 //放这个函数里,是因为模板还未解析,这个函数在自定义事件定义之前,是不会报错滴
	beforeCreate() {
		Vue.prototype.$bus = this //安装全局事件总线,起个名叫$bus,把当前vm给特
	},
    ......
}) 

 3.使用事件总线

接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身

methods(){
  demo(data){......}
}
......
mounted() {
  this.$bus.$on('xxxx',this.demo)
}

4.提供数据

任意一个组件,都可以给上面说的A组件传数据

  methods:{
    sendStudentName(){
        this.$bus.$emit('xxxx',this.name)
    }
}

5.组件销毁前最好解绑

最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。
因为接收数据的组件A中定义的回调函数和自定义事件是绑定的,而这个用来接收数据的组件实例A都销毁了,回调函数也没了,那这个xxxx自定义事件也就没用了,你留着会污染全局环境(这块儿有点迷糊)

beforeDestory(){
    this.$bus.$off('xxxx')
}

二、todolist的孙传父 

 之前我们孙传父都是父亲传给儿子函数,儿子传给孙子函数,然后孙子再调用函数传值,很麻烦,但是现在我们可以用全局事件总线实现孙子给父亲传数据

1.首先安装全局事件中线

new Vue({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this; //创建全局事件总线
    }
});

2.在App中绑定全局自定义事件,并使用之前写好的回调 

mounted() {
    //挂载完成后给全局事件总线添加事件
    this.$bus.$on('changeTodo', this.changeTodo);
    this.$bus.$on('deleteTodo', this.deleteTodo);
},
beforeDestroy() {
   //最好在销毁前解绑
    this.$bus.$off(['changeTodo', 'deleteTodo']);
},

3.Item中触发事件,并把数据id传过去

<input type="checkbox" :checked="todo.done" @click="handleChange(todo.id)" />
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
handleChange(id) {
    //触发全局事件总线中的事件
    this.$bus.$emit('changeTodo', id);
},
handleDelete(id) {
    if (confirm('确定要删除吗?'))  //点确定是true,取消是false
        this.$bus.$emit('deleteTodo', id);
}

 三、消息订阅与发布(pubsub)

1.使用方法

一种组件间的通信方式,适用于任意组件之间。

这玩意儿用的不多,和全局事件总线写法差不多,但是全局事件总线更好,因为是在Vue身上操作,但是这个的话要引入第三方库,库有很多,比如pubsub-js

(1)安装pubsub:npm i pubsub-js
(2)引入:import pubsub from 'pubsub-js

(3)接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
接收两个参数,第一个是消息名字,第二个是传过来的数据

methods(){
  demo(msgName,data){......}
}
......
mounted() {
  this.pubsubId = pubsub.subscribe('xxx',this.demo) //订阅消息
},   
beforeDestroy() {
   pubsub.unsubscribe(this.pubsubId); //销毁时取消订阅
},

(4)提供数据:

methods: {
    sendStudentName() {
        // this.$bus.$emit('hello', this.name);
        pubsub.publish('hello', this.name); //发布消息并传数据
    }
},

(5) 销毁:

 beforeDestroy(){
        // this.$bus.$off('hello')
        pubsub.unsubscribe(this.pubId)
    }

总结: 

2.todolist案例

(1)在App.vue中


import pubsub from 'pubsub-js'
deleteTodo(_,id)

 deleteTodo要用_,占个位

 mounted(){
        this.$bus.$on('checkTodo',checkTodo)
        // this.$bus.$on('deleteTodo', deleteTodo)
        this.pubId=pubsub.subscribe('deleteTodo',this.deleteTodo)
    },
    beforeDestroy(){
        this.$bus.$off('checkTodo')
        // this.$bus.$off('deleteTodo')
        pubsub.unsubscribe(this.pubId)
    }

(2) MyItem.vue


import pubsub from 'pubsub-js'
handleDelete(id) {
            if (confirm('确定删除吗?')) {
                //通知App组件将对应的todo对象删除
                // this.deleteTodo(id)
                // this.$bus.$emit('deleteTodo', id)
                pubsub.publish('deleteTodo',id)
            }
        }

四、todolist的编辑功能实现

1.思路

首先得有一个按钮,点击之后出现input框,框里呢是todo.title,而且原来的span要隐藏。然后修改完之后,失去焦点会自动更新数据,并且span出现,input框隐藏。
除此之外还有个细节,那就是点击编辑要自动获取焦点,要不然会有bug(点击编辑,然后突然不想改了,还得手动点一下input,再点下别的地方,才会变回span)

想实现span和input的来回切换,就要给todo添加新的属性,用来标识这个变换,这里起名叫isEdit

所以大致思路:给标签添加事件 => 点击编辑按钮切换span为input => 失去焦点传数据 => App收数据 => 解决焦点bug

2.给标签添加事件

(1)isEdit一上来是没有的,所以todo.isEdit = false,再加上默认上来显示的是span,所以span加个v-show="!todo.isEdit",input加个v-show="todo.isEdit" ,button加v-show="!todo.isEdit"是因为我们一般编辑时,这个按钮应该消失才对,所以和span一致

(2)由于props接过来的数据不能改,所以使用单向数据绑定:value="todo.title" (不过这里很奇怪,明明isEdit也是给todo添加了属性,奇怪奇怪,画个问号?????)

(3)ref="inputTitle" 是为了方便后面拿到input元素然后操作它写的(nextTick)

(4)@blur="handleBlur(todo, $event)"是失去焦点时触发的事件,用来给App传值

<span v-show="!todo.isEdit">{{ todo.title }}</span>
<input 
type="text" 
v-show="todo.isEdit" 
:value="todo.title" 
ref="inputTitle" //这个我没写
@blur="handleBlur(todo, $event)">
 <button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todo.isEdit">编辑</button>

3.点击编辑按钮切换span为input

点击编辑给todo追加属性,用来切换span为input。这里考虑到给todo追加属性的问题,如果想要让Vue监测到这个属性,那么必须使用$set来添加isEdit,且默认值为true(因为编辑的时候显示的时input啊,想想v-show="todo.isEdit")。

但是这里边有点儿问题,如果已经添加过了isEdit,那每次点击编辑按钮,都会添加一次isEdit属性,这样是不太好的,所以要加个判断,添加过了就改成true,没添加过就添加个true

   handleEdit(todo) {
        if (todo.isEdit !== undefined) {
            console.log('todo里有isEdit属性了')
            todo.isEdit = true;
        } else {
            console.log('todo里没有isEdit属性')
            this.$set(todo, 'isEdit', true);
        }
        this.$nextTick(function () {
            this.$refs.inputTitle.focus();
        })
    },

4.失去焦点传数据

失去焦点首先input得变回span,然后使用全局事件总线传值,传值一定要传当前input框的value值,因为这才是你修改后的值,使用事件对象获取。(当然,别忘了id也要传)

handleBlur(todo, e) {
    todo.isEdit = false;
    if (!e.target.value.trim()) return alert('值不能为空!');  //trim去掉空格
    this.$bus.$emit('editTodo', todo.id, e.target.value);
}

5.App收数据

把input框里你写的东西拿过来,给对应的todo.title

//6.实现编辑todo
methods: {
		......
        editTodo(id, newVal) {
            this.todos.forEach((todo) => {
                if (todo.id === id) { todo.title = newVal }
            });
        }
   }
mounted() {
	......
    //实现编辑功能,接收数据
    this.$bus.$on('editTodo', this.editTodo);
},
beforeDestroy() {
    //最好在销毁前解绑
    this.$bus.$off('editTodo');
},

6、代码总览

App.vue

<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <MyHeader @addTodo="addTodo" />
                <MyList :todos="todos" />
                <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo" />
            </div>
        </div>
    </div>
</template>

<script>
import pubsub from 'pubsub-js'
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter.vue'

export default {
    name: 'App',
    components: { MyHeader, MyList, MyFooter },
    data() {
        return {
            //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
            todos: JSON.parse(localStorage.getItem('todos')) || []
        }
    },
    methods: {
        //添加一个todo
        addTodo(todoObj) {
            this.todos.unshift(todoObj)
        },
        //勾选or取消勾选一个todo
        checkTodo(id) {
            this.todos.forEach((todo) => {
                if (todo.id === id) todo.done = !todo.done
            })
        },
        // 更新修改一个todo
        updateTodo(id,title) {
            this.todos.forEach((todo) => {
                if (todo.id === id) todo.title=title
            })
        },
        //删除一个todo
        deleteTodo(_,id) {
            this.todos = this.todos.filter(todo => todo.id !== id)
        },
        //全选or取消全选
        checkAllTodo(done) {
            this.todos.forEach((todo) => {
                todo.done = done
            })
        },
        //清除所有已经完成的todo
        clearAllTodo() {
            this.todos = this.todos.filter((todo) => {
                return !todo.done
            })
        }
    },
    watch: {
        todos: {
            deep: true,
            handler(value) {
                localStorage.setItem('todos', JSON.stringify(value))
            }
        }
    },
    mounted(){
        this.$bus.$on('checkTodo',this.checkTodo)
        this.$bus.$on('updateTodo',this.updateTodo)
        // this.$bus.$on('deleteTodo', deleteTodo)
        this.pubId=pubsub.subscribe('deleteTodo',this.deleteTodo)
    },
    beforeDestroy(){
        this.$bus.$off('checkTodo')
        this.$bus.$off('updateTodo')
        // this.$bus.$off('deleteTodo')
        pubsub.unsubscribe(this.pubId)
    }
}
</script>

<style>
/*base*/
body {
    background: #fff;
}

.btn {
    display: inline-block;
    padding: 4px 12px;
    margin-bottom: 0;
    font-size: 14px;
    line-height: 20px;
    text-align: center;
    vertical-align: middle;
    cursor: pointer;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
    border-radius: 4px;
}

.btn-danger {
    color: #fff;
    background-color: #da4f49;
    border: 1px solid #bd362f;
}
.btn-edit {
    color: #fff;
    background-color: skyblue;
    border: 1px solid blue;
    margin-right: 5px;
}

.btn-danger:hover {
    color: #fff;
    background-color: #bd362f;
}

.btn:focus {
    outline: none;
}

.todo-container {
    width: 600px;
    margin: 0 auto;
}

.todo-container .todo-wrap {
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 5px;
}</style>

MyItem.vue

<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)" />
            <!-- 如下代码也能实现功能,但是不太推荐,因为有点违反原则,因为修改了props -->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span v-show="!todo.isEdit">{{ todo.title }}</span>
            <input 
            v-show="todo.isEdit" 
            type="text" 
            :value="todo.title"
            @blur="handleBlur(todo,$event)">
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        <button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todoEdit">编辑</button>
    </li>
</template>

<script>
import pubsub from 'pubsub-js'
export default {
    name: 'MyItem',
    //声明接收todo
    props: ['todo'],
    methods: {
        //勾选or取消勾选
        handleCheck(id) {
            //通知App组件将对应的todo对象的done值取反
            // this.checkTodo(id)
            this.$bus.$emit('checkTodo',id)
        },
        //删除
        handleDelete(id) {
            if (confirm('确定删除吗?')) {
                //通知App组件将对应的todo对象删除
                // this.deleteTodo(id)
                // this.$bus.$emit('deleteTodo', id)
                pubsub.publish('deleteTodo',id)
            }
        },
         // 编辑
        handleEdit(todo) {
            if(todo.isEdit !== undefined){
                todo.isEdit = true
            }else{
                console.log('@');
                this.$set(todo, 'isEdit', true)
            }
        },
        // 失去焦点回调(真正执行修改逻辑)
        handleBlur(todo,e){
            todo.isEdit=false
            // console.log('updateTodo', todo.id, e.target.value);
            if(!e.target.value.trim()) return alert('输入不能为空!')
            this.$bus.$emit('updateTodo',todo.id,e.target.value)

        }
    },
}
</script>

<style scoped>
/*item*/
li {
    list-style: none;
    height: 36px;
    line-height: 36px;
    padding: 0 5px;
    border-bottom: 1px solid #ddd;
}

li label {
    float: left;
    cursor: pointer;
}

li label li input {
    vertical-align: middle;
    margin-right: 6px;
    position: relative;
    top: -1px;
}

li button {
    float: right;
    display: none;
    margin-top: 3px;
}

li:before {
    content: initial;
}

li:last-child {
    border-bottom: none;
}

li:hover {
    background-color: #ddd;
}

li:hover button {
    display: block;
}
</style>

五、$nextTick

1、语法:this.$nextTick(回调函数)
2、作用:在下一次 DOM 更新结束,v-for循环结束后执行其指定的回调
3、什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时(如input自动获取焦点),要在nextTick所指定的回调函数中执行。

4、比如刚才点击编辑后,input框没法自动获取焦点,那么我就得先点一下,再点别处儿才能切换回去,如果直接this.$refs.inputTitle.focus();不行,因为这个函数虽然动了isEdit的值,但是模板重新解析也得等这个函数走完啊,那input还没创建出来呢,就focus了,肯定是不行滴。
有个办法就是用异步,也可以解决,但是更好的办法是$nextTick

ref="inputTitle"
 handleEdit(todo) {
            if(todo.isEdit !== undefined){
                todo.isEdit = true
            }else{
                console.log('@');
                this.$set(todo, 'isEdit', true)
            }
            this.$nextTick(function(){
                this.$refs.inputTitle.focus()
            })
        },

六、过度与动画 

1、作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。

2、写法:

准备好样式:

元素进入的样式:
v-enter:进入的起点
v-enter-active:进入过程中
v-enter-to:进入的终点

元素离开的样式:
v-leave:离开的起点
v-leave-active:离开过程中
v-leave-to:离开的终点

使用<transition>包裹要过度的元素,并配置name属性:

<transition name="hello">
	<h1 v-show="isShow">你好啊!</h1>
</transition>

3、备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定key值。

也可以引入第三方库,animate.css.打开终端输入npm i animate.css,下载之后在相应的vue文件中引入inport 'animate.css'就可以使用。

<transition-group 
    name="animate_animated animate_bounce" 
    appear
    enter-active-class="animate_swing"
    leave-active-class="animate_backOutUp"
    >
        <!-- 如果写name下面就要用name-enter-active -->
        <!-- appear意思是一上来就有动画效果 -->
        <h1 v-show="!isShow" key="1">你好啊</h1>
        <h1 v-show="isShow" key="2">椰果</h1>
    </transition-group>

OK今天就到这里结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/490719.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

GPU-CPU-ARM-X86-RISC-CUDA

CPU更适合处理复杂逻辑运算和单线程任务&#xff0c;而GPU则更适合处理大规模并行计算任务。 CPU&#xff08;中央处理器&#xff09;通常具有较少的核心数量&#xff08;一般在2到16个之间&#xff09;&#xff0c;但每个核心的性能较强&#xff0c;擅长执行复杂的运算和逻辑…

SpringCloud Alibaba实战和源码(8)OpenFeign使用

1、 使用Feign实现远程HTTP调用 1.1、常见HTTP客户端 HttpClient HttpClient 是 Apache Jakarta Common 下的子项目&#xff0c;用来提供高效的、最新的、功能丰富的支持 Http 协 议的客户端编程工具包&#xff0c;并且它支持 HTTP 协议最新版本和建议。HttpClient 相比传统 J…

Postman核心功能解析-参数化和测试报告

一、参数化处理 参数化&#xff1a;针对于某一个接口&#xff0c;有大量的的测试数据需要批量验证&#xff0c;一个一个的更改请求参数太耗时耗力&#xff0c;使用参数化批量处理数据会比较高效&#xff0c;常规通过文档参数化实现。 创建文件 格式CSV 文件内第一行信息 需要…

【YOLOv8代码详解】各个任务Loss损失函数梳理

YOLOv8官方将各类任务&#xff08;目标检测&#xff0c;关键点检测&#xff0c;实例分割&#xff0c;旋转目标框检测&#xff0c;图像分类&#xff09;的损失函数封装了在ultralytics\utils\loss.py中&#xff0c;本文主要梳理一下各类任务Loss的大致组成&#xff0c;不涉及到具…

封装-练习

T2、以面向对象的思想&#xff0c;编写自定义类描述IT从业者。设定属性包括&#xff1a;姓名&#xff0c;年龄&#xff0c;技术方向&#xff0c;工作年限&#xff1b;方法包括&#xff1a;工作。 要求&#xff1a; 设置属性的私有访问权限&#xff0c;通过公有的get,set方法实现…

第2章 辐射度、光度和色度学基本理论

一、前言 辐射度学&#xff08;radiology&#xff09;是一门以整个电磁波段&#xff08;electromagnetic band&#xff09;的电磁辐射能&#xff08;electromagnetic radiation energy&#xff09;测量为研究对象的科学。计算机图形学中涉及的辐射度学&#xff0c;则集中于整个…

代码随想录算法训练营三刷day35 |贪心 之 860.柠檬水找零 406.根据身高重建队列 452. 用最少数量的箭引爆气球

三刷day35 860.柠檬水找零406.根据身高重建队列452. 用最少数量的箭引爆气球 860.柠檬水找零 题目链接 解题思路&#xff1a; 局部最优&#xff1a;遇到账单20&#xff0c;优先消耗美元10&#xff0c;完成本次找零。全局最优&#xff1a;完成全部账单的找零。 代码如下&#x…

Internet Explorer 降级

Internet Explorer 降级 1. version2. Internet Explorer 降级References 1. version 帮助 -> 关于Internet Explorer(A) 2. Internet Explorer 降级 开始 -> 控制面板 -> 卸载程序 -> 查看已安装的更新 搜索 Internet -> Internet Explorer 11 -> 卸载 -…

office办公技能|word中的常见使用问题解决方案2.0

一、设置多级列表将表注从0开始&#xff0c;设置为从1开始 问题描述&#xff1a;word中插入题注&#xff0c;出来的是表0-1&#xff0c;不是1-1&#xff0c;怎么办&#xff1f; 写论文时&#xff0c;虽然我设置了“第一章”为一级标题&#xff0c;但是这三个字并不是自动插入的…

[leetcode]118.杨辉三角

前言&#xff1a;剑指offer刷题系列 问题&#xff1a; 给定一个非负整数 *numRows&#xff0c;*生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 示例&#xff1a; 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,…

C# NumericUpDown 控件正整数输入控制

用到了控件的 KeyPress 和 KeyUp事件。 KeyPress 中控制输入“点、空格&#xff0c;负号”&#xff1b; KeyUp 中防止删空&#xff0c;以及防止输入超过最大值或最小值 。 private void nudStart_KeyPress(object sender, KeyPressEventArgs e){numericUpDownKeyPress(sender…

CAPL - 如何实现弹窗提示和弹窗操作(续)

目录 函数介绍 openPanel closePanel 代码示例 1、简单的打开关闭panel面板

美团0309春招笔试题

下面是美团2024-03-09笔试真题&#xff0c;笔者进行了VP&#xff0c;由于未参与评测&#xff0c;故不保证正确性&#xff0c;仅供参考。 第一题 小美的MT 首先找到原来字符串中含有的M和T的数量&#xff0c;记作cnt。然后剩余n - cnt个字符是可以修改的&#xff0c;但这取决于…

二叉树|236.二叉树的最近公共祖先

力扣题目链接 class Solution { public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if (root q || root p || root NULL) return root;TreeNode* left lowestCommonAncestor(root->left, p, q);TreeNode* right lowestCommonAncesto…

【办公类-21-10】三级育婴师 视频转文字docx(等线小五单倍行距),批量改成“宋体小四、1.5倍行距、蓝色字体、去掉五分钟”

作品展示 背景需求 今天将最后3个育婴师操作视频做整理 第1步&#xff1a;视频MP4转MP3 【办公类-40-01】20240311 用Python将MP4转MP3提取音频 &#xff08;家长会系列一&#xff09;-CSDN博客文章浏览阅读393次&#xff0c;点赞9次&#xff0c;收藏6次。【办公类-40-01】20…

鸿蒙应用开发-录音保存并播放音频

功能介绍&#xff1a; 录音并保存为m4a格式的音频&#xff0c;然后播放该音频&#xff0c;参考文档使用AVRecorder开发音频录制功能(ArkTS)&#xff0c;更详细接口信息请查看接口文档&#xff1a;ohos.multimedia.media (媒体服务)。 知识点&#xff1a; 熟悉使用AVRecorder…

码垛机与人工搬运:效率与安全性的比较分析

在现代包装行业中&#xff0c;泡沫箱因其轻便和保温特性被广泛用于商品的包装与运输。随着自动化技术的不断发展&#xff0c;码垛机成为提升泡沫箱生产效率、降低劳动强度的关键技术。本文旨在比较码垛机与人工码垛在泡沫箱生产中的优势&#xff0c;并探讨自动化码垛的未来发展…

c语言文件操作(下)

目录 1.文件的随机读写1.1 fseek1.2 ftell1.3 rewind 2. 文件结束的判定2.1 文本文件读取结束的判断2.2 二进制文件读取结束的判断 3. 文件缓冲区 1.文件的随机读写 1.1 fseek 根据⽂件指针的位置和偏移量来定位⽂件指针。 函数原型&#xff1a; int fseek (FILE * stream,…

【STL学习】(2)string的模拟实现

前言 本文将模拟实现string的一些常见功能&#xff0c;目的在于加深理解string与回顾类与对象的相关知识。 一、前置知识 string是表示可变长的字符序列的类string的底层是使用动态顺序表存储的string对象不以’\0’字符为终止算长度&#xff0c;而是以size有效字符的个数算长…