0
点赞
收藏
分享

微信扫一扫

浅读Vue源码

工程与房产肖律师 2022-03-21 阅读 72
前端vue.js

vue三大核心系统
Compiler模块:编译模板系统;
Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
Reactivity模块:响应式系统;
渲染系统,该模块主要包含三个功能:
功能一:h函数,用于返回一个VNode对象;
功能二:mount函数,用于将VNode挂载到DOM上;
功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
h函数的实现:
直接返回一个VNode对象即可
在这里插入图片描述
Mount函数 – 挂载VNode
第一步:根据tag,创建HTML元素,并且存储
到vnode的el中;
第二步:处理props属性
如果以on开头,那么监听事件;
普通属性直接通过 setAttribute 添加即可;
第三步:处理子节点
如果是字符串节点,那么直接设置textContent;
如果是数组节点,那么遍历调用 mount 函数;

在这里插入图片描述

Patch函数 – 对比两个VNode(diff算法)
patch函数的实现,分为两种情况
n1和n2是不同类型的节点:
找到n1的el父节点,删除原来的n1节点的el; p 挂载n2节点到n1的el父节点上;
n1和n2节点是相同的节点:
处理props的情况
先将新节点的props全部挂载到el上;
判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
处理children的情况
如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren; ü 如果新节点不同一个字符串类型:
旧节点是一个字符串类型
将el的textContent设置为空字符串;
就节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
旧节点也是一个数组类型
取出数组的最小长度;
遍历所有的节点,新节点和旧节点进行path操作;
如果新节点的length更长,那么剩余的新节点进行挂载操作;
如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
在这里插入图片描述
依赖收集系统
构建一个depends类,里面有依赖收集的set集合(不用数组是因为set不会重复添加副作用函数),还有一个添加副作用函数的函数addEffet。还有一个notify,用于执行set集合中的函数。

class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  addEffect(effect) {
    this.subscribers.add(effect);
  }

  notify() {
    this.subscribers.forEach(effect => {
      effect();
    })
  }
}


const info = {counter: 100};

const dep = new Dep();

function doubleCounter() {
  console.log(info.counter * 2);
}

function powerCounter() {
  console.log(info.counter * info.counter);
}

dep.addEffect(doubleCounter);
dep.addEffect(powerCounter);

info.counter++;
dep.notify();

改进·
在这里插入图片描述
depend方法为添加副作用函数
声明全局变量,有watcheffet函数用于执行depend方法,并且可以设置立即执行(effet())
问题:现在如果被监听的info对象的name属性改变了
那么notify会执行所以的副作用函数,怎么做到执行对应的副作用函数呢?
所以我们需要很多dep对象,那么需要一种数据结构进行管理
对象和map和weakmap的区别
对象的键必须是字符串,map的键可以是任意类型的,Map 是为了解决对象中的 key 只能为字符串的缺陷,weakmap和map的区别是Map 的键可以是任意类型,WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键。 WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。
正确的收集依赖
每个属性对应一个dep对象

const targetMap = new WeakMap();
//target是{counter: 100, name: "why"},key是counter和name
function getDep(target, key) {
  // 1.根据对象(target)取出对应的Map对象,weakmap里存的键是{counter: 100, name: "why"},值是new Map()
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  // 2.取出具体的dep对象,map里键是counter和name等属性,值是new Dep()对象。
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }
  return dep;
}

怎么做到自动收集依赖?
数据劫持

function reactive(raw) {
  Object.keys(raw).forEach(key => {
    const dep = getDep(raw, key);
    let value = raw[key];

    Object.defineProperty(raw, key, {
      get() {
        dep.depend();
        return value;
      },
      set(newValue) {
        if (value !== newValue) {
          value = newValue;
          dep.notify();
        }
      }
    })
  })

  return raw;
}


// 测试代码
const info = reactive({counter: 100, name: "why"});
const foo = reactive({height: 1.88});

副作用函数
下面代码只是改变height属性,所以只有第四个watchEffect执行。

// watchEffect1
watchEffect(function () {
  console.log("effect1:", info.counter * 2, info.name);
})

// watchEffect2
watchEffect(function () {
  console.log("effect2:", info.counter * info.counter);
})

// watchEffect3
watchEffect(function () {
  console.log("effect3:", info.counter + 10, info.name);
})

watchEffect(function () {
  console.log("effect4:", foo.height);
})

// info.counter++;
// info.name = "why";

foo.height = 2;

Vue2响应式原理就是Object.defineProperty结合观察者模式(发布订阅)。
总共分为三步骤:
init 阶段: VUE 的 data的属性都会被reactive化,也就是加上 setter/getter函数
其中这里的Dep就是一个观察者类,每一个data的属性都会有一个dep对象。当getter调用的时候,去dep里注册函数,
至于注册了什么函数,我们等会再说。
setter的时候,就是去通知执行刚刚注册的函数。

mount 阶段:
mount 阶段的时候,会创建一个Watcher类的对象。这个Watcher实际上是连接Vue组件与Dep的桥梁。
每一个Watcher对应一个vue component。
这里可以看出new Watcher的时候,constructor 里的this.getter.call(vm, vm)函数会被执行。getter就是updateComponent。这个函数会调用组件的render函数来更新重新渲染。

mountComponent(vm: Component, el: ?Element, ...) {
    vm.$el = el

    ...

    updateComponent = () => {
      vm._update(vm._render(), ...)
    }

    new Watcher(vm, updateComponent, ...)
    ...
}

class Watcher {
  getter: Function;

  // 代码经过简化
  constructor(vm: Component, expOrFn: string | Function, ...) {
    ...
    this.getter = expOrFn
    Dep.target = this                      // 注意这里将当前的Watcher赋值给了Dep.target
    this.value = this.getter.call(vm, vm)  // 调用组件的更新函数
    ...
  }
}

而render函数里,会访问data的属性,比如

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

此时会去调用这个属性blogTitle的getter函数,即:

// getter函数
get: function reactiveGetter () {
    ....
    dep.depend()
    return value
    ....
 },

// dep的depend函数
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}

在depend的函数里,Dep.target就是watcher本身(我们在class Watch里讲过,不记得可以往上第三段代码),这里做的事情就是给blogTitle注册了Watcher这个对象。这样每次render一个vue 组件的时候,如果这个组件用到了blogTitle,那么这个组件相对应的Watcher对象都会被注册到blogTitle的Dep中。
这个过程就叫做依赖收集。
收集完所有依赖blogTitle属性的组件所对应的Watcher之后,当它发生改变的时候,就会去通知Watcher更新关联的组件。
3、更新阶段:
当blogTitle 发生改变的时候,就去调用Dep的notify函数,然后通知所有的Watcher调用dep
的update函数更新。
在这里插入图片描述
每个组件对应一个watcher,当某个组件内使用了响应式变量以后,响应式变量都有一个dep类会收集组件的watcher,当组件发生变化以后dep会调用组件的watcher里的getter方法进行对应组件的更新,
也就是组件初始化的时候,先给每一个Data属性都注册getter,setter,也就是reactive化。然后再new 一个自己的Watcher对象,此时watcher会立即调用组件的render函数去生成虚拟DOM。在调用render的时候,就会需要用到data的属性值,此时会触发getter函数,将当前的Watcher函数注册进sub里。
当data属性发生改变之后,就会遍历sub里所有的watcher对象,通知它们去重新渲染组件。

举报

相关推荐

0 条评论