Skip to content

实现极简 vue

Vue3

逻辑图

逻辑图

实现

js
const isObject = obj => typeof obj === 'object' && obj !== null

// 已有代理的存储
let toProxy = new WeakMap()
let toRaw = new WeakMap()

// 当前激活的 effect 栈
let activeEffectStack = []
// 依赖映射表
let targetMap = new Map()

// core
const baseHandler = {
  // Proxy + Reflect
  // target: 实例化 Proxy 时使用的对象
  // key: 属性名
  // receiver: 实例化 Proxy 自身,即 proxy
  get(target, key, receiver) {
    // Object 对象可能无法访问 key、target[key] 是否成功不会报错,所以使用 Reflect
    const result = Reflect.get(target, key, receiver) // => target[key]
    // 触发 get 时进行依赖收集
    track(target, key)
    console.log('%cGetter: ', 'color: orange', target, key)
    return result
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver) // 返回 true/false
    // 在触发 set 的时候进行触发依赖
    trigger(target, key)
    console.log('%cSetter: ', 'color: green', target, key) // => target[key] = value
    return result
  }
}

function reactive(target) {
  // 创建一个响应式对象 set、map、array、object
  return createReactiveObject(target, baseHandler)
}

function createReactiveObject(target, baseHandler) {
  // 如果不是对象,不需要代理
  if (!isObject(target)) return target

  // 检查是否已代理过该对象
  let proxy = toProxy.get(target)
  if (proxy) return proxy

  if (toRaw.has(target)) return target

  let observed = new Proxy(target, baseHandler)

  // 维护 proxy 表
  toProxy.set(target, observed)
  toRaw.set(observed, target)

  return observed
}

// dep & track & trigger
function createDep(effects) {
  const dep = new Set(effects)
  return dep
}

function track(target, key) {
  // 当前激活栈的栈顶就是当前激活的 effect
  const effect = activeEffectStack[activeEffectStack.length - 1]
  if (effect) {
    // 维护依赖表
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      depsMap = new Map()
      targetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (!dep) {
      // 新建依赖项
      dep = createDep()
      depsMap.set(key, dep)
    }
    if (!dep.has(effect)) {
      dep.add(effect)
    }
  }
}

function trigger(target, key) {
  // 从依赖表中获取 effect 并逐个执行
  let depsMap = targetMap.get(target)
  if (depsMap) {
    let dep = depsMap.get(key)
    if (dep) {
      dep.forEach(effect => {
        effect()
      })
    }
  }
}

// 依赖函数
function effect(fn) {
  const effectFunc = function () {
    try {
      // 加入 effect 栈
      activeEffectStack.push(effectFunc)
      return fn()
    } finally {
      activeEffectStack.pop()
    }
  }
  // 创建 effect 就会触发一次
  effectFunc()
  return effectFunc
}

const state = reactive({
  msg: 'this is a message',
  name: 'zs',
  age: 20,
  tip: 'a tip',
  hobby: ['eat', 'sleep', 'play'],
  attr: {
    prop: {
      a: '1',
      b: 2,
      c: false,
      d: null,
      e: undefined
    }
  }
})

// 测试
console.log('before: ', state)
state.msg = 'this is a new message'
state.attr.prop.a = 2
state.attr.prop.b = '3'
state.attr.prop.c = true
state.attr.prop.d = 123n
state.attr.prop.e = Symbol('symbol')
console.log('after: ', state)

effect(() => {
  console.log(state.name, state.age, state.tip)
})
state.name = 'lisi'
state.age = 19
state.tip = 'new tip'

Vue2

入口文件

js
import Observer from './observer.js'
import Compiler from './compiler.js'

export default class Vue {
  constructor (options) {
    this.$options = options
    this.$data = options.data
    this.$methods = options.methods

    // 获取根元素,并存储到 Vue 实例
    this.initRootElement(options)
    // 利用 Object.defineProperty 将 data 属性注入到 Vue 实例
    this._proxyData(this.$data)

    // 实例化 Observer 对象,监听数据变化
    new Observer(this.$data)
    // 实例化 Compiler 对象,解析指令和模版表达式
    new Compiler(this)
  }

  // 获取根元素,并存储到 vue 实例,简单检查以下传入的 el 是否合规
  initRootElement(options) {
    if (typeof options.el === 'string') {
      this.$el = document.querySelector(options.el)
    } else if (options.el instanceof HTMLElement) {
      this.$el = options.el
    }

    if (!this.$el) {
      throw new Error('传入的 el 不合法,请传入 css 选择器或 HTMLElement')
    }
  }

