数据如何驱动视图发生变化?
Vue魔法的核心是数据驱动,本章将探究数据是如何驱动视图进行更新的?
我们的的编码目标是下面的demo能够成功渲染,并在1s后自动更新。
let vm = new Vue({
el: '#app',
data () {
return {
name: 'vue'
}
},
render (h) {
return h('h1', `Hello ${this.name}!`)
}
})
setTimeout(() => {
vm.name = 'world'
}, 1000)
学习Object.defineProperty
Object.defineProperty
用于在对象上定义新属性或修改原有的属性,借助getter/setter
可以实现属性劫持,进行元编程。
观察下面demo,通过vm.name = 'hello xiaohong'
可以直接修改_data.name
属性,当我们访问时会自动添加问候语hello
。
let vm = {
_data: {
name: 'xiaoming'
}
}
Object.defineProperty(vm, 'name', {
get: function (value) {
return 'hi ' + vm._data.name
},
set: function (value) {
vm._data.name = value.replace(/hello\s*/, '')
}
})
vm.name = 'hello xiaohong'
console.log(vm.name)
学习Proxy
Proxy用于定义基本操作的自定义行为(如属性查找、复制、枚举、函数调用等),Proxy功能更强大,天然支持数组的各种操作。
但是,Proxy直接包装了整个目标对象,针对对象属性(key)设置不同劫持函数需求,需要进行一层封装。
const proxyFlag = Symbol('[object Proxy]')
const defaultStrategy = {
get(target: any, key: string) {
return Reflect.get(target, key)
},
set(target: any, key: string, newVal: any) {
return Reflect.set(target, key, newVal)
}
}
export function createProxy(obj: any) {
if (!Object.isExtensible(obj) || isProxy(obj)) return obj
let privateObj: any = {}
privateObj.__strategys = { default: defaultStrategy }
privateObj.__proxyFlag = proxyFlag
let __strategys: Strategy = privateObj.__strategys
let proxy: any = new Proxy(obj, {
get(target, key: string) {
if (isPrivateKey(key)) {
return privateObj[key]
}
const strategy: StrategyMethod = (__strategys[key] || __strategys['default']) as StrategyMethod
return strategy.get(target, key)
},
set(target, key: string, val: any) {
if (isPrivateKey(key)) {
privateObj[key] = val
return
}
const strategy: StrategyMethod = (__strategys[key] || __strategys['default']) as StrategyMethod
return strategy.set(target, key, val)
},
ownKeys(target) {
const privateKeys = Object.keys(privateObj)
return Object.keys(target).filter(v => !privateKeys.includes(v))
}
})
function isPrivateKey(key: string) {
return hasOwn(privateObj, key)
}
return proxy
}
export function isProxy(val: any): boolean {
return val.__proxyFlag === proxyFlag
}
我们定义createProxy
函数,返回一个Proxy对象。依赖闭包对象privateObj.__strategys
存储数据的劫持方法,如果未匹配到对应的方法,则执行默认函数。下面的demo直接调用cvm.__strategys[key]
赋值劫持方法。
观察下面的demo,最终输出值同上。
let vm = {
_data: {
name: 'xiaoming'
}
}
let cvm = createProxy(vm)
cvm.__strategys['name'] = {
get: function () {
return 'hi ' + vm._data.name
},
set: function (target, key, value) {
vm._data.name = value.replace(/hello\s*/, '')
}
}
cvm.name = 'hello xiaohong'
console.log(cvm.name)
Vue的响应式原理
笔者在学习时,忽略了源码中Observer类,只关注了:Dep声明依赖,Watch创建监听。
运行下面demo,会发现控制台先输出init: ccc
,1秒后输出update: lll
。
数据驱动构建过程大致分为4步:
- 遍历data属性,并执行
let dep = new Dep()
,创建dep实例 - 当执行
new Watch()
时,给Dep.Target赋值当前Watch实例 - 当获取data的属性时(
console.log('init:', this._data.name)
),属性拦截并执行dep.depend()
,建立dep和watch实例之间的关系 - 当修改data的属性时(
v.name = 'lll'
),属性拦截并执行dep.notify()
,通知watch实例执行渲染函数,即输出update: lll
完整代码如下:
class Dep {
static Target
constructor () {
this._subs = []
}
addWat (w) {
this._subs.push(w)
}
depend () {
Dep.Target.addDep(this)
}
notify () {
this._subs.forEach(v => {
v.update()
})
}
}
class Watcher {
constructor (vm, cb) {
this.deps = []
this.vm = vm
this.cb = cb
Dep.Target = this
}
addDep (dep) {
this.deps.push(dep)
dep.addWat(this)
}
update () {
this.cb.call(this.vm)
}
}
class Vue {
constructor (data) {
this._data = {}
this.observe(data)
this.render()
}
observe(data) {
for (let key in data) {
defineKey(this._data, key, data[key])
}
}
render () {
new Watcher(this, () => {
console.log('update:', this._data.name)
})
console.log('init:', this._data.name)
}
}
function defineKey (obj, key, value) {
let dep = new Dep()
Object.defineProperty(obj, key, {
get () {
dep.depend()
return value
},
set (newValue) {
value = newValue
dep.notify()
}
})
}
let v = new Vue({name: 'ccc'})
setTimeout(() => {
v._data.name = 'lll'
}, 1000)
简易代码
我们根据上面的理解实现下功能吧。
首先实现Dep类,前面我们知道Dep.Target是建立dep和watch实例关系的重要变量。在这里,Dep模块定义了两个函数pushTarget
和popTarget
用于管理Dep.Target
。
let targetPool: ArrayWatch = []
class Dep {
static Target: Watch | undefined
private watches: ArrayWatch
constructor() {
this.watches = []
}
addWatch(watch: Watch) {
this.watches.push(watch)
}
depend() {
Dep.Target && Dep.Target.addDep(this)
}
notify() {
this.watches.forEach(v => {
v.update()
})
}
}
export function pushTarget(watch: Watch): void {
Dep.Target && targetPool.push(Dep.Target)
Dep.Target = watch
}
export function popTarget(): void {
Dep.Target = targetPool.pop()
}
接着我们实现Watch类,此处的Watch类与上面有简单不同。其实例化后会产生两个可执行函数,一个是this.getter
,一个是this.cb
。前者用于收集依赖,后者在option.watch中使用,如new Watch({el: 'app', watch: {message (newVal, val) {}}})
。
class Watch {
private deps: ArrayDep
private cb: noopFn
private getter: any
public vm: any
public id: number
public value: any
constructor(vm: Vue, key: any, cb: noopFn) {
this.vm = vm
this.deps = []
this.cb = cb
this.getter = isFunction(key) ? key : parsePath(key) || noop
this.value = this.get()
}
private get(): any {
let vm = this.vm
pushTarget(this)
let value = this.getter.call(vm, vm)
popTarget()
return value
}
addDep(dep: Dep) {
!this.deps.includes(dep) && this.deps.push(dep)
dep.addWatch(this)
}
update() {
queueWatcher(this)
}
depend() {
for (let dep of this.deps) {
dep.depend()
}
}
run() {
this.getAndInvoke(this.cb)
}
private getAndInvoke(cb: Function) {
let vm: Vue = this.vm
let value = this.get()
if (value !== this.value) {
cb.call(vm, value, this.value)
this.value = value
}
}
}
function parsePath(key: string): any {
return function(vm: any) {
return vm[key]
}
}
为了将数据进行响应式改造,我们定义了observe
函数。
observe
为数据创建代理对象,defineProxyObject
为数据的属性创建dep,defineProxyObject
的本质是修改proxyObj.__strategys['name']
的值,为对象的属性配置自定义的拦截函数。
export function observe(obj: any): Object {
// 字面量类型或已经为响应式类型则直接返回
if (isPrimitive(obj) || isProxy(obj)) {
return obj
}
let proxyObj = createProxy(obj)
for (let key in proxyObj) {
defineObject(proxyObj, key)
}
return proxyObj
}
export function defineObject(
obj: any,
key: string,
val?: any,
customSetter?: Function,
shallow?: boolean
): void {
if (!isProxy(obj)) return
let dep: Dep = new Dep()
val = isDef(val) ? val : obj[key]
val = isTruth(shallow) ? val : observe(val)
defineProxyObject(obj, key, {
get(target: any, key: string) {
Dep.Target && dep.depend()
return val
},
set(target: any, key: string, newVal) {
if (val === newVal || newVal === val.__originObj) return true
if (customSetter) {
customSetter(val, newVal)
}
newVal = isTruth(shallow) ? newVal : observe(newVal)
val = newVal
let status = Reflect.set(target, key, val)
dep.notify()
return status
}
})
}
最后我们定义Vue类,在Vue实例化过程中。首先是this._initData(this)
将数据变为响应式的,接着调用new Watch(this._proxyThis, updateComponent, noop)
用于监听数据的变化。
proxyForVm
函数主要目的是构建一层代理,让vm.name
可以直接访问到vm.$options.data.name
。
class Vue {
constructor (options) {
this.$options = options
this._vnode = null
this._proxyThis = createProxy(this)
this._initData(this)
if(options.el) {
this.$mount(options.el)
}
return this._proxyThis
_initData (vm) {
let proxyData: any
let originData: any = vm.$options.data
let data: VNodeData = vm.$options.data = originData()
vm.$options.data = proxyData = observe(data)
for (let key in proxyData) {
proxyForVm(vm._proxyThis, proxyData, key)
}
}
_render () {
return this.$options.render.call(this, h)
},
_update (vnode) {
let oldVnode = this._vnode
this._vnode = vnode
patch(oldVnode, vnode)
}
$mount (el) {
this._vnode = createNodeAt(documeng.querySelector(options.el))
const updateComponent = () => {
this._update(this._render())
}
new Watch(this._proxyThis, updateComponent, noop)
}
}
总结
综上,vue将依赖和监听进行分开,通过Dep.Target建立联系,当获取数据时绑定dep和watch,当设置数据时触发watch.update进行更新,从而实现视图层的更新。
杠精一下
Object.defineProperty和proxy的区别在哪里?[juejin.cn/post/684490…]
元编程和Proxy?[juejin.cn/post/684490…]
现代框架存在的根本原因?(www.zcfy.cc/article/the…)(www.jianshu.com/p/08ff598ec…)