本文共 6242 字,大约阅读时间需要 20 分钟。
首先我们上一张图
由图可知,MVVM是由两大块构成,Observer
劫持监听响应式 以及 Compile
指令解析。 下面我们就从这两方面来组合实现Vue。
class Vue { constructor(options) { this.$options = options this.$data = options.data this.observe(this.$data) // 运行created生命周期 this.$options.created && this.$options.created.call(this) } observe(data) { if (!data || typeof data !== 'object') { return } Object.keys(data).forEach((key) => { this.defineProperty(data, key, data[key]) this.poxyData(key) }) } defineProperty(obj, key, val) { const dep = new Dep() // 递归遍历 this.observe(val) Object.defineProperty(obj, key, { get() { // 初始化时添加wather进行观察 Dep.target && dep.addWather(Dep.target) return val }, set(newVal) { val = newVal // 被赋值时,通知更新 dep.notify() } }) } poxyData(key) { this.$data[key] && Object.defineProperty(this, key, { get() { return this.$data[key] }, set(newVal) { this.$data[key] = newVal } }) }}复制代码
const pan = new Vue({ el: '#app', data: { name: "I am test.", age: 12, fag: { bar: 'bar' }, html: '' } })复制代码
在这里,我们首先运用Object.defineProperty()
方法对我们传入Vue配置的data进行数据劫持,defineProperty(obj, key, val)
方法我们再次调用this.observe(val)
,这里是因为数据可能不止一层,我们需要把data下面的所以数据都拦截到。 poxyData(key)
这个方法就是把data里的数据代理到this
上,我们可以pan.html
访问到pan.$data.html
里的数据。
class Dep { constructor() { this.wathers = [] } addWather(wather) { this.wathers.push(wather) } notify() { this.wathers.forEach(wather => wather.update()) }}class Wather { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 把新生成的wather附加到Dep.target Dep.target = this // 访问一次被代理的属性 this.vm[this.key] // Dep.target 置为空,等待下一个wather的生成 Dep.target = null } update() { this.cb && this.cb.call(this.vm, this.vm[this.key]) }}复制代码
Dep
就相当简单了,只是两个方法一个addWather
和notify
两个方法, Wather
就一个更新方法。这里就是我们常说的观察者模式。Dep
保存多个wather
,当Dep
发现Wather
有更新时,Dep
会调用notify
方法取通知所有的wather
方法update
进行更新。
class Compile { constructor(el, vm) { this.$el = document.querySelector(el) this.$vm = vm // 模板移动到文档片段 this.$fragment = this.node2Fragment(this.$el) // 编译 this.compile(this.$fragment) // 把编译好的文档片段添加到el this.$el.appendChild(this.$fragment) } node2Fragment(el) { const fragment = document.createDocumentFragment() let firstNode while (firstNode = el.firstChild) { fragment.appendChild(firstNode) } return fragment } compile(el) { const nodes = el.childNodes Array.from(nodes, (node) => { // 标签 if (node.nodeType === 1) { this.compileElement(node) } // 文本 else if (this.isInter(node)) { this.compileText(node) } // 编译子节点 if (node.children && node.childNodes.length > 0) { this.compile(node) } }) } isInter(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent) } compileElement(node) { const nodeAttrs = node.attributes; Array.from(nodeAttrs, (nodeAttr) => { // 获取到标签内的属性名以及属性值 const attrName = nodeAttr.name const attrValue = nodeAttr.value // 匹配p-开头的属性名 if (attrName.includes('p-')) { const dir = attrName.substring(2) this[dir] && this[dir](node, attrValue) } // 匹配@开头的属性名 if (attrName.includes('@')) { const dir = attrName.substring(1) this[dir] && this[dir](node, attrValue) } }) } compileText(node) { // 拿取到文本标签里的{ {xxx}} // console.log(RegExp.$1); // { {name}}花括号内匹配的值 this.update(node, RegExp.$1, 'text') } update(node, key, dir) { const updator = this[dir + 'Updator'].bind(this) updator && updator(node, this.$vm[key]) new Wather(this.$vm, key, (value) => { updator && updator(node, value) }) } eventListener(node, key, type) { const options = this.$vm.$options // 处理this指向问题 const eventFn = options.methods[key].bind(this.$vm) eventFn && this.addEventListener(node, type, eventFn) } textUpdator(node, value) { node.textContent = value } htmlUpdator(node, value) { node.innerHTML = value this.compile(node) } modelUpdator(node, value) { node.value = value } text(node, key) { this.update(node, key, 'text') } html(node, key) { this.update(node, key, 'html') } model(node, key) { this.update(node, key, 'model') // 通过input 事件双向绑定表单数据 node.addEventListener('input', () => { this.$vm[key] = node.value }) } click(node, key) { this.eventListener(node, key, 'click') } dblclick(node, key) { this.eventListener(node, key, 'dblclick') } addEventListener(node, key, fn) { node.addEventListener(key, fn) }}复制代码
这里面我们模板编译的思路主要是3步:
这里有一个重点,为什么要添加到文档片段,文档片段用作一个临时的占位符放置项目,然后用appendChild()添加到dom中,这里做到最小化现场更新(一次更新),提升了连续dom操作的性能瓶颈
我们重点来讲讲如何编译文档片段,首先遍历出我们所有的dom,然后分为标签和文本两块来进行解析
首先获取出标签中的属性,我们用node.attributes
获取当前node的所有属性
取出所有的属性名以及属性值
分情况处理p-
和@
(本文只针对这两种情况做处理)
p-text 改变node.textContent
的值,并添加wather监听后续变化
p-html 改变node.innerHTML
的值,并添加wather监听后续变化
p-model 改变node.value
的值,并添加wather监听后续变化,并且给node添加input
事件,实现表单的双向绑定
@click 通过node.addEventListener('click', fn)
添加click事件
@dblclick 通过node.addEventListener('dblclick', fn)
dblclick事件
node.textContent
的值最后再把编译完成的文档片段添加到dom完成整个流程,详细代码请参考 。
作者:潘勇旭 链接:https://juejin.im/post/5e034b816fb9a0161f306657 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。