Vue的实现原理
三种双向数据绑定的方式
发布-订阅者模式(backbone.js)
-
一般通过pub、sub的方式来实现数据和视图的绑定,但是使用起来比较麻烦
-
发布-订阅者模式,也叫观察者模式
-
它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合
-
例子:微信公众号
- 订阅者:只需要订阅微信公众号
- 发布者(公众号):发布新文章的时候,推送给所有订阅者
-
优点:
- 解耦合
- 订阅者不用每次去查看公众号是否有新的文章
- 发布者不用关心谁订阅了它,只要给所有订阅者推送即可
脏值检查(angular.js)
angular.js是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,类似于通过定时器轮询检测数据是否发生了改变。
数据劫持
vue.js则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回掉。
vuejs不兼容IE8以下版本
vue的实现思路
-
实现一个Compile模板解析器,能够对模板中的指令和插值表达式进行解析,并赋予不同的操作
let fragment = document.createDocumentFragment(); //创建了一个存在于内存中的虚拟DOM树 -
实现一个Observer数据监听器,能够对数据对象的所有属性进行监听
Object.defineProperty() <div id="app"> <p>您好,<sapn id="name"></sapn></p> </div> <script> var obj={} Object.defineProperty(obj,"name",{ get(){ return document.querySelector("#name").innerHTML; }, set(nick){ document.querySelector("#name").innerHTML = nick } }) obj.name = "jerry"; </script> -
实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM
-
创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,也就是vue
vue的核心实现源码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>mini-vue</title>
</head>
<body>
<div id="app">
<p>{{msg}}</p>
<!-- 你好 -->
<p v-text="msg"></p>
<p v-html="msg"></p>
<p>{{car.brand}}</p>
<p v-text="car.color"></p>
<input type="text" v-model="msg">
<button v-on:click="clickFn">按钮</button>
</div>
<script src="./src/watcher.js"></script>
<script src="./src/observe.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/vue.js"></script>
<script>
let app = document.querySelector('#app')
const vm = new Vue({
//el:'#app',
el: app,
data:{
msg:'hello vue',
car:{
brand:'大众',
color:'blue'
}
},
methods: {
clickFn(){
console.log(this.$data.msg);
}
}
})
</script>
</body>
</html>
vue.js
//vue实例类
class Vue {
constructor(options = {}) {
//绑定属性
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
//监视data中的数据
new Observer(this.$data)
//把data中所有的数据代理到vm上
this.proxy(this.$data)
//把method中的数据代理到vm上
this.proxy(this.$methods)
if (this.$el) {
//把app模板和Vue实例传给Compile进行解析
new Compile(this.$el,this)
}
}
proxy(data){
Object.keys(data).forEach(key => {
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get(){
return data[key]
},
set(newVal){
if (data[key] === newVal) {
return;
}
data[key] = newVal
}
})
})
}
}
compile.js
/**
* compile对html进行解析
*/
class Compile {
/**
* 构造函数
* @param {app模板} el
* @param {vue实例} vm
*/
constructor(el, vm) {
this.el = typeof el === "string" ? document.querySelector(el) : el
this.vm = vm
//编译模板
if (this.el) {
//第一步:把传进来的真是DOM树放到虚拟DOM树中并返回树节点
// 1.在el中所有的子节点都放入到内存中,用虚拟DOM franment
let franment = this.node2franment(this.el)
//第二步:把虚拟DOM树节点传给编译函数,进行文本节点,元素节点解析
// 2.在内存中编译fragment
this.compile(franment)
// 3.把fragment一次性的添加到页面
this.el.appendChild(franment)
}
}
/* 核心方法 */
//把节点放到虚拟DOM中
node2franment(node) {
let franment = document.createDocumentFragment()
let childNodes = node.childNodes
Array.from(childNodes).forEach(node => {
franment.appendChild(node)
})
return franment;
}
//解析虚拟DOM中的节点
compile(franment) {
let childNodes = franment.childNodes
Array.from(childNodes).forEach(node => {
//编译子节点
if (this.isElementNode(node)) {
// 如果是元素需要解析指令
this.compileElement(node)
}
if (this.isTextNode(node)) {
// 如果是文本节点,需要解析表达式
this.compileText(node)
}
//判断当前节点还有子节点,需要递归的解析
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
//解析html标签
compileElement(node) {
let attribute = node.attributes
//遍历属性,拿到属性名和属性值
Array.from(attribute).forEach(attr => {
//判断属性名是不是指令
if (this.isDirective(attr.name)) {
let type = attr.name.slice(2)
//判断指令的类型
if (this.isEventDirective(type)) {
CompileUtil['eventHandler'](node, this.vm, type, attr.value)
} else {
CompileUtil[type] && CompileUtil[type](node, this.vm, attr.value)
}
}
})
}
//解析文本
compileText(node) {
CompileUtil.mustache(node, this.vm)
}
/* 工具方法 */
//是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
//是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
//判断是否是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
//判断是否是事件指令
isEventDirective(type) {
return type.split(':')[0] === 'on';
}
}
let CompileUtil = {
mustache(node, vm) {
//获取文本
let txt = node.textContent
//用正则表达式匹配双花括号
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
//替换花括号里的内容
node.textContent = txt.replace(reg, this.getVmValue(vm, RegExp.$1))
new Watcher(vm, RegExp.$1, newValue => {
node.textContent = txt.replace(reg, newValue)
})
}
},
//解析v-text
text(node, vm, expr) {
node.textContent = this.getVmValue(vm, expr)
new Watcher(vm, expr, newValue => {
node.textContent = newValue
})
},
//解析v-html
html(node, vm, expr) {
node.innerHTML = this.getVmValue(vm, expr)
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue
})
},
//解析v-model
model(node, vm, expr) {
let _this = this
node.value = this.getVmValue(vm, expr)
node.addEventListener('input', function() {
_this.setVmValue(vm,expr,this.value)
})
new Watcher(vm, expr, newValue => {
node.value = newValue
})
},
//解析事件
eventHandler(node, vm, type, expr) {
let eventType = type.split(":")[1]
let fn = vm.$methods && vm.$methods[expr]
if (eventType && fn) {
node.addEventListener(eventType, vm.$methods[expr].bind(vm))
}
},
//获取vm中的数据
getVmValue(vm, expr) {
let data = vm.$data
expr.split(".").forEach(key => {
data = data[key]
})
return data
},
setVmValue(vm,expr,value){
let data = vm.$data
let arr = expr.split(".")
arr.forEach((key,index)=>{
if (index < arr.length -1 ) {
data = data[key]
}else{
data[key] = value
}
})
}
}
observe.js
/**
* observer用于给data中所有的数据加上getter和setter,
* 方便获取或设置data中数据的时候,实现我们的逻辑
*/
class Observer {
constructor(data){
this.data = data
this.walk(this.data)
}
/* 核心方法 */
//遍历data中所有的数据,都添加上getter和setter
walk(data){
if (!data || typeof data != 'object') {
return;
}
Object.keys(data).forEach(key => {
//给data对象的key添加getter和setter
this.defineReactive(data,key,data[key])
//递归遍历data里的数据
this.walk(data[key])
})
}
//定义响应式数据(数据劫持)
// data中的每一个对象都应该维护一个dep对象
//dep保存了所有的订阅了该数据的订阅者
defineReactive(obj,key,value){
let _this = this
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
// 如果Dep.target中有watcher对象,存储到订阅者数组中
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue){
if (value === newValue) {
return;
}
value = newValue
//如果newValue也是一个对象,也应该对他进行劫持
_this.walk(newValue)
// 发布通知,让所有的订阅者更新内容
dep.notify()
}
})
}
}
watcher.js
/**
* watcher模块负责把compile和observe关联起来
*/
class Watcher {
/**
*
* @param {当前的vue实例} vm
* @param {data中数据的名字} expr
* @param {数据变化的回调} cb
*/
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb
// this表示的就是新创建的watcher对象
// 存储到Dep的target属性上
Dep.target = this
this.oldValue = this.getVmValue(this.vm,this.expr)
//清空Dep
Dep.target = null
}
//对外暴露一个方法,更新页面数据
update(){
// 对比expr 是否发生了变化,如果发生了变化需要调用cb
let oldValue = this.oldValue
let newValue = this.getVmValue(this.vm,this.expr)
if (oldValue != newValue) {
this.cb(newValue,oldValue)
}
}
//获取vm中的数据
getVmValue(vm,expr){
let data = vm.$data
expr.split(".").forEach(key =>{
data = data[key]
})
return data
}
}
/**
* dep类用于管理所有的订阅者和通知这些订阅者
*/
class Dep{
constructor(){
//用于管理订阅者
this.subs = []
}
//添加订阅者
addSub(watcher){
this.subs.push(watcher)
}
//通知
notify(){
// 通知所有的订阅者,调用watcher的update方法
this.subs.forEach(sub => {
sub.update()
})
}
}
vue的工作机制
-
初始化
调用Vue原型上的
_init()进行初始化,会初始化vue的生命周期,props,data,methods,computed,watch,最重要的是利用Object.definedPropty()对data对象里面的属性设置setter和getter函数,也就是来实现响应式和依赖收集 -
挂载组件
调用$mount挂载组件
-
编译
编译三部曲,
parse(解析)、optimize(标记静态节点做优化)、generate(转成字符串) 3.1 parse:利用正则将模板转换成抽象语法树(AST); 3.2 optimize: 标记静态节点,以后update的时候,diff算法可以跳过静态节点 3.3 generate:将抽象语法树(AST)转成字符串,供render去渲染DOM经过以上步骤,就可以得到render funciton
-
响应式
响应式是vue中我认为最核心的部分,利用
Object.definedPropty设置data所返回的对象后,在进行render function被渲染的时候,会对data对象进行数据读取,会触发getter函数,从而把data里面的属性进行依赖收集,依赖收集的目的是将这些属性放到观察者(Watcher)的观察队列中,一旦我们对data里面的属性进行修改时,就会触发setter函数,setter告诉观察者数据变化,需要重新渲染视图,观察者调用update来更新视图 -
虚拟DOM
render funtion 会被转换成虚拟DOM,虚拟DOM实际上就是一个js对象,从顶层DOM层层描述DOM,有tag, children, isStatic, isComment等等许多属性来做DOM描述
-
更新视图
当数据发生变化时候,会经历
setter=>Watcher=>update这些步骤,那么最终是怎么更新视图的呢? 在update的时候,会执行patch,将新旧VNode传进去,通过diff算法算出差异,局部更新视图,做到最优化。
依赖收集与追踪
编译compile
