宝刀未老,凿石开路识vue2(一)

宝刀未老,凿石开路识vue2(一)

技术杂谈小彩虹2021-07-10 20:53:03100A+A-

这是我参与更文挑战的第 11 天,活动详情查看: 更文挑战

2021-06-11 总结 TANGJIE Vue2 手写vue

宝刀未老,凿石开路识vue2(一)

1. 对象的响应式原理(vue2)

1.1 理解对象

在js中一般会使用内部的一些特性来描述对象的属性特性,属性特征分为两种——数据属性和访问器属性

  • 数据属性:ConfigurableEnumerableWritableValue

    • Configurable:表示数据是否能被delete操作符删除并重新定义,是否能修改它的特性,以及是否可以把它改为访问器属性,默认为true
    • Enumerable:表示可被枚举,即可以for-in循环返回,默认为true
    • Writable:表示属性值可被修改,默认为true
    • Value:表示属性值,默认为:undefined
  • 访问器属性:getter(get()函数)、setter(set()函数)

对象的描述属性是不能直接被定义访问的,必须要使用Object.defineProperty();

1.2 基于Object.defineProperty()的对象响应式原理

vue2的响应式函数其实就是基于Object.defineProperty()实现的,借助了对象的属性特征

const getDataType = function (val = 0) {
    let type = typeof val
    // object需要使用Object.prototype.toString.call判断
    if (type === 'object') {
        let typeStr = Object.prototype.toString.call(val)
        // 解析[object String]
        typeStr = typeStr.split(' ')[1]
        type = typeStr.substring(0, typeStr.length - 1)
    }
    return type.toLowerCase()
}	

// 对象的响应式原理
function defineReactive( obj,key,value){ // 看到这个入参模式,有没有想起Vue.set/ vm.$set();
	objserve(value);
	Object.defineProperty(obj,key,{
		get(){
			console.log(value,'get')
			return value
		},
		set(newValue){
			if( newValue !== value){
				// 触发视图更新
				console.log('触发视图更新','set')
				objserve(newValue);
				value = newValue;
			}
		}
	})
	
}

// 对象的响应式处理
function objserve(obj){
	if(getDataType(obj) !=='object' ){
		return
	}
	Object.keys( obj ).forEach( key => defineReactive(obj, key, obj[key]))
}

2. Vue2的响应式

2.1 基本分析

  • new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中;【Observer执行数据的响应化】
  • 对模板执行编译,找到其中的动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中;【Compile,编译模板,初始化视图,收集依赖】
  • 定义了一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数;【Watcher,执行更新函数,更新dom】
  • 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家dep来管理多个Watcher
  • 将来的data中数据一旦发生变化,会首先找到对应的Dep,通知所有的Watcher执行更新函数;【Dep管理多个Watcher,批量更新】

如图:

5-1.png

2.2 vue响应式数据的基本实现

  1. 构建Vue类,初始化选项
const proxy = (vm)=>{
	Object.keys( vm.$data ).forEach( key=>{
		Object.defineProperty(vm,key,{
			get(){
				return vm.$data[key]
			},
			set(v){
				vm.$data[key] = v
			}
		})
	} )
}

class Vue {
	constructor(options){
		// 保存选项 跟着文档学的
		// Vue的文档说用$options能获取到Vue实例初始化选项,因此就这样先保存一下
		this.$options = options;
		// vm.$data,Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。
		this.$data = options.data;
		// 响应化处理
		objserve( this.$data )
		// 访问data,不是用this.$data.xxx 而是直接用的this.xxx 因此这里我们需要对$data上的属性做一次代理
		// 使得它成为vue实例上面的一个属性
		proxy(this)
		new Compile(this.$options.el,this)
	}
}
  1. vue响应化函数

因为vue的$data是响应式的,所以需要一个响应式函数来解决该问题,同时结合了第一大点讲的对象响应式原理,于是有

const getDataType = function (val = 0) {
    let type = typeof val
    // object需要使用Object.prototype.toString.call判断
    if (type === 'object') {
        let typeStr = Object.prototype.toString.call(val)
        // 解析[object String]
        typeStr = typeStr.split(' ')[1]
        type = typeStr.substring(0, typeStr.length - 1)
    }
    return type.toLowerCase()
}	

// 对象的响应式原理
function defineReactive( obj,key,value){ // 看到这个入参模式,有没有想起Vue.set/ vm.$set();
	objserve(value);
	Object.defineProperty(obj,key,{
		get(){
			console.log(value,'get')
			return value
		},
		set(newValue){
			if( newValue !== value){
				// 触发视图更新
				console.log('触发视图更新','set')
				objserve(newValue);
				value = newValue
			}
		}
	})
	
}

