image-20220214140159456

先放一张store的代码结构图

image-20210929161132720

vuex初始化

Vuex 存在一个静态的 install 方法,在beforeCreate混入了vuexInit,我们只看关键逻辑

1
2
3
4
5
6
7
8
9
10
11
12

function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}

通过声明周期函数,将store挂载到每一个组件上

Store实例化

我们把 Store 的实例化过程拆成 3 个部分,分别是

  • 初始化模块
  • 安装模块
  • 初始化 store._vm

初始化模块

构建module树

其实就是处理用户输入的配置,重点在于通过module层级建立联系,如下图所示

image-20210929161649181

1
this._modules = new ModuleCollection(options)

ModuleCollection 实例化的过程就是执行了 register 方法,register方法递归遍历所有module,生成module实例,并通过_children属性与path和建立下级关系

建立关联的代码很精妙,建议结合源码回忆

安装模块

installModule(this, state, [], this._modules.root)

递归执行,它的目标就是对各个模块中的 state、getters、mutations、actions 做初始化工作

installModule函数主要做了下述几个事情:

挂载子module state到rootState

1
2
3
4
5
6
7
f (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}

根据namespace配置构建module映射

为了根据命名空间快速查找到对应的module

1
2
3
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}

构造模块上下文环境

image-20210929171104463

构造了一个模块上下文环境:保证模块内两种方法的正确运行和两种数据的正常访问

例如 模块内的dispatch(“B”) 会映射到 rootStore.dispatch(“A/B”)

注册mutation action getter

其实就是把三个属性对应用户输入挂载到rootStore上去,这里也用到了模块上下文

举一个mutation的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
// 在root module下 注册了 命名空间 mutation 下同
registerMutation(store, namespacedType, mutation, local)
})

function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
// 数组 说明 同一 type 的 _mutations 可以对应多个方法
entry.push(function wrappedMutationHandler(payload) {
// store 对应模块的store
handler.call(store, local.state, payload)
})
}

从这里就可以看出,所有的mutations都是挂载在根store的_mutations中,并且同名mutation不会覆盖,而且推入一个队列,顺序执行,下图是实际的代码

image-20210929172730903

可以看出来,开启命名空间实际上就是在mutation事件名前拼接了一个路径

这个地方也解除了我对vuex官方文档一段话的不解:

1
2
3
默认情况下,模块内部的 action、mutation 和 getter 是注册在**全局命名空间**的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 `namespaced: true` 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

初始化 store._vm

Store 实例化的最后一步,就是执行初始化 store._vm 的逻辑,它的入口代码是:

1
resetStoreVM(this, state)

resetStoreVM 的作用实际上是想建立 gettersstate 的联系

利用vue的data和computed机制实现联系,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})

// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent

本章将针对 HTTP 协议结构进行讲解

HTTP 协议用于客户端和服务器端之间的通信

HTTP 协议和 TCP/IP 协议族内的其他众多的协议相同,用于客户端和服务器之间的通信。
请求访问文本或图像等资源的一端称为客户端,而提供资源响应的一端称为服务器端。

通过请求和响应的交换达成通信

HTTP 协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有 接收到请求之前不会发送响应。
下面,我们来看一个具体的示例。
实例1

实例2
图:请求报文的构成

实例3
图:响应报文的构成

HTTP 是不保存状态的协议

HTTP 是一种不保存状态,即无状态(stateless)协议。HTTP 协议自 身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。
这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设 计成如此简单的。

HTTP/1.1 虽然是无状态协议,但为了实现期望的保持状态功能,于 是引入了 Cookie 技术。

请求 URI 定位资源

HTTP 协议使用 URI 定位互联网上的资源。
URI 定位资源
图:以 http://hackr.jp/index.htm 作为请求的例子
除此之外,如果不是访问特定资源而是对服务器本身发起请求,可以 用一个 *来代替请求 URI。下面这个例子是查询 HTTP 服务器端支持 的 HTTP 方法种类。

OPTIONS* HTTP/1.1

告知服务器意图的 HTTP 方法

  • GET :获取资源
    GET 方法用来请求访问已被 URI 识别的资源。指定的资源经服务器 端解析后返回响应内容。

  • POST:传输实体主体
    虽然用 GET 方法也可以传输实体的主体,但一般不用 GET 方法进行 传输,而是用 POST 方法。虽说 POST 的功能与 GET 很相似,但 POST 的主要目的并不是获取响应的主体内容。

  • PUT:传输文件
    PUT 方法用来传输文件。就像 FTP 协议的文件上传一样,要求在请 求报文的主体中包含文件内容,然后保存到请求 URI 指定的位置。
    鉴于 HTTP/1.1 的 PUT 方法自身不带验证机制,任何人都可以 上传文件 , 存在安全性问题,因此一般的 Web 网站不使用该方法。若 配合 Web 应用程序的验证机制,或架构设计采用 REST(REpresentational State Transfer,表征状态转移)标准的同类 Web 网站,就可能会开放使用 PUT 方法。

  • HEAD:获得报文首部
    HEAD 方法和 GET 方法一样,只是不返回报文主体部分。用于确认 URI 的有效性及资源更新的日期时间等。

  • DELETE:删除文件
    DELETE 方法用来删除文件,是与 PUT 相反的方法。DELETE 方法按 请求 URI 删除指定的资源。
    HTTP/1.1 的 DELETE 方法本身和 PUT 方法一样不带验证机 制,所以一般的 Web 网站也不使用 DELETE 方法。当配合 Web 应用 程序的验证机制,或遵守 REST 标准时还是有可能会开放使用的

  • OPTIONS:询问支持的方法
    OPTIONS 方法用来查询针对请求 URI 指定的资源支持的方法
    OPTIONS

  • TRACE:追踪路径
    TRACE 方法是让 Web 服务器端将之前的请求通信环回给客户端的方 法。
    客户端通过 TRACE 方法可以查询发送出去的请求是怎样被加工修改 / 篡改的。这是因为,请求想要连接到源目标服务器可能会通过代理 中转,TRACE 方法就是用来确认连接过程中发生的一系列操作。 但是,TRACE 方法本来就不怎么常用,再加上它容易引发 XST(Cross-Site Tracing,跨站追踪)攻击,通常就更不会用到了。

  • CONNECT:要求用隧道协议连接代理
    CONNECT 方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行 TCP 通信。主要使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
    HTTP/1.0 和 HTTP/1.1 支持的方法
    HTTP/1.0 和 HTTP/1.1 支持的方法

