双向数据绑定
双向数据绑定就是 页面 和 数据 无论哪一方发生改变,都可以改变当前的数据。
Vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
基础知识
体验Object.defineProperty
如果不了解Object.defineProperty 传送门
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="text" id="input" />
<p id="data"></p>
</body>
<script>
const obj = {}
// 获取 input标签原生dom
const input = document.getElementById('input')
// 数据劫持。
// 当 obj.name 时必须触发get()才会返回出来。
// 当 obj.name = "xx" 时,这里是 obj.name = input.value ,触发了set 进而将data中的值也赋值了。
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
get() {
console.log(`obj.name 也就是获取值时触发`);
return input.value
},
set(newVal) {
console.log(`obj.name='xx' 也就是设置值时触发`);
input.value = newVal
document.getElementById('data').innerHTML = newVal
},
})
// 监听输入框,实现视图->数据的绑定
input.addEventListener('keyup', () => {
obj.name = input.value
})
// 这个domo是有问题了,get是无法触发,只有set触发了,只是一个deomo而已。
// 后面的代码,get() 将是至关重要
</script>
</html>
完整版
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<div id="app">
<div>
<input v-model="value" />
<p v-text="value"></p>
</div>
</div>
</body>
<script>
// 收集依赖
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend() {
if (window.target) {
this.addSub(window.target)
}
}
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 使数据变成setter getter
/**
* Observer类会附加到每一个被侦测的object上。
* 一旦被附加上,Observer会将object的所有属性转换为getter/setter的形式
* 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
*/
class Observer {
constructor(value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
/**
* walk会将每一个属性都转换成getter/setter的形式来侦测变化
* 这个方法只有在数据类型为Object时被调用
*/
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive(data, key, val) {
// 新增,递归子属性
if (typeof val === 'object') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('触发了get' + val)
dep.depend()
return val
},
set: function (newVal) {
console.log('触发了set' + newVal)
if (val === newVal) {
return
}
val = newVal
dep.notify()
},
})
}
class Watcher {
constructor(vm, node, name) {
this.vm = vm
this.node = node
this.name = name
this.value = this.update()
}
get() {
window.target = this
// 收集依赖,触发get,
let value = this.vm.data[this.name]
window.target = undefined
return value
}
update() {
this.value = this.get()
this.node.innerText = this.value
}
}
// 模板解析
function Compile(el, vue, data) {
// 关联自定义特性
if (el.attributes) {
// [].forEach.call(this,()=>{}) ==> Array.prototype.forEach(()=>{})
// 为什么不直接使用 el.attributes.forEach(()=>{}) , 因为这是dom结构不支持,使用原生for循环可以,但是采用下面方案比较
[].forEach.call(el.attributes, (attribute) => {
if (attribute.name.includes('v-')) {
Update[attribute.name](el, vue, data, attribute.value)
}
})
}
// 递归解析所有DOM
;[].forEach.call(el.childNodes, (child) => Compile(child, vue, data))
}
// 自定义特性对应的事件
const Update = {
'v-text'(el, vue, data, key) {
// 初始化DOM内容
el.innerText = data[key]
// 收集依赖
new Watcher(vue, el, key)
},
'v-model'(input, vue, data, key) {
// 收集依赖
new Watcher(vue, input, key)
// 初始化Input默认值
input.value = data[key]
// 监听控件的输入事件,并更新数据
input.addEventListener('keyup', (e) => {
vue.data[key] = e.target.value
})
},
}
function Vue(options) {
this.data = options.data
new Observer(this.data) // 劫持数据
let el = document.getElementById(options.el)
let vue = this
Compile(el, vue, this.data) // 编译模版
}
let vm = new Vue({
el: 'app',
data: {
value: 'juice',
},
})
</script>
</html>
proxy 和Object.definepropety的优缺点
vue2.0+把Object.definepropety用的出神入化,最终也没有把监听数组的操作「要么直接赋值,要么使用splice」,对象的增删也是无法检测,但是他提供了$set 以及 $delete 。最终遗憾的没有实现最直接的监听。
vue3.0 使用的是proxy 优点就是上面所说的他都能实现,而且还拓展了13种方法,性能也可以得到提升,传送门,缺点就是浏览器兼容性的,这也是尤大大考虑将proxy放在vue3.0吧。
Vue源码解读
入口文件
文件路径:core/instance/init
initMixin(){
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm); // vm.$slots
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
}
initState(vm) 初始化并监听数据的调用方法,这里设计 props watch computed data
文件路径:core/instance/state
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props); // 初始化props
if (opts.methods) initMethods(vm, opts.methods); // 初始化methods
if (opts.data) {
initData(vm); // 初始化data
} else {
observe((vm._data = {}), true /* asRootData */); // 如果data为空 创建一个观察对象
}
if (opts.computed) initComputed(vm, opts.computed); // 初始化计算属性
if (opts.watch && opts.watch !== nativeWatch) {
// 初始化watch
initWatch(vm, opts.watch); // 初始化watch
}
}
initData(vm) 初始化监听data方法,数据双向数据绑定在data上面更加全面
判断对象中的是否存在重复的属性,比如 methods 、 computed、props 、data 在同一个实例上面是不能有重名的
function initData(vm: Component) {
let data = vm.$options.data;
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
// 判断如果不是对象,警告
if (!isPlainObject(data)) {
data = {};
process.env.NODE_ENV !== "production" &&
warn(
"data functions should return an object:\n" +
"https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
vm
);
}
// proxy data on instance
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
while (i--) {
const key = keys[i];
if (process.env.NODE_ENV !== "production") {
// 判断 methods 是否已经存在data中的key,存在,则警告
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
);
}
}
// 判断 props 是否已经存在data中的key,存在,则警告
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== "production" &&
warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
);
}
// _ 或者 $ 开头的key是无法被代理的,因为他们都是私有的属性,比如$store、$router
else if (!isReserved(key)) {
// 这行代码的作用是为了把监听的数据代理到了 data 上面,方便我们使用this方法
proxy(vm, `_data`, key);
}
}
// observe data
observe(data, true /* asRootData */);
}
observe(data,true) 是准备开始监听,到了另外一个 observe 模块
Observe
Observe 是主要用于监听数据,并收集依赖。 文件路径:core/observer/index
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
// 判断当前value是否需要监听,
// 需要则 new Observer(value)
// 不需要则 value.__ob__ 返回缓存
export function observe(value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob: Observer | void;
// 如果该值已经被 observer 了,直接返回即可
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
}
// 判断符合监听规则的数据
else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
// 监听的数量+1
ob.vmCount++;
}
return ob;
}
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
// 这是一个监听数据的发起函数,通过 getter/setter 去处理
export class Observer {
value: any; // 监听值
dep: Dep; // 收集依赖
vmCount: number; // number of vms that have this object as root $data
constructor(value: any) {
this.value = value;
this.dep = new Dep(); // 给当前Observer 增加dep属性
this.vmCount = 0; // 当前监听数据的数量
// Define a property,将数据都定义在 __ob__ 上
def(value, "__ob__", this);
if (Array.isArray(value)) {
// 判断 能不能使用 __proto__
if (hasProto) {
// 将数组的方法进行监听,当用户使用数组改变data中的数组的时候就会触发 setter
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
// 监听数组的,这个方法的原理就是递归监听数据里面的每一项
this.observeArray(value);
} else {
// 监听对象/普通值
this.walk(value);
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
// 如果是对象的话,走defineReactive,最后面都会走这里
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
/**
* Observe a list of Array items.
*/
// 是数组的话
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
/**
* Define a reactive property on an Object.
*/
// 定义响应式数据
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();
// Object.getOwnPropertyDescriptor获取当前对象的描述
// {
// configurable: true
// enumerable: true
// value: 1
// writable: true
// }
// 如果不可以配置直接return,property.configurable === false
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
// 对子数据进行观测
let childOb = !shallow && observe(val);
// 核心方法 Object.defineProperty 去监听数据的变化
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 取数据时进行依赖收集
const value = getter ? getter.call(obj) : val;
// 全局收集依赖对象
if (Dep.target) {
dep.depend(); // dep中收集的是watcher
if (childOb) {
// 让对象本身进行依赖收集
childOb.dep.depend(); // {a:1} => {} 外层对象
if (Array.isArray(value)) {
// 如果是数组 {arr:[[],[]]} vm.arr取值只会让arr属性和外层数组进行收集
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
// getter收集到的状态,让watcher去更新
dep.notify();
},
});
}
更新
由上面知道 dep.notify() ,是触发更新的,其实去走watch中的代码,下面来看一下具体流程。
文件路径:core/observer/dep
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub: Watcher) {
this.subs.push(sub);
}
removeSub(sub: Watcher) {
remove(this.subs, sub);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice();
if (process.env.NODE_ENV !== "production" && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id);
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
subs[i].update() 触发 watch中的 update方法
文件路径:core/server/watcher
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
调用 flushSchedulerQueue 文件路径:core/observer/scheduler
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
最后面执行 watch 中的run 方法
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
if (this.active) {
const value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(
e,
this.vm,
`callback for watcher "${this.expression}"`
);
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
Proxy
在这里引用mdn的一句话,Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。原来的对象将不会受影响。
只接收两个参数。
target:代理的对象。
handler:13种方法,详见mdn。
const p = new Proxy(target, handler)
当我们代理数组的时候。
原数组
const arr = [1, 2, 3, 4]
代理数组
const newArr = new Proxy(arr, {
get: function (target, key, receiver) {
console.log('target:', target, 'key:', key, 'receiver:', receiver)
return target[key]
},
set: function (target, key, value, receiver) {
console.log('target:', target, 'key:', key, 'value:', value, 'receiver:', receiver)
return (target[key] = value)
},
})
console.log(newArr, 'newArr') // Proxy {0: 1, 1: 2, 2: 3, 3: 4} "newArr" 所代理的新对象
console.log(newArr[0]) // 触发get target: (4) [1, 2, 3, 4] key: 0 receiver: Proxy {0: 1, 1: 2, 2: 3, 3: 4} ,打印1
console.log((newArr[0] = 11)) // 触发set target: (4) [1, 2, 3, 4] key: 0 value: 11 receiver: Proxy {0: 1, 1: 2, 2: 3, 3: 4} ,打印11
console.log(newArr[0]) // 修改了newArr target: (4) [11, 2, 3, 4] key: 0 receiver: Proxy {0: 11, 1: 2, 2: 3, 3: 4}
console.log(newArr.push(5)) // 5
console.log(newArr) // Proxy {0: 11, 1: 2, 2: 3, 3: 4, 4: 5}
当我们代理对象的时候。
// 原对象
const obj = { name: 'juice', age: '18' }
// 代理的对象
const newObj = new Proxy(obj, {
get: function (target, key, receiver) {
console.log('target:', target, 'key:', key, 'receiver:', receiver)
return target[key]
},
set: function (target, key, value, receiver) {
console.log('target:', target, 'key:', key, 'value:', value, 'receiver:', receiver)
return (target[key] = value)
},
})
console.log(newObj, 'newObj') // Proxy {name: "juice", age: "23"} "newObj"
console.log(newObj.name) // juice
console.log((newObj.name = 'JUICE')) // JUICE
console.log(newObj.name) // JUICE
// 新增属性
console.log((newObj.hobby = '王者荣耀')) // 王者荣耀
console.log(newObj) // Proxy {name: "JUICE", age: "23", hobby: "王者荣耀"}
那么按照代理的话,把Object.defindpropety 替换成proxy其实是特别简单的,基本Vue2.0差不多,而且新增数据,和数组都能监听得到。
Vue3.0中的Proxy
Vue2.0
对象:添加属性的时候 obj.a = 1 会无法被Vue2劫持,必须使用Vue2提供的$set方法来进行更新
因为defineProperty只能对当前对象中已有的属性进行监听一个个去监听,新增加进来的,需要重新使用defineProperty,所以Vue是不知道什么时候去使用defineProperty。
Vue3.0中,使用proxy来进行数据代理没有这个顾虑
proxy对于数据的代理,是能够响应新增的属性,当新增一个属性的时候,可以响应到get中,对当前对象进行代理
const a = new Proxy({
a: 1,
b: 2,
}, {
get: function(target, value) {
console.log('get', obj, value);
return target[key]
},
set: function(target, prop, value) {
console.log('set', obj, prop, value);
return (target[key] = value)
},
})
数组:Vue针对数组是多做了一层处理,代理了数组的7个方法,这是因为使用Object.defineProperty在数组上面无法监听数组的变化,需要通过方法去修改值。
const a = new Proxy([1, 2], {
get: function(target, key) {
console.log('get', 'target', target, 'key', key)
return target[key]
},
set: function(target, key, value) {
console.log('set', 'target', target, 'key', key, value)
return (target[key] = value)
}
})
console.log(a, 'a')
a.push(1)
get [1,2] push
get [1,2] length
set [1,2] 2 3
set [1,2, 3] length 3
由于proxy会触发两次,Vue3只会在prop为length值才进行更新
Vue2 如果没有设置Object.freeze默认递归data里面的数据做响应式处理,所以不建议在data中的数据定义嵌套太多层,Vue3的proxy是懒递归,不会一上来就递归,性能就相对好点
各位看官如遇上不理解的地方,或者我文章有不足、错误的地方,欢迎在评论区指出,感谢阅读。