响应式原理

Object.defineProperty

/*
    obj: 目标对象
    prop: 需要操作的目标对象的属性名
    descriptor: 描述符
    
    return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)

可观察的 observer

首先我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图,内部可以是一些更新视图的方法。

function cb (val) {
    /* 渲染视图 */
    console.log("视图更新啦~");
}

我们定义definereactive ,通过调用object。defineproperty 来实现对对象的响应式

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 属性可枚举 */
        configurable: true,     /* 属性可被修改或删除 */
        get: function reactiveGetter () {
            return val;         /* 实际上会依赖收集,下一小节会讲 */
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            cb(newVal);
        }
    });
}

需要在上面在封装一层observer, 这个函数传入一个value。

用observer 来封装一个vue

响应式系统的依赖收集追踪原理

为什么要有依赖收集 将依赖的数据进行让有变动的通知

依赖收集

订阅者dep

  1. 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;

  2. 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

观察者watcher

依赖收集

我们在闭包中增加了一个 Dep 类的对象,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

总结一下。

首先在 observer 的过程中会注册 get 方法,该方法用来进行「依赖收集」。在它的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher 对象的实例。其实「依赖收集」的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。

这是 Object.defineProperty 的 set/get 方法处理的事情,那么「依赖收集」的前提条件还有两个:

触发 get 方法; 新建一个 Watcher 对象

这个我们在 Vue 的构造类中处理。新建一个 Watcher 对象只需要 new 出来,这时候 Dep.target 已经指向了这个 new 出来的 Watcher 对象来。而触发 get 方法也很简单,实际上只要把 render function 进行渲染,那么其中的依赖的对象都会被「读取」,这里我们通过打印来模拟这个过程,读取 test 来触发 get 进行「依赖收集」。

实现virtual dom 下的一个vnode 节点

什么是vnode

render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。

js 代码形式

template 模版是怎样通过 compile 编译的

compile 编译可以氛围parse 、optimize 和 generate 三个阶段, 最终需要得到render function

parse 首先是 parse,parse 会用正则等方式将 template 模板中进行字符串解析,得到指令、class、style等数据,形成 AST

正则

optimize optimize 主要作用就跟它的名字一样,用作「优化」。

这个涉及 patch 的过程,因为 patch 的过程实际上是将 VNode 节点进行一层一层的比对,然后将「差异」更新到视图上。那么一些静态节点是不会根据数据变化而产生变化的,这些节点我们没有比对的需求,是不是可以跳过这些静态节点的比对,从而节省一些性能呢?

那么我们就需要为静态的节点做上一些「标记」,在 patch 的时候我们就可以直接跳过这些被标记的节点的比对,从而达到「优化」的目的。

经过 optimize 这层的处理,每个节点会加上 static 属性,用来标记是否是静态的。 isStatic

首先实现一个 isStatic 函数,传入一个 node 判断该 node 是否是静态节点。判断的标准是当 type 为 2(表达式节点)则是非静态节点,当 type 为 3(文本节点)的时候则是静态节点,当然,如果存在 if 或者 for这样的条件的时候(表达式节点),也是非静态节点。

markstatic

markStatic 为所有的节点标记上 static,遍历所有节点通过 isStatic 来判断当前节点是否是静态节点,此外,会遍历当前节点的所有子节点,如果子节点是非静态节点,那么当前节点也是非静态节点。

generate generate 会将 AST 转化成 render funtion 字符串,最终得到 render 的字符串以及 staticRenderFns 字符串。

数据状态更新时的差异 diff 及 patch 机制

对 model 进行操作对时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。最终是将新产生的 VNode 节点与老 VNode 进行一个 patch 的过程,比对得出「差异」,最终将这些「差异」更新到视图上。

nodeOps 对象做适配,根据 platform 区分不同平台来执行当前平台对应的API,而对外则是提供了一致的接口,供 Virtual DOM 来调用。

一些 api

insert 用来在 parent 这个父节点下插入一个子节点,如果指定了 ref 则插入到 ref 这个子节点前面。

patch

diff 算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有 O(n),是一种相当高效的算法,如下图。

diff

samevnode sameVnode 其实很简单,只有当 key、 tag、 isComment(是否为注释节点)、 data同时定义(或不定义),同时满足当标签类型为 input 的时候 type 相同(某些浏览器不支持动态修改<input>类型,所以他们被视为不同类型)即可。

patchvnode

首先我们定义 oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 分别是新老两个 VNode 的两边的索引,同时 oldStartVnode、newStartVnode、oldEndVnode 以及 newEndVnode 分别指向这几个索引对应的 VNode 节点。

接下来是一个 while 循环,在这过程中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 会逐渐向中间靠拢。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

原理图

dep 的实现 是起到桥梁

  • data 和 prop 会通过 defineReactive 把对象变成响应式的, 内部会持有dep 的实例, 当我们访问到数据的时候会触发 depend 收集依赖, 收集当前正在计算的 watcher ,也就是dep.target,subscribe 订阅这个变化。

  • computed watcher 特殊watcher, 持有dep 实例, 会调用depend, 收集起来, 进行计算, 触发notify 进行更新。

  • user watcher可以理解为用户自定义的watcher, 可以观察data 和computed, 就会通知dep,调用update 方法,然后在执行run ,执行用户自定义的回调函数

  • render watcher 组件$mount,会创建唯一render watcher 会访问到data、 computed , 订阅计算属性的变化,会触发render watcher 的update, 执行run 方法 在执行updateComponent

最后更新于

这有帮助吗?