持久连接节省通信量

HTTP 协议的初始版本中,每进行一次 HTTP 通信就要断开一次 TCP 连接。
持久连接节省通信量
使用浏览器浏览一个包含多张图片的 HTML 页面时,在发送请求访问 HTML 页面资源的同时,也会请求该 HTML 页面里包含的 其他资源。因此,每次的请求都会造成无谓的 TCP 连接建立和断开,增加通信量的开销。
持久连接节省通信量

持久连接

为解决上述 TCP 连接的问题,HTTP/1.1 和一部分的 HTTP/1.0 想出了持久连接(HTTP Persistent Connections,也称为 HTTP keep-alive 或 HTTP connection reuse)的方法。持久连接的特点是,只要任意一端 没有明确提出断开连接,则保持 TCP 连接状态
持久连接节省通信量
图:持久连接旨在建立 1 次 TCP 连接后进行多次请求和响应的交互

持久连接的好处在于减少了 TCP 连接的重复建立和断开所造成的额 外开销,减轻了服务器端的负载
在 HTTP/1.1 中,所有的连接默认都是持久连接

管线化

持久连接使得多数请求以管线化(pipelining)方式发送成为可能
从前发送请求后需等待并收到响应,才能发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。
持久连接节省通信量
图:不等待响应,直接发送下一个请求

保留无状态协议这个特征的同时又要解决类似的矛盾问题,于是引入了 Cookie 技术。Cookie 技术通过在请求和响应报文中写入 Cookie 信 息来控制客户端的状态。
Cookie 会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie 的 首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器 发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。
服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。
持久连接节省通信量

简易流程

vue-router的整体流程不难理解,难点在于一些功能的实现。

image-20220213105205628

首先初始化vue-router实例,然后vue.use,再然后根vue初始化,作为配置传入

  • vue.use vue-router

  • Vue-router install

    • 混入,根组件保存router和route属性,通过混入beforeCreated 子组件递归持有根组件(Vue)

      • Object.defineProperty(Vue.prototype, '$route', {
          // 混入beforeCreated 保证所有组件都能访问到 _routerRoot vue根实例
          get () { return this._routerRoot._route }
        })
        
        1
        2
        3
        4
          
        - ```
        // 非根组件递归持有根组件Vue
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
        > 所有的组件都持有_RouterRoot属性(Vue),Vue根实例持有_route和router属性
    • vue-router 初始化

      • 首先生成实例,执行constructor
        • 生成matcher,createMatcher
          • 根据routes创建一个路由映射表 {pathList, pathMap, nameMap}
          • 提供match方法
        • 根据mode,初始化相应history
      • 执行init方法-vue根实例初始化的时候执行
        • history.transitionTo 根据当前路径渲染组件
          • const route = this.router.match(location, this.current) 匹配路由
        • History.listen 定义 history.cb 在多种情况下更新 vue._route,保证其正确性,方便被watch
    • Vue.util.defineReactive(this, _route, this._router.history.current) 定义响应式

    • registerInstance router-view相关 主要是在route.instance保存当前rv实例

    • Object.defineProperty(Vue.prototype, $router$route)方便组件内使用

    • Vue.component RouterView, RouterLink

    • 定义合并策略

重要部分介绍

mather介绍

1
2
3
4
function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher
  • createRouteMap 根据传入的routes配置,创建一个路由映射表 {pathList, pathMap, nameMap}

    pathList 存储所有的 path

    pathMap 表示一个 pathRouteRecord 的映射关系

    nameMap 表示 nameRouteRecord 的映射关系

    • 遍历routes数组,调用addRouteRecord

      • 根据routes创建相关映射表,如果存在children,则递归处理,保证每一个路由地址都有一个与之对应的routeRecord,这条记录还会包含子路由所有层级的父record记录

      • RouteRecord
        const record: RouteRecord = {
            path: normalizedPath,
            // path 解析成一个正则表达式
            regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
            components: route.components || { default: route.component },
            alias: route.alias
              ? typeof route.alias === 'string'
                ? [route.alias]
                : route.alias
              : [],
            instances: {}, // 表示rv组件的实例
            enteredCbs: {},
            name,
            parent, // 表示父的 RouteRecord 只能向上寻找
            matchAs,
            redirect: route.redirect,
            beforeEnter: route.beforeEnter,
            meta: route.meta || {},
            props:
              route.props == null
                ? {}
                : route.components
                  ? route.props
                  : { default: route.props }
          }
        
        1
        2
        3
        4
        5

        - 保证*匹配符保持在最后

        - match方法解析 匹配出对应的record,然后通过`createRoute`创建`Route`

    function match (
    raw: RawLocation(string | location),
    currentRoute?: Route,
    redirectedFrom?: Location
    ): Route

    1
    2
    3

    - createRoute函数, `createRoute` 可以根据 `record` 和 `location` 创建出来,最终返回的是一条 `Route` 路径

    export function createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: ?Location,
    router?: VueRouter
    ): Route {
    const stringifyQuery = router && router.options.stringifyQuery

    let query: any = location.query || {}
    try {
    query = clone(query)
    } catch (e) {}

    const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || ‘/‘,
    hash: location.hash || ‘’,
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
    }
    if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
    }
    return Object.freeze(route)
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    - `Route` 对象中有一个非常重要属性是 `matched`,它通过 `formatMatch(record)` 计算而来:

    ````
    function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
    const res = []
    while (record) {
    res.unshift(record)
    record = record.parent
    }
    return res
    }

    ````

    可以看它是通过 `record` 循环向上找 `parent`,直到找到最外层,并把所有的 `record` 都 push 到一个数组中,最终返回的就是 `record` 的数组,它记录了一条线路上的所有 `record`。==`matched` 属性非常有用,它为之后渲染组件提供了依据==。

    #### 路径切换 history.transitonTo

    - 点击 `router-link` 的时候,实际上最终会执行 `router.push`

    push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)
    }
    push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
    //
    // https://zhuanlan.zhihu.com/p/35036172
    pushHash(route.fullPath)
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
    }, onAbort)
    }

    1
    2
    3

    - 在history的初始化中,针对历史栈做了一个监听

    window.addEventListener(supportsPushState ? ‘popstate’ : ‘hashchange’….

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48

    之所以做监听,是为了用户在使用前进后退时,渲染正确的组件

    - Router-view

    当我们执行 `transitionTo` 来更改路由线路后,组件是如何重新渲染的呢

    - Router-Link

    ### 附录:源码重要类-类型注解

    - history类 src/history/*.js

    ````javascript
    router: Router
    base: string
    current: Route
    pending: ?Route
    cb: (r: Route) => void
    ready: boolean
    readyCbs: Array<Function>
    readyErrorCbs: Array<Function>
    errorCbs: Array<Function>
    listeners: Array<Function>
    cleanupListeners: Function

    // implemented by sub-classes
    +go: (n: number) => void
    +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
    +replace: (
    loc: RawLocation,
    onComplete?: Function,
    onAbort?: Function
    ) => void
    +ensureURL: (push?: boolean) => void
    +getCurrentLocation: () => string
    +setupListeners: Function
    ````

    - matcher类 src/create-matcher.js

    ```javascript
    export type Matcher = {
    match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
    addRoutes: (routes: Array<RouteConfig>) => void;
    addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
    getRoutes: () => Array<RouteRecord>;
    };
  • createRouteMap src/creat-route-map

    • createRouteMap 函数的目标是把用户的路由配置转换成一张路由映射表,它包含 3 个部分,
      • pathList 存储所有的 path
      • pathMap 表示一个 pathRouteRecord 的映射关系,
      • nameMap 表示 nameRouteRecord 的映射关系。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    export function createRouteMap (
    routes: Array<RouteConfig>,
    oldPathList?: Array<string>,
    oldPathMap?: Dictionary<RouteRecord>,
    oldNameMap?: Dictionary<RouteRecord>,
    parentRoute?: RouteRecord
    ): {
    pathList: Array<string>,
    pathMap: Dictionary<RouteRecord>,
    nameMap: Dictionary<RouteRecord>
    } {...}
    • addRouterRecord 生成并添加一条routerRecord

      1
      2
      3
      4
      5
      6
      7
      8
      function addRouteRecord (
      pathList: Array<string>,
      pathMap: Dictionary<RouteRecord>,
      nameMap: Dictionary<RouteRecord>,
      route: RouteConfig,
      parent?: RouteRecord,
      matchAs?: string
      )
  • Location RawLocation

    • Vue-Router 中定义的 Location 数据结构和浏览器提供的 window.location 部分结构有点类似,它们都是对 url 的结构化描述。举个例子:/abc?foo=bar&baz=qux#hello,它的 path/abcquery{foo:'bar',baz:'qux'}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    declare type Location = {
    _normalized?: boolean;
    name?: string;
    path?: string;
    hash?: string;
    query?: Dictionary<string>;
    params?: Dictionary<string>;
    append?: boolean;
    replace?: boolean;
    }
    declare type RawLocation = string | Location
  • Route

    • Route 表示的是路由中的一条线路,它除了描述了类似 Loctaionpathqueryhash 这些概念,还有 matched 表示匹配到的所有的 RouteRecordRoute 的其他属性我们之后会介绍。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    declare type Route = {
    path: string;
    name: ?string;
    hash: string;
    query: Dictionary<string>;
    params: Dictionary<string>;
    fullPath: string;
    matched: Array<RouteRecord>;
    redirectedFrom?: string;
    meta?: any;
    }

    可以说location 经过了match之后变成了routerRecord,routerRecord经过_createRoute变成了route

    这样比较好理解

  • RouterRecord

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    declare type RouteRecord = {
    path: string;
    alias: Array<string>;
    regex: RouteRegExp;
    components: Dictionary<any>;
    instances: Dictionary<any>;
    enteredCbs: Dictionary<Array<Function>>;
    name: ?string;
    parent: ?RouteRecord;
    redirect: ?RedirectOption;
    matchAs: ?string;
    beforeEnter: ?NavigationGuard;
    meta: any;
    props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
    }

