IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 手写一个响应式vue系统,一篇带你精通vue -> 正文阅读

[JavaScript知识库]手写一个响应式vue系统,一篇带你精通vue

碎碎念:

亲爱的读者:你好!我叫 Changlon —— 一个非科班程序员、一个致力于前端的开发者、一个热爱生活且又时有忧郁的思考者。

如果我的文章能给你带来一些收获,你的点赞收藏将是对我莫大的鼓励!

我的邮箱:thinker_changlon@163.com

我的Github: https://github.com/Changlon

项目源码:myvue github源码地址

本章我们通过手写实现一个简单的Vue响应式系统,只用300多行代码实现包括Vue的响应式系统,模板解析,指令解析,data, methods选项的处理等。
难度不算大,沉下心来,拿起键盘跟我一起来撸一个出来,相信之后你就会豁然开朗的!

建议将源码下载下来仔细研究,源码地址已在上面给出。

一 、什么是MVVM?

概念

MVVM 模式,顾名思义即 Model-View-ViewModel 模式。它萌芽于2005年微软推出的基于 Windows 的用户界面框架
WPF ,前端最早的 MVVM 框架 knockout 在2010年发布。

一句话总结 Web 前端 MVVM:操作数据,就是操作视图,就是操作 DOM(所以无须操作 DOM )。
无须操作 DOM !借助 MVVM 框架,开发者只需完成包含 声明绑定 的视图模板,编写 ViewModel 中业务数据变更逻辑,View 层则完全实现了自动化。这将极大的降低前端应用的操作复杂度、极大提升应用的开发效率。MVVM 最标志性的特性就是 数据绑定 ,MVVM 的核心理念就是通过 声明式的数据绑定 来实现 View 层和其他层的分离。完全解耦 View 层这种理念,也使得 Web 前端的单元测试用例编写变得更容易。

MVVM,说到底还是一种分层架构。它的分层如下:

  • Model: 域模型,用于持久化
  • View: 作为视图模板存在
  • ViewModel: 作为视图的模型,为视图服务

Model 层

Model 层,对应数据层的域模型,它主要做域模型的同步。通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。在层间关系里,它主要用于抽象出 ViewModel 中视图的 Model。

简单的来讲就是你要展示在页面中的数据。比如Vue实例中的data

View 层

View 层,作为视图模板存在,在 MVVM 里,整个 View 是一个动态模板。除了定义结构、布局外,它展示的是 ViewModel 层的数据和状态。View 层不负责处理状态,View 层做的是 数据绑定的声明、 指令的声明、 事件绑定的声明。

简单的理解就是需要操作的dom模型

ViewModel 层

ViewModel 层把 View 需要的层数据暴露,并对 View 层的 数据绑定声明、 指令声明、 事件绑定声明 负责,也就是处理 View 层的具体业务逻辑。ViewModel 底层会做好绑定属性的监听。当 ViewModel 中数据变化,View 层会得到更新;而当 View 中声明了数据的双向绑定(通常是表单元素),框架也会监听 View 层(表单)值的变化。一旦值变化,View 层绑定的 ViewModel 中的数据也会得到自动更新。

简单的理解就是 数据变化了 viewModel会将变更的数据自动更新到dom中

前端MVVM模型图解

在这里插入图片描述

二 、Object.defineProperty()

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • 语法 Object.defineProperty(obj, prop, descriptor)
  • 参数 obj要定义属性的对象。prop要定义或修改的属性的名称或 Symbol 。descriptor 要定义或修改的属性描述符。
  • 用法
	const object1 = {};
	
	Object.defineProperty(object1, 'property1', {
	  value: 42,
	  writable: false
	});
	
	object1.property1 = 77;
	// 报错,property1属性描述符指定这个属性是不可能修改的
	
	console.log(object1.property1);
	//42

  • setter & getter
    我们可以为一个对象的属性描述符中设置get 和set函数;get函数可以确保在访问属性值时可以做哪些操作, set函数可以确保我们在更改属性值时可以做哪些操作。
	let data = { 
	    name :'changlon'
	} 
	
	let  name = data.name
	
	Object.defineProperty(data,'name',{
	    set(newValue) {  
	        console.log(`数据劫持:${newValue}`) 
	        name = newValue
	    },
	    get() {
	        console.log(`访问属性name:${name}`) 
	        return name
	    }
	})
	
	 console.log(data.name) //访问属性name:changlon changlon
	 data.name = "jack" //数据劫持:jack
	 console.log(data)//{"name":"jack"}

