이 글은 vue에 대한 관련 지식을 제공합니다. 주로 vue, 반응성 및 구성 요소화에 대한 두 가지 핵심 문제를 소개합니다. 모두에게 도움이 되기를 바랍니다.
【관련 권장사항: javascript 비디오 튜토리얼, vue.js tutorial】
Object.defineProperty(obj, prop, descriptor)
메서드는 객체에 대한 새 속성을 직접 정의하거나 객체의 기존 속성을 수정하고 이 객체를 반환합니다. Object.defineProperty(obj, prop, descriptor)
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
obj——要定义属性的对象
prop——要定义或修改的属性的名称或Symbol
descriptor——对象,要定义或修改的属性描述符
// descriptor{ value: undefined, // 属性的值 get: undefined, // 获取属性值时触发的方法 set: undefined, // 设置属性值时触发的方法 writable: false, // 属性值是否可修改,false不可改 enumerable: false, // 属性是否可以用for...in 和 Object.keys()枚举 configurable: false // 该属性是否可以用delete删除,false不可删除,为false时也不能再修改该参数}
通过赋值操作添加的普通属性是可枚举的,在枚举对象属性时会被枚举到(for…in 或 Object.keys 方法),可以改变这些属性的值,也可以删除这些属性。这个方法允许修改默认的额外选项(或配置)。
而默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。
示例:
const a = {b : 1} console.log(Object.getOwnPropertyDescriptor(a, 'b')) // {value: 1, writable: true, enumerable: true, configurable: true} Object.defineProperty(a, 'c', {value: '2'}) console.log(Object.getOwnPropertyDescriptor(a, 'c')) // {value: '2', writable: false, enumerable: false, configurable: false} a.c = 3 console.log(a.c) // 2 Object.defineProperty(a, 'c', {value: '4'}) console.log(a.c) // error: Uncaught TypeError: Cannot redefine property: c
// 模拟vue响应式过程const app = document.getElementById('app')const data = { a: { b: { c: 1 } }}function render () { const virtualDom = `这是我的内容${data.a.b.c}` app.innerHTML = virtualDom}function observer (obj) { let value for (const key in obj) { // 递归设置set和get value = obj[key] if (typeof value === 'object'){ arguments.callee(value) } else { Object.defineProperty(obj, key, { get: function(){ return value }, set: function(newValue){ value = newValue render() } }) } }}render()observer(data)setTimeout(() => { data.a.b.c = 22}, 2000)setTimeout(() => { data.a.b.c = 88}, 5000)
上述方法实现了数据的响应,但存在很大的问题,我们触发一次set,就需要整个页面重新渲染,然而这个值可能只在某一个组件中使用了。
所以将get和set优化:
Object.defineProperty(data, key, { get: function(){ dep.depend() // 这里进行依赖收集 return value }, set: function(newValue){ value = newValue // render() dep.notify() // 这里进行virtualDom更新,通知需要更新的组件render }});
dep是Vue负责管理依赖的一个类
补充: Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的
const vm = new Vue({ data: { a: 1 }})// vm.a是响应式的vm.b = 2// vm.b是非响应式的
vue 中处理数组的变化,直接通过下标触发视图的更改,只能使用push、shift等方法,而数组不能使用Object.defineProperty()
其实 Vue用装饰者模式来重写了数组这些方法
Object.create(proto,[propertiesObject])
方法是创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
proto
——新创建对象的原型对象;propertiesObject
obj - 속성을 정의하는 개체prop - 정의하거나 수정할 속성의 이름 또는 기호
descriptor - 정의하거나 수정할 개체, 속성 설명자
const a = {} // 相当于 const a = Object.create(Object.prototype) const person = { isHuman: false, printIntroduction: function () { console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`); } }; const me = Object.create(person); me.name = 'Matthew'; // "name" is a property set on "me", but not on "person" me.isHuman = true; // inherited properties can be overwritten me.printIntroduction(); // expected output: "My name is Matthew. Am I human? true"
할당 작업을 통해 추가된 일반 속성은 열거 가능하며 개체 속성을 열거할 때 열거됩니다(for...in 또는 Object.keys 메서드). 이러한 속성의 값은 변경되거나 삭제될 수 있습니다. 이 방법을 사용하면 기본 추가 옵션(또는 구성)을 수정할 수 있습니다.기본적으로 Object.defineProperty()를 사용하여 추가된 속성 값은 변경할 수 없습니다.
예:
const o = Object.create(Object.prototype, { foo: { // foo会成为所创建对象的数据属性 writable:true, configurable:true, value: "hello" }, bar: { // bar会成为所创建对象的访问器属性 configurable: false, get: function() { return 10 }, set: function(value) { console.log("Setting `o.bar` to", value); } }});console.log(o) // {foo: 'hello'}1.2 set and get
const arraypro = Array.prototype // 获取Array的原型 const arrob = Object.create(arraypro) // 用Array的原型创建一个新对象,arrob.__proto__ === arraypro,免得污染原生Array; const arr=['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] // 需要重写的方法 arr.forEach(function(method) { arrob[method] = function () { arraypro[method].apply(this, arguments) // 重写时先调用原生方法 dep.notify() // 并且同时更新 } }) // 对于用户定义的数组,手动将数组的__proto__指向我们修改过的原型 const a = [1, 2, 3] a.__proto__ = arrob위의 방법으로 데이터 응답을 얻을 수 있지만 set을 한 번 실행하면 전체 페이지를 다시 렌더링해야 한다는 문제가 있습니다. 값은 구성 요소에서만 사용할 수 있습니다.
추가: Vue는 속성의 추가 또는 제거를 감지할 수 없습니다. Vue는 인스턴스를 초기화할 때 속성에 대해 getter/setter 변환을 수행하므로 Vue가 이를 반응형 |
2로 변환하려면 데이터 객체에 속성이 있어야 합니다. 배열은 첨자를 통해 직접 뷰 변경을 트리거하며 push, Shift 및 기타 메소드만 사용할 수 있으며 배열은 Object.defineProperty()를 사용할 수 없습니다 | 실제로 Vue는 데코레이터 모드를 사용하여 이러한 배열 메소드를 다시 작성합니다
---|---|
propertiesObject - 선택 사항입니다. 이 매개 변수가 지정되고 정의되지 않은 경우 전달된 객체의 자체 열거 가능한 속성(예: 자체 프로토타입 체인의 열거 속성이 아닌 정의된 속성)은 지정된 속성 값과 해당 속성 설명자를 새로 생성된 개체에 추가합니다 |
vue |
组件内部 slot预留位置 默认name:default <slot></slot> 调用组件的时候 //只有一个的时候可以不用template包裹 <test> <div>头部导航</div> </test> | 3 보충: 객체의 데이터 속성 및 접근자 속성|
4개의 설명자 데이터 속성: | |
의미 |
접속자 속성: 이 속성에는 데이터 값이 포함되어 있지 않습니다. 여기에는 한 쌍의 get 및 set 메서드가 포함되어 있습니다. 접근자 속성을 읽고 쓸 때 이 두 메서드를 사용하여 작업을 수행합니다.
접속자 속성의 네 가지 설명자:
의미 | |
---|---|
configurable | 은 속성을 삭제를 통해 삭제하여 재정의할 수 있는지, 속성의 특성을 수정할 수 있는지 여부를 나타냅니다. 접근자 속성에 대한 속성을 수정할지 여부, 기본값은 false입니다 |
enumerable | for-in 루프를 통해 속성을 반환할 수 있는지 여부를 나타냅니다. 기본값은 false |
get | 입니다. 속성을 읽을 때 호출되는 함수, 기본값은 정의되지 않음 |
set | 속성을 쓸 때 호출되는 함수, 기본값은 정의되지 않음 |
vue3.0的响应式和vue2.0响应式原理类似,都是在get中收集依赖,在set中通知依赖更新视图,但vue3.0使用了es6新增的proxy来代替Object.defineProperty()
proxy相对于Object.defineProperty()的好处:
- Object.defineProperty需要指定对象和属性,对于多层嵌套的对象需要递归监听,Proxy可以直接监听整个对象,不需要递归;
- Object.defineProperty的get方法没有传入参数,如果我们需要返回原值,需要在外部缓存一遍之前的值,Proxy的get方法会传入对象和属性,可以直接在函数内部操作,不需要外部变量;
- set方法也有类似的问题,Object.defineProperty的set方法传入参数只有newValue,也需要手动将newValue赋给外部变量,Proxy的set也会传入对象和属性,可以直接在函数内部操作;
- new Proxy()会返回一个新对象,不会污染源原对象
- Proxy可以监听数组,不用单独处理数组
proxy劣势: vue3.0将放弃对低版本浏览器的兼容(兼容版本ie11以上)
这样上边的observe方法就可以优化成:
function observer () { var self = this; data = new Proxy(data, { get: function(target, key){ dep.depend() // 这里进行依赖收集 return target[key] }, set: function(target, key, newValue){ target[key] = newValue; // render() dep.notify() // 这里进行virtualDom更新,通知需要更新的组件render } });}
按照功能(或按照复用性)把一个页面拆成各个板块(模块),每一个模块都是一个单独的文件(单独的组件),最后把各个模块(组件)拼在一起即可!!
目的 :方便团队协作开发 实现复用
功能型组件「UI组件库中提供的一般都是功能型组件:element/iview/antdv/vant/cube..」
业务型组件
以后开发项目,拿到设计稿的第一件事情:划分组件「按照功能版块划分、本着复用性原则,拆的越细越好(这样才能更好的实现复用)」
组件的创建及使用
创建一个 Xxx.vue 就是创建一个vue组件{局部组件、私有组件},组件中包含:结构、样式、功能
结构:基于template构建
+ 只能有一个根元素节点(vue2)
+ vue的视图就是基于template语法构建的(各种指令&小胡子...),最后vue会把其编译为真实的DOM插入到页面指定的容器中
首先基于 vue-template-compiler 插件把template语法编译为虚拟DOM「vnode」
其次把本次编译出来的vnode和上一次的进行对比,计算出差异化的部分「DOM-DIFF」
最后把差异化的部分变为真实的DOM放在页面中渲染
样式:基于style来处理
功能:通过script处理
+ 导出的这个对象是VueComponent类的实例(也是Vue的实例):对象 -> VueComponent.prototype -> Vue.prototype
+ 在对象中基于各种 options api 「例如:data、methods、computed、watch、filters、生命周期函数...」实现当前组件的功能
+ 在组件中的data不再是一个对象,而是一个“闭包”
+ 各个组件最后会合并在一起渲染,为了保证组件中指定的响应式数据是“私有的”,组件之间数据即使名字相同,也不会相互污染...所以需要基于闭包来管理
注意;App.vue页面入口相当于首页,写好的组件都导入到这个里面
<template> <div> {{ msg }} </div> </template> <script> export default { name: "Test", data() { return { //编写响应式数据 msg: "你好,世界", }; }, }; </script> <style> .box { font-size: 20px; color: red; } </style>
私有组件(使用的时候首先进行导入,然后注册,这样视图中就可以调用组件进行渲染了)
需要使用私有组件的时候,需要先导入import Test from "./Test.vue";
然后注册:这样就可以调用组件进行渲染了
<template> <div> //3.使用组件:可以使用单闭合或双闭合 <test></test> <test> </test> </div> </template> <script> //1、导入组件 import Test from "./Test.vue"; export default { name: "App", components:{ //2、注册使用的组件 Test, } }; </script>
创建全局组件
1. 创建一个局部组件
<template> <div>{{ msg }}</div> </template> <script> export default { name: "Vote", data() { return { msg: "今夜阳光明媚", }; }, }; </script>
@2 在main.js入口中,导入局部组件Vote,把其注册为全局组件
import Vote from './Vote.vue'; Vue.component('Vote', Vote)
@3 这样在任何组件(视图中),无需基于components注册,直接可以在视图中调用
调用组件的方式
调用组件的时候,可以使用:
双闭合
双闭合的方式可以使用插槽slot
@1 在封装的组件中,基于
标签预留位置
@2 调用组件的时候,基于双闭合的方式,把要插入到插槽中的内容写在双闭合之间
单闭合
组件的名字可以在“kebab-case”和“CamelCase”来切换:官方建议组件名字基于CamelCase命名,渲染的时候基于kebab-case模式使用!
插槽的作用
插槽分为了默认插槽、具名插槽、作用域插槽,
默认插槽:只需要在调用组件<test><test></test></test>
内插入我们想要的插入的html代码,会默认放到组件源代码的<slot name="default"></slot>
插槽中
组件内部 slot预留位置 默认name:default <slot></slot> 调用组件的时候 //只有一个的时候可以不用template包裹 <test> <div>头部导航</div> </test>
具名插槽:组件中预设好多插槽位置,为了后期可以区分插入到哪,我们把插槽设置名字
<test><test></test></test>
内自己写的代码,我们用template包裹代码,并把v-slot:xxx写在template上,这时就会将xxx里面的代码,包裹到组件源代码的<slot name="”xxx“"></slot>
的标签中 <slot name="xxx"></slot>
默认名字是default组件内部 <slot> 默认名字是default 调用组件:需要把v-slot写在template上 <template> ... </template> <template> ... </template> v-slot可以简写为#:#xxx</slot>
作用域插槽:把组件内部定义的数据,拿到调用组件时候的视图中使用
组件中data内的数据只能在本模块中使用,如果想让调用组件的插槽也能获取数据,就需要对组件内对的slot做bind绑定数据,调用组件的template标签做#top="AAA"
,获取数
==组件内部==: <slot name="top" :list="list" :msg="msg"></slot>
==调用组件==: <template #top="AAA"></template>
v-slot="AAA"
或:default="AAA"
获取数据组件内部 <slot></slot> 把组件中的list赋值给list属性,把msg赋值给msg属性,插槽中提供了两个作用域属性:list/msg 调用组件 <template></template> 定义一个叫做AAA的变量,来接收插槽中绑定的数据 AAA={ list:[...], msg:... }
调用组件的时候
每创建一个组件其实相当于创建一个自定义类,而调用这个组件就是创建VueCommponent(或者Vue)类的实例
组件中的script中存在的状态值和属性值?
_vode
对象的私有属性中(所以状态值和属性值名字不能重复)template标签
中调用状态值和属性值,不需要加this,直接调用状态名或属性名
script标签
中调用状态值和属性值,需要加this调用
vue中的单向数据流
父子组件传递数据时,只能由父组件流向子组件,不能由子组件流向父组件。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
组件传参的分类7种:
1.父组件向子组件传参
父组件向子组件传参:props
VueComponent
的实例第一步:父组件在组件调用标签中自定义属性
//如果想把data中的状态值传递过去需要v-bind绑定 <coma></coma>
注意 如果想把data中的状态值传递过去需要v-bind绑定
第二步:子组件通过props接收(数组,对象)
// props的值是只读的 能改,会报错 <input> //数组格式 props:["msg","num"] //对象格式 props: { msg: { //传参类型必须是字符串 type: String, //必须传参 required: true, }, num: { type: Number, //如果不传参默认是102 default: 102, }, }, //---------------------------------------- //用自定义变量numa接收num,然后页面使用numa(解决只读问题) <h1>我是子组件 coma------{{ msg }}----{{ numa }}</h1> <input> props: ["msg", "num"], data() { return { numa: this.num, }; },
2.子组件向父组件传参
子组件向父组件传参,基于==发布订阅(@xxx给子组件标签自定义事件、$emit)==
第一步:父组件在调用子组件的标签上需要自定义一个事件,这个事件及绑定的方法就会添加到子组件的事件池中:底层实质上是调用了this.$on("myEvent",fn)
<coma></coma> methods: { getData() {}, },
第二步:子组件用this.$emit()接受(this.$emit(myEvent,参数1,参数2)), 参数可以是子组件的,顺便传给父组件,实现子组件向父组件传值
<button>向父组件发送数据</button> data() { return { flag: "你很美", n: 101, }; methods: { goParentData() { //执行父组件自定义的事件 this.$emit("myEvent", this.flag, this.n); }, },
第三步:父组件使用传递过来的数据
data() { return { n: 0, flag: "", }; }, methods: { getData(...parans) { console.log(parans); //传递过来的是数组 this.n = parans[0]; this.flag = parans[1]; }, },
3.组件之间相互传参 原生事件法 (发布订阅)
b--->c发送数据
第一步:全局的main.js中创建一个全局的EventBus,挂载 vue的原型上 this.$bus
$bus.$on()
绑定的事件函数,在哪个vue实例上都可以基于$bus.$empty()
执行,还可以传值//创建一个全局的 Eventbus let Eventbus=new Vue(); //挂载 vue的原型上 后期基于this.$bus Vue.prototype.$bus=Eventbus;
第二步:comc向事件池中绑定事件:this.$bus.$on("事件名",函数)
created() { //向事件池中添加方法 this.$bus.$on("myEvent", () => {}); },
第三步:comb从事件池中获取事件函数并执行:this.$bus.$emit("事件名",想传的参数)
<button>发送数据给comc</button> data() { return { msg: "我是comb", }; methods: { send() { //执行事件池中的方法,并且传参 this.$bus.$emit("myEvent", this.msg); },
第四步 comc使用传递过来的数据
<h1>组件 comc----{{ msg }}</h1> data() { return { msg: "", }; //创建之后的钩子函数向事件池中添加方法 created() { //向事件池中添加方法 this.$bus.$on("myEvent", (value) => { console.log(value); this.msg = value; }); },
4.祖先和后代相互传参
data() { return { title: "我是about祖先", }; }, provide() { return { title: this.title, }; },
第二步:后代使用inject属性接受祖先中的参数,inject是data中的数据,是数组类型
inject: ["title"],因为inject是数组类型,所以它符合如果数据项不是对象类型,则不做劫持,如果数据项是对象,则这个对象中的属性会做劫持。
data() { return { title: "我是about祖先", }; }, //祖先 传递的title是非响应式 provide() { return { title: this.title, }; }, //------------------------------ data() { return { //obj非响应式 obj: { //title是响应式 title: "我是about祖先", }, }; }, //祖先 传递的参数失去响应式,但里面的值会是响应式 provide() { return { obj: this.obj, }; },
vue的实例中存在一些属性能够获取不同关系的元素,获取之后就可以基于这个元素获取其中的数据或方法了:
created() { console.log(this.$parent.title); },
this.$children[n]
:获取第n个子元素的vm实例mounted() { console.log(this.$children[0].msg); },
this.$root
:获取根元素的vm实例(main.js中new 的Vue实例)et mv = new Vue({ router, data() { return { rootmsg: "我是草根" } }, render: h => h(App) }).$mount('#app') --------------------- mounted() { console.log(this.$root.rootmsg); },
this.$refs
:this的子元素中需要定义ref
属性:比如ref="xxx"
:this.$refs.xxx
获取的是DOM对象this.$refs.xxx
获取的是子组件的vm实例//获取的是dom元素 <div>11111</div> mounted() { console.log(this.$refs.one); }, ----------------------------------- 获取的是组件 <comb></comb> mounted() { console.log(this.$refs.b); }, //如果不是组件获取的就是dom元素,如果是组件,获取的就是组件的实例
重点:父组件更新默认不会触发子组件更新,但是**==如果子组件中绑定调用了父组件的数据aaa,父组件的aaa数据更新触发重新渲染时,使用aaa数据{{$parent.aaa}}的子组件也会触发更新==**
一、父子组件生命周期执行过程:
beforeCreated
created
beforeMount
beforeCreate
created
beforeMount
mounted
mounted
二、子组件更新过程:
berforeUpdate
berforeUpdate
updated
updated
三、父组件更新过程:
berforeUpdate
updated
四、父组件销毁过程:
beforeDestory
beforeDestory
destoryed
destoryed
@xxx.native
: 监听组件根元素的原生事件。
例子:<my-component></my-component>
原理:在父组件中给子组件绑定一个==原生(click/mouseover...)==的事件,就将子组件变成了普通的HTML标签,不加'. native'父组件绑定给子组件标签的事件是无法触发的
虚拟DOM
虚拟DOM对象:_vnode
,作用:
第一步:vue内部自己定义的一套对象,基于自己规定的键值对,来描述视图中每一个节点的特征:
- tag标签名
- text文本节点,存储文本内容
- children:子节点
- data:属性
vue-template-compiler
去渲染解析 template 视图,最后构建出上述的虚拟DOM对象组件库
element-ui
:饿了么antdv
:蚂蚁金服iview
:京东
- Element - The world's most popular Vue UI framework
- ==vue2.xx==:elemnetui
- ==vue3.xx==:element plus
如何在项目中使用功能性组件?
==第一步==:安装element-ui
:$npm i element-ui -s
==第二步==:导入:
完整导入:整个组件库都导入进来,想用什么直接用Vue.use(xxx)即可
缺点:如果我们只用几个组件,则无用的导入组件会造成项目打包体积变大[不好],所以项目中推荐使用按需导入
按需导入:
1、需要安装依赖$ npm install babel-plugin-component
样式私有化
在Vue中我们基于scoped设置样式私有化之后:
会给组件创建一个唯一的ID(例如:data-v-5f109989
)
在组件视图中,我们编写所有元素(包含元素调用的UI组件),都设置了这个ID属性;但是我们调用的组件内部的元素,并没有设置这个属性!!
<div> <button> <span>新增任务</span> </button> </div>
而我们编写的样式,最后会自动加上属性选择器:
.task-box { box-sizing: border-box; ... } ---------编译后成为:--------- .task-box[data-v-5f1969a9]{ box-sizing: border-box; }
/deep/
:/deep/.el-textarea__inner, /deep/.el-input__inner{ border-radius: 0; }
在真实项目中,我们会把数据请求和axios的二次封装,都会放到src/api路径下进行管理
//main.js import api from '@/api/index'; // 把存储接口请求的api对象挂载搭配Vue的原型上: 后续在各个组件基于this.$api.xxx()就可以发送请求了,无需在每个组件中再单独导入这个api对象。 Vue.prototype.$api=api;
【相关推荐:javascript视频教程、web前端】
위 내용은 VUE의 두 가지 핵심인 반응형 개발과 구성 요소 기반 개발에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!