目录 § ⇧
- 0. 内存图 §
- 1.
options里有什么 § - 2. 入门属性 §
- 3.
Vue对data做了什么 § - 4. 数据响应式 §
- 5.
Vue2.x的bug§ - 6.
Vue.set和this.$set§ - 7.
data中的数组 § - 8.
ES6手写数组新push方法 V.S.ES5写法 § - 9. 总结 §
- 10. 面试题 §
[toc]
搞清构造选项,Vue 实例
0. 内存图 § ⇧
作者习惯将
Vue实例命名为vm,const vm = new Vue(options)
vm对象封装了对视图的(视图层)所有操作,包括- 数据读写
- 事件绑定
DOM更新- 不包括网络层的
Ajax
vm的构造函数是Vue,按照ES6,vm所属的类是Vuevm.__proto__ === Vue.prototype- 举例
Vue:#202; 自身属性:(*3?);Vue.prototype:#419;Vue.__proto__:(*6?) vm:#101; 自身属性:(*2?);vm.__proto__:#419Vue.prototype:#419; 自身属性:(*4?);Vue.prototype.__proto__:(*5?);- 注意点
(*1)、(*2)、(*3)、(*4)、(*5)、(*6)分别是什么 (*1)初始化传入的参数options是什么(*2)实例vm有哪些自身属性(*3)Vue函数有哪些自身属性(*4)Vue.prototype有哪些自身属性(*5)Vue.prototype.__proto__的指向(*6):Vue.__proto__ === Function.prototype基本原则:任何函数的__proto__都指向Function.prototype构造函数
options:(*1?)是new Vue()的参数,一般称之为选项或构造选项
1. options里有什么 § ⇧
文档
- 英文文档搜
options - 中文文档搜
选项
options的五类属性
对比
Vue3.x,加粗斜体代表新增,删除线代表移除,斜体代表有改动
- 数据(
Data) DOM容器/挂载点eltemplate模板内容;与render互斥render与template互斥;不手写,加载器生成renderError
- 生命周期钩子
beforeCreatedcreatedbeforeMountmountedbeforeUpdateupdatedactivateddeactivatedbeforeUnmount()beforeDestroyunmounted()destroyederrorCapturedrenderTrackedrenderTriggered
- 资源
directives指令components组件(建议用filterscomputed代替实现) 过滤器;表面上有用,实际上没用
- 组合
- 其他:先不看
属性分阶段
2. 入门属性 § ⇧
el挂载点templatedata内部数据methods方法components组件- 四个钩子
props外部数据
el 挂载点
el: "#id"- 可用
.$mount('#id')代替 - 替换
HTML的#id中的节点 - 慢速网络(
slow 3G)下,可能可以看见原来写在页面上的节点内容
template
- 语法
v-ifv-for
data 内部数据
- 支持对象和函数,优先用函数(返回值)
- 初始值 存储记录
数据响应式中 data 有 bug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24const Vue = window.Vue new Vue({ /* data: { n: 0 }, */ data() { return { // 每次都创建一个新的对象 n: 0 } }, template: ` <div class="red"> {{n}} <button @click="add">+1s</button> </div> `, methods: { add() { this.n+=1 } }, }).$mount('#xmas')import Demo from './Demo.vue'把组件传给
new Vue({render: h => h(Demo))引入的组件本质上就是一个对象
为什么推荐用函数声明
data() {...},而不是用对象的形式
- 文档中说 限制:组件的定义只接受
function,而没说明原因 - 组件是可复用的 Vue 实例
- 两个组件复用
new Vue({render: h => h(X,[h(Demo), h(Demo)])) - 如果是使用对象,创建两个相同组件,引用的
data就是指向同一个内存地址,对象在这个组件的所有实例中共享 - 有时组件需要复用,必须使得每个组件都有一份
data的拷贝,防止不同组件修改数据时被相互覆盖,用函数返回一个data对象的独立的拷贝 - 文档的说明:一个组件的
data选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝 - 为了组件间不互相影响数据(如果 Vue 没有这条规则,点击一个按钮就可能会影响到其它所有实例)
- 注意同一个组件中重复定义的数据指向同一个内存地址
官方示例
|
|
|
|
demo
See the Pen button-demo-data-use-obj by Joel Xu (@xmasuhai) on CodePen.
methods 方法
- 事件处理函数或普通函数(可在模板里调用)
- 绑定的方法必须写到
methods属性中,否则会报错function is not defined - 可以代替
filters,举例 显示数组中偶数 作为普通函数写到模板中时,必须有函数返回值(
{{filterEven(array)}})1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31const Vue = window.Vue const vm = new Vue({ data() { return { array: [1,2,3,4,5,6,7,8], } }, template: ` <div> <hr> {{array.filter(i => i % 2 === 0)}} <hr> {{filterEven(array)}} <hr> {{filterOdd()}} </div> `, methods: { filterEven(array) { return array.filter(i => i % 2 === 0) // JS Array.prototype.filter() }, // 主动在模板中调用 filterOdd() { console.log('执行了 filter 函数') return this.array.filter(i => i % 2 !== 0) }, }, })$mount('#xmas') // [ 2, 4, 6, 8 ] // [ 2, 4, 6, 8 ] // [ 1, 3, 5, 7 ]主动在模板中调用
{{filterOdd()}}的缺点是每次渲染就会调用一次进行其他页面操作也会重新调用
用
computed解决
components组件
什么是组件
- 抽象概念,可以与其他部分组合使用
- 模块化
Vue实例对象vmV.S.Vue组件components
new Vue创建的是实例Vue.component创建的是组件import xxx From '*.vue'引入的是组件
components组件的三种引入方式 组件注册
import方式- 全局方式
- 混合方式
import方式,模块化引入外部单文件组件
Vue组件,注意大小写- 三种
Vue使用方式的最后一种:单文件组件Demo.vue - 通过
import Demo from './Demo.vue'引入 - 在
Vue实例中使用另一个组件 - 命名组件
components: {Xmas: Demo} - 写到实例
vm的template中 在
template中的形式为<componentName/>1 2 3 4 5 6 7 8 9 10 11 12 13 14// 单文件组件 const Vue = window.Vue import Demo from './Demo.vue' const vm = new Vue({ components: { Xmas: Demo, // 定义组件名 }, template: ` <div class="red"> <Xmas/> </div> `, }) vm.$mount('#xmas')
JS方式 全局声明 全局注册 ⇧
- 写在全局
Vue的属性Vue.component('componentName', options)中,options是一个对象 - 在
template中的形式为<componentName/> - 第二个参数传入一个对象,此对象的和实例中传入
new Vue({})的第一个参数是类似的,可以接受的属性一样 - 可以理解为将一个复杂组件取一个名字(第一个参数),组件对象放到
Vue.component()第二个参数中 - 即
options的写法是一样的 - 可以简单理解为给实例取一个名字
不需要额外引入,可被所有其他组件引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19// 属性命名组件 const Vue = window.Vue Vue.component('Demo2', { template: ` <div>demo2</div> `, }) // 可嵌套 const vm = new Vue({ components: { Xmas: Demo, }, template: ` <div class="red"> <Demo2/> </div> `, }) vm.$mount('#xmas')
全局注册的组件在注册之后可以用在任何新创建的
Vue根实例 (new Vue) 的模板中。比如
|
|
|
|
在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用
- 全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生
全局注册的缺点
混合方式(结合以上两种方式)
在实例中的
components属性中,直接定义1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33const Demo4 = { template: ` <div>demo444<div/> `, } // 在实例中使用组件 const vm = new Vue({ // 定义组件 components: { Xmas2: { data() { return { n: 0 } }, template: ` <div> demo333's n <span>{{n}}</span> </div> `, }, Xmas3: Demo4, }, template: ` <div> <Xmas2/> <Xmas3/> </div> ` }) vm.$mount('#xmas')
组件可以理解为实例中的实例
- 组件
options的写法是一样的 - 优先使用第一种
import方式 ,优点是模块化:将独立的功能放到单独的文件中 - 取名可以相同:
components: {Demo: Demo} - 取名相同可以简化(
ES6):components: {Demo} - 可以导入多个
components: {Demo, App}分别导入Demo和App template中用相同的名字<Demo/><App/><Demo/>1 2 3 4 5 6 7 8 9 10 11 12// ES6 import Demo from './Demo.vue' const vm = new Vue({ components: {Demo}, data() {...}. template: ` <div> <Demo/> </div> `, methods: {...}, })
组件名取名规范 Vue 风格指南
- 在单文件组件、字符串模板和 JSX 中使用自闭合组件
- 组件名就是
Vue.component的第一个参数 - 为什么文件名要小写? by 阮一峰
- 避免重名冲突:直接在
DOM中使用一个组件 的时候,推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突 - 字符串模板或单文件组件的时候, 组件名大写,避免在
template中分不清<Button/>和<button><button/> - 其他文件名小写
- 命名中明确包含用途与归属
SearchButton
全局注册 V.S. 局部注册
四个钩子 ⇧
created创mounted挂updated更(destroyedunmountedvue3.x) 消- “钩子”:即切入点
created 创 ⇧
用
debugger证明组件或实例出现在内存中,而未出现在页面中1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22new Vue({ data() { return { n: 0 } }, template: ` <div> {{n}} <button @click="add">+1</button> </div> `, created() { debugger console.log('已出现在内存中,未出现在页面中') }, methods: { add() { this.n += 1 }, } }).$mount('#xmas');
mounted 挂
用
debugger证明组件或实例出现在内存中,并且也出现在页面中1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26new Vue({ data() { return { n: 0 } }, template: ` <div> {{n}} <button @click="add">+1</button> </div> `, created() { // debugger console.log('已出现在内存中,未出现在页面中') }, mounted() { debugger console.log('已出现在页面中') }, methods: { add() { this.n += 1 }, } }).$mount('#xmas');
updated 更
能保证数据更新才触发当前的事件绑定的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30new Vue({ data() { return { n: 0 } }, template: ` <div> {{n}} <button @click="add">+1</button> </div> `, created() { // debugger console.log('已出现在内存中,未出现在页面中') }, mounted() { // debugger console.log('已出现在页面中') }, updated() { console.log('已点击 更新页面') console.log(this.n) }, methods: { add() { this.n += 1 }, } }).$mount('#xmas');
destroyed(unmounted vue3.x) 消
destroyed- 需要一个子组件
destroy.vue - 从页面中消失就是
destroy - 每次消除后重新创建的都是新的,
+1后消除在创建,数据归零
Destroy.vue
|
|
怎么使用
destroy的?
- 用最简单的例子说明
在
main.js中引入
|
|
- 从页面中消失就是
destroyed v-if监听数据变化- 取反
this.isVisible = !this.isVisible
props 外部数据
- 也称属性,是由组件外部传入值的(对比内部数据
data,内部自己传值) - 从外部接受一个
message,并自动绑定到this - 在
template中,可写{{this.message}},也可以省略 this ,写成:{{message}}
声明传入内容的类型
- 传字符串:
<Props message="props"/> - 传变量:之前加冒号
:message="n"传入this.n数据,优先在data里查找变量 - 传方法:
:fn="add"传入this.add函数
区别 传字符串 V.S. 传数字(其他数据类型)
- 在
message前加冒号,声明传入的外部数据是变量,可包含数字或其他数据类型<Props :message="0"/>、<Props :message="true"/>、<Props :message=" '0' "/> - 不加冒号,就只是传字符串
将内部数据
data放到组件中的外部数据message使用
<Props :message="n"/>:message=""双引号里的是 JS 代码:message="n"传入的变量n,优先在data里查找
传递参数,调用方法
<Props :fn="add"/>,methods: {add() {...}},<button @click="fn">call fn</button>- 按钮被点击时,调用
fn fn从外部传入的一个方法props: ['message', 'fn']fn取名符合规范即可- 在外部的实例中,组件
Props的属性:fn"add"调用了实例的methods: {add() {...}}中的方法 - 可以认为是
main.js实例的方法传给了组件Props - 组件
Props里的@click="fn"调用add方法
同步更新内外数据
<div>{{n}}<Props :fn="add" :message="n"/></div>
main.js
|
|
Props.demo.vue
|
|
3. Vue对data做了什么 § ⇧
data实验
在实例外面变更
data(而非在methods绑定变更的函数 绑到对应节点的事件上)
- 示例代码 1:
myData变化 一开始
{n: 0},传给new Vue之后立即变成{n: (...)}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37const Vue = window.Vue // 禁用警告 Vue.config.productionTip = false; const myData = { n: 0 }; // 精髓1 console.log(myData) new Vue({ data: myData, template: ` <div> <div>{{n}}</div> <hr> <button @click="add">+10</button> </div> `, methods: { add() { this.n += 10; } } }).$mount("#app"); setTimeout(() => { myData.n += 10; // 精髓2 console.log(myData) }, 3000); // 控制台 // {__ob__: we} // n: (...) // __ob__: we {value: {...}, dep: ce, vmCount: 1} // get n: f () // set n: f (t) // __proto__: Object
{n: (...)}是什么,为什么表现和{n: 0}一致
了解ES6的计算属性 getter和setter
第一次打印的和第二次的区别,在后台展开查看用
setter设置属性的对象
- 调用
obj1.姓名()要加括号,调用obj2.姓名不用加括号 obj2.姓名是属性,只不过使用函数形式定义的计算属性的
setter可以用来设置之前的原始属性1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58let obj0 = { 姓: "高", 名: "圆圆", age: 18 }; // 需求一,得到姓名 let obj1 = { 姓: "高", 名: "圆圆", 姓名() { return this.姓 + this.名; }, age: 18 }; console.log("需求一:" + obj1.姓名()); // 姓名后面的括号能删掉吗?不能,因为它是函数 // 怎么去掉括号? // 需求二,姓名不要括号也能得出值 let obj2 = { 姓: "高", 名: "圆圆", get 姓名() { return this.姓 + this.名; }, age: 18 }; console.log("需求二:" + obj2.姓名); // 总结:getter 就是这样用的。不加括号的函数,仅此而已。 // 需求三:姓名可以被写 let obj3 = { 姓: "高", 名: "圆圆", get 姓名() { return this.姓 + this.名; }, set 姓名(xxx){ this.姓 = xxx[0] this.名 = xxx.slice(1) }, age: 18 }; obj3.姓名 = '高媛媛' console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`) // 总结:setter 就是这样用的。用 = xxx 触发 set 函数 console.log(obj0); console.log(obj3);可以看到属性
{姓名: (...)}并不存在一个名为
n的属性,而是有一个get n和set n来模拟对n的读写操作
为什么要使用
getter和setter模拟{n: (...)}
Object.defineProperty
- 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
- 应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用
- 语法
Object.defineProperty(obj, prop, descriptor) -
1 2 3 4 5 6 7 8 9 10let _xxx = 0 // 用来存储 xxx 的值 Object.defineProperty(obj3, 'xxx', { get() { return _xxx }, set(value) { _xxx = value }, })
_xxx是什么
代理和监听
需求一:用
Object.defineProperty定义 属性n
|
|
n的值就是0,而不是value: 0- 属性的属性
需求二:属性
n不能小于 0,即data2.n = -1应该无效,但data2.n = 1有效
- 用点运算符不能实现
用
setter1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21// 需求二:n 不能小于 0 // 即 data2.n = -1 应该无效,但 data2.n = 1 有效 let data2 = {} data2._n = 0 // _n 用来偷偷存储 n 的值 Object.defineProperty(data2, 'n', { get() { return this._n // 给外部读取值 }, set(value) { if (value < 0) return // 白写 this._n = value // 写入符合条件的值 } }) console.log(`需求二:${data2.n}`) data2.n = -1 console.log(`需求二:${data2.n} 设置为 -1 失败`) data2.n = 1 console.log(`需求二:${data2.n} 设置为 1 成功`) // 抬杠:那如果对方直接使用 data2._n 呢?this适用于任何命名(data2),表示当前对象setter按条件筛选传入的属性值Object.defineProperty可以在声明完了之后添加getter和setter计算属性即在定义完一个对象之后,添加新的计算属性
注意
Object.defineProperty定义的属性是“不存在的”Object.defineProperty的get() {}中不能返回命名的属性(xxx)本身,会造成死循环可以实现按条件筛选传入的属性值
理解为属性的属性
需求三:使用代理
proxy
|
|
- 参数为匿名的对象
{data: {n: 0}}包裹的数据data: {n: 0} - 读取
proxyObj的属性,就返回data的属性 - 设置
proxyObj的属性,就写入data的属性 proxyObj就是代理data3暴露给外部访问的代理对象- 无法擅自更改属性(
n)
需求四:绕过代理
proxy
|
|
需求五:就算用户擅自修改 myData,也要拦截他
- 限制绕过代理
proxy - 用
value存储原始的data.n delete data.n删除了原始数据,使得无法修改delete data.n可以不写,Object.defineProperty(data, 'n', {...})声明新的虚拟对象会覆盖原来的1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45// 需求五:就算用户擅自修改 myData,也要拦截他 let myData5 = { n: 0 } let data5 = proxy2({ data: myData5 }) // 括号里是匿名对象,无法访问 function proxy2({ data }/* 解构赋值 */) { // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化 // 因为我怕你们看不懂 let value = data.n // 可变的值 // delete data.n Object.defineProperty(data, 'n', { get() { return value }, set(newValue) { if (newValue < 0) return value = newValue } }) // 就加了上面几句,这几句话会监听 data // 声明代理 const proxyObj = {} Object.defineProperty(proxyObj, 'n', { get() { return data.n }, set(value) { if (value < 0) return // 这句话多余了 data.n = value } }) return proxyObj // proxyObj 就是代理 } // data3 就是 proxyObj console.log(`需求五:${data5.n}`) myData5.n = -1 console.log(`需求五:${data5.n},设置为 -1 失败了`) myData5.n = 1 console.log(`需求五:${data5.n},设置为 1 成功了`) // 这代码看着眼熟吗? // let data5 = proxy2({ data:myData5 }) // let vm = new Vue({data: myData})监听的逻辑
代理的逻辑
防止绕过条件限制修改数据
注意
- 研究方法比知识本身更重要(
console.log) - 不读源码,也可以了解真相
- 初级读源码自杀
data小结
Object.defineProperty
- 可以给对象添加属性
value - 可以给对象添加
getter/setter getter/setter用于对属性的读写进行监控
代理(设计模式)
- 对
myData对象的属性读写,全权由另一个对象vm负责 - 那么
vm就是myData的代理 - 比如
myData.n不用,偏要用vm.n(this.n)来操作myData.n
vm = new Vue({data: myData})
- 1.会让
vm成为myData的代理proxy - 2.会让
myData的所有属性进行监控 - 为什么需要监控,为了防止
myData的属性变了,vm未知 Vue可以监听到任何对myData的读写- 监控的目的是,
vm已知,属性变了就可以调用render(data) - 自动更新
UI = render(data) - 通过
this访问vm
示意
- 将数据用匿名的对象
{data:{n: 0}}传参给new Vue() - 经过
new Vue()的改造加监听,暂存let value = 0,变成{get n() {return value}, set n(v) {value = v}},实现覆盖数据原来的属性 - 并且作为原来数据的代理,将返回值
{get n() {data.n}, set n(v) {data.n = v}}赋值给变量vm - 没有删除对象,只是覆盖了对象的属性,没有改变原来的引用,即没有断开与对象的关联
如果
data有多个属性n、、就会有get n/get m/get k等
- 对所有属性循环进行监听和代理(闭包隔开作用域)
4. 数据响应式 § ⇧
什么是响应式
- 对外界刺激的反应(抽象)
Vue的data是响应式
const vm = new Vue({data: {n: 0}})- 如果修改(更新)数据
vm.n的值,那么UI中的n就会响应用户操作 Vue2通过Object.defineProperty来实现数据响应式(ES5+)
响应式网页
- 如果改变窗口大小,网页内容会做出响应,即响应式网页
- 比如 示例响应式网站 smashingMagazine
- 但是注意,一般用户不会拖动网页大小,除非
iPad分屏 - 用响应式浏览器测试 responsively
- 区别窗口响应式与媒体查询适配设备
5. Vue2.x的bug § ⇧
Object.defineProperty的问题
Object.defineProperty(obj, 'n', {...})- 必须要有一个
key(即n),才能监听和代理obj.n
没有
data.n会发生
示例一:
Vue会给出一个警告1 2 3 4 5 6 7 8 9 10// 页面无显示 new Vue({ data: { // 空 }, template: ` <div>{{n}}</div> ` }).$mount("#app"); // 控制台报错 not defined but referenced示例二:
Vue无警告Vue只会检查第一层属性是否被定义Vue不会深入检查下一层只有属性
a,却要显示属性bVue并没有监听属性b,只监听了a,即对b的操作都无效控制台无报错,
Vue无警告1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20new Vue({ data: { obj: { a: 0, // obj.a 会被 Vue 监听 & 代理 // b: undefined } }, template: ` <div> {{obj.b}} <button @click="setB">set b</button> </div> `, methods: { setB() { this.obj.b = 1; // 页面中不显示 1 // Vue.set(this.obj, 'b', 1) } } }).$mount("#app");此时如果点击
set b,视图中不会显示1因为
Vue没法监听一开始不存在的obj.b
解决办法
- 将
key都声明好,后面不用再加属性b: undefined,即在传入的参数中预留会用到的属性 使用
Vue.set或者this.$set(防止重名)1 2 3 4 5 6 7setB() { // ... // this.obj.b = 1; // 页面中不显示 1 Vue.set(this.obj, 'b', 1) this.$set(this.obj, 'b', 1) console.log(Vue.set === this.$set) }
6. Vue.set和this.$set § ⇧
作用
- 新增
key,在实例化后增加新属性,不用事先写在data里 - 自动创建代理和监听(如果没有创建过)
- 触发
UI更新(副作用,并不会立刻更新,异步更新) Vue.set(this.obj, 'b', 1)将this.obj声明传给Object.defineProperty- 之后就会响应对数据
b的操作,更新 UI
语法
|
|
举例
this.$set(this.object, 'm', 100)
小结
Vue只会监听实例化之前传进来的属性和实例化之后用Vue.set或this.$set设置的数据- 在实例化后如果直接在对象上增加新属性,
Vue不会将新属性转化为getter/setter,因为没有setter,所以在新属性的数据发生改变时,就不会通知watcher重新渲染视图 - 在传入的参数中预留会用到的属性,这样就可以对属性进行监听
- 或者是调用
Vue提供的方法来添加新属性 添加多个属性,使用
Object.assign()或_.extend()也不会让新属性被监听,在这种情况下,用原对象与要混合进去的对象的属性一起创建一个新的对象1this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })
7. data中的数组怎么破 § ⇧
data: {n: 0, obj: {}, array: []}
- 没法提前声明所有
key(‘0’, ‘1’, ‘2’…) - 示例 1: 数组的长度可以一直增加,长度无法预测,下标就是
key - 本质上
array: {0: 'a', 1: 'b', 2: 'c', length: ...} - 无法提前把数组的
key都声明出来 Vue也不能检测数组新增的下标1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25new Vue({ data: { array: ["a", "b", "c"] }, template: ` <div> {{array}} <button @click="setD">set d</button> </div> `, methods: { setD() { // this.array[3] = "d"; // 页面中不显示 'd' this.$set(this.array, 3, 'd') // this.array.push('d') console.log(this.array) // 加了一层__proto__ Vue.set(this.array, 4, 'e') this.array[0] += 0 this.array[1] += 1 this.array[2] += 2 this.array[3] += 3 this.array[4] += 4 } } }).$mount("#app");
语法
|
|
- 如何不用每次改数组都要用
Vue.set或者this.$set this.$set作用于数组时,并不会自动添加监听和代理,所以不会更新 UI
尤雨溪的做法
- 篡改数组的栈方法
API,文档中「变更方法」章节 - 7 个重新封装的
API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()添加到数组新的一层原型链上,覆盖原来同名的方法 - 例如:新的
push会做 1. 调用以前的push操作;2. 通知Vue添加监听和代理,帮你this.$set(...) 这 7 个
API都会被Vue篡改,调用后会更新UI1vm.items.splice(indexOfItem, 1, newValue)
改写数组的原型方法:
ES6写法 V.S.ES5写法
8. ES6改写数组原型方法 V.S. ES5写法 § ⇧
增加一层原型链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32// ES6 class VueArray extends Array { push(...args) { const oldLength = this.length // this 即当前数组 super.push(...args) // 先调下一层(父类)的原型方法 // this.length 改变 console.log('你 push 了') // 记录原来下标,遍历新增下标 for(let i = oldLength; i < this.length; i++) { Vue.set(this, i, this[i]) // this 即当前数组 // 将每个新增的 key 都告诉 Vue // i 的值即push的所有元素的下标 // 将数组每个变动都告诉vue,去添加监听 和代理 } }, // ... } const a = new VueArray(1, 2, 3) console.log(a) a.push(4) // ES5 - 原型 var vueArrayPrototype = { push: function() { console.log('你 push 了') return Array.prototype.push.apply(this, arguments) } } VueArrayPrototype.__proto__ = Array.prototype // 重新指向数组的原型 // 非标属性,仅供学习 var array = Object.create(vueArrayPrototype) // 将实例的原型和vueArrayPrototype连起来 array.push(1)
9. 总结 § ⇧
对象中新增的
key
Vue没有办法事先监听和代理- 使用
set来新增key,创建监听和代理,更新UI - 最好提前把属性都写出来,不要新增
key - 但数组做不到「不新增 key」
数组中新增的
key
- 不可以用
set来新增key,创建监听和代理,更新UI this.$set作用于数组时,并不会自动添加监听和代理- 不过尤雨溪篡改了 7 个数组 API 方便对数组进行增删
- 改查不需要,改即改原来的,原来本身就有监听
- 删除多余的监听器,节省内存
- 使用 Vue 提供的数组变异 API 时,会自动添加监听和代理,并异步更新
UI - 结论:数组新增
key最好通过 7 个API - 深入响应式原理-检测变化的注意事项
Vue3.x的解决方案
10. 面试题 § ⇧
- 答案解析文章
- 异步更新 UI
对 Vue 数据响应式的理解
- 数据响应式就是,将 Model 绑定到 View,当更新 Model 时,View 会自动更新
- 数据响应式强调数据驱动 DOM 生成,而不是直接操作 DOM
- 即所谓的数据驱动,指视图是由数据驱动生成的,对视图的修改,不会直接操作 DOM,而是通过修改数据
- Vue 的响应式原理依赖于
Object.defineProperty中的setter和getter - Vue 通过设定对象属性
setter/getter方法来监听并代理数据的变化 - 将一个 JS 对象作为参数传入到 Vue 中时,Vue 会遍历该对象的所有属性,使用
Object.defineProperty把这些属性全部转为getter/setter,实现监听,并且会返回一个组件实例作为该对象的代理 - 当操作组件实例的数据时,就会同时修改传入的 JavaScript 对象的数据,而且 vue 同时会监听这个 JS 对象,在 JS 对象的数据发生修改时,同时也会更新组件实例的数据
- 每个组件实例都对应一个
watcher实例,watcher实例会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染
理解了
data才能继续学习computed和watch
参考文章
相关文章
- 无