其他重要内容

  • 当我们执行 transitionTo 来更改路由线路后,组件是如何重新渲染的呢

    由于我们把根 Vue 实例的 _route 属性定义成响应式的,我们在每个 <router-view> 执行 render 函数的时候,都会访问 parent.$route,如我们之前分析会访问 this._routerRoot._route,触发了它的 getter,相当于 <router-view> 对它有依赖,然后再执行完 transitionTo 后,修改 app._route 的时候,又触发了setter,因此会通知 <router-view> 的渲染 watcher 更新,重新渲染组件。

  • 所有组件都是访问到的$router$router是怎么来的

    1.设置Vue根实例的_routerRoot属性为Vue根实例

    2.混入Vue生命周期,beforeCreate函数层层传递_routerRoot属性,是所有组件都可以通过_routerRoot访问到Vue根实例

    3.定义Vue的原型属性$route $router的getter方法

    1
    2
    3
    Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
    })
  • 非常经典的异步函数队列化执行的模式,这也就是为什么官方文档会说只有执行 next 方法来 resolve 这个钩子函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    function runQueue (queue, fn, cb) {
    const step = index => {
    if (index >= queue.length) {
    cb()
    } else {
    if (queue[index]) {
    fn(queue[index], () => {
    step(index + 1)
    })
    } else {
    step(index + 1)
    }
    }
    }
    step(0)
    }
    // 代表一个个hooks函数
    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]

    const iterator = (hook, next) => {
    console.log(hook);
    next()
    }
    runQueue(arr, iterator, () => {
    console.log("遍历完了");
    })
    // 1 2 3 4 5 6 7 8 9 遍历完了

为了理解 HTTP,我们有必要事先了解一下 TCP/IP 协议族

网络基础 TCP/IP

通常使用的网络(包括互联网)是在 TCP/IP 协议族的基础上运作 的。而 HTTP 属于它内部的一个子集。

TCP/IP 协议族

计算机与网络设备要相互通信,双方就必须基于相同的方法。比如, 如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通 信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之 间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为 协议(protocol)。

TCP/IP 是互联网相关的各类协议族的总称

TCP/IP 的分层管理

