VUE运行机制全局预览
【初始化及挂载】 =》 【编译】(parse-optimize-generate)】=》【render Function渲染】(响应式)=》【Virtual DOM】=》更新视图
初始化及挂载
在new了vue之后,Vue会调用_init()函数进行(全局)初始化。初始化生命周期、事件、props、methods、data、computed、watch等。其中最重要的是通过Object.defineProperty设置getter和setter函数,用来实现【响应式】和【依赖收集】
初始化之后调用 $mount 挂载组件——如果是运行时编译,即不存在render function但是存在 template 的情况,此后还需要进行【编译】步骤。
编译:
- parse:用正则等方法解析template模版中的指令、class、style等数据,形成AST(抽象语法树——源代码的抽象语法结构的树状表现形式)
- optimize:主要为了标记static节点。这是VUE的一处优化——后面当update更新页面时,会有一个patch过程,diff算法会直接跳过静态节点。从而减少了比较的过程,优化了patch的性能
- generate:将AST转化成render function 字符串的过程,得到了 render&staticRenderFns 字符串
经过了以上三个阶段后,组件中就会存在渲染VNode所需的render function 了。
响应式
Object.defineProperty 的setter和getter为VUE的响应式作出了巨大贡献。
当 render function 被渲染时,因为会读取所需对象的值,所以会触发getter进行【依赖收集】,其目的是将观察者watcher对象存放到当前闭包中的订阅者 Dep 的 subs 中,形成如下关系:
在修改对象值时,会触发响应的setter,setter通知之前【依赖收集】到的Dep 中的所有watcher,告诉他们自己的值变了,需要重新渲染视图。这时候这些watcher就开始调用update来渲染视图——当然,这中间还有一个patch的过程,以及使用队列来异步更新的策略。
Virtual DOM
前面说render function会转化成 VNode 节点,而Virtual DOM实际上一颗以JavaScript对象为基础的树,这个树结构的JS对象包含了整个DOM结构的信息,用对象属性描述节点。它实际上只有一层是对真DOM的抽象。
正是由于Virtual DOM是以JavaScript对象为基础而不依赖平台环境,所以它有了跨平台的能力。
响应式系统
VUE的响应式是基于 Object.defineProperty 实现的。
Object.defineProperty(obj, props, desc) 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回该对象。
obj:要定义属性的对象
props:一个字符串或 Symbol,指定了要定义或修改的属性键
desc:要定义或修改的属性的描述符
返回值:传入函数的对象,其指定的属性已被添加或修改
Object.defineProperty() 的get 方法用来获取值,set 方法用来拦截设置值。
例子:
var obj = {
val: "test"
};
let val = "hello world"
Object.defineProperty(obj, 'val', {
get () {
console.log('获取对象的值');
return val
},
set (newVal) {
console.log(newVal);
console.log('设置对象的新的值为:' + newVal);
val = newVal
}
})
obj.val = val
console.log(obj);
MVVM框架的核心就是数据双向绑定,其原理是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体步骤:
1.设置一个监听器 observe,用来劫持并监听所有属性,如果有变动,就通知订阅者
2.设置一个订阅者 Watcher,每一个 Watcher 都绑定一个更新函数,Watcher 可以收到属性的变化通知并执行相应的函数,从而更新视图
3.设置一个解析器 Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在 v-model,v-on等指令,则解析器 Compile 初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)
实现Observer
首先定义一个函数cb,用来模拟视图更新。里面是一些更新视图的方法:
然后定义一个defineReactive,这个方法通过Object.defineProperty实现对对象的【响应式】化。入参当然是obj(需要绑定的对象)、key(obj的一些属性)、val(具体值),经过defineReactive处理后,obj的key属性会在读的时候触发reactiveGetter方法,在写的时候触发reactiveSetter方法:
这些当然是不够的,我们要在这上面再封装一层observe——它传入一个value(需要“响应式”化的对象)的参数,通过遍历它所有属性的方式,来对该对象的每一个属性做defineReactive处理:
最后把他们封装到“Vue”中:
这时就可以使用了:
响应式系统的依赖收集追踪原理
为什么要“追踪”?
假设现在有一个Vue对象:
如果修改了text3的数据,但视图中不需要text3,所以并不需要调用cb函数。
要解决这个问题,就需要大名鼎鼎的【订阅】&【观察者】模式了:
订阅者Dep——存放watcher对象(可以说成是一个“消息管理中心”)
class Dep {
constructor() {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
//通知所有Watcher更新视图
notify () {
this.subs.forEach((sub) => {
sub.update()
})
}
}
观察者Watcher:
class Watcher {
constructor() {
//在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到
Dep.target = this;
}
update () {
console.log("视图更新啦");
}
}
Dep.target = null;
接下来要去修改一下 defineReactive 以及Vue的构造函数,来完成【依赖收集的注入】(不再需要cb函数了):
function defineReactive (obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumable: true,
configurable: true,
get: function reactiveGetter () {
dep.addSub(Dep.target)
console.log(val);
return val;
},
set: function reactiveSetter (newVal) {
if (newVal == val) return;
dep.notify()
}
})
}
class Vue {
constructor(options) {
this._data = options.data;
observe(this._data);
new Watcher();
//这里模拟render的过程,为了触发test属性的get函数
console.log('render', this._data.test);
}
}
let vm = new Vue({
data: {
test: 'i am test'
}
});
vm._data.test = "hello world"
我们在闭包中增加了一个Dep类的对象,用来收集Watcher对象。在对象被读的时候,会触发reactiveGetter,把当前Watcher对象(存放在dep.target中)收集到Dep类中;写的时候触发reactiveSetter,通知Dep类调用notify来触发所有Watcher的update更新对应视图。
批量异步更新——nextTick原理
回顾上面的内容,我们大概已经知晓Vue是如何在我们修改过data后修改视图了:这其实是一个 setter - Dep - Watcher - patch - 视图的过程。
现在假设有如下情况:
<template>
<div>
<div>{{num}}</div>
<div @click="hClick">click me</div>
</div>
</template>
<script>
export default {
data () {
return {
num: 0
}
},
methods: {
hClick () {
for (let i = 0; i < 100; i++) {
this.num++
}
}
}
}
</script>
按下按钮,num被循环自增100次。
按照之前的理解,每次num自增时,都会触发num的setter,也就是说,上面这个“流程”在这个demo中要被跑100次。
Vue肯定不能用这样的方式。实际上Vue默认每当触发某个数据的setter时,对应的Watcher对象其实会被push进一个队伍queue中,在下一个tick时,将这个queue全部拿出来run(Watcher内的一个方法,用来触发patch操作)一遍。
简单实现nextTick(这里用的是setTimeOut):
let callbacks=[];
let pending=false;
function nextTick(cb){
callbacks.push(cb);
if(!pending){
pending=true;
setTimeout(flushCallbacks,0);
}
}
function flushCallback(){
pending=false;
const pipes=callbacks.slice(0);
callbacks.length=0;
for(let i=0;i<pipes.length;i++){
pipes[i]();
}
}
既然如此,在上面自增1000的例子中,我们并不需要在下一个tick时执行1000个同样的Watcher对象去修改界面,而只需一个Watcher对象,使其将界面上的0变成1000即可。
那么,我们就需要一个“过滤”操作 —— 同一个Watcher在同一个tick时应该只被执行一次。即 队列中不应该出现重复的Watcher对象
重写Watcher
实现update,在修改数据后由Dep来调用,而run才是真正触发patch的方法:
let uid=0;
let has={};
let queue=[];
let waiting=false;
class Watcher{
constructor(){
this.id=++uid;
Dep.target=this;
}
update(){
console.log('watch'+this.id+'update');
queueWatcher(this); //将update自身传进去
}
run(){
console.log('watch'+this.id+'->视图更新啦...');
}
}
Dep.target=null;
function queueWatcher(watcher){
const id=watcher.id;
if(has[id]===null){
has[id]=true;
queue.push(watcher);
if(!waiting){
waiting=true;
nextTick(flushScheduleQueue); //上面重写nextTick部分的代码
}
}
}
function flushScheduleQueue(){
let watcher,id;
for(index=0;index<queue.length;index++){
watcher=queue[index];
id=watcher.id;
has[id]=null;
watcher.run();
}
waiting=false;
}
————————————————
版权声明:本文为CSDN博主「恪愚」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43624878/article/details/103761483