vue简版源码解析以及简版的实现

vue的设计思想(MVVM模式)

在这里插入图片描述
MVVM框架的三要素包括: 数据响应式 模板引擎及其渲染

  1. 数据响应式: 监听数据变化并在视图中更新
    Object.defineProperty() ---- vue2中采用的数据劫持,数据拦截的策略
    Proxy — vue3中采用的是代理模式
  2. 模板引擎: 提供描述视图的模板语法
    插值: {{}}
    指令: v–on, v-bind, v-model, v-for, v-if 等等
  3. 渲染: 如何将模板转换成为html
    模板 => vdom => dom

数据响应式原理

数据变更能够响应在视图中,就是数据数据响应式. vue2中利用Object.defineProperty()实现变更检测.
在这里插入图片描述

简单实现
// Object.defineProperty()
// 对传入对象的key进行一次拦截, 对某个对象的某个key做拦截
function definReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key)
      return val
    },
    set(newValue) {
      if(val !== newValue) {
        val = newValue
        console.log('set', key, val)
      }
    }
  })
}

const obj = {}

definReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'foooooooooooooo1212'
结合视图
<meta charset="UTF-8">
<div id="app"></div>

<script>
function definReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key)
      return val
    },
    /**
    在上面定义函数的时候放了一个val, val是一个形参,
    但是对一个形参的赋值看起来是没有意义的,但是这里却产生了影响,
    产生影响的原因是这里形成了闭包(因为局部变量val的存在,其次内部
    get函数将val返回出去形成了闭包)使val在内存中驻留
    **/
    set(newValue) {
      if(val !== newValue) {
        val = newValue
        console.log('set', key, val)
	    update()
      }
    }
  })
}
const obj = {}
definReactive(obj, 'foo', new Date().toLocaleTimeString())
function update() {
	app.innerHTML = obj.foo
}
setInterval(() => {
	obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>

遍历需要响应化的对象

// 遍历指定数据对象中的每个key,并拦截他们
function observe(obj) {
	// 判断是否是一个对象
	if(typeof obj !== 'object' || obj === null) {
		return obj
	}
	Object.keys(obj).forEach(key => {
		definReactive(obj, key, obj[key])
	})
}

测试结果

const obj = {foo:'foo',bar:'bar',baz:{a:1}}
observe(obj)
obj.foo
obj.foo = 'foooooooooooo'
obj.bar
obj.bar = 'barrrrrrrrrrr'
obj.baz.a = 10 // 嵌套对象no ok 当有嵌套对象的时候,拦截不到,
// 需要另外处理

解决嵌套对象问题

function defineReactive(obj, key, val) {
	observe(val)
	Object.defineProperty(obj, key, {
//...

解决赋的值是对象的情况, 当出现下面的这种赋值,又会出现问题,因此在set操作的时候,进行一次响应式处理

obj.baz = {a:1}
obj.baz.a = 10 // no ok
set(newVal) {
	if (newVal !== val) {
	observe(newVal) // 新值是对象的情况
	notifyUpdate()

但是如果添加/删除了新属性⽆法检测,例如下面的情况

obj.dong = 'dong'
obj.dong // 并没有get信息

因此需要写set方法,进行一次响应式处理

function set(obj, key, val) {
   defineReactive(obj, key, val) 
}

测试代码:

set(obj, 'dong', 'dong')
obj.dong

但是 defineProperty() 对数组的支持并不友好,在vue源码中,对数组的七个方法进行了重写,这里先不进行讲解

Vue中的数据响应化

目标代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <p>{{counter}}</p>
  </div>
  <script src="../node_modules/vue/dist/vue.js"></script>  <script>
    const app = new Vue({
      el: '#app',
      data: {
        counter: 1
      },
    })
    setInterval(() => {
      app.counter++
    }, 1000);
  </script>
</body>

</html>
原理分析
  • new Vue() ⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在Observer中
  • 同时对模板执⾏编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发⽣在Compile中
  • 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调⽤更新函数
  • 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  • 将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数
    在这里插入图片描述
涉及的类型介绍
  • kVue: 框架的构造函数
  • Observe: 执行数据响应化(分辨数据是对象还是数组, 这里我只对对象进行处理)
  • Compile: 编译模板,初始化视图, 收集依赖(更新函数, watcher创建)
  • Watcher: 执行更新函数(更新dom)
  • Dep: 管理多个Watcher, 批量更新
KVue

框架构造函数:执⾏初始化
执⾏初始化,对data执⾏响应化处理,kvue.js

// 遍历指定数据对象中的每个key,并拦截他们
function observe(obj) {
	// 判断是否是一个对象
	if(typeof obj !== 'object' || obj === null) {
		return obj
	}
   // 每遇到一个对象,就创建一个Observer的实例
  // 创建一个Observer 实例去做拦截操作
  new Observer(obj)
}
// 根据value 类型做不同的操作(数组和对象)
class Observer {
  constructor(value) {
    this.value = value
    // 应该需要判断value 类型, 然后对对象和数组进行不同的操作
    // 这里只处理对象
    // 遍历对象
    this.walk(value)
  }

  walk(obj) {
    Object.keys(obj).forEach(key => {
      definReactive(obj, key, obj[key])
    })
  }
}

class kvue {
  constructor(options) {
    // 0: 保存options
    this.$options = options
    this.$data = options.data
    // 1. 将data做响应式处理
    observe(this.$data)
    // 2. 为$data做代理
    proxy(this, '$data')
  }
}

为$data做代理, 代码如下,若不做代理则需要app.$data.counter++ 进行访问, 代理代码如下

// proxy代理函数: 让用户可以直接访问data里的属性
function proxy(vm, key) {
  Object.keys(vm[key]).forEach(k => {
    Object.defineProperty(vm, k, {
      get() {
        return vm[key][k]
      },
      set(v) {
        vm[key][k] = v
      }
    })
  })
}
编译 - Compile

编译模板中vue模板特殊语法,初始化视图、更新视图
在这里插入图片描述

初始化视图

根据节点类型编译,compile.js

class Compile {
  // el - 宿主元素, vm - kvue 实例
  constructor(el, vm) {
    this.$el = document.querySelector(el)
    this.$vm = vm
    // 解析模板
    if (this.$el) {
      // 执行编译方法
      this.compile(this.$el)
    }
  }
  compile(el) {
    // el是宿主元素
    // 遍历,判断当前遍历元素的类型
    el.childNodes.forEach(node => {
      if (node.nodeType === 1) {
        console.log('编译元素', node.nodeName)
      } else if(this.isInter(node)) {
        // 文本, {{xxxx}}
        console.log('编译文本', node.textContent, RegExp.$1)
      }
      // 递归 ,判断node是否还有child
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 判断插值表达式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}

编译插值,compile.js

compile(el) {
	 // ...
	 } else if (this.isInerpolation(node)) {
	 // console.log("编译插值⽂本" + node.textContent);
	     this.compileText(node);
	}
	});
}
compileText(node) {
	console.log(RegExp.$1);
	node.textContent = this.$vm[RegExp.$1];
}

编译元素

compile(el) {
	//...
	if (this.isElement(node)) {
		// console.log("编译元素" + node.nodeName);
		this.compileElement(node)
	} 
}
compileElement(node) {
	let nodeAttrs = node.attributes;
	Array.from(nodeAttrs).forEach(attr => {
		let attrName = attr.name;
		let exp = attr.value;
		if (this.isDirective(attrName)) {
			let dir = attrName.substring(2);
			this[dir] && this[dir](node, exp);
		 }
	 });
}
 // 判断是否是指令
isDirective(attr) {
  return attr.indexOf("k-") == 0; 
}
// k-text 对应的操作函数
text(node, exp) {
  node.textContent = this.$vm[exp];
}
// k-html 对应的操作函数
html(node, exp) {
  node.innerHTML = this.$vm[exp]
}
依赖收集

视图中会⽤到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来⽤⼀个Watcher来维护它们,此过程称为依赖收集。
多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。看下⾯案例,理出思路:

new Vue({
  	template:
		`<div>
			<p>{{name1}}</p>
			<p>{{name2}}</p>
			<p>{{name1}}</p>
		<div>`,
	data: {
		name1: 'name1',
		name2: 'name2'
	 }
});

上面代码出现了两次name1和一次name2,总共有三个依赖,会有3个Watcher, 但是每个key只会是一个Dep
在这里插入图片描述
实现思路

  1. defineReactive时为每⼀个key创建⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  3. 由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep中
  4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
创建Watcher,kvue.js
const watchers = [];//临时⽤于保存watcher测试⽤
// 监听器:负责更新视图
class Watcher {
	constructor(vm, key, updateFn) {
		// kvue实例
		this.vm = vm;
		// 依赖key
		this.key = key;
		// 更新函数
		this.updateFn = updateFn;
		// 临时放⼊watchers数组
		watchers.push(this)
    }
	// 更新
	update() {
	   this.updateFn.call(this.vm, this.vm[this.key]);
	}
}
编写更新函数、创建watcher
// 调⽤update函数执插值⽂本赋值
compileText(node) {
	// console.log(RegExp.$1);
	// node.textContent = this.$vm[RegExp.$1];
	this.update(node, RegExp.$1, 'text') 
}
text(node, exp) {
    this.update(node, exp, 'text')
}
html(node, exp) {
    this.update(node, exp, 'html')
}
update(node, exp, dir) {
	const fn = this[dir+'Updater']
	fn && fn(node, this.$vm[exp])
	new Watcher(this.$vm, exp, function(val){
	  fn && fn(node, val)
	})
}
textUpdater(node, val) {
    node.textContent = val; 
}
htmlUpdater(node, val) {
    node.innerHTML = val
}
声明Dep
class Dep {
	constructor () {
	    this.deps = []
	}
	addDep (dep) {
	    this.deps.push(dep)
	}
	notify() {
	   this.deps.forEach(dep => dep.update());
	}
}
创建watcher时触发getter
class Watcher {
  constructor(vm, key, updaterFn) {
    this.vm = vm
    this.key = key
    this.updaterFn = updaterFn
    // 依赖收集
    // watchers.push(this)
    Dep.target = this
    this.vm[this.key] // 触发上面的get
    Dep.target = null
  }

  update() {
    this.updaterFn.call(this.vm, this.vm[this.key])
  }
}
依赖收集,创建Dep实例
// 对传入对象的key进行一次拦截, 对某个对象的某个key做拦截
function definReactive(obj, key, val) {
  // 如果val 是对象, 则需要继续递归处理
  observe(val)
  // Dep创建的最佳时刻
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key)
      // 依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newValue) {
      if(val !== newValue) {
        // 如果newValue 是对象也需要做响应式处理
        observe(newValue)
        val = newValue
        console.log('set', key, val)
        // console.log(watchers)
        // watchers.forEach(w => w.update())
        // 通知更新
        dep.notify()
      }
    }
  })
}

下面附上kvue,js 的完整代码

// 实现kvue 的构造函数
// 1. 将data做响应式处理


// Object.defineProperty()
// 对传入对象的key进行一次拦截, 对某个对象的某个key做拦截
function definReactive(obj, key, val) {
  // 如果val 是对象, 则需要继续递归处理
  observe(val)
  // Dep创建的最佳时刻
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key)
      // 依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newValue) {
      if(val !== newValue) {
        // 如果newValue 是对象也需要做响应式处理
        observe(newValue)
        val = newValue
        console.log('set', key, val)
        // console.log(watchers)
        // watchers.forEach(w => w.update())
        // 通知更新
        dep.notify()
      }
    }
  })
}

