# Vue原理浅析
# Vue工作机制
初始化Vue实例时候,会先调用初始化函数init,此时会初始化生命周期,事件,props, methods,data,computed,watch等。
初始化之后调用$mount挂载组件。
其中,挂载组件之前会进行模板编译compile,模板编译分为三个阶段:
- parse(解析):使用正则解析template中的vue的指令(v-xxx)变量等,形成
语法树AST。
并将值进行依赖收集,与数据产生挂钩关系,将来在某个时刻数据发生
变化的时候则可以找到关联的函数执行。
2.optimize(优化):标记一些静态节点,用作后面的性能优化,在diff的时候直接略过。
3.generate:把第一步生成的AST转化为渲染函数render function。
有了渲染方法,下一步就是更新DOM,注意并不是直接更新,而是通过vnode,将虚拟DOM 和真实DOM产生对应关系。之后操作的数据都是虚拟DOM,虚拟DOM变化后,通过diff算法 在更新到DOM树。
之后,当数据发生变化的时候,会触发setter,然后监听器Watcher会通知进行修改, 通过对比两个DOM,进行DOM更新。
所以,我们可以了解到vue核心的内容就是,初始化的时候通过defineProperty 进行数据绑定,设置通知的机制,当编译生成的渲染函数被实际渲染的时候,触发getter 进行依赖收集,在数据变化的时候,触发setter进行数据更新。
# 实现简单的Vue
# 实现思路
定义观察者Observer函数,以劫持监听所有属性,并收集编译阶段Compile发现的 依赖收集到依赖管理器Dep中,Dep中以数组的形式管理着所有的Watcher,Watcher中 即存在着真正执行视图更新的执行者Updater。当发现数据变化时候,Observer通知 Dep,然后调用Watcher实现视图的更新。
实现Observer,对数据进行劫持。
class Vue{
constructor(options){
this.$data = options.data //保存data
this.observe(this.$data) // 执行响应式,观察者
}
observe(value){
// 遍历data
// 判断data是否存在且为对象类型
if(!value || typeof value !== 'object'){
return
}
//遍历data
Object.keys(value).forEach(key=>{
//为每一个key定义响应式
this.defineReactive(value,key,value[key])
})
}
//其中接受三个参数:要定义的对象,键值key和对应的值value
defineReactive(obj,key,val){
//递归查找嵌套的属性
this.observe(val)
// 为data对象定义属性
Object.defineProperty(obj,key,{
enumerable: true,//可枚举
configurable: true, //可修改或删除
get(){
return val
},
set(newVal){
if(newVal === val){
return
}
console.log('数据发生变化~')
val = newVal
}
})
}
}
接着实现依赖收集Dep,并放置监听器Watcher实现视图更新。
//依赖管理器,负责将视图中所有依赖收集管理,包括依赖添加和通知
class Dep{
constructor(){
this.deps = [] //deps里面存放的是Watcher实例
}
addDep(dep){
this.deps.push(dep)
}
//通知所有watcher执行更新
notify(){
this.deps.forEach(dep=>{
dep.update()
})
}
}
//Watcher:具体的更新执行者
class Watcher {
constructor(){
//将来new一个监听器时候,将当前Watcher实例附加到Dep.target上
// 当执行数据属性get时候,可以获取到对应依赖的实例
Dep.target = this
}
//更新
update(){
console.log('视图更新')
}
}
这时,需要编译器Compiler完成对模板编译,得到更新函数后,才能给Watcher调用, 所以编译阶段才能创建Watcher。即编译器在做依赖收集时候创建Watcher。
// 扫描模板中所有依赖,{{}},dom属性,v-xxx指令,@方法等创建函数和Watcher
class Compile{
//el 宿主元素或选择器
//vm 当前Vue实例
constructor(el,vm){
this.$vm = vm
this.$el = document.querySelector(el)//设置为默认选择器
if(this.$el){
// 将dom转换成代码块Fragment,提高性能,效率
this.$fragment = this.node2Fragment(this.$el)
//执行编译
this.compile(this.$fragment)
//将生成的结果追加至宿主元素
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el){
// 创建新的Fragment
const fragment = document.createDocumentFragment()
let child;
// 将原生节点拷贝至fragment
while(child = el.firstChild){ //获取el任意子节点,有可能是注释,文本节点,元素等
// appendChild是移动操作
fragment.appendChild(child)
}
return fragment
}
// 编译指定片段
compile(el){
let childNodes = el.childNodes
// 将类数组转为数组并遍历节点
Array.from(childNodes).forEach(node => {
//判断node类型,做相应处理
if(this.isElementNode(node)){
//元素节点,要识别v-xx或者@xx
this.compileElement(node)
} else if(this.isTextNode(node) && /\{\{(.*)\}\}/.test(node.textContent)){
// 文本节点,只关系{{xx}}形式
this.compileText(node,RegExp.$1); //RegExp.$1,匹配内容
}
// 递归,遍历可能存在的子节点
if(node.childNodes && node.childNodes.length){
this.compile(node)
}
})
}
//编译元素节点
compileElement(node){
//<div v-test='test' @click='handleClick'></div>
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
//规定指令 v-test='test' @click='handleClick'
const attrName = attr.name //属性名
const exp = attr.value //属性值
if(this.isDirective(attrName)){ //指令
const dir = attrName.substr(2) //v-text -> text
this[dir] && this[dir](node,this.$vm,exp)
} else if (this.isEventDirective(attrName)){ //事件
const dir = attrName.substr(1) //click
this.eventHandler(node,this.$vm,exp,dir)
}
})
}
//编译文本节点
compileText(node,exp){
this.text(node,this.$vm,exp)
}
isElementNode(node){
return node.nodeType == 1 //元素节点
}
isTextNode(node){
return node.nodeType == 3 //文本节点
}
isDirective(attr){
return attr.indexOf('v-') == 0
}
isEventDirective(dir){
return dir.indexOf('@') == 0
}
//文本更新
text(node,vm,exp){
this.update(node,vm,exp,'text')
}
//处理html
html(node,vm,exp){
this.update(node,vm,exp,'html')
}
//双向绑定
model(node,vm,exp){
this.update(node,vm,exp,'model')
const val = vm.exp
//双向绑定还要处理视图对模型的更新
node.addEventListener('input',e=>{
vm[exp] = e.target.value
})
}
//更新函数Updater
update(node,vm,exp,dir){
let updateFn = this[dir+'Updater']
updateFn && updateFn(node,vm[exp]) //立刻执行更新,get Compile -> Updater
new Watcher(vm,exp,function (value) { //当监听到变化时再执行更新 Watcher -> Updater
updateFn && updateFn(node,value)
})
}
//执行DOM操作
textUpdater(node,value){
node.textContent = value
}
htmlUpdater(node,value){
node.innerHTML = value
}
modelUpdater(node,value){
node.value = value
}
eventHandler(node,vm,exp,dir){
let fn = vm.$options.methods && vm.$options.methods[exp]
if(dir && fn){
node.addEventListener(dir,fn.bind(vm)) // fn.bind,绑定当前实例vm
}
}
}
最后,完善Watcher并实例化compile。
//vue.js
class Vue{
constructor(options){
this.$options = options //Compile调用
this.$compile = new Compile(options.el,this)
}
...
}
//Watcher:具体的更新执行者
class Watcher {
constructor(vm,key,cb){
this.vm = vm
this.key = key
this.cb = cb
//将来new一个监听器时候,将当前Watcher实例附加到Dep.target上
// 当执行数据属性get时候,可以获取到对应依赖的实例
Dep.target = this
this.vm[this.key] // 触发data的get
Dep.target = null //避免重复添加defineReactive中defineProperty重复set
}
//更新
update(){
console.log('视图更新')
this.cb.call(this.vm,this.vm[this.key])
}
}
# 迷你版vue链接
代码地址:https://github.com/PCAaron/blogCode/tree/master/vue/easy-vue
# 概念了解
# 虚拟DOM
虚拟DOM就是用JavaScript对象来描述DOM结构。数据修改的时候,先修改虚拟DOM中的 数据,然后数据做diff算法,最后再汇总所有的diff差异,找出这个最小差别并且在真实 DOM上只更新这个最小的变化内容,极大程度地降低了对DOM的操作带来的性能开销。
//dom
<div id='app' style='color:red' @click='handleClick'>
<a>click me</a>
</div>
//virtual dom
{
tag:'div', // 元素标签
attrs:{ // 属性
id:'app'
style:{color:red},
onClick: handleClick
},
children:[
{
tag:'a',
text:'click me'
}
] // 子元素
}
# diff算法
Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。
Diff算法有三大策略,其执行顺序也是顺序执行:
- Tree Diff
Tree Diff 是对树每一层进行遍历,找出不同。 - Component Diff
Component Diff 是数据层面的差异比较,如果都是同一类型的组件,按照原策略继续比较Virtual DOM树即可; 如果出现不是同一类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点。 - Element Diff
Element Diff真实DOM渲染,结构差异的比较。
# Object.defineProperty(obj, prop, descriptor)
Object对象方法,defineProperty方法会直接在一个对象上定义一个新属性, 或者修改一个对象的现有属性, 并返回这个对象。
其中接受三个参数分别表示:
- obj:要在其上定义属性的对象。
- prop:要定义或修改的属性的名称。
- descriptor:将被定义或修改的属性描述符。
其中,属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有 值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter 函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
描述符可同时具有的键值:
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | Yes | Yes | Yes | Yes | No | No |
存取描述符 | Yes | Yes | No | No | Yes | Yes |
<div id="app">
<p>
hello
<span id="name"></span>
</p>
</div>
<script>
var obj = {}
Object.defineProperty(obj,'name',{
get(){
return document.getElementById('name').innerHTML
},
set(inner){
document.getElementById('name').innerHTML = inner
}
})
console.log(obj.name) // ''
obj.name='set'
console.log(obj.name) // 'set'
</script>
# 推荐阅读
vuejs原理浅析:https://blog.csdn.net/saucxs/article/details/87550808