TCP/IP 协议族里重要的一点就是分层。TCP/IP 协议族按层次分别分 为以下 4 层:应用层、传输层、网络层和数据链路层。

  • 应用层
    应用层决定了向用户提供应用服务时通信的活动。
    TCP/IP 协议族内预存了各类通用的应用服务。比如,FTP(File Transfer Protocol,文件传输协议)和 DNS(Domain Name System,域 名系统)服务就是其中两类。 HTTP 协议也处于该层。
  • 传输层
    传输层对上层应用层,提供处于网络连接中的两台计算机之间的数据 传输。
    在传输层有两个性质不同的协议:TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Data Protocol,用户数据报 协议)。
  • 网络层(又名网络互连层)
    网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数 据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计 算机,并把数据包传送给对方。
    与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所 起的作用就是在众多的选项内选择一条传输路线。
  • 链路层(又名数据链路层,网络接口层)
    用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱 动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等 物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在 链路层的作用范围之内。

TCP/IP 通信传输流

通信传输流
利用 TCP/IP 协议族进行网络通信时,会通过分层顺序与对方进行通 信。发送端从应用层往下走,接收端则往应用层往上走。
我们用 HTTP 举例来说明,首先作为发送端的客户端在应用层 (HTTP 协议)发出一个想看某个 Web 页面的 HTTP 请求。 接着,为了传输方便,在传输层(TCP 协议)把从应用层处收到的数 据(HTTP 请求报文)进行分割,并在各个报文上打上标记序号及端 口号后转发给网络层。

在网络层(IP 协议),增加作为通信目的地的 MAC 地址后转发给链 路层。这样一来,发往网络的通信请求就准备齐全了。
接收端的服务器在链路层接收到数据,按序往上层发送,一直到应用 层。当传输到应用层,才能算真正接收到由客户端发送过来的 HTTP 请求。
封装
发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该 层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层 时会把对应的首部消去。
这种把数据信息包装起来的做法称为封装(encapsulate)。

与 HTTP 关系密切的协议 : IP、TCP 和 DNS

负责传输的 IP 协议

IP(Internet Protocol 网际协议)协议的作用是把各种数据包传送给对方。而要保证确实传送到对方那里,则需要满足各类条件。
其中两个重要的条件是

  • IP 地址
  • MAC 地址(Media Access Control Address)
    IP 地址指明了节点被分配到的地址,MAC 地址是指网卡所属的固定 地址。IP 地址可以和 MAC 地址进行配对。IP 地址可变换,但 MAC 地址基本上不会更改。

确保可靠性的 TCP 协议

按层次分,TCP 位于传输层,提供可靠的字节流服务。
所谓的字节流服务(Byte Stream Service)是指,为了方便传输,将大块数据分割成以报文段(segment)为单位的数据包进行管理。而可靠的传输服务是指,能够把数据准确可靠地传给对方。一言以蔽之, TCP 协议为了更容易传送大数据才把数据分割,而且 TCP 协议能够确认数据最终是否送达到对方。
为了准确无误地将数据送达目标处,TCP 协议采用了三次握手 (three-way handshaking)策略。
握手过程中使用了 TCP 的标志(flag) —— SYN(synchronize) 和 ACK(acknowledgement)。发送端首先发送一个带 SYN 标志的数据包给对方。接收端收到后, 回传一个带有 SYN/ACK 标志的数据包以示传达确认信息。最后,发送端再回传一个带 ACK 标志的数据包,代表“握手”结束。 若在握手过程中某个阶段莫名中断,TCP 协议会再次以相同的顺序发 送相同的数据包。
三次握手
除了上述三次握手,TCP 协议还有其他各种手段来保证通信的可靠性。

负责域名解析的 DNS 服务

DNS(Domain Name System)服务是和 HTTP 协议一样位于应用层的协议。它提供域名到 IP 地址之间的解析服务。
DNS 协议提供通过域名 查找 IP 地址,或逆向从 IP 地址反查域名的服务

各种协议与 HTTP 协议的关系

三次握手

URI 和 URL

与 URI(统一资源标识符)相比,我们更熟悉 URL(Uniform Resource Locator,统一资源定位符)。

统一资源标识符

URI 是 Uniform Resource Identifier 的缩写。RFC2396 分别对这 3 个单词进行了如下定义。

  • Uniform

规定统一的格式可方便处理多种不同类型的资源,而不用根据上下文环境来识别资源指定的访问方式。另外,加入新增的协议方案(如 http: 或 ftp:)也更容易。

  • Resource

资源的定义是“可标识的任何东西”。除了文档文件、图像或服务(例如当天的天气预报)等能够区别于其他类型的,全都可作为资源。另外,资源不仅可以是单一的,也可以是多数的集合体。

  • Identifier

表示可标识的对象。也称为标识符。

综上所述,URI 就是由某个协议方案表示的资源的定位标识符。协议方案是指访问资源所使用的协议类型名称。
采用 HTTP 协议时,协议方案就是 http。除此之外,还有 ftp、mailto、telnet、file 等。

URI 格式

表示指定的 URI,要使用涵盖全部必要信息的绝对 URI、绝对 URL 以及相对 URL。相对 URL,是指从浏览器中基本 URI 处指定的 URL, 形如 /image/logo.gif。
让我们先来了解一下绝对 URI 的格式

URI 格式

  • 协议方案
    使用 http: 或 https: 等协议方案名获取访问资源时要指定协议类型。不区分字母大小写,最后附一个冒号(:)。 也可使用 data: 或 javascript: 这类指定数据或脚本程序的方案名。
  • 登录信息(认证)
    指定用户名和密码作为从服务器端获取资源时必要的登录信息(身份 认证)。此项是可选项。
  • 服务器地址
    使用绝对 URI 必须指定待访问的服务器地址。地址可以是类似 hackr.jp 这种 DNS 可解析的名称,或是 192.168.1.1 这类 IPv4 地址名,还可以是 [0:0:0:0:0:0:0:1] 这样用方括号括起来的 IPv6 地址名。
  • 服务器端口号
    指定服务器连接的网络端口号。此项也是可选项,若用户省略则自动 使用默认端口号。
  • 带层次的文件路径
    指定服务器上的文件路径来定位特指的资源。这与 UNIX 系统的文件目录结构相似。
  • 查询字符串
    针对已指定的文件路径内的资源,可以使用查询字符串传入任意参数。此项可选。
  • 片段标识符
    使用片段标识符通常可标记出已获取资源中的子资源(文档内的某个位置)。但在 RFC 中并没有明确规定其使用方法。该项也为可选项。

