前言
使用Vue已经一段时间,所以对Vue的核心功能——双向的数据绑定原理进行了研究和实现。下面直接上代码,因为代码里基本上都写清楚了注释,所以就不多说废话了。(直接复制代码到本地就可以运行)
上菜~~
正文
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
<h1>{{text}}</h1>
<button v-on:click="changeText">change text</button>
</div>
<script>
//vue双向数据绑定的原理几个关键点
//1.observe 数据劫持
//2.Dep 消息订阅器(收集订阅者,发布消息)
//3.Watcher (订阅者)
//4.Compile HTML模板解析器
//5.Vue 入口函数
//1.observe 对数据进行拦截
function observe (data) {
if(!data || typeof data !== 'object'){
return
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
})
}
function defineReactive (data, key, value) {
//如果子属性为object也进行遍历监听
observe(value)
//每一个key都会有一个dep实例来管理自己的订阅者
let dep = new Dep()
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get: function() {
//在Watcher初始化实例的时候回触发对应属性的get函数
//此时将对应的watcher添加到对应的subs中
if(Dep.target){
dep.addSub(Dep.target);
}
return value
},
set: function(newValue) {
if(value === newValue){
return
}
value = newValue
dep.notice()
}
})
}
//2.Dep消息订阅器(收集订阅者,发布消息)
function Dep () {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notice: function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
}
//临时缓存watcher
Dep.target = null
//3. Watcher 观察者
function Watcher (vm, key, callback) {
this.callback = callback
this.vm = vm
this.key = key
//触发属性的get函数,然后添加到对应的消息订阅器上
this.value = this.get()
}
Watcher.prototype = {
update: function() {
this.run()
},
run: function() {
//这里虽然也会触发get函数
//但是不会再次添加观察者到消息订阅器中
let value = this.vm[this.key]
let oldValue = this.value
if(oldValue !== value){
this.callback.call(this.vm, value, oldValue)
}
},
get: function() {
//缓存下watcher自己
Dep.target = this
//在第一次new Watcher执行到这里的时候
//会触发get函数,此时会添加watcher到相应的sub中
let value = this.vm[this.key]
//添加成功
Dep.target = null
return value
}
}
//4.compile 模板解析器
function Compile (el, vm) {
this.vm = vm
this.$el = document.querySelector(el)
if(this.$el){
//初始化dom片段,防止频繁的操作dom
this.$fragment = this.createFragment(this.$el)
//解析节点
this.init()
this.$el.appendChild(this.$fragment)
}
}
Compile.prototype = {
createFragment: function (el) {
let fragment = document.createDocumentFragment()
let child
while (child = el.firstChild) {
// firstChild和firstElementChild区别
// 若文档已存在了该节点,则会先删除,然后在插入到新的位置
fragment.appendChild(child)
}
return fragment
},
init: function () {
this.compileElement(this.$fragment)
},
compileElement: function (el) {
// childNodes获取元素节点和文本节点,children只获取元素节点
let childNodes = el.childNodes
let self = this
Array.from(childNodes).forEach(function (node) {
let text = node.textContent
let reg = /\{\{(.*)\}\}/
if (node.nodeType === 1){
//按元素节点处理
self.compile(node)
}else if (node.nodeType === 3 && reg.test(text)) {
//按文本节点处理
self.compileText(node, RegExp.$1)
}
})
},
compileText: function (node, exp) {
let text = this.vm[exp]
//更新文本节点的值
node.textContent = text
//生成订阅器并绑定更新函数, model => view
new Watcher(this.vm, exp, function (value) {
node.textContent = value
})
},
compile: function (node) {
//解析元素节点的属性
let nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach((attr) => {
let attrName = attr.name
//判断是否是规范的指令,v-开头
if(this.isDireactive(attrName)){
let exp = attr.value
let dir = attrName.slice(2)
//判断是什么指令,事件指令?还是普通指令
if(this.isEventDireactive(dir)){
//根据事件指令集进行处理
compileUtil.eventHander(node, this.vm, exp, dir)
} else {
//普通指令
//按普通指令处理
//这里假设是最简单的v-model指令
compileUtil[dir](node, this.vm, exp)
}
}
})
//继续解析该元素的子节点
this.compileElement(node)
},
isDireactive: function (attrName) {
if(attrName.includes('v-')){
return true
}
return false
},
isEventDireactive: function (dir) {
if(dir.includes('on')){
return true
}
return false
}
}
let compileUtil = {
model: function (node, vm, exp) {
//初始化值
node.value = vm[exp]
//view => model
node.addEventListener('input', function (event) {
vm[exp] = event.target.value
})
//modal => view
new Watcher(vm, exp, function (value) {
node.value = value
})
//强制触发对应的get函数,来通知其他的观察者更新数据
node.nodeValue = vm[exp]
},
eventHander: function (node, vm, exp, dir) {
//截取事件名称,click
let eventName = dir.slice(3)
//给指定的节点绑定事件监听
node.addEventListener(eventName, function (event) {
vm[exp]()
})
}
}
//5.Vue函数入口
function Vue (options) {
//检测是否通过new关键调用Vue
if (!this instanceof Vue) {
//如果是当成普通函数调用,this=>window
alert('please use Vue by "new" key word!')
}
let vm = this
vm.$data = options.data
vm.$methods = options.methods
let data = options.data
let methods = options.methods
//将vm.key代理到vm.$data.key
Object.keys(data).forEach(function (key) {
vm.proxyKey(vm.$data, key)
})
//将vm.key代理到vm.$methods.key
Object.keys(methods).forEach(function (key) {
vm.proxyKey(vm.$methods, key)
})
observe(data)
vm.$compile = new Compile(options.el, vm)
}
Vue.prototype = {
proxyKey: function (targetObj, key) {
let vm = this
Object.defineProperty(vm, key, {
configurable: false,
enumerable: true,
get: function () {
return targetObj[key]
},
set: function (newValue) {
targetObj[key] = newValue
}
})
}
}
let vm = new Vue({
el: '#app',
data: {
text: 'hello world!'
},
methods: {
changeText: function () {
this.$data.text = 'hello vue world!'
}
}
})
</script>
</body>
</html>
网友评论