上面的例子中我们对data中的name属性做了一个数据劫持,当访问name属性时进行一个打印操作,当更改name属性时打印并返回原来的值。这就是vue2中的响应式基本原理。我们可以在数据更改时去改变dom,这样就能实现dom和数据的同步。

三、一个简单的例子

下面我们通过一个简单的例子来加深这一原理的理解。

响应式系统的基本思路

接下来我们将重点讲解数据响应系统的实现,在具体到源码之前我们有必要了解一下数据响应系统实现的基本思路,这有助于我们更好的去实现vue的响应式代码。

Vue 中,我们可以使用 $watch 观测一个字段,当字段的值发生变化的时候执行指定的观察者,如下:

const ins = new Vue({
  data: {
    a: 1
  }
})

ins.$watch('a', () => {
  console.log('修改了 a')
})

这样当我们试图修改 a 的值时:ins.a = 2,在控制台将会打印 '修改了 a'。现在我们将这个问题抽象一下,假设我们有数据对象 data,如下:

const data = {
  a: 1
}

我们还有一个叫做 $watch 的函数:

function $watch () {...}

$watch 函数接收两个参数,第一个参数是要观测的字段,第二个参数是当该字段的值发生变化后要执行的函数,如下:

$watch('a', () => {
  console.log('修改了 a')
})

要实现这个功能,说复杂也复杂说简单也简单,复杂在于我们需要考虑的内容比较多,比如如何避免收集重复的依赖,如何深度观测,如何处理数组以及其他边界条件等等。简单在于如果不考虑那么多边界条件的话,要实现这样一个功能还是很容易的,这一小节我们就从简入手,致力于让大家思路清晰。

要实现上文的功能,我们面临的第一个问题是,如何才能知道属性被修改了(或被设置了)。这时候我们就要依赖 Object.defineProperty 函数,通过该函数为对象的每个属性设置一对 getter/setter 从而得知属性被读取和被设置,如下:

Object.defineProperty(data, 'a', {
  set () {
    console.log('设置了属性 a')
  },
  get () {
    console.log('读取了属性 a')
  }
})

这样我们就实现了对属性 a 的设置和获取操作的拦截,有了它我们就可以大胆地思考一些事情,比如: 能不能在获取属性 a 的时候收集依赖,然后在设置属性 a 的时候触发之前收集的依赖呢? 嗯,这是一个好思路,不过既然要收集依赖,我们起码需要一个”筐“,然后将所有收集到的依赖通通放到这个”筐”里,当属性被设置的时候将“筐”里所有的依赖都拿出来执行就可以了,落实到代码如下:

// dep 数组就是我们所谓的“筐”
const dep = []
Object.defineProperty(data, 'a', {
  set () {
    // 当属性被设置的时候,将“筐”里的依赖都执行一次
    dep.forEach(fn => fn())
  },
  get () {
    // 当属性被获取的时候,把依赖放到“筐”里
    dep.push(fn)
  }
})

如上代码所示,我们定义了常量 dep,它是一个数组,这个数组就是我们所说的“筐”,当获取属性 a 的值时将触发 get 函数,在 get 函数中,我们将收集到的依赖放入“筐”内,当设置属性 a 的值时将触发 set 函数,在 set 函数内我们将“筐”里的依赖全部拿出来执行。

但是新的问题出现了,上面的代码中我们假设 fn 函数就是我们需要收集的依赖(观察者),但 fn 从何而来呢? 也就是说如何在获取属性 a 的值时收集依赖呢? 为了解决这个问题我们需要思考一下我们现在都掌握了哪些条件,这个时候我们就需要在 $watch 函数中做文章了,我们知道 $watch 函数接收两个参数,第一个参数是一个字符串,即数据字段名,比如 'a',第二个参数是依赖该字段的函数:

