Vue 核心源码(下)- 04

111 阅读1分钟

深入理解响应式设计

svelte svelte

<script>
  let count = 1

  function handleIncrease() {
    count++
  }

  $: doubleCount = count * 2
</script>

<div>{count}</div>
<div>{doubleCount}</div>
<button on:click={handleIncrease}>+</button>

不用this.data 原理就是静态编译,纯函数

Vue2 响应式

vue 响应式要对依赖进行依赖收集,本质是对data 进行拦截 -> ObjectDefineProperty 操作 get set

export class Vue {
  constructor(options = {}) {
    // 值挂载实例上
    this.$options = options
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    this.$data = options.data
    this.$methods = options.methods

    this.proxy(this.$data)

    // Observer 拦截 this$data
    new Observer(this.$data)
  }
  
  // 代理 $this.data -> $this.$data.xxx -> this.xxx 属性值代理到实例上
  // data: { count: 0 }; this.$data.count++ -> this.count++
  proxy(data) {
    Object.keys(data).forEach(key => {
      // this
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          // 相当于 this.data.count 直接拿到this.count
          return data[key]
        },
        set(newVal) {
          // NaN !== NaN
          if (data[key] === newVal || __isNaN(data[key], newVal)) return
          data[key] = newVal
        }
      })
    })
  }
}

function __isNaN(a, b) {
  return Number.isNaN(a) && Number.isNaN(b)
}

Observer 数据拦截

Vue2 递归地使用defineProperty

edge case: array

class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    if (data || typeof data !== 'object') return
    Object.keys(data).forEach(key => this.defineReactive(data, key, data[key]))
  }

  // 拿到对象和值进行操作
  defineReactive(obj, key, value) {
    // 存 this 指针
    let _this = this
    this.walk(value) // 因为值也可能是对象 {a: {b: 12}}
    
    // 对象的每个属性 做劫持,可以通过get set控制属性
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get() { // 调用 this.count 会触发
        return value
      },
      set() { // 调用 this.count++ 会触发
        // NaN
        if (value === newVal || __isNaN(value, newVal)) return
        value = newValue

        // 值可能是对象
        _this.walk(newValue)
      }
    })
  }
}

所谓响应式:如 有个 data: { count: 0 },操作this.count++ 页面都会响应

目前对整个属性做了劫持,比如不论访问还是赋值 count 都会被拦截到

-> 发布订阅模式:触发get,知道可能会用哪个值,收集起来;当赋值count时,之前订阅与count有关的更改都应该触发

收集依赖,触发

class Dep {
  constructor() {
    this.deps = new Set()
  }
  // 收集副作用代码,页面依赖 count 渲染的代码 - 副作用
  add(dep) {
    if (dep && dep.update) this.deps.add(dep)
  }
  // 触发
  notify(){
    this.deps.forEach(dep => dep.update())
  }
}

在Observer 中 收集

class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    if (data || typeof data !== 'object') return
    Object.keys(data).forEach(key => this.defineReactive(data, key, data[key]))
  }

  // 拿到对象和值进行操作
  defineReactive(obj, key, value) {
    // 存 this 指针
    let _this = this
    this.walk(value) // 因为值也可能是对象 {a: {b: 12}}

    // 收集依赖
    let dep = new Dep()
    
    // 对象的每个属性 做劫持,可以通过get set控制属性
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get() { // 调用 this.count 会触发
        // 收集
        Dep.target && dep.add(Dep.target)
        return value
      },
      set() { // 调用 this.count++ 会触发
        // NaN
        if (value === newVal || __isNaN(value, newVal)) return
        value = newValue

        // 值可能是对象
        _this.walk(newValue)

        // 通知
        dep.notify()
      }
    })
  }
}

target 为 Watcher 的实例 观察count,对应有个 update 方法,劫持count 发生的变化,把变化渲染到页面(监听的副作用)

class Watcher { //在编译html的时候用
  // cb key 变了之后回调方法,页面重新渲染,或者 diff -> patch
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb // 今天的例子中就是绘制数据到页面

    Dep.target = this
    this.__old = vm[key] // 当前的count 的值,存下,触发了 getter
    Dep.target = null // 防止溢出
  }

  update() {
    let newValue = this.vm[this.key]
    if (this.__old === newValue || __isValue(newValue, this.__old)) return
    this.cb(newValue)
  }
}

整体过程: html 字符串 -> <h1> {{ count }} </h1> -> compiler 解析时发现有 {{ count }} 在h1节点 new 一个Watcher -> new Watcher(vm, 'count', () => renderToView(count)) new Watcher 的时候会走

Dep.target = this
this.__old = vm[key] 
Dep.target = null

-> 触发 count getter -> getter 触发 会走 dep.add(watcher实例) -> this.count++ -> cont setter -> dep.notify -> () => renderToView(count)) -> 页面就变了

再来写 compiler

export class Vue {
  constructor(options = {}) {
    // 值挂载实例上
    this.$options = options
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    this.$data = options.data
    this.$methods = options.methods

    this.proxy(this.$data)

    // Observer 拦截 this$data
    new Observer(this.$data)

    // complier
    new Complier(this) // 绑定this
  }

// Complier
class Complier {
  constructor(vm) {
    this.el = vm.el // 拿到dom节点
    this.vm = vm
    this.methods = vm.$methods

    this.compile(vm.$el)
  }

