赞
踩
有些鸟来到世间是为了做自己认为对的事,不是躲枪子儿来的。 如果没被干掉,就继续彪悍、嚣张下去; 如果被干掉了,老子就认了。
响应式基础结构:
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <title>Vue 基础结构</title> </head> <body> <div id="app"> <h1>差值表达式</h1> <h3>{{ msg }}</h3> <h3>{{ count }}</h3> <h1>v-text</h1> <div v-text="msg"></div> <h1>v-model</h1> <input type="text" v-model="msg"> <input type="text" v-model="count"> </div> <script src="./vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 20, items: ['a', 'b', 'c'] } }) </script> </body> </html>
页面展示:
官方文档: 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
模拟 Vue 中的 data 选项:
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>defineProperty</title> </head> <body> <div id="app"> hello </div> <script> // 模拟 Vue 中的 data 选项 let data = { msg: 'hello' } // 模拟 Vue 的实例 let vm = {} // 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作 Object.defineProperty(vm, 'msg', { // 可枚举(可遍历) enumerable: true, // 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义) configurable: true, // 当获取值的时候执行 get () { console.log('get: ', data.msg) return data.msg }, // 当设置值的时候执行 set (newValue) { console.log('set: ', newValue) if (newValue === data.msg) { return } data.msg = newValue // 数据更改,更新 DOM 的值 document.querySelector('#app').textContent = data.msg } }) // 测试 vm.msg = 'Hello World' console.log(vm.msg) </script> </body> </html>
对对象中的所有的属性进行双向数据绑定,进行遍历:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>defineProperty 多个成员</title> </head> <body> <div id="app"> hello </div> <script> // 模拟 Vue 中的 data 选项 let data = { msg: 'hello', count: 10 } // 模拟 Vue 的实例 let vm = {} proxyData(data) function proxyData(data) { // 遍历 data 对象的所有属性 Object.keys(data).forEach(key => { // 把 data 中的属性,转换成 vm 的 setter/setter Object.defineProperty(vm, key, { enumerable: true, configurable: true, get () { console.log('get: ', key, data[key]) return data[key] }, set (newValue) { console.log('set: ', key, newValue) if (newValue === data[key]) { return } data[key] = newValue // 数据更改,更新 DOM 的值 document.querySelector('#app').textContent = data[key] } }) }) } // 测试 vm.msg = 'Hello World' console.log(vm.msg) </script> </body> </html>
模拟 Vue 中的 data 选项:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Proxy</title> </head> <body> <div id="app"> hello </div> <script> // 模拟 Vue 中的 data 选项 let data = { msg: 'hello', count: 0 } // 模拟 Vue 实例 let vm = new Proxy(data, { // 执行代理行为的函数 // 当访问 vm 的成员会执行 get (target, key) { console.log('get, key: ', key, target[key]) return target[key] }, // 当设置 vm 的成员会执行 set (target, key, newValue) { console.log('set, key: ', key, newValue) if (target[key] === newValue) { return } target[key] = newValue document.querySelector('#app').textContent = target[key] } }) // 测试 vm.msg = 'Hello World' console.log(vm.msg) </script> </body> </html>
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)
let vm = new Vue()
// { 'click': [fn1, fn2], 'change': [fn] }
// 注册事件(订阅消息)
vm.$on('dataChange', () => {
console.log('dataChange')
})
// 触发事件(发布消息)
vm.$emit('dataChange')
// 这里很难体现发布者、订阅者、观察者,他们都是 vm
// eventBus.js // 事件中心 let eventHub = new Vue() // ComponentA.vue // 发布者 addTodo: function () { // 发布消息(事件) eventHub.$emit('add-todo', { text: this.newTodoText }) this.newTodoText = '' } // ComponentB.vue // 订阅者 created: function () { // 订阅消息(事件) eventHub.$on('add-todo', this.addTodo) }
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <title>发布订阅模式</title> </head> <body> <script> // 事件触发器 class EventEmitter { constructor () { // { 'click': [fn1, fn2], 'change': [fn] } this.subs = Object.create(null) // 创建的对象原型属性为null } // 注册事件 $on (eventType, handler) { this.subs[eventType] = this.subs[eventType] || [] this.subs[eventType].push(handler) } // 触发事件 $emit (eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach(handler => { handler() }) } } } // 测试 let em = new EventEmitter() em.$on('click', () => { console.log('click1') }) em.$on('click', () => { console.log('click2') }) em.$emit('click') </script> </body> </html>
Watcher
update()
: 当事件发生时,具体要做的事情subs
数组:存储所有的观察者addSub()
: 添加观察者notify()
: 当事件发生,调用所有观察者的update()
方法<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>观察者模式</title> </head> <body> <script> // 发布者-目标 class Dep { constructor () { // 记录所有的订阅者 this.subs = [] } // 添加订阅者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发布通知 notify () { this.subs.forEach(sub => { sub.update() }) } } // 订阅者-观察者 class Watcher { update () { console.log('update') } } // 测试 let dep = new Dep() let watcher = new Watcher() dep.addSub(watcher) dep.notify() </script> </body> </html>
发布订阅模式的执行过程:
组件A
中新增了一个待办事件add
,那么此时会调用事件中心的$emit
方法来触发add
事件$emit
会找到事件中心的add
事件处理函数并执行,这个事件处理函数是由组件B
提供的B组件
想要知道组件A
中的数据是否发生了变化就要订阅事件中心中的add
事件<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Vue 基础结构</title> </head> <body> <div id="app"> <h1>差值表达式</h1> <h3>{{ msg }}</h3> <h3>{{ count }}</h3> <h1>v-text</h1> <div v-text="msg"></div> <h1>v-model</h1> <input type="text" v-model="msg"> <input type="text" v-model="count"> </div> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 20, items: ['a', 'b', 'c'] } }) </script> </body> </html>
Vue 实例
观察模拟的成员:观察到data
中的成员具有get
和set
属性,所以需要为其定义响应式转换成getter
和setter
注入到Vue实例中,这样可以直接通过this.msg
,this.count
来使用
需要模拟的属性:
data
选项中的成员被记录到 $data 中, 并且转换成 getter
和 setter
, $data 中的 setter
是真正监视数据变化的地方VueRouter
时,router
实例也将作为参数记录到 $options 中_data
和 $data
指向的是同一个对象,_
开头的是私有成员,$
开头的是公共成员,我们只需要模拟 $data
就可以了el
, 我们设置 el
选项时,可以是选择器,也可以是一个DOM对象。如果是选择器,vue
构造函数内部需要把这个选择器转换成对应的DOM对象。data
中的成员注入到 Vue
实例,并且把 data
中的成员转成 getter
/ setter
+
是对外公开的方法,-
是静态方法
// js/vue.js class Vue { constructor (options) { // 1. 通过属性保存选项的数据 this.$options = options || {} this.$data = options.data || {} // $data就是我们实例化时传递进去的data对象参数 // 如果是字符串就准换成dom对象,如果要是选择器选中的dom就直接使用 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 2. 把data中的成员转换成getter和setter,注入到vue实例中 this._proxyData(this.$data) // 3. 调用observer对象,将数据传递到$data中,监听$data的数据变化 new Observer(this.$data) // 4. 调用compiler对象,解析指令和差值表达式 new Compiler(this) } _proxyData (data) { // 只是定义了get和set,后续的响应式是由observer定义的 // 遍历data中的所有属性 Object.keys(data).forEach(key => { // 把data的属性注入到vue实例中 Object.defineProperty(this, key, { // this指向vue实例,而不是vue中的data enumerable: true, configurable: true, get () { return data[key] // vue本身是不存储数据的,访问的其实是$data中的数据 }, set (newValue) { // 能接收一个变化后的值 if (newValue === data[key]) { return } data[key] = newValue } }) }) } }
walk: 遍历对象中的所有属性
defineReactive: 定义响应式数据(把属性转换成 getter、setter)
// js/observer.js class Observer { constructor (data) { this.walk(data) } walk (data) { // 1. 判断data是否是对象 if (!data || typeof data !== 'object') { return } // 2. 遍历data对象的所有属性 Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive (obj, key, val) { let that = this ... // 如果val是对象,把val内部的属性转换成响应式数据 this.walk(val) Object.defineProperty(obj, key, { // 这个obj就是$data,存在对get的引用,所以产生闭包 enumerable: true, configurable: true, // 此处使用 val 而不使用obj[key]的原因是防止发生死递归,即obj[key]又会触发get get () { // return obj[key] // $data引用了get发生闭包 不会释放 val变量 return val }, set (newValue) { if (newValue === val) { return } val = newValue // 新赋值的属性如果是对象,会遍历对象调用 defineReactive // 注意this指向,此时为data对象 因此需要在外部记录 that = this that.walk(newValue) // 发送通知 } }) } }
observer
定义的getter
和setter
,为啥一开始_proxyData
也定义了一次?
Object.defineProperty
说起,他的三个参数分别为数据劫持的对象,劫持的属性,设置get
和set
的配置对象。_proxyData
是在Vue
类中使用的,实例化的时候,类中的this
会指向实例对象,开辟新的空间来存储类中定义的数据,比如vue
类中定义的this.$data = options.data || {}
,实例化后每个实例都会开辟一个新的对象来存储这个options
中的data
数据,并且不会互相干扰_proxyData中
使用Object.defineProperty(this,key,{})
是在向vue实例中注入$data
中的数据,让vue
实例对象本身就具备这些数据,可以通过http://vm.xxx
的形式直接访问这些数据,并为这些成员添加上了get
和set
使其具备响应式observer
中使用的Object.defineProperty(this,key,{})
是在为vue
实例中的$data
中的成员定义get
和set
,并进行其他操作,如响应式_proxyData
是关联vue
实例对象本身的属性数据和$data数据源中的数据,是二者之间的交互,二者之间的数据响应式Observer
是关联vue
实例中数据源$data
中的数据和页面之间的交互以及变动响应式watcher
实例对象el
是Vue
传递过来的options.el
,要将其转换成DOM
对象vm
是Vue
的实例,后面的方法要用到vm
实例中的数据compile(node)
接收并遍历DOM对象的所有节点,判断节点类型compileElement(node)
解析元素节点中的指令compileText(node)
解析文本节点中的插值表达式isTextNode(node)
判断是否为文本节点isElementNode(node)
判断是否为元素节点isDirective(attrName)
判断是否是个指令// js/compiler.js class Compiler { constructor (vm) { this.el = vm.$el this.vm = vm this.compile(this.el) } // 编译模板,处理文本节点和元素节点 compile (el) { } // 编译元素节点,处理指令 compileElement (node) { } // 编译文本节点,处理差值表达式 compileText (node) { } // 判断元素属性是否是指令,在compileElement中调用 isDirective (attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点,在compileText中调用 isTextNode (node) { return node.nodeType === 3 } // 判断节点是否是元素节点,在compileElement中调用 isElementNode (node) { return node.nodeType === 1 } }
遍历所有节点,而不是元素,因为文本节点中包含着插值表达式
// 编译模板,处理文本节点和元素节点 compile (el) { let childNodes = el.childNodes // 子节点 // 用 Array.from() 将伪数组转化为数组 Array.from(childNodes).forEach(node => { if (this.isTextNode(node)) { // 处理文本节点 this.compileText(node) } else if (this.isElementNode(node)) { // 处理元素节点 this.compileElement(node) } // 判断node节点,是否有子节点,如果有子节点,要递归调用compile if (node.childNodes && node.childNodes.length) { this.compile(node) } }) }
编译插值表达式
// 编译文本节点,处理差值表达式 compileText (node) { // console.dir(node) 以对象的形式打印参数 // {{ msg }} // . 匹配任意的单个字符 // + 匹配前面修饰的内容出现一或多次 // ? 非贪婪模式 尽可能早的结束匹配 // () 分组 提取某个位置的内容 let reg = /\{\{(.+?)\}\}/ // 通过查看node 可以使用textContent或者nodeValue获取文本节点内容 let value = node.textContent if (reg.test(value)) { // RegExp是正则的构造函数; 使用$1可以获取第一个分组的内容; trim() 去空格 let key = RegExp.$1.trim() // 将文本节点原来内容的插值表达式替换成变量对应的值 node.textContent = value.replace(reg, this.vm[key]) ... } }
// 编译元素节点,处理指令 compileElement (node) { // console.log(node.attributes) // 遍历所有的属性节点 // 用 Array.from() 将伪数组转化为数组 Array.from(node.attributes).forEach(attr => { // 通过查看 node.attributes // 可以使用 node.attributes.name 获取属性名称 // 使用 node.attributes.value 获取属性值 // 判断是否是指令(以v-开头的) let attrName = attr.name if (this.isDirective(attrName)) { // v-text --> text 去掉v-这个前缀 attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update (node, key, attrName) { let updateFn = this[attrName + 'Updater'] updateFn && updateFn(node, this.vm[key]) } // 下面这三个都是首次渲染时来解析vue指令的,后续变化需要依赖后面的watcher实例对象 // 处理 v-text 指令 textUpdater (node, value) { node.textContent = value // value是指令属性的属性值 } // 处理 v-model 指令 modelUpdater (node, value) { node.value = value } // 编译文本节点,处理差值表达式 compileText (node) { let reg = /\{\{(.+?)\}\}/ // 通过查看node 可以使用textContent或者nodeValue获取文本节点内容 let value = node.textContent if (reg.test(value)) { // RegExp是正则的构造函数; 使用$1可以获取第一个分组的内容; trim() 去空格 let key = RegExp.$1.trim() // 将文本节点原来内容的插值表达式替换成变量对应的值 node.textContent = value.replace(reg, this.vm[key]) ... } }
subs
用来保存所有观察者watcher
addSubs(sub)
向subs
数组中添加观察者notify
当数据发生变化时就调用notify
通知所有的观察者// js/dep.js class Dep { constructor () { // 存储所有的观察者(Watcher) this.subs = [] } // 添加观察者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发送通知 notify () { this.subs.forEach(sub => { sub.update() }) } }
// js/observer.js class Observer { ... defineReactive (obj, key, val) { let that = this // 负责收集依赖,并发送通知 let dep = new Dep() this.walk(val) Object.defineProperty(obj, key, { ... get () { // 在get方法中收集依赖,这个依赖就是某个对象使用了当前数据 // target属性(观察者对象)是在 Watcher 中添加的 Dep.target && dep.addSub(Dep.target) return val }, set (newValue) { ... // 在set方法中发送通知 dep.notify() } }) } }
dep
通知所有的 Watcher
实例更新视图dep
对象中的subs
数组中添加自己
Vue
实例,用来获取data
数据data
中的属性名称watcher
更新视图时所作的事是不一样的,所以watcher
里面有个回调函数,实例化update() 用来更新视图
// js/watcher.js class Watcher { constructor (vm, key, cb) { // 实例化时候传入的参数 this.vm = vm // data中的属性名称 this.key = key // 回调函数负责更新视图 this.cb = cb // 把watcher实例对象记录到Dep类的静态属性target中 // 也就是向Dep类上添加一个新的静态属性target // 便执行Observer 的 get() 方法来向subs中增加内容: // Dep.target && dep.addSub(Dep.target) Dep.target = this // 触发get方法,在get方法中会调用addSub // Dep.target && dep.addSub(Dep.target) this.oldValue = vm[key] // 自身就作为一个watcher,依赖了data中的数据 // 又因为每当有对象引用当前数据都会触发get // 为了防止重复叠加target中的内容,所以每次向subs中添加后要清空 Dep.target,保证每个引用对象每次只添加一个当前所属的watcher Dep.target = null } // 当数据发生变化的时候更新视图,每当数据变化后会调用setter,setter中会调用每个watcher的update方法,如果要调用update就说明数据已经变化了,也就是说这个函数是在vm数据变化后调用的,所以此时vm中的数据是最新的变化后的数据 update () { // 当调用Update的时候数据已经发生了变化,所以调用时可以以参数形式传入 let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) // 这个函数定义在compile模块中,把当前最新的数据传到compile中 } }
Watcher
对象的时候传递的 cb
回调函数,最终调用这个 cb
回调函数会将 newValue
传递给 cb
DOM
,而我们所有的 DOM
操作都在 Compile
中Compile
中创建 watcher
实例对象,找到把数据渲染到 DOM
的位置,也就是处理指令和插值表达式的位置都有数据的变化,然后把变化后的数据渲染到视图,所以这里要创建 watcher
的实例化对象,进行数据的监听class Compiler { ... update (node, key, attrName) { let updateFn = this[attrName + 'Updater'] // this是由函数的调用形式觉得的,由于下面这句代码中没有使用this来调用而是直接调用的 // 所以updateFn内部的this默认并不执行compiler实例,所以 通过 call 来解决 updateFn && updateFn.call(this, node, this.vm[key], key) // this.updateFn 这样内部是指向compiler实例,上面的不是所以需要用call来修改 } // 下面这三个处理默认都是首次渲染,解析vue指令的,后续变化要使用watcher实例对象 // 处理 v-text 指令 textUpdater (node, value, key) { node.textContent = value // vm是当前vue实例,key是当前vue实例中的data中的属性,newValue是调用时候传递的值 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // 处理 v-model 指令 modelUpdater (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定,此处在下一节详细实现 // 注册 input 事件: 事件名称 处理函数 // node.addEventListener('input', () => { // this.vm[key] = node.value // }) // } // 编译文本节点,处理差值表达式 compileText (node) { ... if (reg.test(value)) { ... node.textContent = value.replace(reg, this.vm[key]) // 创建watcher对象,当数据改变更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } ... }
DOM
,最终都是把数据渲染到 DOM
元素上,这三个方法都是在页面首次加载的时候执行的。Watcher
对象,当数据改变的时候,Dep
对象会通知所有的 Watcher
对象重新渲染视图。// compiler.js
// v-model
modelUpdater (node, value, key) {
// ...
// 双向绑定
// 注册 input 事件: 事件名称 处理函数
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
是响应式的。set 方法 记录新的值 调用 walk 方法,其中会遍历这个对象的所有属性重新把它定义成响应式数据
不是响应式的。
Vue
实例后,新增一个成员,此时data
并没有定义该成员,data
中的成员是在创建Vue
对象的时候new Observer
来将其设置成响应式数据,当Vue
实例化完成之后,再添加一个成员,此时仅仅是给vm
上增加了一个js
属性而已,因此并不是响应式的
对于已经创建的实例,Vue
不允许动态添加根级别的响应式属性。但是可以使用Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式属性。还可以使用vm.$set
实例方法,这也是全局Vue.set
方法的别名。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。