$watch('a', () => {
  console.log('设置了 a')
})

重点在于 $watch 函数是知道当前正在观测的是哪一个字段的,所以一个思路是我们在 $watch 函数中读取该字段的值,从而触发字段的 get 函数,同时将依赖收集,如下代码:

const data = {
  a: 1
}

const dep = []
Object.defineProperty(data, 'a', {
  set () {
    dep.forEach(fn => fn())
  },
  get () {
    // 此时 Target 变量中保存的就是依赖函数
    dep.push(Target)
  }
})

// Target 是全局变量
let Target = null
function $watch (exp, fn) {
  // 将 Target 的值设置为 fn
  Target = fn
  // 读取字段值,触发 get 函数
  data[exp]
}

上面的代码中,首先我们定义了全局变量 Target,然后在 $watch 中将 Target 的值设置为 fn 也就是依赖,接着读取字段的值 data[exp] 从而触发被设置的属性的 get 函数,在 get 函数中,由于此时 Target 变量就是我们要收集的依赖,所以将 Target 添加到 dep 数组。现在我们添加如下测试代码:

$watch('a', () => {
  console.log('第一个依赖')
})
$watch('a', () => {
  console.log('第二个依赖')
})

此时当你尝试设置 data.a = 3 时,在控制台将分别打印字符串 '第一个依赖''第二个依赖'。我们仅仅用十几行代码就实现了这样一个最基本的功能,但其实现在的实现存在很多缺陷,比如目前的代码仅仅能够实现对字段 a 的观测,如果添加一个字段 b 呢?所以最起码我们应该使用一个循环将定义访问器属性的代码包裹起来,如下:

const data = {
  a: 1,
  b: 1
}

for (const key in data) {
  const dep = []
  Object.defineProperty(data, key, {
    set () {
      dep.forEach(fn => fn())
    },
    get () {
      dep.push(Target)
    }
  })
}

这样我们就可以使用 $watch 函数观测任意一个 data 对象下的字段了,但是细心的同学可能早已发现上面代码的坑,即:

console.log(data.a) // undefined

直接在控制台打印 data.a 输出的值为 undefined,这是因为 get 函数没有任何返回值,所以获取任何属性的值都将是 undefined,其实这个问题很好解决,如下:

for (let key in data) {
  const dep = []
  let val = data[key] // 缓存字段原有的值
  Object.defineProperty(data, key, {
    set (newVal) {
      // 如果值没有变什么都不做
      if (newVal === val) return
      // 使用新值替换旧值
      val = newVal
      dep.forEach(fn => fn())
    },
    get () {
      dep.push(Target)
      return val  // 将该值返回
    }
  })
}

只需要在使用 Object.defineProperty 函数定义访问器属性之前缓存一下原来的值即 val,然后在 get 函数中将 val 返回即可,除此之外还要记得在 set 函数中使用新值(newVal)重写旧值(val)。

但这样就完美了吗?当然没有,这距离完美可以说还相差十万八千里,比如当数据 data 是嵌套的对象时,我们的程序只能检测到第一层对象的属性,如果数据对象如下:

const data = {
  a: {
    b: 1
  }
}

对于以上对象结构,我们的程序只能把 data.a 字段转换成响应式属性,而 data.a.b 依然不是响应式属性,但是这个问题还是比较容易解决的,只需要递归定义即可:

function walk (data) {
  for (let key in data) {
    const dep = []
    let val = data[key]
    // 如果 val 是对象,递归调用 walk 函数将其转为访问器属性
    const nativeString = Object.prototype.toString.call(val)
    if (nativeString === '[object Object]') {
      walk(val)
    }
    Object.defineProperty(data, key, {
      set (newVal) {
        if (newVal === val) return
        val = newVal
        dep.forEach(fn => fn())
      },
      get () {
        dep.push(Target)
        return val
      }
    })
  }
}

