目录
this.$watch(expOrFn,callback,[option])
this.$set(target,propertyName/idnex,value)
this.$delete(target,propertyName/index)
this.$on(myEvent,function(){})
this.$once(myEvent,function(){})
this.$emit(eventName,arg1,...,argn)
Vue实例方法(数据)
this.$watch(expOrFn,callback,[option])
用于监听数据的变化,发生监听的数据发生变化后调用回调函数。
expOrFn可以是字符串(对应data中的数据名)或函数(监听return 值),callback为expOrFn的值或返回值发生变化后调用的函数(默认接收2个参数,分别是变化的新值,和变化之前的旧值)。option是可选参数为一个对象,可以设置deep或immediate属性的值为true((默认都为false)),deep是为了发现对象内部值的变化,immediate是立即以表达式的当前值触发回调(此时callback中无参数即监听的值还未发生变化)。最后该方法调用完后会返回一个取消观察函数。
注意:在带有 immediate
选项时,你不能在第一次回调时取消侦听给定的 property。
例子:
// 监听路径为this.a.b.c
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时
// 处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()
//在带有 immediate 选项时,如果你希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性
var unwatch = vm.$watch(
'value',
function () {
doSomething()
if (unwatch) {
unwatch()
}
},
{ immediate: true }
)
this.$set(target,propertyName/idnex,value)
用于解决vue2的限制(见响应式原理中的限制)。实现通过实例方法定义属性。第一个参数target为对象或数组,第二个参数根据第一个参数为属性或序号,第三个参数为值,相当于target[propertyName/index]=value,不同在于触发dom更新。
this.$delete(target,propertyName/index)
用于解决vue2的限制(见响应式原理中的限制)。实现响应式删除属性(触发dom更新即触发监听),直接通过delete删除属性时不会触发data的监听,也就不会使dom上内容响应式变化。第一个参数t为对象或数组,第二个参数根据第一个参数为属性或序号,相当于delete target[propertyName/index,不同在于触发dom更新]。
Vue实例方法(事件)
this.$on(myEvent,function(){})
自定义事件myEvent。事件可以由 vm.$emit
触发,回调函数会接收所有传入事件触发函数的额外参数。
this.$once(myEvent,function(){})
同上,只能被触发一次
this.$off(event,fun)
移除自定义事件监听器。如果没有提供参数,则移除所有的事件监听器;如果只提供了事件,则移除该事件所有的监听器;如果同时提供了事件与回调,则只移除这个回调的监听器。
this.$emit(eventName,arg1,...,argn)
触发当前实例上的自定义事件,附加参数都会传给监听器回调。
响应式原理
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty把这些 property 全部转为getter/setter。Object.defineProperty是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
响应式原理详解
Object.defineProperty函数介绍
Object.defineProperty() - JavaScript | MDN
限制
由于 JavaScript (Object.defineProperty)的限制,Vue不能检测数组(直接去赋值)和对象(添加属性或删除已有属性)的变化。例如vue的data中有obj:{a:'a'},arr:[0,1,2],当我们通过obj.b='b'(添加属性)、delete obj.a(删除已有属性)或arr[0]=9,arr[3]=1(数组直接赋值)这种形式去赋值时,vue不会监听到变化,因此dom节点完全不会有改变。
注意:通过数组的对象上改变原生数组的方法(被vue重写了)去改变的是可以监听,改变对象上的已有属性(非删除)也是可以监听的。
还有一类是直接通过vue实例定义的属性,这种属性也是无法监听的,这种属性的解决方法是只能通过vm.$set( target, propertyName/index, value )去定义。例如下面代码中的数据b:
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
//解决
Vue.set(vm, 'b', 2)
//此时b是响应的
解决
对于限制中的Vue不能检测数组(直接去赋值)和对象(添加属性或删除已有属性)的变化的解决。
不改变引用
1.改变其它数据,触发vue的监听(改变其它数据时,vue会自动比较所有数据的变化,当发现对象和数组的变化时,也会触发dom更新)。
2.对于数组使用原型上改变原生数组的方法(splice、pop、push等)。
3.使用vue提供的重新渲染方法vm.$forceUpdate()(该方法仅影响实例本身和插入插槽内容的子组件,而不是所有子组件)。
4.使用vm.$set( target, propertyName/index, value )方法。
改变引用
1.对于对象使用Object.assign方法。
2.对于数组使用原型上的其它方法(slice等)赋值。
自定义简易Vue实现
实现v-model、v-text指令以v-on绑定事件和响应式触发界面更新。使用和Vue基本相同,new Vue(el:'#app',data:{}),然后创建对应html。
注意:没有加入调度器Scheduler和各种生命周期方法。
Watcher
页面渲染时,每个被替换的表达式对应一个watcher对象,用于当值被更新时调用update方法中的cb方法更新页面视图。
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中会调用addSub被dep所记录
this.oldValue = vm[key]
Dep.target = null
}
update() {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
Dep
用于记录依赖,通常一个属性对应一个Dep对应多个Watcher对象。当属性值更新时,调用该属性对应Dep中的所有Watcher对象的update方法更新页面视图上所有用到该属性的地方。
// 发布者-目标
class Dep {
constructor() {
// 记录所有的订阅者
this.subs = []
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Observer
设置vue中的data属性变成响应式,并且对每个设置的属性添加一个Dep对象记录依赖。
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
let that = this
// 收集依赖,发送通知
let dep = new Dep()
// 如果val是的对象,将val中的属性转换为响应式。
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
return val
},
set(newValue) {
if (newValue === data[key]) {
return
}
val = newValue
// 这里的this是obj
that.walk(newValue)
// 发送更新通知
dep.notify()
}
})
}
}
compiler
替换差值表达式,以及识别v-text、v-model、v-on指令,完成对应操作并对html进行渲染。
注意:渲染的时候会创建watcher来用于之后值更新触发视图更新。
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes
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)
}
})
}
// 编译元素节点,处理指令
compileElement(node) {
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if (this.isDirective(attrName)) {
attrName = attrName.substr(2)
let key = attr.value
if (this.isEventDirective(attrName)) {
// 处理v-on事件绑定
this.eventHandler(node, attrName, key)
} else {
// 处理v-model和v-text
this.update(node, key, attrName)
}
}
})
}
// 判断是否是处理事件的指令
isEventDirective(attrName) {
return attrName.includes('on')
}
eventHandler(node, attrName, fnName) {
let eventType = attrName.substr(attrName.indexOf(':') + 1)
let fn = this.vm.$options.methods && this.vm.$options.methods[fnName]
fn && node.addEventListener(eventType, fn.bind(this.vm))
}
update(node, key, attrName) {
let updateFn = this[attrName + ' Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 处理v-text指令
textUpdater(node, value, key) {
node.textContent = value
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
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 编译文本节点,处理差值表达式
compileText(node) {
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
vue
- 把data的属性注入vue实例中(可以vm.属性名访问和设置vm.data下的属性)。
- 调用observer对象,监听数据变化。
- 调用compiler对象,解析指令和差值表达式进行页面渲染。
class Vue {
constructor(options) {
// 1.通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ?
document.querySelector(options.el) : options.el
// 2.把data中的成员转换成getter和setter,注入到Vue实例中
this._proxyData(this.$data)
// 3.调用observer对象,监听数据的变化
new Observer(this.$data)
// 4.调用compiler对象,解析指令和差值表达式
new Compiler(this)
}
_proxyData(data) {
// 遍历data中的所有属性
Object.keys(data).forEach(key => {
// 把data的属性注入vue实例中
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}