// 遍历指定数据对象中的每个key,并拦截他们
function observe(obj) {
	// 判断是否是一个对象
	if(typeof obj !== 'object' || obj === null) {
		return obj
	}
	// Object.keys(obj).forEach(key => {
	// 	definReactive(obj, key, obj[key])
	// })
  // 每遇到一个对象,就创建一个Observer的实例
  // 创建一个Observer 实例去做拦截操作
  new Observer(obj)
}

function set(obj, key, val) {
  // 进行响应式处理
  definReactive(obj, key, val)
}

// proxy代理函数: 让用户可以直接访问data里的属性
function proxy(vm, key) {
  Object.keys(vm[key]).forEach(k => {
    Object.defineProperty(vm, k, {
      get() {
        return vm[key][k]
      },
      set(v) {
        vm[key][k] = v
      }
    })
  })
}

// 根据value 类型做不同的操作(数组和对象)
class Observer {
  constructor(value) {
    this.value = value
    // 应该需要判断value 类型, 然后对对象和数组进行不同的操作
    // 这里只处理对象
    // 遍历对象
    this.walk(value)
  }

  walk(obj) {
    Object.keys(obj).forEach(key => {
      definReactive(obj, key, obj[key])
    })
  }
}

class kvue {
  constructor(options) {
    // 0: 保存options
    this.$options = options
    this.$data = options.data
    // 1. 将data做响应式处理
    observe(this.$data)
    // 2. 为$data做代理
    proxy(this, '$data')
    // 3. 编译模板
    new Compile('#app', this)
  }
}

