目录

  • 框架设计概览
  • 响应式系统
  • 渲染器
  • 组件化
  • 编译器
  • 服务端渲染 

第一篇 框架设计概览

第1章 权衡的艺术

1.1.命令式和声明式
声明式(注重结果)和命令式(注重过程)。
Vue封装了命令式的过程,对外暴露出了声明式的结果。
1.2.性能和可维护性的权衡
命令式(简单js操作)的性能优于声明式,命令式可维护性差于声明式。
1.3.虚拟DOM的性能到底如何
image.png
image.png  
1.4.运行时和编译时
参考:读懂vue源码必懂什么叫运行时和编译时_vue 运行时编译-CSDN博客
image.png

第2章 框架设计的核心要素

2.1.提升用户的开发体验
提供友好的错误提示(console.warn),有助于开发者快速定位问题;
自定义console打印值initCustomFormatter。
2.2.控制框架代码的体积
区分开发环境、生产环境,减轻生产环境代码体积。
2.3.框架要做到良好的Tree-Shaking
消除那些永远不会执行的代码;想要实现Tree-Shaking,模块必须是ES Module。
javascript中的esm - 知乎 (zhihu.com)
执行rollup会自动把一些无用函数删除;一些引用的函数,可能也没有什么作用的不会自动删除,通过标记/#_PURE_/让rollup识别该函数可以删除。
2.4.框架应该输出怎样的构建产物

应对不同的使用场景,输出不同的产物

  • 场景一:script直接引入
    方案一:需要输出一个立即执行的函数表达式(在rollup.js中配置format:iife来输出这种形式的资源)
    方案二:输出ESM格式资源(使用时sctipt标签内写type=”module”,rollup.js输出格式配置为format:esm) 
  • 场景二:通过require引入资源
    const Vue=require('vue')
    解决方案:在rollup.config.js中设置format:cjs

2.5.特性开关
可以选择关闭原有框架的特性,减少资源体积;也可以在框架升级做兼容时,用户通过特性开关可以取消兼容,减少兼容这部分的代码逻辑。
__VUE_OPTIONS_API__ 

2.6.错误处理
 callWithErrorHandling统一处理错误,避免过多的try catch
用户可以自定义统一的错误处理函数
vue.js中也就可以注册统一的处理函数:

const app=createApp(app)
app.confing.errorHandler=()=>{
}

2.7.良好的TypeScript类型支持
runtime-core/arc/apiDefineComponent.ts,为类型支持服务。

第3章 Vue.js 3的设计思路

3.1.声明式地描述UI
使用与HTML标签一致的方式来描述属性,例如<div></div>;
使用与HTML标签一致的形式描述属性:<div id="app"></div>;
使用:或v-bind来描述动态绑定的属性,例如<div :id="dynamicId"></div>;
使用或v-on来描述事件,例如点击事件<div @click="handler"></div>;
使用与HTML标签一致的方式来描述层级结构,例如:
<div><span></span></div>;
虚拟DOM,用js对象的方式来描述UI
3.2.初始渲染器
渲染器的作用:把虚拟DOM渲染为真实DOM