文章只是简易的记录下源码里比较重要的节点,方便压缩信息,进行二次记忆。
在github中fork仓库里有当时学习的注释,建议结合黄轶的源码分析加源码加深记忆

构造函数创建对象

我们先使用构造函数创建一个对象:

1
2
3
4
5
6
function Person() {

}
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin

在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person。

很简单吧,接下来进入正题:

prototype

每个函数都有一个 prototype 属性,就是我们经常在各种例子中看到的那个 prototype ,比如:

1
2
3
4
5
6
7
8
9
10
function Person() {

}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin

那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?

其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。

那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。

让我们用一张图表示构造函数和实例原型之间的关系:

构造函数和实例原型的关系图

在这张图中我们用 Object.prototype 表示实例原型。

那么我们该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性:

proto

这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。

为了证明这一点,我们可以在火狐或者谷歌中输入:

1
2
3
4
5
function Person() {

}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

于是我们更新下关系图:

实例与实例原型的关系图

既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

constructor

指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。

为了验证这一点,我们可以尝试:

1
2
3
4
function Person() {

}
console.log(Person === Person.prototype.constructor); // true

所以再更新下关系图:

实例原型与构造函数的关系图

综上我们已经得出:

1
2
3
4
5
6
7
8
9
10
function Person() {

}

var person = new Person();

console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系:

实例与原型

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {

}

Person.prototype.name = 'Kevin';

var person = new Person();

person.name = 'Daisy';
console.log(person.name) // Daisy

delete person.name;
console.log(person.name) // Kevin

在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。

但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。

但是万一还没有找到呢?原型的原型又是什么呢?

原型的原型

在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

1
2
3
var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin

其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 proto 指向构造函数的 prototype ,所以我们再更新下关系图:

原型的原型关系图

原型链

那 Object.prototype 的原型呢?

null,我们可以打印:

1
console.log(Object.prototype.__proto__ === null) // true

然而 null 究竟代表了什么呢?

引用阮一峰老师的 《undefined与null的区别》 就是:

null 表示“没有对象”,即该处不应该有值。

所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。

所以查找属性的时候查到 Object.prototype 就可以停止查找了。

最后一张关系图也可以更新为:

原型链示意图

顺便还要说一下,图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

补充

最后,补充三点大家可能不会注意的地方:

constructor

首先是 constructor 属性,我们看个例子:

1
2
3
4
5
function Person() {

}
var person = new Person();
console.log(person.constructor === Person); // true

当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:

1
person.constructor === Person.prototype.constructor

proto

其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

真的是继承吗?

最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》 上册中的话,就是:

​ 继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

image-20220325102130556

本文简单的分析一下vue从初始化到完成渲染的流程,重点还是在于响应式的分析,生命周期函数简单带过。

以下代码均经过不同程度的精简

vue.init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Vue.prototype._init = function(options) {
vm.$options = mergeOptions( // 合并options
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
// ...
initLifecycle(vm) // 开始一系列的初始化
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') //执行 beforeCreate 钩子
initInjections(vm)
initState(vm) // observe
initProvide(vm)
callHook(vm, 'created') //执行 created 钩子
// ...
// 挂载dom
if (vm.$options.el) {
// 可以认为是运行了 mountComponent
vm.$mount(vm.$options.el)
}
}

其实上述代码已经包含了整个生命周期的行为,简要分析一下函数所做的事情

初始化到created

首先,将用户提供的options对象,父组件定义在子组件上的eventprops(子组件实例化时),vm原型方法,和Vue构造函数内置的选项合并成一个新的options对象,赋值给vm.$options
接下来,执行 3 个初始化方法:

  • initLifecycle(vm): 主要作用是确认组件的父子关系(定位非抽象父级)和初始化某些实例属性。找到父组件实例赋值给vm.$parent,将自己push给父组件的$children

  • image-20230308115942545

  • initEvents(vm): 主要作用是将父组件使用v-on@注册的自定义事件添加到子组件的私有属性vm._events中;

  • initRender(vm): 主要作用是初始化用来将render函数转为vnode的方法vm.$createElement。用户自定义的render函数的参数h就是vm.$createElement方法,它可以返回vnode。此阶段还会进行$attrs $listeners $slots $scopedSlots的处理

    等以上操作全部完成,就会执行beforeCreate钩子函数,此时用户可以在函数中通过this访问到vm.$parentvm.$createElement $attrs $listeners $slots $scopedSlots等有限的属性和方法。

    image-20220118205925251

  • 触发beforeCreate

  • initInjections(vm): 初始化inject,使得vm可以访问到对应的依赖;

  • initState(vm): 初始化会被使用到的状态,状态包括propsmethodsdatacomputedwatch五个选项。调用相应的init方法,使用vm.$options中提供的选项对这些状态进行初始化,其中initData方法会调用observe(data, true),实现对data中属性的监听,实际上是使用Object.defineProperty方法定义属性的gettersetter方法

    • Computed 和 watch 初始化 会创建computed-watch和user-watch
  • **initProvide(vm)**:初始化provide,使得vm可以为子组件提供依赖。

    这 3 个初始化方法先初始化inject,然后初始化props/data状态,最后初始化provide,这样做的目的是可以在props/data中使用inject内所注入的内容。等以上操作全部完成,就会执行created钩子函数,此时用户可以在函数中通过this访问到vm中的propsmethodsdatacomputedwatchinject等大部分属性和方法。

  • 触发created

beforeMouted到mouted

此阶段即是vm.$mount(vm.$options.el)过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
运行时版本:
Vue.prototype.$mount = function(el) { // 最初的定义
return mountComponent(this, query(el));
}
完整版:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el) { // 拓展编译后的
var options = this.$options;
if(!options.render) {
if(options.template) {
... //一些判断
} else if (el) { //传入的 el 选项不为空
options.template = getOuterHTML(el);
}

if (options.template) {
options.render = compileToFunctions(template, ...).render //将 template 编译成 render 函数
}
}
...
return mount.call(this, query(el)) //即 Vue.prototype.$mount.call(this, query(el))
}