walk(data)

如上代码我们将定义访问器属性的逻辑放到了函数 walk 中,并增加了一段判断逻辑如果某个属性的值仍然是对象,则递归调用 walk 函数。这样我们就实现了深度定义访问器属性

但是虽然经过上面的改造 data.a.b 已经是访问器属性了,但是如下代码依然不能正确执行:

$watch('a.b', () => {
  console.log('修改了字段 a.b')
})
来看看目前 $watch 函数的代码:

function $watch (exp, fn) {
  Target = fn
  // 读取字段值,触发 get 函数
  data[exp]
}

读取字段值的时候我们直接使用 data[exp],如果按照 $watch('a.b', fn) 这样调用 $watch 函数,那么 data[exp] 等价于 data['a.b'],这显然是不正确的,正确的读取字段值的方式应该是 data['a']['b']。所以我们需要稍微做一点小小的改造:

const data = {
  a: {
    b: 1
  }
}

function $watch (exp, fn) {
  Target = fn
  let pathArr,
      obj = data
  // 检查 exp 中是否包含 .
  if (/\./.test(exp)) {
    // 将字符串转为数组,例:'a.b' => ['a', 'b']
    pathArr = exp.split('.')
    // 使用循环读取到 data.a.b
    pathArr.forEach(p => {
      obj = obj[p]
    })
    return
  }
  data[exp]
}

我们对 $watch 函数做了一些改造,首先检查要读取的字段是否包含 .,如果包含 . 说明读取嵌套对象的字段,这时候我们使用字符串的 split('.') 函数将字符串转为数组,所以如果访问的路径是 a.b 那么转换后的数组就是 ['a', 'b'],然后使用一个循环从而读取到嵌套对象的属性值,不过需要注意的是读取到嵌套对象的属性值之后应该立即 return,不需要再执行后面的代码。

下面我们再进一步,我们思考一下 $watch 函数的原理是什么?其实 $watch 函数所做的事情就是想方设法地访问到你要观测的字段,从而触发该字段的 get 函数,进而收集依赖(观察者)。现在我们传递给 $watch 函数的第一个参数是一个字符串,代表要访问数据的哪一个字段属性,那么除了字符串之外可不可以是一个函数呢?假设我们有一个函数叫做 render,如下

const data = {
  name: '霍春阳',
  age: 24
}

function render () {
  return document.write(`姓名:${data.name}; 年龄:${data.age}`)
}

可以看到 render 函数依赖了数据对象 data,那么 render 函数的执行是不是会触发 data.name 和 data.age 这两个字段的 get 拦截器呢?答案是肯定的,当然会!所以我们可以将 render 函数作为 $watch 函数的第一个参数:

$watch(render, render)

为了能够保证 $watch 函数正常执行,我们需要对 $watch 函数做如下修改:

function $watch (exp, fn) {
  Target = fn
  let pathArr,
      obj = data
  // 如果 exp 是函数,直接执行该函数
  if (typeof exp === 'function') {
    exp()
    return
  }
  if (/\./.test(exp)) {
    pathArr = exp.split('.')
    pathArr.forEach(p => {
      obj = obj[p]
    })
    return
  }
  data[exp]
}

在上面的代码中,我们检测了 exp 的类型,如果是函数则直接执行之,由于 render 函数的执行会触发数据字段的 get 拦截器,所以依赖会被收集。同时我们要注意传递给 $watch 函数的第二个参数:

$watch(render, render)

第二个参数依然是 render 函数,也就是说当依赖发生变化时,会重新执行 render 函数,这样我们就实现了数据变化,并将变化自动应用到 DOM。其实这大概就是 Vue 的原理,但我们做的还远远不够,比如上面这句代码,第一个参数中 render 函数的执行使得我们能够收集依赖,当依赖变化时会重新执行第二个参数中的 render 函数,但不要忘了这又会触发一次数据字段的 get 拦截器,所以此时已经收集了两遍重复的依赖,那么我们是不是要想办法避免收集冗余的依赖呢?除此之外我们也没有对数组做处理,我们将这些问题留到后面。

