一、引言
Vue 作为近几年发展最快的JS框架, 其崛起主要原因不单单是因为粉丝的过度追捧,也并不是因为某个大公司的权威推动。
为什么使用Vue,道理很简单,因为Vue好用,并且它同时具备angular和react的优点,轻量级,api简单,文档齐全,简单强大。那么为什么开发者觉得他好用呢?我归纳为以下几点:
1.声明式渲染
2.响应式的数据绑定
3.组件化
4.Virtual DOM
在这个章节,作者通过Vue源码和思维导图的方式深入讲解『响应式的数据绑定』即双向绑定原理(由于售卖前端团队使用的Vue源码版本Vue2.6,因此本文基于Vue2.6源码讲解),后面还会实现一个简易的demo模拟Vue双向绑定原理。
二、深入理解
1.双向绑定
学术圈对Vue双向绑定的答案有很多,但既然是讲原理,我就本着通俗易懂的方式,简单来说就是数据和视图其中一方做出修改,另一方也会随之变动,即视图能够驱动数据,数据也能驱动视图。为了方便理解,我们对比一下数据单项绑定和双向绑定的有什么区别
单向绑定 我们以小程序为例:
wxml:
<view>
<text>text:</text>
<text>{{name}}</text>
</view>
<view style="display:flex;">
<text>input:</text>
<input type="text" value='{{name}}' bindinput='bindName'/>
</view>
js:
const app = getApp()
Page({
data: {
name: '坤坤'
},
})
此处应有动图展示效果,以后补全
双向绑定我们当然要用Vue举例了:
<label>
{{keyword}}
<el-input
size="small"
v-model="keyword"
@keyup.enter.native="handlelist"
placeholder="请输入节点IP"
clearable
/>
<label>
<el-button
size="small"
type="primary"
@click="handlelist"
>搜索</el-button>
</label>
</label>
keyword: string = ''
此处应有动图展示效果,以后补全
小程序需要通过bind和setData的方式才能实现双向绑定
wxml:
<view>
<text>text:</text>
<text>{{name}}</text>
</view>
<view style="display:flex;">
<text>input:</text>
<input type="text" value='{{name}}' bindinput='bindName'/>
</view>
js:
const app = getApp()
Page({
data: {
name: '坤坤'
},
bindName: function (event) {
this.setData({
name: event.detail.value
})
}
})
此处应有动图展示效果,以后补全
由此,我们了解到双向绑定是什么,即:
1.视图驱动数据——>事件绑定
2.数据驱动视图——>数据劫持和订阅发布
本章我们主要讲解第二点,也是Vue响应式原理的核心部分:数据驱动
下面给出一个概括性的思维导图:
监听器Observer:vue对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()方法对上述每个属性都加上setter和getter方法,这样,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据的变化。
解析器Compile:解析Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一但数据有变动,收到通知,调用更新函数进行数据更新。
订阅者Watcher:Watcher订阅者是Observer和Compiler之间通信的桥梁,主要的任务是订阅Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Complier中对应的更新函数。
订阅器Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者Watcher,对监听器Observer和订阅者Watcher进行统一管理。
通过这个导图我们看到数据劫持和发布订阅都是通过什么实现的,首先看数据劫持
2.数据劫持
在Vue2.0版本中 数据劫持是通过Object.DefineProperty() 实现的,当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 会遍历此对象所有的 property,并使用 Object.DefineProperty() 把这些 property 全部转为 getter/setter
额外声明:Object.DefineProperty() 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。(es5-shim可以让一些低级的浏览器支持最新的ecmascript5的一些特性)
3.发布订阅
发布订阅是一种设计模式,我们看看Vue源码中是如何实现的:
首先我们要找到处理响应式的入口:
- 在构造函数中,调用了 src/core/instance/init.js 中的 _init 方法
- 在 _init 方法中调用了 src/core/instance/state.js中定义的 initState(vm):初始化 Vue 实例的状态
- 初始化了 data、props、methods等
- initState(vm) 方法中判断:
- 如果定义了 data 就调用 initData(vm)把 data 中的成员注入到 Vue 实例,并且把它转换成响应式对象。
- initData() 方法最后调用了 observe() 方法
- 如果没有定义 data,就直接调用 observe()
- 初始化一个空的响应式对象
- observe 就是响应式处理的入口。
observe() 方法在src\core\observer\index.js中定义。
observer(监听器) 文件夹中的文件,都是和响应式处理相关的。
①监听器
/* @flow */
import Dep from './dep'
import VNode from '../vdom/vnode'
import { arrayMethods } from './array'
import {
def,
warn,
hasOwn,
hasProto,
isObject,
isPlainObject,
isPrimitive,
isUndef,
isValidArrayIndex,
isServerRendering
} from '../util/index'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
shouldObserve = value
}
export class Observer {
// 用Flow 语法为属性指定类型
value: any; // 观测对象
dep: Dep;// 依赖对象
vmCount: number; // 计数器
constructor (value: any) {//初始化
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)// 将实例 this 挂载到观察对象 value 的 __ob__ 属性,这也是我们打印属性会看到__ob__的由来
if (Array.isArray(value)) {// 判断属性值是否是数组
if (hasProto) {//数组响应式处理
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)// 为数组中的每一个对象元素创建一个 observer
} else {
this.walk(value)//对象响应式处理
}
}
walk (obj: Object) {//遍历每一个属性,调用 defineReactive方法将每个属性做响应式处理
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {//数组每个属性递归响应式处理
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])//遍历并通过Object.defineProperty 挂载方法
}
}
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&//某些情况会通过设置false禁用响应,这是逻辑上的优化.例如实例上定义props,没有意义但是不会报错
!isServerRendering() &&//判断是否是服务器渲染
(Array.isArray(value) || isPlainObject(value)) &&//判断是否是纯粹的 [object object] 对象
Object.isExtensible(value) &&
!value._isVue//判断是否是Vue实例
) {
ob = new Observer(value)//对value进行响应式操作
}
if (asRootData && ob) {//如是根数据,计数
ob.vmCount++
}
return ob
}
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()// 创建依赖对象实例,为当前属性收集依赖
const property = Object.getOwnPropertyDescriptor(obj, key)// 获取属性的描述符对象
if (property && property.configurable === false) {// 判断属性是否可配置
return
}
// 提供预定义的存取器函数,如果obj是用户定义的对象,可能自定义了get/set,所以获取用户定义的get/set,在其基础上扩展 收集依赖 和 派发更新 的功能
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(obj, key, {// 把属性转换成 getter/setter
enumerable: true,
configurable: true,
get: function reactiveGetter () {// 首先判断是否预定义了getter,如果存在则获取并返回getter调用的返回值,如果不存在则直接返回之前获取的 初始值
const value = getter ? getter.call(obj) : val
if (Dep.target) {// 依赖收集
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {// 判断值是否发生了变化
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return// 判断是否只读属性
if (setter) {// 存在预定义的setter则调用
setter.call(obj, newVal)
} else {//没有则直接赋值
val = newVal
}
childOb = !shallow && observe(newVal)// 如果新值是对象,并且是深层监听,则将这个对象转化为响应式,并返回给 childOb
dep.notify()//分发更新
}
})
}
我们看到,Oberve中实现响应式的核心方法为defineReactive(),它调用了Object.defineProperty(),而在Object.defineProperty()主要进行了两项操作,首先在get中进行了依赖收集,然后在set中进行了分发更新,它们都用到了dep中的方法,那下面我们来看看dep都包含了什么
②订阅器
在src\core\observer\dep.js中
/* @flow */
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
export default class Dep {
static target: ?Watcher;//一个watcher类型的指针变量 收集依赖的关键性道具
id: number;// dep的id
subs: Array<Watcher>;//用来收集watcher对象的数组 收集依赖的容器
constructor () {//每创建一个Dep实例+1,作为唯一标识
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)// 如果这个指向已经存在,就把订阅器对象添加到订阅者的依赖中,addDep为订阅者的方法,Dep.target相当于正要建立依赖的那个订阅者,也就是watcher
}
}
notify () {//分发更新
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null//全局唯一,同一时间只有一个watcher被使用
const targetStack = []
export function pushTarget (target: ?Watcher) {//入栈
targetStack.push(target)
Dep.target = target
}
export function popTarget () {//出堆
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
这里我们看到前面observe中调用的两个方法,depend和notify:
- depend通过调用watcher中的adddep方法建立依赖,设置 Dep.target 是在创建 watcher 对象的位置:src\core\instance\lifecycle.js的mountComponent()方法中new了一个Watcher,然后在Watcher的构造函数中调用了get()方法,get()放开中调用了 pushTarget()方法,内部将自己记录到了Dep.target。
- notify通过id进行排序,按顺序分发,更新时按照顺序添加到任务队列
既然是队列,那么为什么这里会有进栈出栈的方法呢?
由于Vue2.0以后每一个组件都会调用mountComponent,在其中创建一个watcher对象,如果组件有嵌套的话,当渲染A父件的时候发现,自己还有子组件吗,于是要先去渲染子组件,这时子组件的渲染过程就被挂载起来,父组件所对应的watcher对象也应该被存储到栈中(pushTarget),当子组件渲染完成后,会把自己对应的watcher从栈中弹出(popTarget),然后继续去执行父组件的渲染。
这时我们想到了什么?是不是想到了Vue中父子组件的生命周期?
- 父beforeCreate->
- 父created->
- 父beforeMount->
- 子beforeCreate->
- 子created->
- 子beforeMount->
- 子mounted->
- 父mounted
(默默复习一遍父子组件生命周期,是不是恍然大悟)
③订阅者
在src\core\observer\watcher.js中
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {//是否是渲染Watcher
vm._watcher = this//把渲染Watcher记录到_watcher
}
vm._watchers.push(this)//把渲染 watcher、计算属性、侦听器,记录到_watchers,存储所有的watcher
if (options) {//Watcehr中最终要更新视图
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy//是否延迟更新视图,计算属性的Watcher:lazy为true 因为计算属性要在数据变化后才会更新视图
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid //订阅者唯一标识
this.active = true//标识当前watcher是否是活动的
this.dirty = this.lazy//dirty用于缓存计算属性watcher的值,计算属性watcher的lazy为true,这里初始化dirty为true,当计算属性watcher的getter触发时,会判断dirty,如果dirty为false,直接获取它的缓存值:watcher.value,如果dirty为true,则调用 watcher.evaluate -> watcher.get 获取,并把dirty设置false,当计算属性watcher的update触发时会将dirty设置为true
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {//设置watcher的getter
this.getter = expOrFn 如果 expOrFn 是方法,直接设置为getter
} else {
this.getter = parsePath(expOrFn)//监听器的Watcher会传入字符串(侦听的属性),例如'person.name',parsePath('person.name')返回一个函数,用于获取person.name的值
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy//判断是否延迟执行,get中调用了当前组件的getter,并将this指向Vue实例,将get的返回值记录到 this.value中
? undefined
: this.get()
}
get () {
pushTarget(this)// 把当前的 Watcher 对象存入到 targetStack(target栈)中
let value
const vm = this.vm//缓存自己
try {
value = this.getter.call(vm, vm)//get方法的核心:用getter
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {//深度监听
traverse(value)
}
popTarget()//订阅者出栈
this.cleanupDeps()//释放依赖
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {//newDepIds newDeps用于存储watcher对象依赖的所有dep,最后会被清空,depIds deps 保留最终的结果,判断是否已经依赖了当前dep对象
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)//调用dep对象的addSub方法把当前watcher对象添加到subs大数组
}
}
}
cleanupDeps () {//清空newDepIds和newDeps
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
update () {
/* istanbul ignore else */
if (this.lazy) {//lazy默认为false
this.dirty = true
} else if (this.sync) {//sync默认为false
this.run()
} else {
queueWatcher(this)//把当前watcher放到一个队列中
}
}
run () {
if (this.active) {
const value = this.get()//调用watcher的get方法,并记录返回结果,对渲染watcher来说updateComponent是没有返回值的,所以渲染watcher在这里value = undefined
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value//辞旧
this.value = value//迎新
if (this.user) { //判断如果当前是用户的watcher
try {//就调用它的callback回调函数(例如侦听器对应的function),使用try catch 是避免用户定义的回调发生异常
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)//渲染 watcher的callback为空
}
}
}
}
}
由此我们看到如果数据更新后,监听器会调用 dep 对象的 notify 方法,而notify方法中调用了 watcher 的 update,update又调用了queueWatcher,先把watcher放到队列中,然后遍历这个队列,然后去调用每个watcher的run方法,run 方法中最终:
- 渲染 watcher:调用watcher的updateComponent()函数,函数内部调用了 _render 和 _update 方法:
- _render:调用用户传入的 render 或 编译器生成的 render,创建 VNode
- _update:调用 patch 函数生成真实DOM,渲染到视图
- 其他 watcher:调用用户定义的回调函数
在响应式实现的过程中还有一个很重要的解析器Compiler,我们会在后面的章节中单独讲解。
三、源码实战
以上从依赖收集到分发更新分析了一遍源码,自己也尝试撸一个demo,加深理解:
首先我根据自己的理解画一张导图:
准备一个空文件夹,新建一个html页面,添加init.js(后续章节讲解),observer.js,dep.js,watcher.js
结合导图,根据上面的理解,那么首先要对数据进行劫持监听,所以我们首先要设置一个监听器Observer,通过Object.defineProperty()方法来监听所有的属性,get收集依赖,当属性变化时,就需要用set发起通知,通知每个订阅者Watcher,看是否需要更新。因为属性可能是多个,所以可能会有多个订阅者,因此我们需要一个消息订阅器Dep用数组subs来收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理。由于在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图,由于解析器原理比较复杂,本章这里只用watcher的callback函数直接返回更新后的dom,简单略过解析过程,具体的解析器原理将在以后的章节中详细讲解。
首先,写一个最简单的vue动态交互,即通过v-model进行双向绑定。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="name">{{name}}</div>
</body>
<script src="observe.js"></script>
<script src="dep.js"></script>
<script src="watcher.js"></script>
<script src="init.js"></script>
<script>
var el = document.querySelector('#name');
var Vue = new Vue({
name:'五一假期'
}, el, 'name');
setTimeout(function() {//这里通过延时代替事件简化修改data属性值的过程
Vue.data.name = '离端午不远了';
},5000);
</script>
1.实现一个监听器Observer
数据监听器的核心方法就是Object.defineProperty(),通过遍历循环对所有属性值进行监听,并对其进行Object.defineProperty()处理:
function observe(data) {
if(!data || typeof data !== 'object') {//判断data是否是对象
return;
}
Object.keys(data).forEach(function(key){//循环调用核心方法给属性添加get和set
defineReactive(data,key,data[key]);
});
}
//给每个属性添加set和get
function defineReactive(data,key,val) {//顾名思义 定义一个响应式的对象
observe(val);//递归监视所有子属性(注意这里和3.0的不同,后边有时间我们对比一下,3.0并不是要递归所有子属性的)
var dep = new Dep();//声明一个订阅器 由源码可见 每个属性值都对应一个dep
Object.defineProperty(data, key, {//数据劫持的核心方法:JS原生方法,IE8以下都不行,3.0的proxy是IE都不可以,而且无法shim,即不能通过第几浏览器的方法来实现
get: function() {//
if(Dep.target) { //判断是否需要添加订阅者,
dep.addSub(Dep.target);
}
return val;
},
set: function(newVal) {//监视变化,分发更新
if (val === newVal) {
return;
}
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
dep.notify(); // 如果数据变化,通知所有订阅者
}
});
}
我们将订阅器Dep添加一个订阅者设计在get里面,这是为了让Watcher在初始化时触发,因此判断是否需要需要添加订阅者,至于具体实现的方法,我们在下后面的章节详细讲解。在set方法中,如果函数变化,就会通知所有的订阅者,订阅者们将会执行相对应的更新函数,到目前为止,一个比较完善的Observer已经成型了,下面我们要写订阅者Watcher。
我们已经知道监听器Observer是在get函数中执行了添加订阅者的操作的,所以我们只需要在订阅者Watcher在初始化时触发相对应的get函数来执行添加订阅者的操作即可。那么我们只需要获取对应的属性值,就可以通过Object.defineProperty()触发对应的get了。
function Watcher(vm, exp, cb) {
this.vm = vm; //指向Vue的作用域
this.exp = exp; //绑定属性的key值
this.cb = cb; //闭包
this.value = this.get();
}
Watcher.prototype = {
update:function() {
this.run();
},
run:function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if(value !== oldVal) {
this.value = value;
this.cb.call(this.vm,value,oldVal);
}
},
get:function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
实现简单的dep,收集watcher,并通知每个订阅者检查更新:
function Dep() {
this.subs = [];//收集watcher对象的数组,收集依赖的容器
}
Dep.prototype = {
addSub:function(sub) {
this.subs.push(sub); //将订阅者统一管理
},
notify:function() {
this.subs.forEach(function(sub) {
sub.update(); //通知每个订阅者检查更新
})
}
}
Dep.target = null;
还是缺少点什么,缺少一个init类,
后面我们只需要将订阅者Watcher和监听器Observer关联起来,就可以实现一个简单的双向绑定。因为本章节还没有设计指令解析器,所以对于模板数据我们都进行写死处理,假设模板上有一个节点元素,且id为’name’,并且双向绑定的绑定变量也是’name’,嵌在差值表达式中,如上图html:
<div id="name">{{name}}</div>
- 上面我们提到在Vue构造函数中,调用了 src/core/instance/init.js 中的 _init 方法,在 _init 方法中调用了 src/core/instance/state.js中定义的 initState(vm):初始化 Vue 实例的状态,初始化了 data、props、methods等,initState(vm) 方法中判断:如果定义了 data 就调用 initData(vm)把 data 中的成员注入到 Vue 实例,并且把它转换成响应式对象。
- 还提到src/core/instance/lifecycle.js中的mountComponent方法中,组件渲染前会调用它,因此每个组件都会对应new一个watcher(2.0版本)。
因此我们需要在init中做两件事: - 实现监听
- new一个wathcer
//将Observer和Watcher关联起来,实现简单的init
function littleVue(data, el, exp) {
this.data = data;//获取data
observe(data);//监控data
el.innerHTML = this.data[exp];//给差值表达式赋值,模拟渲染过程
new Watcher(this, exp, function(value) {//当前作用域,要监视的属性的key值,这里callback相当于从compiler中兜了一圈回来得到真实的修改后的dom
el.innerHTML = value;//更新dom
});
}
然后在页面上new一个littleVue,就可以实现双向绑定了:
<body>
<div id="name">{{name}}</div>
</body>
<script src="observe.js"></script>
<script src="dep.js"></script>
<script src="watcher.js"></script>
<script src="init.js"></script>
<script>
var el = document.querySelector('#name');
var littleVue= new littleVue({
name:'五一假期'
}, el, 'name');
setTimeout(function() {
alert('五一马上结束了')
littleVue.data.name = '端午来了';
},5000);
//我们在为属性赋值的时候形式是: 'littleVue.data.name = 'XXX' ',
//而我们理想的形式是:littleVue.name = 'YYY',那么怎么实现这种形式呢,
//只需要在new littleVue时做一个代理处理,让访问littleVue的属性代理为访
//问littleVue.data的属性,原理还是使用Object.defineProperty()对属性在外
//面再包装一层,具体会在后面的init章节详细讲解。
</script>
启动页面,页面中显示"五一假期",过5秒后,点击完alert中的确定按钮,你会发现"端午来了",双向绑定成功。
附言:
由于Vue2.6版本相当于是Vue2.0到Vue3.0的一个过度版本,而3.0中响应式原理的核心方法是proxy(),我们看看proxy()到底比2.0中监视对象的方式好在了哪里,这里做一个简单的介绍:
首先我们看上面讲到的defineReactive()方法:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()// 创建依赖对象实例,为当前属性收集依赖
...
let childOb = !shallow && observe(val)// 判断是否递归观察子对象(深度监听),则将子对象属性转换成响应式,并返回子观察对象
Object.defineProperty(obj, key, {// 把属性转换成 getter/setter
enumerable: true,
configurable: true,
get: function reactiveGetter () {
...
},
set: function reactiveSetter (newVal) {
...
}
})
}
该方法在shallow === ture的情况我们会在以后的章节中讲解,
这里针对data属性值,shallow值为false,位深度监听,因此我们看到observe(val)不管我用不用这个子属性val,都直接进行了递归处理。
下面我们再看看3.0中,proxy()一个经典的例子:
let obj={a:1,b:{c:2}};
let handler={
get:function(obj,prop){
const v = Reflect.get(obj,prop);
if(v !== null && typeof v === 'object'){
return new Proxy(v,handler);//代理内层
}else{
return v; // 返回obj[prop]
}
},
set(obj,prop,value){
return Reflect.set(obj,prop,value);//设置成功返回true
}
};
let p=new Proxy(obj,handler);
p.a//会触发get方法
p.b.c//会先触发get方法获取p.b,然后触发返回的新代理对象的.c的set。
这里我们看到,默认proxy只代理外层属性,递归是在get方法中实现了,当中判断了子属性,如果子属性存在,且被访问到,才会执行递归操作,因此性能得到了提升。
因此本文作者认为,与其说对比Object.defineProperty()方法和proxy()方法的区别,不如说是实现方式的改进,因为其实本质上都是利用的Object.defineProperty(),只不过proxy()中Object.defineProperty()方法被定义到的Reflect对象中,proxy和reflect都是ES6的内容,由此可见Vue3.0是高度拥抱了ES6,包括TS。只不过是Vue作者将递归操作的时机优化了,区别就是这么简单,而严格意义上讲,我认为proxy的出现实质上是对defineReactive()方法或者说是对observe监听方式的优化,而不是像网上大多数文章讨论的竟然是Object.defineProperty()和proxy()的区别,这多少对读者会有一定的误导性,我认为那样说是不严谨的。
转载请注明出处
下期见!
参考资料:
2.ES6 全套教程 ECMAScript6 (原著:阮一峰)
3.深入响应式原理