在完整版的vm.$mount方法中,如果用户未提供render函数,就会将template或者el.outerHTML编译成render函数。然后会执行mountComponent函数:

mountComponent如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
export function mountComponent(
vm,
el,
hydrating
) {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// updateComponent 函数包括vnode生成及挂载到真实dom

// 生成一个watcher实例,updateComponent作为watcher函数的回调

new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

// 挂载完成后 运行mouted
if (vm.$vnode == null) {
// actived 需要判断组件已经加载过了
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
  • 在完整版的vm.$mount方法中,如果用户未提供render函数,就会将template或者el.outerHTML编译成render函数。然后会执行mountComponent函数:如果用户提供了el选项,则会获取用于挂载的真实节点,将此节点赋值给vm.$el属性。

  • 触发beforeMount

  • mountComponent方法中,会实例化一个watcher,watcher执行完内部逻辑后(响应式关键),执行updateComponent方法将vm._render()返回的vnode挂载到真实节点中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 生成一个watcher实例,updateComponent作为watcher函数的回调

new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

updateComponent = () => {
vm._update(vm._render(), hydrating)
}
Vue.prototype._render = function() {
const vm = this
const { render } = vm.$options
const vnode = render.call(vm, vm.$createElement)
return vnode
}
  • 触发mouted

至此vue的生命周期就结束了,下面重点介绍 mountComponent 实例化watcher发生了什么

依赖收集

再讲解依赖收集之前,我们需要先了解,Object.defineProperties是什么时候设置的

observer

initState阶段,vue会针对对象数据类型进行observer函数处理,方法的作用就是给非 VNode 的对象类型数据添加一个Observer

举个例子, 下面的data 会有6次observer处理,会有6个Observer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data = {
a: 1,
b: 2,
c: [1, 2, {a: 1}],
d: {
a: {
b: 1
}
}
}
data 一次
c 一次
c {a:1} 一次
d 一次
a 一次
b 一次

Observer会递归处理对象类型,有一个dep实例属性,用处在 defineReactive 中 childOb.dep.depend(),(父对象变化会影响子对象)

Observer的主要目的是对对象进行遍历并定义getter和setter,这也是依赖收集的基础,逻辑在函数 defineReactive 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
// 在 defineReactive 中 childOb.dep.depend()
this.vmCount = 0
// vm._Data.__ob__ 指向这个Observer
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}

/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

defineReactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

这里需要注意函数每次运行都会实例化一个dep,getter 和 setter都是针对dep做出了处理

Dep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

addSub (sub: Watcher) {
this.subs.push(sub)
}

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

depend () {
if (Dep.target) {
// watcher addDep
// 简单来说 就是一个watcher dep 互相持有的过程
Dep.target.addDep(this)
}
}

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的,主要看一下dependnotify

Watcher

上一节我们提到在mountComponent 函数中实例化了一个**render watcher** 实例,现在来重点分析一下。

render watcher 是一个比较特殊的watcher实例,会挂载到vm._watcher上,并且持有该组件响应数据所有的dep实例,所有的dep实例也会持有**render watcher** ,每个组件有且仅有一个render watch

首先看一下watcher的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { pushTarget, popTarget } from './dep';

let uid = 0
export class Watcher {
constructor() {
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.id = uid++
this.value = this.get()
}
get () {
pushTarget(this)
// pushTarget 代码如下,入栈并把当前的watcher赋值给dep

/* function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
} */

if (this.deep) {
// 递归去访问 value,触发它所有子项的 getter
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}

addDep (dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

update() {
console.log("接受到更新消息");
// 推入队列等待触发
queueWatcher(this);
}
run() {

}
}

首先实例化wather会运行get函数中的pushTaget

1
2
3
4
5
6
7
8
9
pushTarget(this)
// pushTarget 代码如下,入栈并把当前的watcher赋值给dep

/* function pushTarget (_target) {
targetStack.push(target)
Dep.target = target
} */

// 黄轶电子书的源码 和 2.6.14的源码有出入

首先说明,Dep.targettargetStack 均为全局属性

实际上就是把 Dep.target 赋值为当前的渲染 watcher(貌似一个实例仅有一个) 并压栈(为了恢复用,vue3中是为了满足嵌套watch)。接着又执行了:

1
value = this.getter.call(vm, vm)

this.getter 对应就是 updateComponent 函数,这实际上就是在执行:

1
vm._update(vm._render(), hydrating)

它会先执行 vm._render() 方法,因为之前分析过这个方法会生成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter。

那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行Dep.target.addDep(this)

刚才我们提到这个时候 Dep.target 已经被赋值为渲染 watcher,那么就执行到 addDep 方法:

1
2
3
4
5
6
7
8
9
10
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

这时候会做一些逻辑判断(保证同一数据不会被添加多次)后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 depsubs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了么,其实并没有,在完成依赖收集后,还有几个逻辑要执行,首先是:

1
2
3
if (this.deep) {
traverse(value)
}

这个是要递归去访问 value,触发它所有子项的 getter,这个之后会详细讲。接下来执行:

1
popTarget()

popTarget 的定义在 src/core/observer/dep.js 中:

1
Dep.target = targetStack.pop()

实际上就是把 Dep.target 恢复成上一个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。最后执行:

1
this.cleanupDeps()

其实很多人都分析过并了解到 Vue 有依赖收集的过程,但我几乎没有看到有人分析依赖清空的过程,其实这是大部分同学会忽视的一点,也是 Vue 考虑特别细的一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}

考虑到 Vue 是数据驱动的,所以每次数据变化都会重新 render,那么 vm._render() 方法又会再次执行,并再次触发数据的 getters,所以 Watcher 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组。

在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIdsdepIds 交换,newDepsdeps 交换,并把 newDepIdsnewDeps 清空。

那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。

考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。

代码反推

首先看vue文件,

  • data包含三个数据,其中一个是对象类型
  • computed有一个,依赖于data1,data2
  • wather有一个,依赖于data1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div class="home">
<p>{{data1}}</p>
<span>{{data2}}</span>
<div>{{data3.a}}</div>
<a href="">{{com1}}</a>
</div>
</template>

<script>
// @ is an alias to /src
import SlopScope from '@/components/slotScope.vue'
export default {
name: 'Home',
data() {
return {
data1: 1,
data2: 2,
data3: {
a: "1sdfds",
b: "fasaga"
}
}
},
methods: {

},
mounted() {
console.log("mounted");
console.log(this);
},
computed: {
com1(){
return this.data1 + this.data2
}
},
watch: {
data1() {
console.log(111111);
}
}
}
</script>

我们在控制台打印一下vm,先看一下_data,可见每一个对象类型均持有一个__ob__即Oberver实例

image-20210909153539012

然后看一下water,其中有两个watcher相关的属性

image-20210909153816298

  • _watcher属性指向的就是render watcher,每个组件有且仅有一个(真实渲染到页面上的)
  • _wathers属性是组件相关的所有watcher
    • 一个render watcher 就是 _watcher指向的那个
    • 一个computed watcher 可以看到属性中 lazy = true
    • 一个user watcher 可以看到属性 user = true
  • 我们再重点关注一下computed watcher中的deps
    • 其中一个是data1对应的dep,subs存在三个watcher,对应render user computed
    • 还有一个是data2对应的dep,subs存在二个watcher,对应render computed
  • // TODO 为什么根data的__ob__持有的dep没有关联的watcher,也没有watcher关联它

总结

根据自己的理解,进行一个简短的总结,方便理解记忆

  • vue在init阶段中的initData
    • initProps(vm, opts.props) 对prop进行defineReactive设置setget
    • initMethods(vm, opts.methods)
    • initData(vm) 对data进行初始化,针对对象类型进行递归处理,使用observer函数处理
    • initComputed 针对每个computed生成一个对应的watcher,并在访问get函数时触发依赖收集
    • initWatch(vm, opts.watch) 生成user watcher,并完成依赖收集
  • 每一个observer函数,都会实例化一个Observer实例,挂载到对象类型的__ob__
  • Observer在实例化的过程中,会针对对象的每一个key用defineReactive进行处理
  • defineReactive会生成一个dep实例,并设置key的getter和setter
  • 在mouted阶段,vue会初始化一个render watcher实例,watcher在实例化的过程中会将Dep.target指向自身,然后运行回调函数,并且有一个入栈的操作(可能是递归处理的时候方便恢复,因为是子组件先mouted,这是我自己猜测的)
  • render watch对应的回调函数就是vm._update(vm._render(), hydrating),在构建虚拟dom的过程中,会触发视图依赖数据的getter函数(在构建虚拟dom的过程中,应该会深度遍历子组件,先完成子组件的依赖收集,这也是watch入栈出栈的原因吧)
  • getter函数 会将数据对应的dep和当前的render watch 互相链接(持有)
  • 虚拟dom及挂载完成,render watch 会进行出栈
  • 执行 cleanupDeps ,用新的订阅替换旧订阅(性能优化,详细参考黄轶blog)

其它需要掌握的点

keep-alive组件

https://ustbhuangyi.github.io/vue-analysis/v2/extend/keep-alive.html#%E5%86%85%E7%BD%AE%E7%BB%84%E4%BB%B6

需要注意的点

  • Keep-alive缓存的是vnode
  • keep-alive 只缓存第一个子组件
  • vnode.elm 缓存了 vnode 创建生成的 DOM 节点
  • 再次激活的时候跳过mount过程,直接把dom插入目标元素中

数组原型拦截

简单的描述一下过程:

在observe数组时,会将数组的__proto__指向由Array.prototype拓展而来的原型对象,该对象会原原本本的执行数组原生的方法,并针对七种方法做了重新定义,把新添加的值变成一个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// 重新定义七种方法
def(arrayMethods, method, function mutator (...args) {
// 调用原方法
const result = original.apply(this, args)
// 数组对象的__ob__
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})

只是拓展了数组原型链还不够,还需要将数组的__proto__指向拓展后的类,这段代码在vue源码中Observe类中

1
2
3
4
5
6
7
8
9
10
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}

生命周期

img

要掌握每个生命周期什么时候被调用

Snipaste_2021-09-06_20-27-19

  1. beforeCreate 在实例初始化之后,数据观测(data observer) 之前被调用。
    1. initLifecycle(vm): 主要作用是确认组件的父子关系和初始化某些实例属性。找到父组件实例赋值给vm.$parent,将自己push给父组件的$children
    2. initEvents(vm): 主要作用是将父组件使用v-on@注册的自定义事件添加到子组件的私有属性vm._events中;
    3. initRender(vm): 主要作用是初始化用来将render函数转为vnode的两个方法vm._cvm.$createElement。用户自定义的render函数的参数h就是vm.$createElement方法,它可以返回vnode。此阶段还会进行$attrs $listeners $slots $scopedSlots的处理, 此时用户可以在函数中通过this访问到vm.$parentvm.$createElement $attrs $listeners $slots $scopedSlots等有限的属性和方法。等以上操作全部完成,就会执行beforeCreate钩子函数
  2. created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算,
    watch/event 事件回调。这里没有$el。这 3 个初始化方法先初始化inject,然后初始化props/data状态,最后初始化provide,这样做的目的是可以在props/data中使用inject内所注入的内容。
    等以上操作全部完成,就会执行created钩子函数,此时用户可以在函数中通过this访问到vm中的propsmethodsdatacomputedwatchinject等大部分属性和方法。
    1. initInjections(vm): 初始化inject,使得vm可以访问到对应的依赖;
    2. initState(vm): 初始化会被使用到的状态,状态包括propsmethodsdatacomputedwatch五个选项。调用相应的init方法,使用vm.$options中提供的选项对这些状态进行初始化,其中initData方法会调用observe(data, true),实现对data中属性的监听,实际上是使用Object.defineProperty方法定义属性的gettersetter方法;
    3. initProvide(vm):初始化provide,使得vm可以为子组件提供依赖(所以在initState后)。
  3. beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  4. mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  5. beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  6. updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  7. beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
  8. destroyed Vue 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用
要掌握每个生命周期内部可以做什么事
  1. created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。
  2. mounted 实例已经挂载完成,可以进行一些DOM操作
  3. beforeUpdate 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  4. updated 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  5. destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
Vue 的父组件和子组件生命周期钩子
  • 加载渲染过程

​ 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程

​ 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 子组件销毁过程

​ 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

diff算法

https://vue3js.cn/interview/vue/diff.html#%E4%BA%8C%E3%80%81%E6%AF%94%E8%BE%83%E6%96%B9%E5%BC%8F

https://juejin.cn/post/6994959998283907102#comment

v-model 的修饰符

.prop

将属性绑定至dom的原生properties,看下边例子

1
2
3
4
5
6
<div v-bind:test="ceshiProp" class="prop"></div>
<div v-bind.prop:test="ceshiProp" class="prop"></div>
ceshiProp: {
a:1,
b:2
}

渲染结果如下:

image-20210121155711009

  • Property:节点对象在内存中存储的属性,可以访问和设置。
  • Attribute:节点对象的其中一个属性( property ),值是一个对象,可以通过点访问法 document.getElementById(‘xx’).attributes 或者 document.getElementById(‘xx’).getAttributes(‘xx’) 读取,通过 document.getElementById(‘xx’).setAttribute(‘xx’,value) 新增和修改。
    在标签里定义的所有属性包括 HTML 属性和自定义属性都会在 attributes 对象里以键值对的方式存在。

太深层的暂不探究

.number

自动将数值转换为number,和el-input type=number 一起用 会有bug

v-on 修饰符

.passive

passive这个修饰符会执行默认方法。你们可能会问,明明默认执行为什么会设置这样一个修饰符。这就要说一下这个修饰符的本意了。

​ 【浏览器只有等内核线程执行到事件监听器对应的JavaScript代码时,才能知道内部是否会调用preventDefault函数来阻止事件的默认行为,所以浏览器本身是没有办法对这种场景进行优化的。这种场景下,用户的手势事件无法快速产生,会导致页面无法快速执行滑动逻辑,从而让用户感觉到页面卡顿。】

​ 通俗点说就是每次事件产生,浏览器都会去查询一下是否有preventDefault阻止该次事件的默认动作。我们加上passive就是为了告诉浏览器,不用查询了,我们没用preventDefault阻止默认动作。

​ 这里一般用在滚动监听,@scoll,@touchmove 。因为滚动监听过程中,移动每个像素都会产生一次事件,每次都使用内核线程查询prevent会使滑动卡顿。我们通过passive将内核线程查询跳过,可以大大提升滑动的流畅度。

获取初始data

在某些情况我们可能要重置data上面的某些属性,比如在表单提交后需要清空form

1
2
3
4
5
this.$data // 组件当前data对象
this.$options.data() // 组件初始化状态下的data对象

Object.assign(this.$data, this.$options.data()) // 重置data对象到初始化状态

实际上这个this.$options.data 是一个函数,也就是组件声明时用来初始化data的函数

在不刷新页面的情况下,更新页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 先注册一个名为 `redirect` 的路由
<script>
export default {
beforeCreate() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
render: function(h) {
return h() // avoid warning message
}
}
</script>


// 手动重定向页面到 '/redirect' 页面 实现更新页面
const { fullPath } = this.$route
this.$router.replace({
path: '/redirect' + fullPath
})

当遇到你需要刷新页面的情况,你就手动重定向页面到redirect页面,它会将页面重新redirect重定向回来,由于页面的 key 发生了变化,从而间接实现了刷新页面组件的效果。

动态清除注册的路由

那就是动态添加的路由,并不能动态的删除。这就是导致一个问题,当用户权限发生变化的时候,或者说用户登出的时候,我们只能通过刷新页面的方式,才能清空我们之前注册的路由。

1
2
3
4
function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}