现在我们这个不严谨的实现暂时就到这里,意图在于让大家明白数据响应系统的整体思路,为接下来实现Vue做必要的铺垫。

四、编写Vue构造函数

现在我们终于可以来实现一个自己的简单的Vue了,有了之前的铺垫相信读者也能大概理解了vue的响应式实现思路。
我们首先建立一个 index.html 用来引入我们自己的vue ,再新建一个src目录用来存放我们的vue源码。

创建项目目录&文件

新建 src 目录 和index.html
在这里插入图片描述
index.html中我们编写如下结构:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>实现一个简单的vue</title>
	</head>
	<body>
	<div id="app"> 
			测试 v-text  
			<hr>
			<p :class="a b c" style="background-color: red;width: 50%;" v-text="name"></p>
			
			<br>
			测试 v-html 
			<hr>
			<div v-html="html"></div>
			<br>
			测试v-model
			<hr>
			<input  type="text" v-model="name" > 
			<hr>
			<br>
			测试v-if 
			<hr>
			<div v-if="isShow">
				我出现了,哈哈哈!!!<br>
				这是vue的响应式,指令原理!!!
			</div>
			 <br>
			测试v-on:event|@click
			<hr>
			<button type="button" v-on:click="click1">show</button>
			<button type="button" v-on:click="click2">render Html</button>
			<button type="button" @click="click3">@绑定事件</button>
		</div>
		<script src="./src/index.js" type="module"></script>
	</body>
</html>

注意 type= “module” 以模块化的方式读取js文件
结构里面包含了我们要测试的功能

接着我们在 src 下新建 index.js

//index.js
 import Vue from './vue.js' 
 var vm = new Vue({
		el:'#app',
		data:{ 
			name:'changlon',
			age:21,
			family:{
				father:{
					name:'henzhong',
					age:50
				},
				mother:{
					name:'haihua',
					age:46
				}
			},
			model:'hello v-model',
			isShow:false,
			html:''
		},
		methods:{
				walk() {
					console.log('walk')
				},
				click1() {
					this.isShow = true	
				},
				click2() {
					this.html = `
						<h1 style="text-align:center;color:red;" >Title</h1>
						<p>this is a simple-vue, about derective v-html usage</p>
					`
				},
				click3() {
					alert(`大家好 我叫${this.name},今年 ${this.age}岁!`)
				}
		}
	  
  }) 

这是我们一般开发vue所要写的内容。

然后在 src 下新建 vue.js 编写我们的vue实例

//vue.js
export default class Vue { 
	constructor(option) { 
	}
}

数据代理

分析一下我们在new 一个Vue的实例中一般会传入: el , data , methods 等参数,我们这里就以这三个参数为例子进行选项的初始化。

  • 检查el参数
checkEl(el){  
		const $el = typeof el ==='string' ? 
			document.querySelector(el)
			: el
		if(!$el) throw new Error('myvue warn: The option el is invalid!') 
		return $el  
	}
  • 初始化参数
		this.$el = this.checkEl(option.el)
		this.$data = option.data || {}
		this.$methods = option.methods ||{} 
  • 代理data数据
    一般我们是可以通过vue实例vm来访问data中的参数的,所以我们需要进行数据的代理。
proxy(data) { 
		Object.keys(data).forEach(key=>{ 
			// this 指向vue实例
			Object.defineProperty(this,key,{
				enumerable:true,
				configurable:true,
				get(){
					return data[key] 
				}, 
				set(newVal) { 
					if(newVal===data[key]) return 
					data[key]= newVal 
				}
			})
			
		})
	}

为什么要进行代理data中的数据呢?
因为这样方便我们直接通过this. 的形式访问数据。

五、Observer & Dep

在初始化选项和代理完数据后我们就要开始建立响应式系统了。我们需要在src目录下新建 Observer.js Dep.js Compiler.js Watcher.js

Observer.js: 主要用来递归遍历data数据下的属性,并设置 setter , getter ;
它依赖 Dep.js