// vnode:虚拟DOM对象;container:一个真实的dom元素,作为挂载点
function renderer(vnode,container){
    // 使用 vnode.tag 作为标签名称创建 DOM元素        
    const el = document.createElement(vnode.tag)    
    // 遍历 vnode.props,将属性、事件添加到 DOM元素    
    for (const key in vnode.props) {    
      if (/^on/.test(key)) {    
  // 如果 key 以 on 开头,说明它是事件
      el.addEventListener(    
  key.substr(2).toLowerCase(),// 事件名称 onclick ---> click
      vnode.props[key] // 事件处理函数    
      )    
    }    
    // 处理 children    
    if (typeof vnode.children === 'string') {    
    // 如果 children 是字符串,说明它是元素的文本子节点    el.appendChild(document.createTextNode(vnode.children))    
    } else if (Array.isArray(vnode.children)) {    
    // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点    
    vnode.children.forEach(child => renderer(child, el))
}

// 将元素添加到挂载点下
    container.appendChild(el)    
}

思路

  • 创建元素: 把vnode.tag作为标签名称来创建DOM元素。
  • 为元素添加属性和事件: 遍历vnode.props对象,如果keyon字符开头,说明它是一个事件,把字符on截取掉后再调用toLowerCase函数将事件名称小写化,最终得到合法的事件名称,例如onClick会变成click,最后调用addEventListener绑定事件处理函数。
  • 处理children: 如果chidlren是一个数组,就递归地调用renderer继续渲染,注意,此时我们要把刚刚创建的元素作为挂载点(父节点);如果children是字符串,则使用createTextNode函数创建一个文本节点,并将其添加到新创建的元素内。

3.3.组件的本质

组件就是一组DOM元素的封装

vue.js中有状态组件是使用对象结构来表达的。

有状态组件:带有数据和逻辑的组件,它们存储组件内部状态和方法。有状态组件通常用于处理动态数据,并使用 reactive 响应式系统处理状态更新。
无状态组件:纯粹的渲染组件,不包含内部状态,仅通过 props 获取数据。因为它们没有状态和逻辑,所以它们比有状态组件更快,因为它们可以被 Vue 进行优化。

//组件内容
const MyComponent={
    reader(){
        return {
            tag:'div',
            props:{
                onClick:()=>alert('hello')
            },
            children:'click me'
        }
    }
}
//定义虚拟dom
const vNode={
  'tag':MyComponent
}
//渲染
const subtree = vnode.tag.reader()
readerer(subtree,container)

3.4.模板的工作原理

编译器:将模板翻译为渲染函数
//模板
<div @click="handler">
    click me
</div>

//编译器将他转化成渲染函数
render(){
    return h('div',{onClick:handler},'click me')
}

3.5.Vue.js是各个模块组成的有机整体

组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟DOM的设计决定的。编译器和渲染器之间是存在信息交流,互相配合提升性能,交流的媒介是虚拟DOM对象。

第二篇 响应系统

第4章 响应系统的作用与实现

4.1.响应式数据与副作用函数

副作用函数:函数的执行会直接或间接影响其他函数的执行,即为函数产生了副作用。(例如一个函数修改了全局变量,这其实也是一个副作用。)

4.2.响应式数据的基本实现

定义一个副作用函数,通过proxy监听属性的读写操作,
get时,将对应副作用函数放入桶(数据结构为set)中;
set时从桶中取出副作用函数并执行,实现数据响应式。

4.3.设计一个完善的响应系统

1.不依赖副作用函数的名字:定义一个全局变量activeEffect存储被注册的副作用函数;
2.将副作用函数与被操作的目标字段之间建立明确关系,通过树型结构明确关系,用WeakMap创建桶。
WeakMap 由target(操作对象)--->Map构成;
Map由key(操作对象的key)和set(存储副作用函数)构成

4.4.分支切换与cleanup

分支切换指某个值改变时,代码执行的分支会跟着改变,比如三元表达式。分支切换可能会产生遗留的副作用函数,导致不必要的更新。
clearup是为了将构造函数从所有与之关联的依赖集合(set)中移除。

注意1:effectFun.deps存储的正是target->key所指向的set(存储副作用函数的集合),因此删除effectFun.deps中的effectFun正是删除桶中的effectFun)

注意2:在监听属性set方法时会遍历执行构造函数,前面删除又新增集合Set,会出现无限循环执行(语言规范:再调用forEach遍历Set时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时forEach遍历没有结束,那么该值会被重新访问),因此需要新创建一个Set再去遍历执行对应的副作用函数。

4.5.嵌套的effect和effect栈

场景:组件发生嵌套。
问题:内层的副作用函数会覆盖activeEffect的值,并且不会恢复。
解决方案:定义一个副作用函数栈effectStack,执行时压入栈顶,执行完毕后弹出,始终让activeEffect指向栈顶。

4.6.避免无限递归循环

场景:当副作用函数既包含读取,又设置当前监听对象的值时。
问题:副作用函数在执行中,就要开始下一次执行,导致无限递归的调用自己,于是就产生了栈溢出。
解决方案:在trigger触发时判断当前触发的函数是否与
activeEffect相同,若相同则不执行。

var data = { text: '2343', exit: '12', flag: true };
// WeakMap为弱引用,不影响垃圾回收
var bucket = new WeakMap();
// 存储当前的副作用函数
let activeEffect = null;
// 将副作用函数和当前target、key绑定,放至桶中
const track = (target, key) => {
  if (!activeEffect) return;
  // target-->key-->effect
  let bucTat = bucket.get(target);
  if (!bucTat) {
    bucket.set(target, (bucTat = new Map()));
  }
  let bucKey = bucTat.get(key);
  if (!bucKey) {
    bucTat.set(key, (bucKey = new Set()));
  }
  bucKey.add(activeEffect);
  // 为了后续避免副作用函数不必要的更新
  activeEffect.depts.push(bucKey);
};
// 执行副作用函数
const trigger = (target, key) => {
  const bucTar = bucket.get(target);
  if (!bucTar) return;
  const bucKey = bucTar.get(key);
  // 避免Set集合(清除副作用函数、新增副作用函数)、forEach导致的死循环
  const effects = new Set(bucKey);
  effects &&
    effects.forEach(fn => {
      // 避免副作用函数无限递归调用自身,造成死循环问题,比如 foo++
      if (fn == activeEffect) return;
      fn();
    });
};
// 代理
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
    return true;
  },
});
// 清除副作用函数上一次建立的相应联系
const cleanUp = effectFn => {
  for (let i = 0; i < effectFn.depts.length; i++) {
    const depts = effectFn.depts[i];
    depts.delete(effectFn);
  }
  effectFn.depts.length = 0;
};
const effect = val => {
  const effectFn = () => {
    cleanUp(effectFn);
    activeEffect = effectFn;
    val();
  };
  effectFn.depts = [];
  effectFn();
};

