vue到现在已经出到了3的版本,我们一步步回顾一下它的发展。以及分析核心原理。
阅读本文需要一些es6基础,不太里了解的同学了可以看一下阮一峰大佬写的ES6入门教程
vue1是没有虚拟dom的,在这个版本我们只关注相应式。在vue1中实现数组双向绑定的是Object.defineProperty。
首先我们先做一个小demo新建一个index.js
function defineReactive(obj, key, val){
Object.defineProperty(obj, key, {
get (){
console.log('get',val)
return val
},
set (newVal){
if(newVal !== val){
console.log('set',newVal)
val = newVal
}
}
})
}
var obj = {bar:123}
defineReactive(obj, bar, obj.bar)
obj.bar // 控制台打印get 123
obj.bar = 456 //控制台打印 set 456
复制代码
我看到这样就可以触发setter和getter。但是如果是对象嵌套对象呢?显然我们的方法是不满足这样的情况的。是否我们在调用之前看一下他的子项是什么呢?看一下这样是否能解决这个问题。
function defineReactive (obj, key, val) {
observer(val)
// 如果是对象类型的重新执行监听更深一级
Object.defineProperty(obj, key, {
get () {
console.log('get', val)
return val
},
set (newVal) {
if (newVal !== val) {
console.log('set', newVal)
val = newVal
observer(val)
// 如果对象中的一项修改成数组重新监听一下
}
}
})
}
function observer (obj) {
if (typeof obj !== 'object' && obj != null) {
return
} // 如果传入的不是个对象返回不去执行
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
var obj = { bar: 123, bar1: { a: 123 } }
observer(obj)
obj.bar1.a // get 123
obj.bar1.a = 465 // set 465
复制代码
数据相应式
接下来我们看一下我们怎么使用vue
new Vue({
el:'#app',
data(){
return {
...
}
},
methods:{
...
}
})
复制代码
我们从调用这段代码可以分析出首先得需要一个vue的类(构造函数)然后传一个对象,对象中有这些属性el,data,methods。这些有可以说是我们使用这个类的时候需要的一个配置,一些可选配置。接下来我们来简单实现一个vue的类
class Vue{
constructor(options){ // 传入的参数
this.$options = options // 保存一下参数
initData(this) // 执行初始化函数
}
}
function initData(vm){
let {data} = vm.$options // 将data单独取出来
// 判断data是否存在
if(!data){
vm_data={} //如果data选项不存在就创建一个空的对象
} else {
vm_data = typeof === 'function' ? data() : data
// 这里判断一下data是不是一个方法因为我们在写的时候可以直接data:{} 或者 data(){return {}} 俩个方式。如果是方法就执行一个返回一个对象。
}
// 这个方法的中重点来了,我们循环执行好的_data然后便利挂上代理,让我们可以直接访问到,而不是this.data.变量 直接 this.变量 这种形式
Object.keys(vm._data).forEach(key => {
proxy(vm, '_data', key)
})
observe(vm._data)
}
// 访问target.key 实际上是返回 target[sourceKey][key] 在这里相当于this.a 实际上访问的是this._data.a 同理当修改的时候也是改的目标上的值
function proxy(target, sourceKey, key){
Object.defineProperty(target, key,{
get() {
return target[sourceKey][key]
},
set(newVal){
target[sourceKey][key] = newVal
}
})
}
function observe(value){
if(typeof value !== 'object' && value != null){
return
}
if(value.__ob__){
return value.__ob__
}
new Observer(value)
}
class Observer{
constructor(value){
Object.defineProperty(value,'__ob__',{
value:this,
enumerable: false,
writable: true,
configurable: true
})
this.walk(value)
}
walk(obj){
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
observe(val)
}
}
})
}
复制代码
现在就可以实现一个没有关注视图的相应式的vue,在initData的时候我们也可以用同样的方法执行代理方法,props等等一些配置项。defineProperty只能给对象做代理,接下来我们给数组也做一下代理。
class Observer{
constructor(value){
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
writable: true,
configurable: true
})
if(Array.isArray(value)){
// 处理数组、先预留位置
} else {
this.walk(value)
}
}
}
复制代码
页面更新
上边我们在数据相应式完成的差不多了,现在要和页面结合在一起。
在此之前我们要先了解这么几个东西
Vue: 框架构造函数
Observer: 执行数据相应化(分析数据是对象还是数组)
Complie:编译模板,初始化试图,收集依赖(更新函数、watcher创建)
Watcher:执行更新函数(更新DOM)
Dep:管理watcher,批量更新
编译模板-complie
编译模板中的vue特殊语法,初始化视图,更新函数
class Compile{
constructor(el, vm){
this.$vm = vm
this.$el = document.queryselector(el)
if(this.$el){
this.compile(this.$el)
}
}
compile(el){
el.childNodes.forEach(node =>{
if(this.isElement(node)){
this.compileElement(node)
} else if(this.isInter(node)){
this.complieText(node)
}
if(node.childNodes){
this.compile(node)
}
})
}
isElement(node){
return node.nodeType === 1
}
isInter(node){
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
complieText(node){
this.update(node, RegExp.$1,'text')
}
compileElement(){
// 获取节点属性
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr=>{
const attrName = attr.name
const exp = attr.value
if(this.isDirective(attrName)){
const dir = attrName.substring(2) //截取出v特殊vue指令 text html model
this[dir] && this[dir](node,exp)
}
if(this.isEvent(attrName)){
const dir = attrName.substring(1)
this.eventHandler(node, exp, dri)
}
})
}
eventHandler(node, exp, dir){
const cb = this.$vm.$options.methods && this.$vm.$options.methods[exp]
node.addEventListener(dir, fn.bind(this.$vm))
}
text(node, exp){
this.update(node, exp, 'text')
}
html(node, exp){
this.update(node, exp, 'html')
}
model(node, exp){
this.update(node, exp, 'model')
const { tagName, type } = node
tagName = tagName.toLowerCase()
if(tagName == 'input' && type == 'text'){
// 如果绑定了初始值 赋值初始值
node.value = this.$vm[exp]
node.addEventListener('input',()=>{
this.$vm[exp] = e.target.value
// 监听text的输入事件改变绑定的值
})
}
else if(tagName == 'input' && type == 'checkbox') {
node.value = this.$vm[exp]
node.addEventListener('change',()=>{
this.$vm[exp] = e.target.checked
// 监听checkbox的change事件改变绑定的值
})
}
else if(tagName == 'select'){
node.value = this.$vm[exp]
node.addEventListener('input',()=>{
this.$vm[exp] = e.target.value
// 监听text的输入事件改变绑定的值
})
}
}
// 所有绑定都需要绑定更新函数以及对应wathcer实例
update(node, exp, dir){
const cb = this[dir + 'Updater']
cb && cb(node, this.$vm(exp))
new Watcher(this.$vm, exp, function(val){
cb && cb(node, val)
})
}
textUpdater(node, value){
node.textContent = value
}
htmlUpdater(node, value){
node.innerHTML = value
}
modelUpadter(node, value){
node.value = value
}
}
复制代码
vue1中key和dep是一一对应的data中返回的对象对应一个dep,对象中每个key都对应这一个dep
class Dep{
constructor(){
this.watchers = []
}
static target = null
depend(){
if (this.watchers.includes(Dep.target)){
return
}
this.watchers.push(Dep.target)
}
notify(){
this.watchers.forEach(watcher=>{
watcher.update()
})
}
}
复制代码
页面中一个依赖对应着一个watcher,在vue1中dep和watcher是1:n的关系
class watcher{
constructor(vm, key, updateFn){
this.vm = vm
this.key = key
this.updateFn = updateFn
// 读一次数据,触发definrReaective里面的get()
Dep.target = this
this.vm[this.key]
Dep.target = null
}
update(){
// 传入当亲的最新值给更新函数
this.updateFn.call(this.vm, this.vm[this.key])
}
}
复制代码
这样就基本上形成了一个相应式的页面,但是我们在重写Array方法是还差一点内容,我们只复制了方法没有通知更新。下面我们来继续完善这个方法。
const orignalProto = Array.prototype;
const arrayProto = Object.create(orignalProto);
// 只有这个7个方法会改变原数组所以我们重写这7个方法
['push','pop','shift','unshift','splice','reverse','sort'].forEach(method =>{
orignalProto[method].apply(this, arguments)
let inserted = []
switch(method) {
case 'push':
case 'unshift':
inserted = arguments
break;
case 'splice':
inserted = arguments.slice(2)
break;
}
if(inserted.length > 0){
inserted.forEach(key=>{
observe(key)
})
}
})
复制代码
一个简易的vue就完成了,我们只做了最简单的分析代码,没有做兼容性处理。千万不要去较真。
总结
vue1在设计之初为了解决我们日常繁琐的获取dom,获取值,监听dom变化,更新dom数据等。他把上述的弊端汇总形成一个自动化的框架,减少我们繁琐的操作。
当然vue1也是有缺点的,vue1中没有虚拟dom和Diff在项目体积庞大内容丰富的时候会消耗很多资源。页面每有一个相应式数据就会产生一个watcher,在大型页面中会有非常多的数据同样也会产生非常多的watcher。总用资源。