Vue源码学习:手写Vue
2024-02-06
Cikayo
前言
最近在学习Vue源码,从网上找到一些学习视频,提升一下自己的技术水平。跟着视频写一写,不喜勿喷。
不得不说,里面涉及到的 JavaScript
知识点还是很不少的,这里记录一下,以备以后复习。里面涉及到的点:
- Object.defineProperty() 语法
- JavaScript Dom 操作相关的 API
日拱一卒,每日精进。
开始
手写KVue,实现以下几个功能点:
- KVue:框架构造函数
- Observer:执行数据响应化
- Compile:编译模版,初始化视图,收集依赖
- Watcher:执行更新函数
- Dep:管理多个Watcher,批量更新
如图所示:
实现数据响应式
// 数据响应式
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())
}
}