需求
- 使用
Vue
的思想实现一个简易版的Vue
,实现一个简单的双向数据绑定
<div id="app">
<h2>{{person.name}}--{{person.age}}</h2>
<h3>{{person.name}}</h3>
<h3>{{msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.name"></div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg">
<button v-on:click="handleClick">按钮v-on</button>
<button @click="handleClick">按钮@</button>
</div>
<script>
var vm = new myVue({
el: '#app',
data() {
return {
person:{
name: 'wmm',
age: 20,
},
msg: 'vue test',
htmlStr: '<h3>我们都是中国人</h3>'
}
},
methods: {
handleClick() {
this.person.name += '6'
}
}
})
</script>
基础
数据劫持
Vue2.x
使用的是Object.defineProperty
实现的
Object.defineProperty
描述符的属性
value
:属性的值get
:给属性提供getter
的方法,如果没有getter
则为undefined
。该方法返回值被用作属性值。默认为undefined
set
:给属性提供setter
的方法,如果没有setter
则为undefined
。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined
writable
:属性是否可写enumerable
:属性是否会出现在for in
或者Object.keys()
的遍历中configurable
:属性是否配置,以及可否删除
<div id="app">
<input type="text" id="test" />
<p id="test1"></p>
</div>
<script>
var obj = {}
// Object.defineProperty 定义 get 和 set 方法
// 在读取 obj.hello 的时候,会走 get 方法
// 在设置的时候 obj.hello = 123 ,会走 set 方法
Object.defineProperty(obj, 'hello', {
get: function() {
console.log("get方法被调用")
},
set: function(newVal) {
console.log("set方法被调用", newVal)
document.getElementById('test').value = newVal
document.getElementById('test1').innerHTML = newVal
}
})
document.getElementById('test').addEventListener('input', function(e) {
obj.hello = e.target.value
})
</script>
Vue3.x
使用的是es6
的proxy
实现的
let proxy = new Proxy(target, handler);
target
:要代理的目标对象。(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
:定义拦截行为的配置对象(也是一个对象,其内部的属性均为执行操作的函数)。
handler.getPrototypeOf()
:在读取代理对象的原型时触发该操作,比如在执行Object.getPrototypeOf(proxy)
时handler.setPrototypeOf()
:在设置代理对象的原型时触发该操作,比如在执行Object.setPrototypeOf(proxy, null)
时handler.isExtensible()
:在判断一个代理对象是否是可扩展时触发该操作,比如在执行Object.isExtensible(proxy)
时handler.preventExtensions()
:在让一个代理对象不可扩展时触发该操作,比如在执行Object.preventExtensions(proxy)
时handler.getOwnPropertyDescriptor()
:在获取代理对象某个属性的属性描述时触发该操作,比如在执行Object.getOwnPropertyDescriptor(proxy, "foo")
时handler.defineProperty()
:在定义代理对象某个属性时的属性描述时触发该操作,比如在执行Object.defineProperty(proxy, "foo", {})
时handler.has()
:在判断代理对象是否拥有某个属性时触发该操作,比如在执行"foo" in proxy
时handler.get()
:在读取代理对象的某个属性时触发该操作,比如在执行proxy.foo
时handler.set()
:在给代理对象的某个属性赋值时触发该操作,比如在执行proxy.foo = 1
时handler.deleteProperty()
:在删除代理对象的某个属性时触发该操作,比如在执行delete proxy.foo
时handler.ownKeys()
:在获取代理对象的所有属性键时触发该操作,比如在执行Object.getOwnPropertyNames(proxy)
时handler.apply()
:在调用一个目标对象为函数的代理对象时触发该操作,比如在执行proxy()
时handler.construct()
:在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy()
时
<div id="app">
<input type="text" id="test" />
<p id="test1"></p>
</div>
<script>
var _obj = {
hello: ''
}
var obj = new Proxy(_obj, {
get: function(target, key) {
console.log("get方法被调用")
return target[key]
},
set: function(target, key, value) {
console.log("set方法被调用", value)
target[key] = value
document.getElementById('test').value = value
document.getElementById('test1').innerHTML = value
}
})
document.getElementById('test').addEventListener('input', function(e) {
obj.hello = e.target.value
})
</script>
DocumentFragment
-
DocumentFragment
(文档片段)可以看作节点容器,它可以包含多个子节点 -
当我们将它插入到
DOM
中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。 -
使用
DocumentFragment
处理节点,速度和性能远远优于直接操作DOM
-
Vue
进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过append
方法,DOM
中的节点会被自动删除)到DocumentFragment
中,经过一番处理后,再将DocumentFragment
整体返回插入挂载目标。
<div id="app">
<input type="text" id="a" />
<span id="b"></span>
</div>
<script>
function nodeToFragment(node) {
var flagment = document.createDocumentFragment()
var child
while (child = node.firstChild) {
// 劫持node的所有子节点
flagment.append(child)
// flagment.appendChild(child)
}
return flagment
}
var dom = nodeToFragment(document.getElementById('app'))
// 返回到app中
document.getElementById('app').appendChild(dom)
</script>
订阅-发布者模式
- 定义一个主题对象
Dep
,用来管理所有的订阅者 - 发布者发布信息,主题对象执行
notify
方法 - 主题对象触发所有订阅者执行
update
方法
// 一个发布者publisher
var pub = {
publish: function() {
dep.notify()
}
}
// 三个订阅者subscribers
var sub1 = { update: function() { console.log(1) } }
var sub2 = { update: function() { console.log(2) } }
var sub3 = { update: function() { console.log(3) } }
// 一个主题对象
function Dep() {
this.subs = [sub1, sub2, sub3]
}
Dep.prototype.notify = function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
// 发布者发布信息,主题对象执行notify方法,进而触发订阅者执行update方法
var dep = new Dep()
pub.publish()
实现
myVue
class myVue {
constructor(options) {
this.$el = options.el
this.$data = typeof options.data === 'function' ? options.data() : options.data
this.$methods = options.methods
this.init()
}
init() {
if (!this.$el) return
// 数据劫持,实现响应式
new Observer(this.$data)
// 编译模板
new Compile(this.$el, this)
// 劫持一下数据,使得可以通过 this.person.name 这种形式访问和设置数据
this.proxyData(this.$data)
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: false, // 不可枚举
configurable: true, // 可以修改
get() {
return data[key]
},
set(val) {
data[key] = val
}
})
})
}
}
Observer
- 通过循环和递归使用
Object.defineProperty
劫持数据
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
if (!data || typeof data !== 'object') return
// 循环调用,将所有的属性都配置 get / set
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, value) {
// value 也有可能是对象,递归调用
this.observe(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newVal) {
if (newVal === value) return
value = newVal
}
})
}
}
- 数组的话,需要重写可以改变数组的方法。有新增的项,需要递归劫持
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
if (!data || typeof data !== 'object') return
if (Array.isArray(data)) {
Object.setPrototypeOf(data, this.arrMethods())
this.observeArr(data)
} else {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(data, key, value) {
this.observe(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newVal) {
if (newVal === value) return
value = newVal
}
})
}
observeArr(arr) {
arr.forEach(item => {
this.observe(item)
})
}
arrMethods() {
const METHODS = [
'push',
'pop',
'shift',
'unshift',
'sort',
'reverse',
'splice'
]
const originArrMethods = Array.prototype
const arrMethods = Object.create(originArrMethods)
const that = this
METHODS.forEach(m => {
arrMethods[m] = function() {
const args = Array.prototype.slice.call(arguments)
const res = originArrMethods[m].apply(this, arguments)
let newArr
switch (m) {
case 'push':
case 'unshift':
newArr = args
break
case 'splice':
newArr = args.slice(2)
break
default:
break
}
newArr && that.observeArr(newArr)
return res
}
})
return arrMethods
}
}
Watcher
Vue
使用的是 订阅-发布者模式 + 观察者模式
- 定义
Watcher
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.value = null
}
update() {}
}
- 定义
Dep
class Dep {
constructor() {
this.subs = []
}
add(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
定义一个静态的Dep.target = null
;
在 new
一个 Watcher
的对象的时候,先赋值一个全局变量Dep.target = this
,用完后清空
- 修改
Watcher
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.init()
}
init() {
Dep.target = this
// 为了触发 defineProperty 方法中的 get 方法
this.value = this.vm.$data[this.expr]
Dep.target = null
}
update() {}
}
- 修改
Observer
const dep = new Dep()
class Observer {
// ...
defineReactive(data, key, value) {
this.observe(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 第一次 new 对象 Watcher 的时候,初始化数据的时候,往订阅者对象里面添加对象
// 第二次后,就不需要再添加了
if (Dep.target) dep.add(Dep.target)
return value
},
set(newVal) {
if (newVal === value) return
value = newVal
// 触发更新:发出通知notify() → 触发订阅者的update()方法
dep.notify()
}
})
}
// ...
}
Compile
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
this.init()
}
init() {
if (this.el) {
// 将挂载目标的所有子节点劫持;使用 DocumentFragment 处理
const fragment = this.nodeToFragment(this.el)
// 编译模板
this.compile(fragment)
// DocumentFragment 处理完成后整体返回插入挂载目标
this.el.appendChild(fragment)
} else {
console.error('没有容器组件')
}
}
nodeToFragment(el) {
// 是一个虚拟节点的容器树,可以存放我们的虚拟节点
const fragment = document.createDocumentFragment()
let firstChild
// 把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点
// 一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
compile(node) {
const childNodes = node.childNodes
Array.prototype.slice.call(childNodes).forEach(child => {
if (this.isElementNode(child)) {
this.compileElement(child)
} else if (this.isTextNode(child)) {
this.compileText(child)
}
// 如果子节点存在子节点,递归编译
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
compileElement(node) {}
compileText(node) {}
isElementNode(node) {
return node.nodeType === 1
}
isTextNode(node) {
return node.nodeType === 3
}
}
Vue
有很多指令,比如:v-text
、v-html
、v-model
、v-bind
、v-on:click
、@click
等
这里我们使用 策略模式 统一处理这些指令
var compileUtil = {
getVal(vm, expr) {
// expr 有这种类似的形式 "person.name"
// 这个时候我们要取出来的是 vm.$data['person']['name']
return expr.split('.').reduce((data, val) => {
return data[val]
}, vm.$data)
},
setVal(vm, expr, value) {
// expr 有这种类似的形式 "person.name"
// 这个时候我们要做的是 vm.$data['person']['name'] = value
const arr = expr.split('.')
const last = arr.pop()
arr.reduce((data, val) => {
return data[val]
}, vm.$data)[last] = value
},
text: function(vm, node, expr) {},
html: function(vm, node, expr) {},
model: function(vm, node, expr) {},
on: function(vm, node, expr, eventName) {},
bind: function(vm, node, expr, attrName) {}
}
- 修改
Compile
,处理element
和text
节点
class Compile {
// ...
compileElement(node) {
const attrs = node.attributes
Array.prototype.slice.call(attrs).forEach(attr => {
// v-text="person.name" ==> name: "v-text" value: "person.name"
// v-on:click="handleClick" ==> name: "v-on:click" value: "handleClick"
// @click="handleClick" ==> name: "@click" value: "handleClick"
const { name, value } = attr
if (this.isDirective(name)) {
// "v-text" ==> directive: "text"
// "v-on:click" ==> directive: "on:click"
const directive = name.slice(2)
// "text" ==> dirName: "text" eventName: undefined
// "on:click" ==> dirName: "on" eventName: "click"
const [dirName, eventName] = directive.split(':')
compileUtil[dirName](this.vm, node, value, eventName)
} else if (this.isEventName(name)) {
// "@click" ==> eventName: "click"
const eventName = name.slice(1)
compileUtil['on'](this.vm, node, value, eventName)
}
})
}
compileText(node) {
const content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](this.vm, node, content)
}
}
// ...
isDirective(attrName) {
return attrName.indexOf('v-') === 0
}
isEventName(attrName) {
return attrName.indexOf('@') === 0
}
}
- 各指令具体方法实现
var compileUtil = {
// ...
getContent(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1])
})
},
text: function(vm, node, expr) {
let value
// expr 有可能是这种形式 {{person.name}}--{{person.age}}
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContent(vm, expr))
})
return this.getVal(vm, args[1])
})
} else {
value = this.getVal(vm, expr)
new Watcher(vm, expr, newVal => {
this.updater.textUpdater(node, newVal)
})
}
this.updater.textUpdater(node, value)
},
html: function(vm, node, expr) {
const value = this.getVal(vm, expr)
// 声明成观察者模式
new Watcher(vm, expr, newVal => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, value)
},
model: function(vm, node, expr) {
let value = this.getVal(vm, expr)
new Watcher(vm, expr, newVal => {
this.updater.modelUpdater(node, newVal)
})
this.updater.modelUpdater(node, value)
// 双向数据绑定,实现 视图更新 → 数据更新
node.addEventListener('input', e => {
const newVal = e.target.value
if (newVal === value) return
this.setVal(vm, expr, newVal)
value = newVal
}, false)
},
on: function(vm, node, expr, eventName) {
const cb = vm.$methods && vm.$methods[expr]
if (eventName && cb) {
node.addEventListener(eventName, cb.bind(vm), false)
}
},
bind: function(vm, node, expr, attrName) {
const value = this.getVal(vm, expr)
new Watcher(vm, expr, newVal => {
this.updater.attrUpdater(node, attrName, newVal)
})
this.updater.attrUpdater(node, attrName, value)
},
updater: {
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
},
attrUpdater(node, name, value) {
node.setAttribute(name, value)
}
}
}
- 实现
Watcher
的update
方法,实现视图更新
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.init()
}
init() {
Dep.target = this
this.value = compileUtil.getVal(this.vm, this.expr)
Dep.target = null
}
// 在这个 update 方法中更新视图
update() {
const newVal = compileUtil.getVal(this.vm, this.expr)
if (newVal === this.value) return
this.cb.call(this.vm, newVal, this.value)
this.value = newVal
}
}
发表评论