它的原理其实很简单,所有的 vue-router 注册的路由信息都是存放在matcher之中的,所以当我们想清空路由的时候,我们只要新建一个空的Router实例,将它的matcher重新赋值给我们之前定义的路由就可以了。巧妙的实现了动态路由的清除。 现在我们只需要调用resetRouter,就能得到一个空的路有实例,之后你就可以重新addRoutes你想要的路由了

运算符优先级,从上到下依次减低

运算符 描述
. [] () 字段访问、数组下标、函数调用以及表达式分组
++ – - ~ ! delete new typeof void 一元运算符、返回数据类型、对象创建、未定义值
* / % 乘法、除法、取模
+ - + 加法、减法、字符串连接
<< >> >>> 移位
< <= > >= instanceof 小于、小于等于、大于、大于等于、instanceof
== != === !== 等于、不等于、严格相等、非严格相等
& 按位与
^ 按位异或
| 按位或
&& 逻辑与
|| 逻辑或
?: 条件
= oP= 赋值、运算赋值
, 多重求值

MDN更详细的优先级

还有一些影响运算结果(左右关联)的因素,参考你不知道的JavaScript中卷 128页左右

运算符左右关联

左关联

&&||

1
2
a && b && c
(a && b) && c

右关联

三元运算符 和 =