class Compile {
  // el - 宿主元素, vm - kvue 实例
  constructor(el, vm) {
    this.$el = document.querySelector(el)
    this.$vm = vm
    // 解析模板
    if (this.$el) {
      // 执行编译方法
      this.compile(this.$el)
    }
  }
  compile(el) {
    // el是宿主元素
    // 遍历,判断当前遍历元素的类型
    el.childNodes.forEach(node => {
      if (node.nodeType === 1) {
        // console.log('编译元素', node.nodeName)
        // 节点编译
        this.compileElement(node)
      } else if(this.isInter(node)) {
        // 文本, {{xxxx}}
        // console.log('编译文本', node.textContent, RegExp.$1)
        // 将文本转义一下
        this.compileText(node)
      }
      // 递归 ,判断node是否还有child
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 判断插值表达式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  // 编译文本
  compileText(node) {
    // node.textContent = this.$vm[RegExp.$1]
    this.update(node, RegExp.$1, 'text')
  }

  // 编译元素节点: 分析指令, @事件
  compileElement(node) {
    // 获取属性遍历值
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr => {
      // 指令: k-xxx 开头
      const attrName = attr.name
      const exp = attr.value
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2) // 得到xxx
        // 指令的实际的操作方法
        this[dir] && this[dir](node, exp)
      }
    })
  }