  // 利用 object.definedProperty 将 data 的属性注入到 Vue 实例中
  _proxyData(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key]
        },
        set(newVal) {
          if (data[key] === newVal) return
          data[key] = newVal
        }
      })
    })
  }
}

解析模版和指令

js
import Watcher from './watcher.js'

export default class Compiler {
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    this.methods = vm.$methods

    // 编译模版
    this.compile(vm.$el)
  }

  // 编译模版
  compile(el) {
    const 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)
      }
    })
  }

  // 编译文本节点
  compileText(node) {
    const reg = /\{\{(.*?)\}\}/g
    const value = node.textContent

    if (reg.test(value)) {
      const key = RegExp.$1.trim() // 获取变量名
      node.textContent = value.replace(reg, this.vm[key])

      // 响应式更新
      new Watcher(this.vm, key, newVal => {
        node.textContent = newVal
      })
    }
  }

  // 编译元素节点
  compileElement(node) {
    if (node.attributes.length) {
      Array.from(node.attributes).forEach(attr => {
        // 遍历元素节点的所有属性
        const attrName = attr.name

        // v-model v-html v-on:click
        if (this.isDirective(attrName)) {
          let directiveName = attrName.indexOf(':') > 1
            ? attrName.subStr(5)
            : attrName.subStr(2)
          let key = attr.value
          // 更新元素节点
          this.update(node, key, directiveName)
        }
      })
    }
  }

  // 更新元素节点
  update(node, key, directiveName) {
    const fn = this[`${directiveName}Updater`]
    fn && fn.call(this, node, this.vm[key], key, directiveName)
  }

  // 解析 v-text
  textUpdater(node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, newVal => {
      node.value = newVal
    })
  }

  // 解析 v-model
  modelUpdater(node, value, key) {
    node.value = value
    new Watcher(this.vm, key, newVal => {
      node.value = newVal
    })
    // 更新值,双向绑定
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }

  // 解析 v-html
  htmlUpdater(node, value, key) {
    node.innerHTML = value
    new Watcher(this.vm, key, newVal => {
      node.innerHTML = newVal
    })
  }

  // 解析 v-on
  clickUpdater(node, value, key, directiveName) {
    node.addEventListener(directiveName, this.methods[key])
  }

  // 判断元素属性是否是指令
  isDirective(attrName) {
    return attrName.startsWith('v-')
  }

  // 判断是否为文本节点
  isTextNode(node) {
    return node.nodeType === 3
  }

  // 判断是否为元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
}

依赖收集

用于收集当前响应式对象的依赖关系,每个响应式对象都有一个 dep 实例

js
export default class Dep {
  constructor() {
    // 存储所有观察者
    this.watchers = []
  }

  // 添加观察者
  addWatcher(watcher) {
    if (watcher && watcher.update) {
      this.watchers.push(watcher)
    }
  }

  // 发送通知
  notify() {
    this.watchers.forEach(watcher => {
      watcher.update()
    })
  }
}

数据劫持

js
import Dep from './dep.js'

export default class Observer {
  constructor(data) {
    this.traverse(data)
  }

  // 递归遍历 data 里的所有属性
  traverse(data) {
    if (!data || typeof data !== 'object') return
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  // 数据劫持, 给传入的数据设置 getter / setter
  defineReactive(obj, key, val) {
    this.traverse(val)

    const _self = this
    // 这里实例化,为了在 setter 中调用 dep.notify()
    const dep = new Dep()

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 获取的时候做依赖收集
        // Dep.target 是为了在此处添加依赖收集用的,添加之后就可以删除了,所以为 null
        Dep.target && dep.addWatcher(Dep.target)
        return val
      },
      set(newVal) {
        if (val === newVal) return
        val = newVal
        // 设置的时候可能设置了一个对象,因此需要递归
        _self.traverse(newVal)
        // 发起更新通知
        dep.notify()
      }
    })
  }
}

派发更新

js
import Dep from './dep.js'

export default class Watcher {
  /**
   * @params vm Vue 实例
   * @params key data 属性名
   * @params cb 负责更新视图的回调
   */
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb

    // 同一时间只维持一个 watcher
    Dep.target = this

    // 触发 get 方法,在 get 方法里去做一些操作
    this.oldVal = vm[key]

    // 为了避免重复添加 watcher,将其设置为 null
    Dep.target = null
  }

  // 当数据变化时,更新视图
  update() {
    // 需要判断新旧两个值的关系
    let newVal = this.vm[this.key]
    if (this.oldVal === newVal) return
    this.cb(newVal)
  }
}