Vue魔法的核心是數據驅動,本章將探究數據是如何驅動視圖進行更新的?javascript
咱們的的編碼目標是下面的demo可以成功渲染,並在1s後自動更新。vue
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
用於在對象上定義新屬性或修改原有的屬性,藉助getter/setter
能夠實現屬性劫持,進行元編程。java
觀察下面demo,經過vm.name = 'hello xiaohong'
能夠直接修改_data.name
屬性,當咱們訪問時會自動添加問候語hello
。node
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直接包裝了整個目標對象,針對對象屬性(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]
賦值劫持方法。bash
觀察下面的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)
複製代碼
筆者在學習時,忽略了源碼中Observer類,只關注了:Dep聲明依賴,Watch建立監聽。app
運行下面demo,會發現控制檯先輸出init: ccc
,1秒後輸出update: lll
。框架
數據驅動構建過程大體分爲4步:
let dep = new Dep()
,建立dep實例new Watch()
時,給Dep.Target賦值當前Watch實例console.log('init:', this._data.name)
),屬性攔截並執行dep.depend()
,創建dep和watch實例之間的關係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.im/post/5acd0c…]
元編程和Proxy?[juejin.im/post/5a0f05…]
現代框架存在的根本緣由?(www.zcfy.cc/article/the…)(www.jianshu.com/p/08ff598ec…)