import Dep from './dep.js' 

export default class Observer {
	
	constructor(data) {  
		this.traverse(data)
	}
	
	traverse(data) {} 
	defineReactive(obj,key,val){}
}


Dep.js : 负责收集依赖和通知更新,针对每一个响应式属性都会new 一个Dep


export default class Dep {
	constructor() { 
		//这是收集依赖的筐	
	    this.subs = [] 
	}

	//添加依赖方法 参数watcher是一个Watch实例
	addSub(watcher){
		if(watcher) {
			this.subs.push(Dep.target)
		}
	}
	
	//通知变更,遍历watcher,执行update更新方法 
	notify(){ 
		this.subs.forEach(watcher=>{
			watcher.update()
		})
	}
}

Watcher.js: 用来触发依赖收集,执行更新方法;它依赖Dep.js

import Dep from './dep.js' 

export default class Watch {
	constructor(vm,key,cb) {   
		this.vm = vm // Vue实例
		this.key = key //属性key
		this.cb = cb //更新方法
		Dep.target = this  //相当于上面的 Target = fn 
		this.oldValue =  vm[key]  //存旧值
		Dep.target = null 
	}
	
	/** 执行更新回调函数 */
	update() {
		let newValue = this.vm[this.key] 
		if(this.oldValue===newValue) return 
		this.cb(this.oldValue,newValue) 
		this.oldValue = newValue
	}	
}

Compiler.js : 用来解析html模板,解析指令,vue语法,生成Watcher实例,触发依赖收集;其依赖Watcher.js;可以将Compiler.js类比为上面的render函数


import Watcher from './watch.js'


export default class Compiler{
	constructor(vm) { 
		this.el = vm.$el   
		this.vm = vm 
		this.methods = vm.$methods
		this.compile(this.el)  
	}
	
	compile(el) { 
	
	}
	
	compileText(node) {
		
	}
	
	
	compileElement(node) { 
	
	}
	
	
	/**
	 * @param {Object} node
	 * @param {Object} key
	 * @param {Object} directiveName
	 * @param {Object} type 指令类型: 0 普通指令 1 事件指令
	 */
	
	update(node,key,directiveName,type) { 
	}
	
	htmlUpdater(node,value,key) {
	}
	
	modelUpdater(node,value,key) {
	}

	ifUpdater(node,value,key) {
	}
	
	clickUpdater(node,value,key) {
	}
	
}

上面几个文件的执行顺序:

  1. 在vue实例化时,先new 执行 observer 建立响应式系统,为每一个属性new Dep 生成依赖。
  2. 然后 new 执行 compiler 编译模板,解析指令,new 生成 watch 观察者,触发依赖的收集(将watch添加到对应dep)。
  3. 当数据变更时触发dep.notify() 通知观察者执行变更(执行watch.update())。

Observer递归响应式

	
	traverse(data) { 
		if(typeof data!=='object') return 
		const that = this 
		Object.keys(data).forEach(key=>{ 
			if(typeof data[key]==='object') {
			//如果数据是对象,继续进行递归遍历
				that.traverse(data[key])
			}else{
				that.defineReactive(data,key,data[key])
			}
		})
	}
	
	defineReactive(obj,key,val) {  
		let dep = new Dep()
		const that = this 
		Object.defineProperty(obj,key,{
			enumerable:true,
			configurable:true,
			get(){
				//将对应的watch实例添加
				Dep.target && dep.addSub(Dep.target)
				return val
			}, 
			set(newVal) { 
				if(newVal===val) return
				val = newVal
				//如果新值是对象,则递归建立响应式
				that.traverse(newVal)
				//通知变更
				dep.notify()
			}
		})
	}		

六、Compiler & Watcher

compile编译文本节点的实现

//compiler.js
compile(el) { 
		const that = this 
		const childNodes = el.childNodes
		const nodeType = el.nodeType 
		
		switch (nodeType) {  
			/**元素节点 */
			case 1:
				that.compileElement(el)
				break
			
			/**文本节点 */	
			case 3:
				that.compileText(el)
				break
		}
		
		if(childNodes&&childNodes.length>0) { 
			for(let i =0;i<childNodes.length;++i) {
				const node = childNodes[i] 
				this.compile(node)
			}
		}
				
	}