// 对象的响应式处理
function objserve(obj){
	if(getDataType(obj) !=='object' ){
		return
	}
	new Observer(obj);
}

// 观测数据响应变化,每一个响应式对象,就伴生一个Observer实例
class Observer{
	constructor(value){
		this.value = value;
		this.walk(value);
	}
	
	walk(obj){
		Object.keys( obj ).forEach( key => defineReactive(obj, key, obj[key]))
	}
	
}

2.3 vue编译的基本实现

在vue中编译是很重要的一步,一个简单的编译思想设计如图:

5-2.png

这样我们会有如下的代码:

class Compile {
	constructor(el,vm){
		this.$vm = vm;
		this.$el = document.querySelector(el);
		if( this.$el ){
			this.compile(this.$el)
		}
	}
	
	compile(el){
		// 递归遍历el,判断其类型
		const childNodes = el.childNodes;
		Array.from(childNodes).forEach( node => {
			if( this.isElement(node)){
				console.log('编译元素',node.nodeName)
				this.compileElement(node)
			}else if (this.isInter(node)){
				console.log('编译插值表达式',node.textContent);
				this.compileText(node);
			}
			
			if(node.childNodes && node.childNodes.length > 0){
				this.compile(node)
			}
			
		})

	}
	
	isElement(node){
		return node.nodeType === 1
	}
	
	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){
		let nodeAttrs = node.attributes;
		Array.from( nodeAttrs ).forEach( attr=>{
			let attrName = attr.name;
			let exp = attr.value;
			console.log(attrName ,exp,'读取属性')
			if(attrName === 'v-html'){
				const temp = attrName.split('-')[1];
				if(this[temp]){
					this[temp](node,exp)
				}else{
					throw new Error('没有该指令')
				}
			}
			// 绑定方法
			if(attrName === '@click'){
				node.addEventListener('click',this.$vm.methods[exp])
			}

		})
	}

	// html方法
	html(node,exp){
		// console.log(this.$vm[exp],exp,'xxasdfas')
		node.innerHTML = this.$vm[exp];
	}
	// 所有动态绑定(如指令,插值语法,这里为了简单只对插值语法做更新)
	// 都需要创建更新函数以及对应的watcher实例
	update(node,exp,dir){
		const fn = this[dir+'Updater'];
		// 初始化
		fn && fn(node,this.$vm[exp]);
	}

	textUpdater(node,value){
		node.textContent = value;
	}

}

2.4 vue的监听

监听是需要我们给对于的key一个监听函数,来监听其改变,于是有

class Watcher {
	constructor(vm,key,updateFn){
		this.vm = vm;
		this.key = key;
		this.updateFn = updateFn;
	}

	update(){
		this.updateFn.call(this.vm,this.vm[this.key])
	}
}

又因为这个监听,是要监听data数据的改变,更新视图 因此他初始化被添加的时机是在编译函数的更新视图的模块中,为此在Compile类中的update方法去进行监听,对更新函数进行收集

class Compile{
	...
	update(node,exp,dir){
		new Watcher(this.$vm,exp,function(val){
			fn && fn(node,val)
		})
	}
	...
}

2.5 声明dep,集中管理

为什么要这个东西呢?例如vue的响应式数据this.content,它可能被用于{{content}},也可能被用于v-if="content",很显然这两种更新策略是不一样的,但是他们的依赖源又是相同的,所以需要dep进行管理,当this.content发生改变,会首先找到对应的Dep,通知所有的Watcher执行更新函数

class Dep{
	constructor(){
		this.deps = []
	}

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

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

正因为当this.content发生改变,会首先找到对应的Dep,通知所有的Watcher执行更新函数 因此他被实例化的时机是在对对象进行响应式处理的时候

因此需要在defineReactive函数里面进行实例化,改写后如下:

function defineReactive( obj,key,value){
	objserve(value);
	const dep = new Dep(); // 给每一个对象属性一个收集
	Object.defineProperty(obj,key,{
		get(){
			console.log(value,'get')
			Dep.target && dep.addDep(Dep.target)
			return value
		},
		set(newValue){
			if( newValue !== value){
				// 触发视图更新
				console.log('触发视图更新','set')
				dep.notify();
				value = newValue
			}
		}
	})
}

因为要通知到Watcher,所以addDep的时候要正确的吧当前的Watcher实例放进去,于是乎Watcher类就变成了这样

class Watcher {
	constructor(vm,key,updateFn){
		this.vm = vm;
		this.key = key;
		this.updateFn = updateFn;
		Dep.target = this;
		this.vm[this.key];
		Dep.target = null;
	}

