Vue源码学习:手写Vue

2024-02-06
Cikayo

前言

最近在学习Vue源码,从网上找到一些学习视频,提升一下自己的技术水平。跟着视频写一写,不喜勿喷。

不得不说,里面涉及到的 JavaScript 知识点还是很不少的,这里记录一下,以备以后复习。里面涉及到的点:

  • Object.defineProperty() 语法
  • JavaScript Dom 操作相关的 API

日拱一卒,每日精进。

开始

手写KVue,实现以下几个功能点:

  • KVue:框架构造函数
  • Observer:执行数据响应化
  • Compile:编译模版,初始化视图,收集依赖
  • Watcher:执行更新函数
  • Dep:管理多个Watcher,批量更新

如图所示:

图示1

实现数据响应式

// 数据响应式
function defineReactive(obj, key, val) {
  // 递归
  observe(val)
  let dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`设置了数据:${key} - ${newVal}`)
        val = newVal
        dep.notify()
      }
    },
  })
}

function observe(obj) {
  if (typeof obj !== "object" || obj == null) {
    // 我们希望传递过来的是object
    return
  }
  new Observer(obj)
}

// 代理函数,将vm.data的属性,代理到 vm 上
function proxy(vm, sourceKey) {
  Object.keys(vm[sourceKey]).forEach((key) => {
    Object.defineProperty(vm, key, {
      get() {
        return vm[sourceKey][key]
      },
      set(newVal) {
        vm[sourceKey][key] = newVal
      },
    })
  })
}

KVue

// 实现KVue
class KVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data
    this.$methods = options.methods

    // 响应化处理data数据
    observe(this.$data)
    // 代理
    proxy(this, "$data")

    // 创建编译器
    new Compiler(options.el, this)
  }
}

Observer

// Observer
class Observer {
  constructor(value) {
    this.value = value
    if (typeof value === "object") {
      this.walk(value)
    }
  }
  // 对象数据的响应化
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key])
    })
  }
}

Compiler

//Compile
// 递归遍历Dom树
// 判断节点类型,如果是文本,则判断是否是插值绑定
// 如果是元素,则遍历其属性判断是否是指令或事件,然后递归子元素
class Compiler {
  // el是宿主元素
  // vm是KVue实例
  constructor(el, vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)
    if (this.$el) {
      this.compile(this.$el)
    }
  }
  compile(el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach((node) => {
      // 判断当前节点是元素节点
      if (this.isElement(node)) {
        this.compileElement(node)
      }
      // 判断当前元素是文本节点
      else if (this.isInter(node)) {
        this.compileText(node)
      }

      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 当前节点是元素节点
  isElement(node) {
    return node.nodeType === 1
  }
  // 当前元素是文本节点
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  compileText(node) {
    this.update(node, RegExp.$1, "text")
  }

  compileElement(node) {
    // 节点是元素
    // 遍历其属性列表
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach((attr) => {
      const attrName = attr.name
      const exp = attr.value
      // 判断是 k- 开头的指令
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2)
        this[dir] && this[dir](node, exp)
      }
      // 判断是 @ 开头的方法
      else if (this.isMethod(attrName)) {
        const method = attrName.substring(1)
        const self = this
        if (this.$vm.$methods[exp]) {
          node.addEventListener(method, function () {
            self.$vm.$methods[exp]()
          })
        }
      }
    })
  }
  // 判断是 k- 开头的命令
  isDirective(attr) {
    return attr.indexOf("k-") === 0
  }
  // 判断是 @ 开头的方法
  isMethod(attr) {
    return attr.indexOf("@") === 0
  }

  // 1. 初始化
  // 2. 创建Watcher实例
  update(node, exp, dir) {
    const fn = this[dir + "Updater"]
    fn && fn(node, this.$vm[exp])

    // 更新处理,封装一个更新函数,可以更新对应Dom元素
    new Watcher(this.$vm, exp, function (val) {
      fn && fn(node, val)
    })
  }

  text(node, exp) {
    this.update(node, exp, "text")
  }
  html(node, exp) {
    this.update(node, exp, "html")
  }

  textUpdater(node, value) {
    node.textContent = value
  }

  htmlUpdater(node, value) {
    node.innerHTML = value
  }
}

Watcher

// 观察者:保存更新函数,值发生变化调用更新函数
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn

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

  update() {
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}

Dep

// 依赖收集
class Dep {
  constructor() {
    this.deps = []
  }

  addDep(dep) {
    this.deps.push(dep)
  }

  notify() {
    this.deps.forEach((dep) => dep.update())
  }
}
文中参考代码或参考文章内容,会注明作者与出处。如有侵权,请联系删除。转载此文章请注明出处。