compileText(node) {
		let content = node.textContent   
		//验证是否是 {{}} 表达式正则
		const expressionReg = /\{\{(.+?)\}\}/g 
		const matches = content.match(expressionReg)  
		
		if(!matches) return 
		for(let match of matches) {
			let matchKey = match.trim() 
		    matchKey =  matchKey.substring(2,matchKey.length) 
		    matchKey =  matchKey.substring(0,matchKey.length-2).trim() 
			// 假如 一个文本内容是 {{ name }} 
			// 则最后matchKey 处理为 name 
			
			const value = this.vm[matchKey]  //获取实例数据 vm['name'] 
			
			if(value===undefined) {
				try {
					value = eval(`${matchKey}`)
				}catch(e) {
					throw new Error(`template render error: the key ${matchKey} is not exists in vm or the expression is invalid !`)
				}
			} 
			content =  content.replace(match,value)      
			
		} 
		
		node.textContent = content
	}

编译元素的实现

compileElement(node) { 
		const attributes = node.attributes 
		if(attributes&&attributes.length>0) {
			for(const attr of attributes){ 
				let  name = attr.name ,value = attr.value 
				let type = 0  
				
				/** 解析v-开头的指令 */
				if(name.startsWith('v-'))  {
					name = name.substring(2,name.length).trim() 
						if(name.indexOf(':')>-1){
							type = 1 
							name = name.substring(name.indexOf(':')+1,name.length)
						}
				
					this.update(node,value,name,type) 
					
				/**解析 @绑定的事件 */	 
				}else if(name.startsWith('@')) { 
					type = 1
					name = name.substring(1,name.length).trim()  
					this.update(node,value,name,type)
				}
				
				
				/** 其他的指令发挥你的创造性才能吧! */ 
				/** 比如考虑如何实现 指令修饰符.sync .stop ... */ 
				/** 当解析完指令后,我们从浏览器还能看到v-* 等属性,如何解析完,在适合的地方
					移除他们,交给你来实现!!!
				*/
				
			} 
		}
	}

	/**处理不同指令的分发函数
	 * @param {Object} node
	 * @param {Object} key
	 * @param {Object} directiveName
	 * @param {Object} type 指令类型: 0 普通指令 1 事件指令
	 */
	update(node,key,directiveName,type) { 
		
		const updateFn = this[`${directiveName}Updater`] 
		if(!updateFn) throw new Error(`invalid directive:${directiveName}`)
		let value = type==0?
					this.vm[key]: 
					 type==1?
					this.vm.$methods[key]: 
					null 
								
		if(value===undefined) {
			try {
				value = eval(`${key}`)
			}catch(e) {
				throw new Error(`template render error: the key ${matchKey} is not exists in vm or the expression is invalid !`)
			}
		} 
		updateFn.call(this,node,value,key)			 
	}

处理v-text,v-html的实现

	// v-text
	textUpdater(node,value,key) {
		node.innerText = value 
		new Watcher(this.vm,key,(oldValue,newValue)=>{  
			node.innerText = newValue 
		})
		
	}
	// v-html
	htmlUpdater(node,value,key) {
		node.innerHTML = value 
		new Watcher(this.vm,key,(oldValue,newValue)=>{
			node.innerHTML = newValue
		})
	}

测试运行

配置 mywebpack :

module.exports = {
	mode:'production',
	entry:{
		index:'./src/index.js'
	},
	output:{
		filename:'myvue.js'
	}
}

打包输出: myvue.js
引入到你的html结构中就可以测试运行了
打包文件github: myvue.min.js


到这里我希望读者可以模仿上面的去自己实现更多的指令和功能,记住一定要把例子源码下载下来好好研究。自己从头到尾写一遍,你对vue的理解才会更加透彻深入!
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-06 12:08:26  更:2021-10-06 12:10:19 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 21:47:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码