1
2
3
a ? b : c ? d : e;

a ? b : (c ? d : e)
1
2
a && b || c ? c || b ? a : c && b : a;
((a && b) || c) ? ((c || b) ? a : (c && b)) : a

&&|||&

逻辑运算符如下表所示 (其中expr可能是任何一种类型, 不一定是布尔值):
&& 优先级高于 ||

控制台输出
如果一个值可以被转换为 true,那么这个值就是所谓的 truthy,如果可以被转换为 false,那么这个值就是所谓的 falsy。
会被转换为 false 的表达式有:

  • null;
  • NaN;
  • 0;
  • 空字符串(”” or ‘’ or ``);
  • undefined。

尽管 && 和 || 运算符能够使用非布尔值的操作数, 但它们依然可以被看作是布尔操作符,因为它们的返回值总是能够被转换为布尔值。如果要显式地将它们的返回值(或者表达式)转换为布尔值,请使用双重非运算符(即!!)或者Boolean构造函数。

短路计算

由于逻辑表达式的运算顺序是从左到右,也可以用以下规则进行”短路”计算:

  • (some falsy expression) && (expr) 短路计算的结果为假。

  • (some truthy expression) || (expr) 短路计算的结果为真。
    常见用法

  • 判断对象属性是否存在

1
if(obj.prop&&obj.prop.prop)
  • 赋值
1
2
3
4
5
function (arg){
var a = arg || 5;
}
// 是否存在 不存在创建并赋值
(this._events[event] || (this._evnet[evnet] = [])).push(fn)
  • 简写
1
2
if(a==b){console.log(1)}
a==b&&console.log(1)

| &属于位运算符

此处不深究

0%