	update(){
		this.updateFn.call(this.vm,this.vm[this.key])
	}
}

为什么要Dep.target = this;this.vm[this.key];Dep.target = null;,其实里就是巧妙的应用到了对象响应式get属性方法;能够在Dep实例化的时候正确的对相应的Watcher进行收集管理

3. 简单vue实现代码

<div id="app">
    <div>{{counter}}</div>
    <div v-html="counter" @click="toAction"></div>
</div>
const getDataType = function (val = 0) {
    let type = typeof val
    // object需要使用Object.prototype.toString.call判断
    if (type === 'object') {
        let typeStr = Object.prototype.toString.call(val)
        // 解析[object String]
        typeStr = typeStr.split(' ')[1]
        type = typeStr.substring(0, typeStr.length - 1)
    }
    return type.toLowerCase()
}	

// 对象的响应式原理
function defineReactive( obj,key,value){ // 看到这个入参模式,有没有想起Vue.set/ vm.$set();
	objserve(value);
	const dep = new Dep(); // 给每一个对象属性一个收集
	Object.defineProperty(obj,key,{
		get(){
			console.log(value,'get')
			Dep.target && dep.addDep(Dep.target)
			return value
		},
		set(newValue){
			if( newValue !== value){
				// 触发视图更新
				console.log('触发视图更新','set')
				dep.notify();
				value = newValue
			}
		}
	})
	
}

// 对象的响应式处理
function objserve(obj){
	if(getDataType(obj) !=='object' ){
		return
	}
	new Observer(obj);
}

const proxy = (vm)=>{
	Object.keys( vm.$data ).forEach( key=>{
		Object.defineProperty(vm,key,{
			get(){
				return vm.$data[key]
			},
			set(v){
				vm.$data[key] = v
			}
		})
	} )
}

class Vue {
	constructor(options){
		// 保存选项 跟着文档学的
		// Vue的文档说用$options能获取到Vue实例初始化选项,因此就这样先保存一下
		this.$options = options;
		// vm.$data,Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。
		this.$data = options.data;
		this.methods = options.methods;
		// 响应化处理
		objserve( this.$data )
		// 访问data,不是用this.$data.xxx 而是直接用的this.xxx 因此这里我们需要对$data上的属性做一次代理
		// 使得它成为vue实例上面的一个属性
		proxy(this)
		new Compile(this.$options.el,this)
	}
}
// 观测数据响应变化,每一个响应式对象,就伴生一个Observer实例
class Observer{
	constructor(value){
		this.value = value;
		this.walk(value);
	}
	
	walk(obj){
		Object.keys( obj ).forEach( key => defineReactive(obj, key, obj[key]))
	}
	
}

// Vue的模板编译
class Compile {
	constructor(el,vm){
		this.$vm = vm;
		this.$el = document.querySelector(el);
		if( this.$el ){
			this.compile(this.$el)
		}
	}
	
	compile(el){
		// 递归遍历el,判断其类型
		const childNodes = el.childNodes;
		Array.from(childNodes).forEach( node => {
			if( this.isElement(node)){
				console.log('编译元素',node.nodeName)
				this.compileElement(node)
			}else if (this.isInter(node)){
				console.log('编译插值表达式',node.textContent);
				this.compileText(node);
			}
			
			if(node.childNodes && node.childNodes.length > 0){
				this.compile(node)
			}
			
		})

	}
	
	isElement(node){
		return node.nodeType === 1
	}
	
	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){
		let nodeAttrs = node.attributes;
		Array.from( nodeAttrs ).forEach( attr=>{
			let attrName = attr.name;
			let exp = attr.value;
			console.log(attrName ,exp,'读取属性')
			if(attrName === 'v-html'){
				const temp = attrName.split('-')[1];
				if(this[temp]){
					this[temp](node,exp)
				}else{
					throw new Error('没有该指令')
				}
			}
			if(attrName === '@click'){
				node.addEventListener('click',this.$vm.methods[exp])
			}

		})
	}

	// html方法
	html(node,exp){
		// console.log(this.$vm[exp],exp,'xxasdfas')
		node.innerHTML = this.$vm[exp];
	}
	// 所有动态绑定(如指令,插值语法,这里为了简单只对插值语法做更新)
	// 都需要创建更新函数以及对应的watcher实例
	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,value){
		node.textContent = value;
	}

}

// 做依赖收集
class Watcher {
	constructor(vm,key,updateFn){
		this.vm = vm;
		this.key = key;
		this.updateFn = updateFn;
		Dep.target = this;
		this.vm[this.key];
		Dep.target = null;
	}

	update(){
		this.updateFn.call(this.vm,this.vm[this.key])
	}
}
// 声明dep
class Dep{
	constructor(){
		this.deps = []
	}

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

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

// 使用

const app = new Vue({
	el:'#app',
	data:{
		counter:1
	},
	methods: {
		toAction:()=>{
			alert('触发事件')
		}
	},
})

setInterval(()=>{
	app.counter ++
},1000)

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们