VUE原理及运行机制

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 中,形成如下关系:
watcher和Dep
在修改对象值时,会触发响应的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);

Object.defineProperty用法
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方法:
defineReactive方法
这些当然是不够的,我们要在这上面再封装一层observe——它传入一个value(需要“响应式”化的对象)的参数,通过遍历它所有属性的方式,来对该对象的每一个属性做defineReactive处理:
observe

最后把他们封装到“Vue”中:
封装到vue中
这时就可以使用了:
使用封装好的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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值