【Ts重構Vue】02-數據如何驅動視圖變化

數據如何驅動視圖發生變化?

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

Object.defineProperty用於在對象上定義新屬性或修改原有的屬性,藉助getter/setter能夠實現屬性劫持,進行元編程。java

觀察下面demo,經過vm.name = 'hello xiaohong'能夠直接修改_data.name屬性,當咱們訪問時會自動添加問候語hellonode

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]賦值劫持方法。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)
複製代碼

Vue的響應式原理

筆者在學習時,忽略了源碼中Observer類,只關注了:Dep聲明依賴,Watch建立監聽app

運行下面demo,會發現控制檯先輸出init: ccc,1秒後輸出update: lll框架

數據驅動構建過程大體分爲4步:

  1. 遍歷data屬性,並執行let dep = new Dep(),建立dep實例
  2. 當執行new Watch()時,給Dep.Target賦值當前Watch實例
  3. 當獲取data的屬性時(console.log('init:', this._data.name)),屬性攔截並執行dep.depend(),創建dep和watch實例之間的關係
  4. 當修改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模塊定義了兩個函數pushTargetpopTarget用於管理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…)

系列文章

【Ts重構Vue】00-Ts重構Vue前言

【Ts重構Vue】01-如何建立虛擬節點

【Ts重構Vue】02-數據如何驅動視圖變化

【Ts重構Vue】03-如何給真實DOM設置樣式

【Ts重構Vue】04-異步渲染

【Ts重構Vue】05-實現computed和watch功能

相關文章
相關標籤/搜索