创立工夫: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 = 0function Dep() { this.id = UUID++ // 寄存以后属性的所有的监听watcher this.subs = []}Dep.prototype.addSub = function(watcher) { this.subs.push(watcher)}// 目标是将以后 dep 实例 传入 watcherDep.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