  compile(el) {
    let childNodes = el.childNodes
    
    // 类数组 - 转一下数组
    Array.from(childNodes).forEach(node => {
      if(this.isTextNode(node)) {
        this.compileText(node)
      }
      else if (this.isElementNode(node)) {
        this.compileElement(node)
      }

      if (node.childNodes && node.childNodes.length) this.compile(node)
      // ..
    })
  }

  // <div v-mode='msg'>
  compileElement(node) {
    if (node.attributes.length) {
      Array.from(node.attributes).forEach(attr => {
        let attrName = attr.name
        if (this.isDirective(attrName)) {
          // v-on: click v-model
          attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) // 'click' : 'model'
          let key = attr.value
          this.update(node, key, attrName, this.vm[key])
        }
      })
    }
  }
  update(node, key, attrName, value) {
    if (attrName === 'text') {
      node.textContent = value
      new Watcher(this.vm, key, val => node.textContent = val)
    } else if (attrName === 'model') {
      node.value = value
      new Watcher(this.vm, key, val => node.value = val)
      node.addEventListener('input' => {
        this.vm[key] = node.value
      })
    } else if (attrName === 'click') {
      node.addEventListener(attrName, this.methods[key].bind(this.vm))
    }
    //...
  }

  // 'this is {{ count }}'
  compileText(node) {
    let reg = /\{\{(.*?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])

      new Watcher(this.vm, key, val => {
        node.textContent = val
      })
    }
  }

  // 判断指令
  isDirective(str) {
    return str.startsWith('v-')
  }
  // 是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3
  }
}

实现双向绑定:

(attrName === 'model') {
  node.value = value
  // 绑定了字段
  new Watcher(this.vm, key, val => node.value = val)
  // 输入时候,修改vm值
  node.addEventListener('input' => {
    this.vm[key] = node.value
  })
}

html 文件: <script type='module'> 因为要有ES6 语法

<!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>
  <script type='module'>
    import { Vue } from './index.js'
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello Vue2.x',
        count: 666
      },
      methods: {
        increase() {
          this.count++
        }
      }
    })
  </script>
</head>
<body>
  <div id="app">
    <h3>{{ msg }}</h3>
    <h3>{{ count }}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg" >
    <input type="text" v-model="count">
    <button v-on:click="increase">按钮</button>
  </div>
</body>
</html>

Vue3

// 判断是否是对象
function isObject (data) {
  return data && typeof data === 'object'
}

// 收集过程
let targetMap = new WeakMap()
let activeEffect
/**
 * {
 *   target: {
 *     key: [effect, effect, effect, effect]
 *   }
 * }
 */

function track(target, key) { // dep.add
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let dep = depsMap.get(key)
  if (!dep) depsMap.set(key, (dep = new Set()))
  if (!dep.has(activeEffect)) dep.add(activeEffect) // Dep.target && dep.add(Dep.target)
}

// 通知过程
function trigger(target, key) { // dep.notify
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  depsMap.get(key).forEach(e => e && e())
}

// 收集副作用 -> 收集的时间(getter)-> 触发副作用执行(setter)
function effect(fn, options = {}) { // compiler + watcher
  const __effect = function(...args) {
    activeEffect = __effect
    return fn(...args) // this.cb()
  }
  if (!options.lazy) {
    __effect()
  }
  return __effect
}

// 响应式
/*
const a = reactive({ count: 0 })
a.count++
*/
// reactive 返回代理对象
export function reactive(data) {
  if (!isObject) return 
  return new Proxy(data, {
    // 同样数据拦截
    get(target, key,receiver) {
      // 反射target[key] -> 继承情况关系下有坑
      // Reflect.get 获取对象身上某个属性的值,类似于 target[name]
      const ret = Reflect.get(target, key, receiver)
      // TODO 依赖收集
      track(target, key)
      return isObject(ret) ? reactive(ret) : ret
    },
    set(target, key, val, receiver) {
      Reflect.set(target, key, val, receiver)
      // TODO 通知
      trigger(target, key)
      return true
    },
    deleteProperty() {
      const ret = Reflect.deleteProperty(target, key, receiver)
      // TODO 通知
      trigger(target, key)
      return ret
    }
  })
}

// proxy 对于基本类型没办法
// 因此需要 ref 方法,将基类型包装成对象
// 基本类型
/**
 * const count = ref(0)
 * count.value++
 */
export function ref(target) {
  let value = target
  const obj = {
    get value() {
      track(obj, 'value')
      return value
    },
    set value(newValue) {
      if (value === newValue) return
      value = newValue
      trigger(obj, 'value')
    }
  }

  return obj
}

export function computed(fn) {//只考虑函数的情况
  // 延迟计算 const c = computed(() => `${count.value} + !!!!`); c.value
  let __computed
  const run = effect(fn, { lazy: true })
  __computed = {
    get value() {
      return run()
    }
  }
  return __computed
}

export function mount(instance, el) {
  effect(function() {
    instance.$data && update(instance, el)
  })
  instance.$data = instance.setup()

  update(instance, el)
  function update(instance, el) {
    el.innerHTML = instance.render()
  }
}