手写一个vue
整体结构
三个组成部分
- Observer:数据处理,通过Object.defineProperty把数据变成响应式,收集观察者,在数据变化时通知观察者。
- Compiler:模板编译,编译模板挂载数据,并生成观察者,在数据变化时接收通知,然后更新视图。
- Dep/Watcher:观察者模式。
手写代码
class SimpleVue {
constructor(options) {
this.$el = typeof options.el === 'string'
? document.querySelector(options.el)
: options.el
this.$data = options.data
this.methods = options.methods
// 把data的属性都挂载到vm实例上,方便vm.msg这样调用,这一步是非必需的,可以不看
this._proxyDataToVm(this.$data)
// 把data变成响应式数据,数据变化时通知观察者
new Observer(this.$data)
// 编译模板挂载数据,并生成观察者,在数据变化时接收通知,然后更新视图
new Compiler(this)
}
_proxyDataToVm(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return data[key]
},
set(newVal) {
if (newVal !== data[key]) {
data[key] = newVal
}
}
})
})
}
}
class Observer {
constructor(data) {
this.observe(data)
}
// 劫持数据,全部属性变为响应式
observe(data) {
Object.keys(data).forEach(key => {
// 不能在get()里直接return data[key],否则会造成死循环
let value = data[key]
let dep = new Dep()
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function () {
// if(!Dep.depMap[key]) {
// Dep.depMap[key] = new Dep()
// }
// 通过targetWatcher中间变量添加warcher,注意看watcher的addSelfToSub方法
if (Dep.targetWatcher) {
console.log('add watcher');
dep.addSub(Dep.targetWatcher)
}
return value
},
set: function (newVal) {
let oldVal = data[key]
if (oldVal !== newVal) {
value = newVal
// 通知观察者更新视图
// Dep.depMap[key].notify(newVal)
dep.notify(newVal)
}
}
})
if (typeof value === 'object') {
this.observe(data[key])
}
})
}
}
// 编译模板
// 1.编译初始化模板数据,第一次挂载页面
// 2.生成观察者,接收数据变化的通知,然后更新视图
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
// 创建文档碎片,减少页面重排重绘,这一步不是必须的
// const fragment = document.createDocumentFragment();
// while (el.firstChild) {
// fragment.appendChild(el.firstChild)
// }
//this.el.appenChild(fragment)
}
// 编译模板
compile(el) {
let childNodes = el.childNodes
childNodes.forEach(node => {
if (this.isElementNode(node)) {
this.compileElementNode(node)
} else if (this.isTextNode(node)) {
this.compileTextNode(node)
}
// 递归处理子节点
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 判断是否元素节点,如div标签,但div里面的文本属于文本节点
isElementNode(node) {
return node.nodeType === 1
}
// 判断是否文本节点
isTextNode(node) {
return node.nodeType === 3
}
// 编译文本节点
compileTextNode(node) {
// 1.匹配{{msg}}
const text = node.textContent
let reg = /\{\{(.+?)\}\}/;// 匹配{{xx}},{{xx.xx}}的正则
// 如果是 {{}} 的文本节点,下面的代码只做原理实现
// {{obj.name}}这种格式的取值的时候需要用reduce构造出this.vm.$data['obj']['name']才行
// 赋值时要构造出`this.vm.$data['obj']['name']=newVal`字符串,然后用eval()强制执行
if (reg.test(text)) {
// 2.渲染msg数据
let key = RegExp.$1.trim()
console.log(key);
node.textContent = this.vm.$data[key]
// 3.添加观察者,在msg改变的时候通知观察者更新视图,这里实现比较巧妙!注意看wathcer的addSelfToDep方法
let watcher = new Watcher(this.vm, key, (newVal) => {
console.log('cb', newVal);
node.textContent = newVal
})
// Dep.depMap[key].addSub(watcher)
}
}
// 编译元素节点,处理指令v-text,v-html
compileElementNode(node) {
console.log(node.attributes);
console.log(Array.from(node.attributes));
Array.from(node.attributes).forEach(attr => {
console.log(attr.nodeName, attr.nodeValue);
let attrName = attr.name
let attrValue = attr.value
if (attrName === 'v-text') {
node.textContent = this.vm.$data[attrValue]
// 添加观察者
new Watcher(this.vm, attrValue, (newVal) => {
console.log('cb', newVal);
node.textContent = newVal
})
}
if (attrName === 'v-html') {
node.innerHTML = this.vm.$data[attrValue]
// 添加观察者
new Watcher(this.vm, attrValue, (newVal) => {
console.log('cb', newVal);
node.innerHTML = newVal
})
}
// v-model,本质上是input事件
if (attrName === 'v-model') {
// 初始值赋给输入框
node.value = this.vm.$data[attrValue]
new Watcher(this.vm, attrValue, (newVal) => {
console.log('cb', newVal);
node.value = newVal
})
node.addEventListener('input', (e) => {
console.log('input', e.target.value);
this.vm.$data[attrValue] = e.target.value
})
}
// v-on:click
if (attrName.startsWith('v-on')) {
let [, event] = attrName.split(':')
node.addEventListener(event, this.vm.methods[attrValue])
}
})
}
}
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify(newVal) {
this.subs.forEach(sub => sub.update(newVal))
}
}
Dep.depMap = {}
Dep.targetWatcher = null
class Watcher {
constructor(vm, key, callback) {
this.vm = vm
this.key = key
this.callback = callback
this.addSelfToDep()
}
addSelfToDep() {
console.log('add self to dep', this.vm, this.key);
// 把当前watcher挂载到中间变量上
Dep.targetWatcher = this
// 通过访问来触发get方法,在get方法中调用addSub,通过targetWatcher中间变量把当前watcher添加到dep中
let oldValue = this.vm.$data[this.key]
Dep.targetWatcher = null
// 这种方法比较巧妙,如果不理解也可以定义一个全局的depMap[key],然后在new Watcher的时候通过depMap[key].addSub添加。
}
update(newVal) {
this.callback(newVal)
}
}
var vm = new SimpleVue({
el: '#app',
data: {
msg: 0,
name: 'a',
pager: {
a: 1
}
},
methods: {
handleClick() {
console.log('1');
}
}
})
// setInterval(() => {
// vm.$data.msg++
// // console.log(vm.$data.msg);
// }, 1000)