  // 判断是否是指令
  isDirective(attrName) {
    return attrName.indexOf('k-') === 0
  }
  // 执行text指令对应的更新函数
  text(node, exp) {
    // node.textContent = this.$vm[exp]
    //需要调用update方法
    this.update(node, exp, 'text')
  }
  // k-text 对应的操作函数
  textUpdater(node, val) {
    node.textContent = val
  }
  // k-html 对应的操作函数
  html(node, exp) {
    // node.innerHTML = this.$vm[exp]
    this.update(node, exp, 'html')
  }
  htmlUpdater(node, val) {
    node.innerHTML = val
  }
  // 提取update, 初始化和更新函数创建
  update(node, exp, dir) {
    const fn = this[dir+'Updater']
    // 初始化过程
    fn && fn(node, this.$vm[exp])
    // 更新
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
}

// Watcher: 跟视图中的依赖是一对一的关系 1:1
// const watchers = []
class Watcher {
  constructor(vm, key, updaterFn) {
    this.vm = vm
    this.key = key
    this.updaterFn = updaterFn
    // 依赖收集
    // watchers.push(this)
    Dep.target = this
    this.vm[this.key] // 触发上面的get
    Dep.target = null
  }

  update() {
    this.updaterFn.call(this.vm, this.vm[this.key])
  }
}
// 和某个key, 一一对应, 管理多个Watcher, 数据更新时通知他们做更新工作
class Dep {
  constructor() {
    this.deps = []
  }

  addDep(watcher) {
    this.deps.push(watcher)
  }

  notify() {
    this.deps.forEach(watcher => watcher.update())
  }
}

该版本并没有涉及到虚拟dom, 虚拟dom讲解, 会放到后面.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值