【Ts重构Vue】02-数据如何驱动视图变化

513 阅读1分钟

数据如何驱动视图发生变化?

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步:

  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.cn/post/684490…]

元编程和Proxy?[juejin.cn/post/684490…]

现代框架存在的根本原因?(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功能