onMounted(() => {
  effect(() => {
    document.getElementById('odiv').innerHTML = obj.flag ? obj.text : obj.exit;
  });
});
setTimeout(() => {
  obj.flag = false;
  obj.text = '111';
}, 3000);

4.7.调度执行

可调度:当trigger触发副作用函数重新执行时,有能力决定副作用函数的执行时机、次数以及方式。

实现方法:为effect函数设计一个选项参数options,允许用户指定调度器

4.8.计算属性computed与lazy

  • 副作用函数不立即执行:不需要副作用函数立即执行的情况下,在computed函数下,创建一个lazy的effect(在options里面增加:lazy:true),在effect中判断lazy,如果为true,返回副作用函数执行过的结果,否则直接执行,就实现了副作用函数不立即执行。
  • 对值进行缓存:避免无效的重新计算,定义value(存储上一次计算的值),dirty(判断是否要重新计算,dirty为true表示以来的值有变化,要重新计算),当依赖的值发生变化时,在刚刚定义的effect中增加调度函数,设置dirty为true,计算之后,将dirty设置为false
  • 当在另外一个effect中读取读取计算属性的值时,修改依赖的值不会重新渲染:当读取计算属性的值时,手动调用track函数进行追踪,当依赖的响应式数据发生变化时,手动调用trigger函数触发响应式。

    function computed(getter) {    
     let value=''    
     let dirty = true    
    
     const effectFn = effect(getter,{    
      lazy: true,    
      scheduler() {    
       if (!dirty) {    
         dirty = true    
         // 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应    
         trigger(obj,'value')    
        }
     })    
     const obj ={    
         get value() {    
           if (dirty){    
               value = effectFn()    
               dirty = false
           }    
           // 当读取 value 时,手动调用 track 函数进行追踪              track(obj,'value')    
           return value    
        }    
     }
    return obj    
    }

4.9.watch的实现原理
原理:观测响应数据,当数据发生变化时通知并执行相应的回调函数。

  watch(source,cb){
      let getter
      //watch第一个函数为getter函数的情况
      if(typeof source=='function'){
        getter=source
      } else {
        //通过读取对象的值实现副作用函数和对象绑定,traverse函数是为了递归读取操作,用于保证绑定对象全部的值
        getter=()=>traverse(source)
      }
      //定义旧值与新值
      let oldValue,newValue
      const effectFn=effect(
        ()=>getter(),
        {
          lazy:true,
          scheduler(){
            newValue=effectFn()
            cd(newValue,oldValue)
            oldValue=newValue
          }
        }
      )
      //第一次执行得到的值
      oldValue=effectFn()
    },
    //traverse函数是为了递归读取操作
    traverse(value,seen=new Set()){
      //seen避免死循环
      if(typeof value!=='object' || value===null || seen.has(value)) return
      seen.add(value)
      for(const k in value){
        traverse(value[k],seen)
      }
      return value;
    }

4.10.立即执行的watch与回调执行时机

原理:接收immediate参数,将schedule调度函数独立出来,判断immediatetrue时立即执行job,触发回调执行,立即执行oldValueundefined

const watchFunc = (source, cb, options = {}) => {
  // 接收参数options,用于传immediate等参数
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else {
    getter = () => traverse(source);
  }
  let newVal;
  let oldVal;
  // 将schedule调度函数独立出来
  const job = () => {
    newVal = effectFunc();
    cb(newVal, oldVal);
    oldVal = newVal;
  };
  const effectFunc = effect(getter, {
    lazy: true,
    schedule: job,
  });
  // immediate为true时立即执行job,触发回调执行
  if (options.immediate) {
    job();
  } else {
    oldVal = effectFunc();
  }
};

4.11.过期的副作用

竞态问题:由于响应时间不同,可能先请求的后返回结果,导致最终的结果是先请求的。
image.png
watch函数的回调函数接收第三个参数onInvalidate,在调用cb回调函数之前,先调用过期函数。


薇薇
298 声望24 粉丝

路漫漫其修远兮,吾将上下而求索