共计 5642 个字符,预计需要花费 15 分钟才能阅读完成。
创立工夫:2020-09-11
在线演示 demo
次要了解、实现如下办法:
Observe
:监听器 监听属性变动并告诉订阅者Watch
:订阅者 收到属性变动,更新视图Compile
:解析器 解析指令,初始化模板,绑定订阅者,绑定事件Dep
:寄存对应的所有 watcher 实例
次要执行流程
将 watcher 装入对应的 dep 实例的订阅列表中的过程
相干 html 代码,用于被解析绑定数据
这里的代码即是 compile 中要解析的模板,解析其中的 v-model
{{}}
v-on
等命令
<div id="app">
<p> 姓名:<input type="text" v-model="name"></p>
<p> 学号:<input type="text" v-model="number"></p>
<p><span> 学号:</span> <span>{{number}}</span></p>
<p><span>computed 实现:</span> <span>{{getStudent}}</span></p>
<p>
<button v-on:click="setData"> 事件绑定 </button>
</p>
</div>
observer 代码
为 data 数据增加 get、set 以便增加 watcher,和创立 Dep 实例,告诉更新视图
const defineProp = function(obj, key, value) {observe(value)
/*
* 事后为每一个不同属性创立一个独有的 dep
*/
const dep = new Dep()
Object.defineProperty(obj, key, {get: function() {
/*
* 依据不同的属性创立且只在创立 Watcher 时调用
*/
if(Dep.target) {dep.targetAddDep()
}
return value
},
set: function(newVal) {if(newVal !== value) {
/*
* 这里的赋值操作,是以便于 get 办法中返回 value, 因为都是赋值后马上就会调用 get 办法
*/
value = newVal
/*
* 告诉监听的属性的所有订阅者
*/
dep.notify()}
}
})
}
const observe = function(obj) {if(!obj || typeof obj !== 'object') return
Object.keys(obj).forEach(function(key) {defineProp(obj, key, obj[key])
})
}
Dep 代码
次要是将 watcher 放入 对应的 dep 订阅列表
let UUID = 0
function Dep() {
this.id = UUID++
// 寄存以后属性的所有的监听 watcher
this.subs = []}
Dep.prototype.addSub = function(watcher) {this.subs.push(watcher)
}
// 目标是将以后 dep 实例 传入 watcher
Dep.prototype.targetAddDep = function() {
// 这里 this 是实例化后的 dep
Dep.target.addDep(this)
}
Dep.prototype.notify = function() {
// 触发以后属性的所有 watcher
this.subs.forEach(_watcher => {_watcher.update()
})
}
Dep.target = null
Watcher 代码
数据更新后,更新视图
function Watcher(vm, prop, callback) {
this.vm = vm
this.prop = prop
this.callback = callback
this.depId = {}
this.value = this.pushWatcher()}
Watcher.prototype = {update: function() {
/* 更新值的变动 */
const value = this.vm[this.prop]
const oldValue = this.value
if (value !== oldValue) {
this.value = value
this.callback(value)
}
},
// 目标是接管 dep 实例,用于将以后 watcher 实例放入 subs
addDep: function(dep) {if(!this.depId.hasOwnProperty(dep.id)) {dep.addSub(this)
this.depId[dep.id] = dep.id
} else {console.log('already exist');
}
},
pushWatcher: function() {
// 存贮订阅器
Dep.target = this
// 触发对象的 get 监听,将下面赋值给 target 的 this 退出到 subs
var value = this.vm[this.prop]
// 退出完后就删除
Dep.target = null
return value
}
}
Compile 代码
解析 html 模板,创立代码片段,绑定数据事件
function Compile(vm) {
this._vm = vm
this._el = vm._el
this.methods = vm._methods
this.fragment = null
this.init()}
Compile.prototype = {init: function() {this.fragment = this.createFragment()
this.compileNode(this.fragment)
// 当代码片段中的内容编译完了之后,插入 DOM 中
this._el.appendChild(this.fragment)
},
// 依据实在的 DOM 节点,创立文档片段
createFragment: function() {const fragment = document.createDocumentFragment()
let child = this._el.firstChild
while(child) {
// 将节点退出到文档片段中后,该节点会被从原来的地位删除,相当于挪动节点地位
fragment.appendChild(child)
child = this._el.firstChild
}
return fragment
},
compileNode: function(fragment) {
let childNodes = fragment.childNodes;
[...childNodes].forEach(node =>{if(this.isElementNode(node)) {this.compileElementNode(node)
}
let reg = /\{\{(.*)\}\}/
// 获取节点下的所有文本内容
let text = node.textContent
// 判断是否已是纯文本节点,且文本内容是否有 {{}}
if(this.isTextNode(node) && reg.test(text)) {let prop = reg.exec(text)[1].trim()
this.compileText(node, prop)
}
if(node.childNodes && node.childNodes.length) {
// 递归编译子节点
this.compileNode(node)
}
})
},
compileElementNode: function(element) {
// 获取属性,只有元素节点有如下办法
let nodeAttrs = element.attributes;
[...nodeAttrs].forEach(attr => {
let name = attr.name
if(this.isDirective(name)) {
/*
* v-model 放在可承受 input 事件的标签上
*/
let prop = attr.value
if (name === 'v-model') {
/*
* 获取到的 value 即为须要绑定的 data
*/
this.compileModel(element, prop)
} else if(this.isEvent(name)) {
/*
* 绑定事件
*/
this.bindEvent(element, name, prop)
}
}
})
},
compileModel: function(element, prop) {let val = this._vm[prop]
this.updateElementValue(element, val)
new Watcher(this._vm, prop, value => {this.updateElementValue(element, value)
})
element.addEventListener('input', event => {
let newValue = event.target.value
if (val === newValue) return
this._vm[prop] = newValue
})
},
compileText: function(textNode, prop) {
let text = ''
if(/\./.test(prop)) {var props = prop.split('.')
text = this._vm[props[0]][props[1]]
} else {text = this._vm[prop]
}
this.updateText(textNode, text)
console.log(text);
new Watcher(this._vm, prop, (value) => {this.updateText(textNode, value)
})
},
bindEvent: function(element, name, prop) {var eventType = name.split(':')[1]
var fn = this._vm._methods[prop]
element.addEventListener(eventType, fn.bind(this._vm))
},
/*
* 判断属性是否为指令
*/
isDirective: function (text) {return /v-/.test(text)
},
isEvent: function(text) {return /v-on/.test(text)
},
isElementNode: function(node) {// 元素节点返回 1 文本节点 ( 元素或属性中的文字)3 属性节点 2(被废除)
return node.nodeType === 1
},
isTextNode: function(node) {return node.nodeType === 3},
updateElementValue: function(element, value) {element.value = value || ''},
updateText: function(textNode, value) {textNode.textContent = value || ''}
}
vue 简要构造函数
次要实现了数据的双向绑定,自定义事件,computed
function FakeVue(options) {
this._data = options.data
this._methods = options.methods
this._computed= options.computed
this._el = document.querySelector(options.el)
// 将 _data 中的属性代理到外层的 vm 上, 这里只代理了_data 的第一层属性
Object.keys(this._data).forEach(key => {this._proxyData(key)
})
this._initComputed()
this._init()}
FakeVue.prototype._init = function() {
// 开始递归监听对象的所有属性,直到属性值为值类型
observe(this._data)
new Compile(this)
}
FakeVue.prototype._proxyData = function(key) {
Object.defineProperty(this, key, {get: function() {return this._data[key]
},
set: function (value) {this._data[key] = value
}
})
}
FakeVue.prototype._initComputed = function() {
// 简略的实现: 将值挂载到跟上即可
const computed = this._computed
Object.keys(computed).forEach((v) => {
Object.defineProperty(this, v, {get: computed[v],
set: function (value) {}})
})
}
创立 vue 实例
try{
let vm = new FakeVue({
el: '#app',
data: {
name: 'warren',
number: '10011',
score: {math: 90}
},
computed: {getStudent: function() {return `${this.name}:学号是 ${this.number}`
}
},
methods:{
// 通过在 compile 中给元素绑定事件实现
setData: function() {alert('name:'+this.name);
}
}
});
} catch(error) {console.error(error)
}
结语
这是从作者的了解角度,论述的一个简略的 vue 实现原理示例,心愿对正在摸索的你有所帮忙
在这个示例中,次要的简单点在于对 html 模板的解析,数据的双向绑定。
倡议跟着代码的执行程序理解整个过程,关键点的代码都有必要的正文,若发现问题请斧正。
最初附上 vue 源码地址,次要关注其中的 core
和 compiler
文件;
欢送交换 Github
正文完
发表至: javascript
2020-09-11