实现简单的vue
1.先来看先要实现的需求,解析下面的文件,实现正确渲染和响应式
// vue-demo/index.html
<!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>
</head>
<body>
<div id="app">
<h6>插值表达式</h6>
<div>{{msg}}</div>
<div>{{count}}</div>
<h6>v-text</h6>
<div v-text="msg"></div>
<div v-text="count"></div>
<h6>v-model</h6>
<input v-model="msg"></input>
<input v-model="count"></input>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'hello',
count: 10
}
})
</script>
</body>
</html>
当前访问,如图所示:
2.大概分为5部分内容
3.new Vue的实现:
主要是把data中的成员注入到Vue实例,并劫持data中的成员,设置getter/setter
// vue-demo/js/vue.js
class Vue{
constructor (options){
// 1.接收并保存初始化参数
this.$options = options || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2.把data中的属性注入到Vue实例,转换成setter/getter
this.$data = options.data || {}
this._proxyData(this.$data)
// 3.调用Observer实现数据劫持
// 4.调用Compiler解析指令和插值表达式
}
_proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
if(newVal === data[key]){
return
}
data[key] = newVal
}
})
})
}
}
html中导入vue.js后,在访问控制vm,可以看到1和2已经实现:
4.Observer的实现:
对vm.$data数据对象的所有属性进行监听
// vue-demo/js/observer.js
// 将$data的成员转化为getter/setter
class Observer {
// 初始的时候拿到$data
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(data, key, val) {
let that = this
Object.defineProperty(data, key, {
get(){
return val
},
set(newVal) {
if (newVal === val) {
return
}
// 如果有个属性的值从字符串改为对象,也需要设置为响应式
// 需要注意this指向的问题,这里需要调用的是Observer的walk方法
that.walk(newVal)
data[key] = newVal
}
})
}
}
html文件中引入observer.js,在vue.js的构造函数中新建Observer对象,并传入this.$data
,然后在控制台访问vm,结果如下图所示:
5.模板解析
解析插值表达式和指令
// vue-demo/js/compiler.js
class Compiler{
constructor(vm){
this.vm = vm
this.el = vm.$el
this.compile(this.el)
}
compile(el) {
const nodes = el.childNodes
Array.from(nodes).forEach(node => {
// 判断是文本节点还是元素节点
if(this.isTextNode(node)){
this.compileText(node)
} else if (this.isElementNode(node)){
this.compileElement(node)
}
// 如果还有子节点,递归调用compile方法
if(node.childNodes && node.childNodes.length){
this.compile(node)
}
})
}
isTextNode(node) {
return node.nodeType === 3
}
isElementNode(node) {
return node.nodeType === 1
}
// 判断是否是指定v-开头的属性
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 处理插值表达式
compileText(node) {
const reg = /\{\{(.+?)\}\}/
// 文本节点内容
const value = node.textContent
if(reg.test(value)) {
// 属性名
const key = RegExp.$1.trim()
// 替换插值表达式为this.$data对应的值
node.textContent = value.replace(reg, this.vm[key])
}
}
// 处理指令
compileElement(node) {
Array.from(node.attributes).forEach(attr => {
// 获取元素名称
let attrName = attr.name
// 判断当前元素属性名称是否是指令
if(this.isDirective(attrName)) {
// 截掉v-
attrName = attrName.substr(2)
// 获取指令对应的值v-text="msg",获取的就是msg
const key = attr.value
// 处理不同指令
this.update(node, key, attrName)
}
})
}
update(node, key, dir) {
const updateFn = this[dir + 'Updater']
updateFn && updateFn(node, this.vm[key])
}
textUpdater(node, value) {
node.textContent = value
}
modelUpdater(node, value) {
node.value = value
}
}
html中引入compiler.js,并在vue.js的构造函数中新建Compiler,访问可以看到,插值表达式和指令已经正确显示:
6.Dep实现
收集依赖,添加观察者watcher,数据变化后通知观察者更新
// vue-demo/js/dep.js
class Dep{
constructor(){
this.subs = []
}
addDep(dep){
if(dep && dep.update) this.subs.push(dep)
}
notify() {
this.subs.forEach(dep => {
dep.update()
})
}
}
在observer.js中依赖收集,发送通知:
// vue-demo/js/observer.js
// defineReactive方法中
// 创建dep对象收集依赖
const dep = new Dep()
// get过程中收集依赖
Dep.target && dep.addDep(Dep.target)
// set中数据变化之后,发送通知
dep.notify()
7.Watcher实现
// vue-demo/js/watcher.js
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中的属性名称
this.key = key
// 数据变化时,调用cb更新视图
this.cb = cb
// 在Dep静态属性上记录当前的watcher对象,当访问数据时,将watcher添加到dep的subs中
Dep.target = this
// 这个访问可以触发getter,让dep为当前key记录watcher,且保存一下更新前的值
this.oldValue = vm[key]
// 清空target,避免重复添加
Dep.target = null
}
update() {
const newVal = this.vm[this.key]
if(this.oldValue === newVal) {
return
}
this.cb(newVal)
}
}
8.在compiler.js为指令和插值表达式都创建watcher对象,也就是界面显示依赖$data的地方,监视数据变化
class Compiler{
constructor(vm){
this.vm = vm
this.el = vm.$el
this.compile(this.el)
}
compile(el) {
const nodes = el.childNodes
Array.from(nodes).forEach(node => {
// 判断是文本节点还是元素节点
if(this.isTextNode(node)){
this.compileText(node)
} else if (this.isElementNode(node)){
this.compileElement(node)
}
// 如果还有子节点,递归调用compile方法
if(node.childNodes && node.childNodes.length){
this.compile(node)
}
})
}
isTextNode(node) {
return node.nodeType === 3
}
isElementNode(node) {
return node.nodeType === 1
}
// 判断是否是指定v-开头的属性
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 处理插值表达式
compileText(node) {
const reg = /\{\{(.+?)\}\}/
// 文本节点内容
const value = node.textContent
if(reg.test(value)) {
// 属性名
const key = RegExp.$1.trim()
// 替换插值表达式为this.$data对应的值
node.textContent = value.replace(reg, this.vm[key])
new Watcher(this.vm, key, value => {
node.textContent = value
})
}
}
// 处理指令
compileElement(node) {
Array.from(node.attributes).forEach(attr => {
// 获取元素名称
let attrName = attr.name
// 判断当前元素属性名称是否是指令
if(this.isDirective(attrName)) {
// 截掉v-
attrName = attrName.substr(2)
// 获取指令对应的值v-text="msg",获取的就是msg
const key = attr.value
// 处理不同指令
this.update(node, key, attrName)
}
})
}
update(node, key, dir) {
const updateFn = this[dir + 'Updater']
// 因为要使用this指向Compiler
updateFn && updateFn.call(this, node, this.vm[key], key)
}
textUpdater(node, value, key) {
node.textContent = value
// 创建watcher,监视数据变化
new Watcher(this.vm, key, value => {
node.textContent = value
})
}
modelUpdater(node, value, key) {
node.value = value
// 创建watcher,监视数据变化
new Watcher(this.vm, key, value => {
node.value = value
})
}
}
index.html中引入dep.js和watcher.js
<!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>
</head>
<body>
<div id="app">
<h6>插值表达式</h6>
<div>{{msg}}</div>
<div>{{count}}</div>
<h6>v-text</h6>
<div v-text="msg"></div>
<div v-text="count"></div>
<h6>v-model</h6>
<input v-model="msg"></input>
<input v-model="count"></input>
</div>
<script src="./js/dep.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'hello',
count: 10
}
})
</script>
</body>
</html>
运行后,在控制台修改vm.msg='xxx',效果如图所示:
差值表达式和指令关联的msg都更新了,说明响应式机制是ok的
9.实现双向绑定
效果就是改变msg, input的值会改变,这个之前已经实现了,现在只需要实现改变input的值,绑定msg的差值表达式和v-text指令都应该得到更新,只需要在compile.js的指令处理方法modelUpdater中,绑定input事件,改变vm[key]即可
效果如图所示:
到此,简单的vue.js就实现了。
源码见:[https://gitee.com/caicai521/vue-study/tree/master/vue-demo](