1. new Vue()
在创建一个vue实例时,主要做了三件事情:
- 挂载data、methods、props等选项到
this上,并对data的每个属性通过Object.defineProperty反向代理到this上 - 调用
Observe(data)对数据做响应式处理 - 调用
Compile(el, vm)编译模板
// vue.js
import Observe from './observe.js'
import Compile from './compile.js'
function Vue(el, options) {
this.el = document.querySelector(el)
this._data = options.data
this._methods = options.methods
Object.keys(this._data).forEach(key => this.setProxy(key)) // 反向代理data所有属性
new Observe(this._data) // 对data做响应式处理
new Compile(this.el, this) // 编译template模板
}
Vue.prototype = {
setProxy(key) {
let self = this
Object.defineProperty(self, key, {
enumerable: false,
configurable: true,
get: () => self._data[key],
set: (newVal) => self._data[key] = newVal
})
}
}
2. Observe对data做响应式处理
调用Observe(data)对数据做响应式处理很简单,在get时让dep收集依赖,在set时让dep通知依赖队列更新
// Observe.js
function Observe(data) {
Object.keys(data).forEach(key => {
if (Array.isArray(data[key])) {
// 数组方法拦截重写...
data[key].forEach(item => new Observe(item))
}
else if (data[key] instanceof Object) {
new Observe(data[key])
}
this.walk(data, key, data[key])
})
}
Observe.prototype = {
walk(data, key, val) {
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: false,
configurable: true,
get() {
if (Dep.target) dep.addSub(Dep.target)
return val
},
set(newVal) {
if (newVal === val) return
dep.notify()
val = newVal
}
})
}
}
function Dep(){
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub)
},
notify() {
this.subs.forEach(sub => sub.update)
}
}
Dep.target = null
3. Compile编译template模板
Compile编译模板大致分为四步:
- 为考虑性能,创建
fragment,将根节点里的内容放入其中,对fragment里的内容进行模板编译完成后append到根节点里 - 遍历
fragment所有的子节点,对元素标签,文本标签区别做编译 - 对于元素标签,解析其所有的
attribute,得到attributeName和attributeValue,若以v-on开头,则给节点监听事件以及事件回调;若是v-model,对节点做双向数据绑定,具体是这样实现的:通过new Watcher(),观察data,触发响应式,当data更新时,视图也会跟着改变,同时,给节点绑定input事件,当事件发生时,data = e.target.value;对于文本节点,解析出{{ xx }}里边的xx,让节点的文本值为xx即可,同时调用new Watcher对xx做响应式处理. - 递归编译当前节点的子节点
// compile.js
function Compile(el, vm) {
this.el = el
this.vm = vm
this.fragment = null
this.init()
}
Compile.prototype = {
init() {
this.fragment = this.nodeToFragment(this.el.children) // 创建fragment,考虑性能!
this.compileElement(this.fragment) // 编译模板
this.el.appendChild(this.fragment)
},
nodeToFragment(node) {
let fragment = document.createDocumentFragment()
fragment.appendChild(node)
return fragment
},
compileElement(el) {
let childNodes = el.childNodes
let self = this
let reg = /\{\{(.*)\}\}/
childNodes.forEach(node => {
if (node.nodeType === 1) { // 元素节点
this.compile(node)
}
else if (node.nodeType === 3 && reg.test(node.textContent)) { // 文本节点
this.compileText(node, reg.exec(node.textContent)[1])
}
if (node.childNodes && node.childNodes.length) { // 递归子节点
this.compileElement(node)
}
})
},
compileText(node, exp) {
// <div>{{ str }}</div>
new Watcher(this.vm, exp, () => node.innerText = this.vm._data[exp])
},
compile(node) {
let attrs = node.attributes
let self = this
attrs.forEach(attr => {
let attrName = attr.name
let exp = attr.value
if (!attrName.startsWith('v-')) return
if (attrName.startsWith('v-on')) {
self.bindEvent(node, attrName, exp)
}
else if (attrName.startsWith="v-model") {
self.bindModel(node, exp)
}
else {
// 其他情况略...
}
node.removeAttribute(attrName)
})
},
bindEvent(node, dir, exp) { // 单向数据绑定
let event = dir.split(':')[1]
let eventCb = this.vm._methods[exp]
// view -> model
node.addEventListener(event, eventCb.bind(this.vm), false)
},
bindModel(node, exp) { // 双向数据绑定
// model -> view
new Watcher(this.vm, exp, () => node.value = this.vm._data[exp])
// view -> model
node.addEventListener('input', (e) => this.vm._data[exp] = e.target.value)
},
compileText(node, exp) { // 单向数据绑定
// model -> view
new Watcher(this.vm, exp, () => node.innerText = this.vm._data[exp])
}
}
// watcher.js
function Watcher(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.value = this.get()
}
Watcher.prototype = {
get() {
Dep.target = this
let value = this.vm._data[this.exp]
this.cb && this.cb()
Dep.target = null
return value
},
update() {
let newValue = this.vm._data[this.exp]
let oldValue = this.value
if (newValue === oldValue) return
this.cb.call(this.vm, newValue, oldValue)
}
}