一、vue的响应式原理
首先要了解vue中的三个核心类
Observer、Dep、Watcher
1、Observer:给对象添加getter和setter,用于依赖收集和派发更新;
2、Dep:用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs = watcher[];dep.subs是watcher实例的数组,当数据有变更时,会通过dep.notify()通知各个watcher。
3、Watcher:观察者对象,实例分为render watcher(渲染)、computed watcher(计算属性)、user watcher(侦听器)三种。
依赖收集:
initState时,对computed属性初始化时,触发computed watcher依赖收集;
initState时,对侦听属性初始化时,触发user watcher依赖收集;
render() 的过程中,触发render watcher依赖收集;
re-render时,vm.render() 再次执行,会移除所有subs中的watcher的订阅,重新赋值。
派发更新:
1、组件中对响应式的数据进行了修改,触发setter的逻辑;
2、调用dep.notify();
3、遍历所有subs(Watcher实例),调用每一个watcher的update方法;
总结:
当创建vue实例时,vue会遍历data选项中的属性,利用Object.defineProperty为其添加getter和setter对数据的读取进行劫持(getter用来依赖收集,setter用来派发更新),每个组件实例都有对应的watcher实例,并且在内部追踪依赖,在属性被访问和修改时通知变化。
二、双向绑定原理
首先,解释下双向绑定:
所谓的双向绑定建立在MVVM的模型基础上的:
- 数据层 Model:应用的数据以及业务逻辑
- 视图层 View:应用的展示效果,各类的UI组件等
- 业务逻辑层 ViewModel: 负责将数据和视图关联起来
1. 数据变化后更新视图
2. 视图变化后更新数据
主要包含两个主要的组成部分
- 监听器 Observer:对所有的数据属性进行监听
- 解析器 Compiler:对每个元素节点的指令进行扫描和解析,根据指令替换数据,绑定对应的更新函数
具体实现原理
- new Vue() 实例的过程中,执行初始化。对data通过Object.defineProperty进行响应化处理,这个过程发生在Observer中,每个key都会有一个dep实例来存储watcher实例数组。
- 对模板进行编译时,v- 开头的关键词作为指令解析,找到动态绑定的数据,从data中获取数据并初始化视图,这个过程发生在 Compiler 里。如果遇到了 v-model,就监听input事件,更新data对应的数值。
- 在解析指令的过程中,会定义一个更新函数和Watcher,之后对应的数据变化时 Watcher 会调用更新函数。new Watcher 的过程中会去读取data的key,触发getter的依赖收集,将对应的watcher添加到dep里。
- 将来data中数据一旦发生变化,会首先找到对应的dep,通知所有的watcher执行更新函数。
来简单实现一个响应式函数?对一个对象内的所有key添加响应式的特性?
const render = (key,val) => {
console.log(`SET key=${key} value=${val}`);
}
// 对对象的每个key做处理
const defineReactive = (obj,key,val) => {
reactive(val); // 递归
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newVal){
if(val === newVal){
// 模拟 diff data
return;
}
val = newVal;
render(key,val);
}
})}
// 对对象做处理
const reactive = (obj) => {
// 可以作为一个递归的终止条件
if(typeof obj === 'object'){
for(const key in obj){
defineReactive(obj,key,obj[key])
}
}}
const data = {
a: 1,
b: 2,
c: {
c1: {
af: 999
},
c2: 4
}}
reactive(data);
data.a = 5 // SET
key=a val=5data.b = 7 // SET
key=b val = 7data.c.c2 = 4
data.c.c1.af = 121 // SET
key=af val = 121
那Vue中对于数组类型是怎么处理的?能简单模拟下对数组算法的监听吗?
const render = (action, ...args) => {
console.log(`Action = ${action}, args = ${args.join(',')}`);
};
const arrPrototype = Array.prototype; // 保存数组的原型
const newArrProtoType = Object.create(arrPrototype); // 创建一个新的数组原型
['push','pop','shift','unshift','sort','splice','reverse'].forEach(methodName => {
newArrProtoType[methodName] = function(){
// 执行原有数组的方法
arrPrototype[methodName].call(this,...arguments) // 改变this指向
// 触发渲染
render(methodName,...arguments);
}});
const reactive = (obj) => {
if(Array.isArray(obj)){
// 把新定义的原型对象指向obj.__proto__
obj.__proto__ = newArrProtoType;
}}
const data = [1,2,3,4];
reactive(data);
data.push(5); // Action = push, args = 5
data.splice(0,2); // Action = splice , args = 0,2
三、计算属性的实现原理
上⾯提到的watcher实例, 就有⼀个叫做computed watcher的东⻄, 这个就是计算属性的
watcher。
computed watcher 持有⼀个 dep 实例, 通过 this.dirty 属性标记计算属性是否需要重新求值。
当computed的依赖值改变时, 就会通知订阅的watcher进⾏更新,对于computed的watcher会
将dirty设置为true并且进⾏计算属性⽅法的调⽤.
1、computed所谓的缓存是指什么?
计算属性是基于它们的响应式依赖进⾏缓存的。只在相关响应式依赖发⽣改变时它们才会重新
求值.
2、那computed缓存存在的意义是什么?
⽐如computed内的操作⾮常耗时, 可能是遍历⼀个⼤数组. 计算⼀次可能要耗时1s, 那么当后
续再通过计算属性获取的时候, 如果依赖的值没有变化, 就⽆需重新计算⼀遍了.
3、以下情况, computed可以监听到数据的变化吗?
computed: {
storageMsg(){
return sessionStorage.getItem('xxx')
},
timer(){
return Date.now()
}
}
答案是不能。因为computed计算的是响应式的数据,这个并没有给它添加defineReactive属性,当直接去修改这些数据时,computed是不会重新计算的。
四、Vue.nextTick的原理
Vue是异步执⾏dom更新的,⼀旦观察到数据变化,Vue就会开启⼀个异步队列,然后把在同
⼀个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个
watcher被触发多次,只会被推送到队列⼀次。
这种缓冲⾏为可以有效的去掉重复数据造成的不必要的计算和DOm操作。⽽在下⼀个事件循
环时,Vue会清空队列,并进⾏必要的DOM更新。
⽽vue内部这个异步队列是怎么开启的? 这⾥有⼀个优先级, Promise.then >
MutationObserver > setImmediate > setTimeout
所以可以理解为, nextTick会优先尝试使⽤微任务, 如果浏览器不⽀持, 就⽤宏任务.
当你设置 vm.someData = ‘new value’,DOM 并不会⻢上更新,⽽是在异步队列被清除,也
就是下⼀个事件循环开始时执⾏更新时才会进⾏必要的DOM更新.
所以nextTick的回调是在下⼀轮事件循环⾥执⾏的.
⼀般在什么时候⽤到nextTick呢?
1、在数据变化后要执⾏的某个操作,⽽这个操作需要使⽤随数据改变⽽改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中
<template>
<div v-if="loaded" ref="test"></div>
</template>
async showDiv(){
this.loaded = true;
await Vue.nextTick();
this.$ref.test.xxxxxxxx
}
尝试手写一个简单的Vue,实现响应式更新,详情见
github: github.com/Luna988/kvu…
简述下思路:
1、目录,核心文件
index.html
vue.js Vue主文件
compile.js编译模版,解析指令
dep.js 收集依赖关系,存储观察者,发布订阅
observer.js 数据劫持
watcher 观察者对象
2、index.html内容
首先要有一个根元素
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue</title>
</head>
<body>
<div id="yun">
<h1>template</h1>
<p>{{ msg }}</p>
<h2>{{count}}</h2>
<br />
<h1>v-text</h1>
<div v-text="msg"></div>
<br>
<h1>v-model</h1>
<div v-html="testHtml"></div>
<br>
<h1>v-model</h1>
<input type="text" v-model="msg">
<input type="text" v-model="count">
<button v-on:click="handleChange">按钮</button>
</div>
<script src="./yunVue.js"></script>
<script>
const app = new yunVue({
el: '#yun',
data: {
msg: '面向工资编程!',
count: 100,
testHtml: '<ul><li>渲染!!</li></ul>'
},
methods: {
handleChange(){
console.log(this.msg + 'msg33333')
}
}
})
</script>
</body>
</html>
3、kvue.js文件
主要结构
class yunVue{}
class Dep{}
class Observer{}
class Watcher{}
class Compile{}
3-1、先初始化Vue class
Vue的类就在vue.js文件里实现,包含构造函数,接收配置等等。
先实现一个constructor,接收传入的数据并存储下来。内部变量这里使用$命名,便于区分.
class yunVue{
constructor(options = {}){
// 存储options, data, methods
this.$options = options;
this.$data = options.data;
this.$methods = options.methods;
this.initRootElement(options); // 利⽤Object.defineProperty将data⾥的属性注⼊到vue实例中
this._proxyData(this.$data);
new Observer(this.$data);
new Compiler(this);
}
// 获取根元素,并存储到Vue实例,
initRootElement(options){
if(typeof options.el === 'string'){ // 传⼊的是元素id或者class
this.$el = document.querySelector(options.el);
}else if(options.el instanceof HTMLElement){
this.$el = options.el;
}
if(!this.$el){
throw new Error('传入的el不合法,请传入css selector或者Element!')
}
}
_proxyData(data){ // 遍历所有data
Object.keys(data).forEach(key => { // 将data属性注入到vue中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get(){
return data[key]
},
set(newValue){
if(data[key] === newValue){
return
}
data[key] = newValue
}
})
})
}}
3-2、核心类:Dep
class Dep{
constructor(){
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub(watcher){
if(watcher && watcher.update){
this.subs.push(watcher)
}
}
// 发送通知
notify(){
this.subs.forEach(watcher => {
watcher.update()
})
}}
addSub,如果watcher没有update方法,那就没必要添加到sub里了;
notify,提供给外界调用的,数据变更时,外界调用notify去通知各个watcher,即执行watcher.update()
dep在哪里实例化?因为触发setter时需要通知所有的watcher更新,那么就要在Observer的defineReactive里面去实例化;
3-3、核心类:Observer
数据被获取的时候去收集依赖
class Observer{
constructor(data){
this.traverse(data)
}
// 递归遍历data里的所有属性
traverse(data){
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 给传入的数据设置getter/setter
defineReactive(obj,key,val){
const that = this
this.traverse(val) // 递归设置
const dep = new Dep() // 负责依赖收集,并发送依赖
Object.defineProperty(obj,key,{
configurable: true,
enumerable: true,
get(){
Dep.target && dep.addSub(Dep.target) // 收集依赖
return val;
},
set(newValue){
if(newValue === val){
return
}
val = newValue
that.traverse(newValue) // newValue可能是个对象
dep.notify() // 通知watcher数据更新了
}
})
}}
3-4、核心类:Watcher
class Watcher{
// vm: vue实例
// key:data中的属性名
// cb:负责更新视图的回调函数
constructor(vm,key,cb){
this.vm = vm;
this.key = key;
this.cb = cb;
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
Dep.target = null;
}
// 当数据发生变化的时候更新视图
update(){
let newValue = this.vm[this.key]
if(this.oldValue === newValue) {
return
}
this.cb(newValue)
}}
3-5、核心类Compiler 模版编译
class Compiler{
constructor(vm){
this.el = vm.$el
this.vm = vm
this.methods = vm.$methods
this.compile(vm.$el)
}
// 编译模版
compile(el){
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if(this.isTextNode(node)){ // 处理文本节点
this.compileText(node)
}else if(this.isElementNode(node)){ // 处理元素节点
this.compileElement(node)
}
// 如果还是有子节点,递归调用
if(node.childNodes && node.childNodes.length > 0){
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement(node){
if(node.attributes.length){
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if(this.isDirective(attrName)){ // 判断是否是指令
attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) // 获取v-后面的值
let key = attr.value // 获取data名称
this.update(node,key,attrName)
}
})
}
}
// 更新
update(node,key,attrName){
const updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this,node,this.vm[key],key,attrName)
}
// 解析 v-text
textUpdater(node,value,key){
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
// 创建watcher对象,当数据改变更新视图
node.textContent = newValue
})
}
// 解析 v-model
modelUpdater(node,value,key){
node.value = value
new Watcher(this.vm, key, (newValue) => {
// 创建watcher对象,当数据改变更新视图
node.value = newValue
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 解析v-html
htmlUpdater(node,value,key){
node.innerHTML = value
new Watcher(this.vm,key,newValue => {
node.textContent = newValue
})
}
// 解析v-on:click
clickUpdater(node,value,key,attrName){
node.addEventListener(attrName,this.methods[key])
}
// 编译文本节点,处理插值表达式,{{ }}
compileText(node){
// 获取 {{ }} 中的值
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if(reg.test(value)){
let key = RegExp.$1.trim() // 返回匹配到的第一个字符串,去掉空格
node.textContent = value.replace(reg,this.vm[key])
new Watcher(this.vm, key, (newValue) => {
// 创建watcher对象,当数据改变更新视图
node.textContent = newValue
})
}
}
// 判断是否是文本节点
isTextNode(node){
return node.nodeType === 3
}
// 判断是否是元素节点
isElementNode(node){
return node.nodeType === 1
}
// 判断元素属性是否是指令
isDirective(attrName){
return attrName.startsWith('v-')
}}
想一下,这些方法之间如何相互调用的,vue初始化过程都应该做些什么?
初始化时,this._proxyData(this.data);实例化编译对象,解析模版,如差值表达式,指令解析等。new Compiler(this);