ReactDOM.render 初始化过程浅析

banner

ReactDOM.render 用法

1
ReactDOM.render(<Root />, document.getElementById('root'))

ReactDOM 定义

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
const ReactDOM: Object = {
// 省略...
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback,
);
},
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
// 省略...
}

hydrate 和 render 本质都是调用 legacyRenderSubtreeIntoContainer 方法,只是第四个参数 forceHydrate 传入有区别,render 我们很清楚知道就是进行渲染调用,那 hydrate 是用来做什么?

hydrate 描述的是 ReactDOM 复用 ReactDOMServer 服务端渲染的内容时尽可能保留结构,并补充事件绑定等 Client 特有内容的过程。

ReactDOM render 过程

  1. render 调用 legacyRenderSubtreeIntoContainer
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
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
// 省略...
let root: Root = (container._reactRootContainer: any);
if (!root) {
// 创建 ReactRoot
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
if (typeof callback === 'function') {
const originalCallback = callback;
// 封装回调函数
callback = function() {
const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
originalCallback.call(instance);
};
}
// 执行渲染
DOMRenderer.unbatchedUpdates(() => {
if (parentComponent != null) {
// 忽略
root.legacy_renderSubtreeIntoContainer(
parentComponent,
children,
callback,
);
} else {
// 调用 ReactRoot 的 render 方法
root.render(children, callback);
}
});
} else {
// 省略...
}
return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

legacyRenderSubtreeIntoContainer 通过 legacyCreateRootFromDOMContainer 这个方法创建了一个 ReactRoot 实例,然后赋值给 root 和 container._reactRootContainer,完成后执行回调函数的封装,接着执行
unbatchedUpdates 的回调函数,会执行 root 的 render 方法,也就是 ReactRoot 的原型方法 render,最后传入 root._internalRoot,执行后返回 DOMRenderer.getPublicRootInstance 的结果;

  1. legacyCreateRootFromDOMContainer 实现过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): Root {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
if (!shouldHydrate) {
let warned = false;
let rootSibling;
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
}
const isConcurrent = false;
return new ReactRoot(container, isConcurrent, shouldHydrate);
}

判断当前的 container 是否需要进行 forceHydrate,forceHydrate 为 false,则移除 container 下的所有子节点,然后标记 isConcurrent 为 false,实例化 ReactRoot;

  1. ReactRoot 的构造函数
1
2
3
4
5
6
7
8
function ReactRoot(
container: Container,
isConcurrent: boolean,
hydrate: boolean,
) {
const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
this._internalRoot = root;
}
1
2
3
4
5
6
7
export function createContainer(
containerInfo: Container,
isConcurrent: boolean,
hydrate: boolean,
): OpaqueRoot {
return createFiberRoot(containerInfo, isConcurrent, hydrate);
}

调用 DOMRenderer.createContainer 创建一个 FiberRoot,并把当前的 FiberRoot 实例化对象记录到 root._internalRoot 中。

  1. ReactRoot 实例化对象 render 调用触发更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
const root = this._internalRoot;
// 创建一个 work
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (callback !== null) {
// 等待当前的 work 执行完成后执行回调函数
work.then(callback);
}
// 触发更新
DOMRenderer.updateContainer(children, root, null, work._onCommit);
return work;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
// container 为当前的 FiberRoot 实例化对象
const current = container.current;
// 用于计算任务调度优先级
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, current);
// 推送和触发更新调度任务
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback,
);
}

ReactRoot 的原型方法 render 创建了一个 work,等待 work 执行完成后会执行 callback 的回调,而 work 的回调执行在于 wor._onCommit 的触发,render 方法最后调用了 DOMRenderer.updateContainer, updateContainer 这个方法一开进行 currentTime 和 expirationTime 来进行任务优先级的计算,然后执行 updateContainerAtExpirationTime 来进行渲染更新任务的触发。

  1. updateContainerAtExpirationTime 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
expirationTime: ExpirationTime,
callback: ?Function,
) {
const current = container.current;
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
return scheduleRootUpdate(current, element, expirationTime, callback);
}
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
function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime,
callback: ?Function,
) {
// 创建一个 update 更新任务
const update = createUpdate(expirationTime);
update.payload = {element};

callback = callback === undefined ? null : callback;
if (callback !== null) {
warningWithoutStack(
typeof callback === 'function',
'render(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callback,
);
update.callback = callback;
}
// 把当前的更新任务和当前的 FiberRoot 压入任务队列
enqueueUpdate(current, update);
// 触发任务队列进行任务执行
scheduleWork(current, expirationTime);
return expirationTime;
}

updateContainerAtExpirationTime 主要是执行 scheduleRootUpdate,scheduleRootUpdate 方法生成一个 update 更新任务,然后把当前的更新任务和当前的 FiberRoot 压入任务队列,接着触发任务队列进行任务执行。

到这里就完成了 ReactDOM render 的初始化,后续界面视图的更新渲染就依赖于 React 的 Fiber Schedule 进行更新调度。

React下Hooks使用姿势

banner

赋予函数组件拥有类组件的能力

State Hook 使用

用 Hooks 实现一个根据按钮点击计数的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import { useState } from 'react'
import { Button } from 'antd'

const Counter = () => {
const [count, setCount] = useState(0)
const onButtonClick = () => {
setCount(count => count + 1)
}
return (
<Button onClick={onButtonClick}>{count}</Button>
)
}

export default Counter

这里使用 useState 的 hook 实现在函数组件声明状态,当需要进行状态变更的时候,使用 useState 返回的 api 变更函数进行状态更新,一般回调函数的方式进行状态更新:

1
setCount(count => count + 1)

在 setCount 传入一个回调函数,这个回调函数会接收到当前 count 的最新状态值,接着回调函数通过 return 一个新的值从而进行状态更新。

Reducer Hook 使用

useState 本身其实是基于 useReducer 进行实现的一个 hook。

我们看下 useReducer 的用法

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
import React from 'react'
import { useReducer } from 'react'
import { Button } from 'antd'

const countReducer = (state, action) => {
switch(action.type) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}

const Counter = () => {
const [count, dispatchCount] = useReducer(countReducer, 0)
const onButtonClick = (type) => {
dispatchCount({ type })
}
return (
<>
<p>{count}</p>
<Button onClick={() => onButtonClick('add')}>Add</Button>
<Button onClick={() => onButtonClick('sub')}>Sub</Button>
</>
)
}

export default Counter

定义一个 countReducer 函数, 通过不同的 action 参数来进行状态行为的变更,然后使用 useReducer 进行声明获得对应状态值 count,和变更触发方法 dispatchCount。

Effect Hook 使用

当要在函数组件实现类似类组件的 componentDidMount, componentWillReceiveProps 的功能的时候,我们需要借助 useEffect 的 hook 来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useEffect, useState } from 'react'

const Counter = () => {
const [autoCount, setAutoCount] = useState(0)

useEffect(() => {
const timer = setInterval(() => {
setAutoCount(autoCount => autoCount + 1)
}, 1000)
return () => { clearInterval(timer) }
}, [])

return (
<>
<p>AutoCount: {autoCount}</p>
</>
)
}

export default Counter

这个例子主要是在函数渲染后,通过 useEffect 的 hook 来实现一个自动计数的功能,useEffect 接收一个回调函数,用来进行当前的 effect hook 的响应操作,第二个参数为一个依赖的变量数组,当传入的依赖数组变量为空,则起到了类似 componentDidMount 的作用,在 useEffect 的回调函数值可以 return 一个函数用于处理当前的 effect hook 需要销毁的尾处理。

我们再举一个例子:

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
import React, { useEffect, useState } from 'react'
import { useReducer } from 'react'
import { Button } from 'antd'

const countReducer = (state, action) => {
switch(action.type) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}

const Counter = () => {
const [count, dispatchCount] = useReducer(countReducer, 0)
const [full, setFull] = useState(false)
const onButtonClick = (type) => {
dispatchCount({ type })
}

useEffect(() => {
count >= 10 ? setFull(true) : setFull(false)
}, [count])

return (
<>
<p>Full: {full ? 'Full' : 'Not Full'}</p>
<p>{count}</p>
<Button onClick={() => onButtonClick('add')}>Add</Button>
<Button onClick={() => onButtonClick('sub')}>Sub</Button>
</>
)
}

export default Counter

这里例子主要是修改了上面 useReducer 所用到的例子,当 count 增加到 超过 10 的时候,useEffect 通过监听 count 的依赖变化,从而来判断并修改 full 的状态值,这里有点 vue 的 watch 的含义。

同样,我们也可以对 props 的参数传入到 useEffect 的依赖中,当 props 中的数据发生变化,可以触发 useEffect 的回调函数的执行,这样就起到了 componentWillReceiveProps 的作用。

Context Hooks 使用

Context Hooks 目的是为了解决多层级组件的数据传递问题,通过 Context 的方式来中心化处理组件的数据更新,同时触发视图的渲染更新。

使用 Context Hooks 的方式如下:

Context 声明定义: context.js

1
2
3
4
5
import React, {createContext} from 'react'

const Context = createContext('')

export default Context

定义父组件:page-context.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import Context from '../components/context'
import Counter from '../components/counter'

const App = () => {
return (
<>
<Context.Provider value="This is Counter">
<Counter></Counter>
</Context.Provider>
</>
)
}

export default App

定义 Counter 子组件:counter.js

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
import React, { useEffect, useState, useContext } from 'react'
import { useReducer } from 'react'
import { Button } from 'antd'
import Context from './context'

const countReducer = (state, action) => {
switch(action.type) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}

const Counter = () => {
const [count, dispatchCount] = useReducer(countReducer, 0)
const [full, setFull] = useState(false)
const context = useContext(Context)
const onButtonClick = (type) => {
dispatchCount({ type })
}

useEffect(() => {
count >= 10 ? setFull(true) : setFull(false)
}, [count])

return (
<>
<p>Context: {context}</p>
<p>Full: {full ? 'Full' : 'Not Full'}</p>
<p>{count}</p>
<Button onClick={() => onButtonClick('add')}>Add</Button>
<Button onClick={() => onButtonClick('sub')}>Sub</Button>
</>
)
}

export default Counter

使用 context 的主要方式是使用 createContext 定义一个上下文对象 Context,接着使用 Context 对象的 Provider 提供者这个属性,提供给到需要当前上下文的子组件。
在子组件中,使用 useContext 把 Context 对象传递进去,获得到对应的消费者并使用该消费者进行视图渲染或数据计算。

Ref Hook 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useEffect, useRef } from 'react'
import { Button } from 'antd'

const Counter = () => {
const ref = useRef()

useEffect(() => {
console.log(ref.current)
}, [])

return (
<>
<Button ref={ref} onClick={() => onButtonClick('add')}>Add</Button>
</>
)
}

export default Counter

使用 useRef 来声明获得当前的 ref 对象,赋值给对应的组件节点,ref.current 则表示为当前 ref 对应的组件的 dom 节点对象。

Memo Hooks 和 Callback Hooks 使用

官网对着两个 Hook 的解释如下

useCallback

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

在传入的依赖值 a, b, 不变的情况下, memoizedCallback 的引用保持不变,useCallback 的第一个入参函数会被缓存。

useMemo

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

在传入的依赖值 a, b, 不变的情况下, memoizedValue 的值保持不变, useMemo函数的第一个入参函数不会被执行。

useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

useCallback 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState, useCallback } from 'react'

const Counter = () => {
const [count, setCount] = useState(0)
const onCount = useCallback(() => {
return setCount(count => count + 1)
}, [])
return (
<>
<p>{count}</p>
<ChildComponent onCount={onCount}></ChildComponent>
</>
)
}

export default Counter
1
2
3
4
5
6
import React, { memo } from 'react'
import { Button } from 'antd'

const ChildComponent = memo(({ onCount }) => {
return <Button onClick={onCount}>Add</Button>
})

因为我们使用 useCallback 缓存了 onCount函数,使得当 count 发生变化时,Counter 重新渲染后 onCount 保持引用不变,传入 ChildComponent 借助 memo 方法使得 ChildComponent 组件避免了不必要的重新渲染。

useMemo 使用

useCallback 是根据传入的依赖,缓存第一个入参函数。useMemo 是根据传入的依赖,缓存第一个入参函数执行后的值。

useMemo 个人理解与 vue 的 computed 属性类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState, useCallback, useMemo } from 'react'

const Counter = () => {
const [count, setCount] = useState(0)
const onCount = useCallback(() => {
return setCount(count => count + 1)
}, [])
const couteComputed = useMemo(() => {
return (count * 1000) / 1024
}, [count])
return (
<>
<p>{count}</p>
<p>{couteComputed}</p>
<ChildComponent onCount={onCount}></ChildComponent>
</>
)
}

export default Counter

useMemo 的依赖就可以只在指定变量值更改时才执行计算,从而达到节约内存消耗。

从零开始打造属于自己的UI库

目前主流的 Vue UI 框架很多, 每个 UI 框架的风格各有各的特点, 国内目前知名度较高的莫过于 Element 和 iView

大概是 16 年的时候,那时候刚开始上手 Vue2.0, 同时也分别使用了 Element 和 iView 两个 UI 框架来实现一些运营系统的前端开发。

从那时候开始就想着,能写出这么完善和优秀的框架得需要多久啊!内心一直很渴望也能够自己写一写 UI 框架,然后自己设计,自己配色。

不求推广到给别人使用!只求能够自己过把瘾!以及在写 UI 框架的过程中能够学习和沉淀多一丁点前端技术。

Vue

The best-ui is not the best !important;

PS: Best UI 大部分学习和借鉴了 Element 和 iView 组件库的源码

项目结构

+– build
+– examples
+– lib
+– packages
+– components
+– theme
+– src
+– mixins
+– utils
+– index.js
+– tests
+– types
+– babel.config.js
+– components.json
+– gulpfile.js
+– package.json

看过 Element 的源码的同学估计一目了然,这里主要参考了 Element UI 的项目结构

简单讲一下这里的项目目录和文件的作用

  • build 主要是和构建相关
  • examples 用于编写一些简单的 demo 代码,开发组件调试所用
  • lib 是打包构建完成后代码的存放目录
  • packages 是每个组件的源码实现, components 表示每个组件独立的目录,theme 存放组件的样式 .scss 文件
  • src 是 UI 框架的入口文件存放位置,以及一些工具函数,mixins,plugins 文件的存放
  • tests 单元测试和集成测试(不好意思,写这篇文章的时候一个组件的单元测试都还没写,我是个辣鸡)
  • types d.ts 的类型声明文件(写这篇文章的时候所有组件的 d.ts 文件还没完成补齐,我是个辣鸡第二遍)
  • babel.config.js babel 的配置声明
  • components.json 用来指定具体组件的源码位置,用于构建多个组件单独的文件
  • gulpfile 用于 gulp 进行样式构建的运行文件

一个前端工程化项目的结构出来了,按照个人的开发习惯,接下来就是要对项目的构建进行配置了,不然光写代码而运行不了,没什么意义。

构建过程配置

Webpack]

对于 Webpack 的配置,本来一开始是使用 vue-cli 来生成对应的构建配置,但是后面发现要去修改适配组件库的构建流程有点鸡肋,所以就干脆自己大致按照 Element 的方式去做一遍配置,每个环节这里都会详细介绍

build 这个构建配置的目录如下:

+– build
+– bin
+– build-component.js
+– build.js
+– watch.js
+– utils
+– config.js
+– webpack.base.js
+– webpack.build.js
+– webpack.component.js
+– webpack.watch.js

bin 目录三个文件代表了三个运行的命令

  • build-component 主要是用于构建多个组件的单文件。
  • build 表示构建整体的组件库,包含多个组件的合并。
  • watch 用于开发构建过程中预览调试

utils 中只有一个 resolve 用来做路径的解析

Webpack 的 Base 配置

webpack.base.js:

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
74
75
76
77
78
79
80
81
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const { VueLoaderPlugin } = require('vue-loader')
const { resolve } = require('./utils/resolve')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')

module.exports = {
resolve: {
extensions: ['.vue', '.js', '.json'],
alias: {
'@': resolve(process.cwd(), 'examples'),
'~': resolve(process.cwd(), 'packages'),
src: resolve(process.cwd(), 'src')
}
},
module: {
rules: [
{
test: /\.js$/,
loader: 'happypack/loader?id=eslint',
enforce: 'pre'
},
{
test: /\.js$/,
loader: 'happypack/loader?id=babel'
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loader: {
js: 'happypack/loader?id=babel'
}
}
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
}
}
]
},
plugins: [
new HappyPack({
id: 'eslint',
loaders: [{
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}],
verbose: false,
threadPool: happyThreadPool
}),
new HappyPack({
id: 'babel',
loaders: [{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}],
verbose: false,
threadPool: happyThreadPool
}),
new ProgressBarPlugin(),
new VueLoaderPlugin()
]
}

使用 happypack 来进行 babel 的转码,配置对应需要的 loader 和 plugins

配置 resolve.alisa 对一些常用的目录文件路径进行缩写配置

这里没有指定 entry 和 output 主要是让其他的环境配置进行指定

Watch 模式

webpack.base.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseConf = require('./webpack.base')

const config = merge(baseConf, {
mode: 'development',
entry: './examples/main.js',
plugins: [
new HtmlWebpackPlugin({
template: './examples/template/index.html'
})
]
})

module.exports = config

bin/watch.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const webpack = require('webpack')
const config = require('../webpack.watch')
const middleware = require('webpack-dev-middleware')

const compiler = webpack(config)
const express = require('express')
const app = express()

app.use(
middleware(compiler, {
})
)

app.listen(3000, () => console.log('Example app listening on port 3000!'))

Watch 模式下使用 webpack-dev-middleware 结合 express 实现一个简单的 dev-server 进行开发预览,对应的入口文件为我们 examples 下的 main.js 文件。

build 模式

webpack.build.js:

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
const merge = require('webpack-merge')
const baseConf = require('./webpack.base')
const { externals } = require('./config')
const { resolve } = require('./utils/resolve')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

const config = merge(baseConf, {
mode: 'production',
entry: resolve(process.cwd(), 'src', 'index.js'),
output: {
path: resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: 'index.js',
library: 'BEST',
libraryTarget: 'umd'
},
performance: {
hints: false
},
externals,
plugins: [
new CleanWebpackPlugin()
]
})

module.exports = config

指定 mode 为 production,配置对应的 entry 为 src 下的 index.js , 用于进行全局构建,对应的 output 路径为 lib,指定的 libraryTarget 为 umd 模块。

bin/build.js:

1
2
3
4
5
6
7
8
9
10
const webpack = require('webpack')
const config = require('../webpack.build')

const compiler = webpack(config)

compiler.run(function (err, stats) {
if (err) {
return console.error(err)
}
})

直接借助 webapck 的 compiler 执行构建

Build Component 模式

这里说明一下为什么要构建单个的组件文件,因为涉及到 按需加载 的使用,我们需要对单个的组件进行构建,这里可以借助 webpack 的多文件构建模式来执行

webapck.component.js:

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
const merge = require('webpack-merge')
const baseConf = require('./webpack.base')
const { externals } = require('./config')
const { resolve } = require('./utils/resolve')

const components = require('../components.json')

const entrys = {}
Object.keys(components).forEach(component => {
entrys[component] = components[component]
})

const config = merge(baseConf, {
mode: 'production',
entry: entrys,
output: {
path: resolve(process.cwd(), 'lib'),
publicPath: '/dist/',
filename: '[name].js',
libraryTarget: 'commonjs2'
},
optimization: {
minimize: false
},
performance: {
hints: false
},
externals
})

module.exports = config

通过读取 component.json 的配置文件来获取构建的组件对应的入口文件路径,对应的 output 路径为 lib

样式构建

Gulp]

Best UI 组件库是用 Sass 来进行样式的编写,我们这里为了和 js 的构建区分开,所以在样式的构建上,单独使用 gulp 对 .scss 的样式文件进行构建

gulpfile.js:

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
'use strict'

const { series, src, dest } = require('gulp')
const sass = require('gulp-sass')
const autoprefixer = require('gulp-autoprefixer')
const cssmin = require('gulp-cssmin')
const basepath = './packages/theme'

function compile () {
return src(`${basepath}/src/*.scss`)
.pipe(sass.sync())
.pipe(autoprefixer({
cascade: false
}))
.pipe(cssmin())
.pipe(dest('./lib/theme'))
}

function copyfont () {
return src(`${basepath}/src/fonts/**`)
.pipe(cssmin())
.pipe(dest('./lib/theme/fonts'))
}

exports.build = series(compile, copyfont)

这里沿用了 Element 的 gulpfile。这个 gulp 的构建过程很简单,主要做两件事情:

  • 将 packages/theme/src 下的每个 .scss 和 .css 的文件进行编译,压缩,最后输出到 lib/theme 的目录下
  • 将 packages/theme/src 下的字体文件进行压缩并输出到 lib/theme/fonts 的目录下

package.json 的 script 配置

通过以上完成了对应的构建配置,为了快速执行以上的命令,我们可以在 npm 的 script 中进行配置:

package.json:

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"watch": "node ./build/bin/watch.js",
"build:component": "node ./build/bin/build-component.js",
"build": "node ./build/bin/build.js",
"build:all": "npm run build & npm run build:component & npm run build:css",
"build:css": "gulp build --gulpfile ./gulpfile.js",
"lint": "eslint --fix --ext .js,.vue src packages"
}
}

尝试开发一个组件

Vue

以上已经完成了对整个构建过程的配置,当我们把必要的工程化工具都配置好了后,可以尝试上手开发一个组件,我们以开发一个 Button 组件为具体实例

组件交互逻辑开发

在 package 的目录下新建一个 button 的目录,然后新建以下的文件:

button/index.js:

1
2
3
4
5
6
7
import BTButton from './src/button'

BTButton.install = function (Vue) {
Vue.component(BTButton.name, BTButton)
}

export default BTButton

index.js 作为 button 组件的入口文件,这里给 BTButton 定义 install 方法,目的是方便通过 vue 的插件形式进行全局组件注册。

组件的逻辑实现在 button/src/button.vue:

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
74
75
76
<template>
<button
:disabled="disabled"
type="button"
:class="[
'bt-button',
type ? `bt-button--${type}` : '',
{
'is-circle': circle,
'is-disabled': disabled
}
]"
@click="handleClick">
<i :class="icon" v-if="icon"></i>
<span v-if="$slots.default">
<!-- @slot 默认插槽 -->
<slot></slot>
</span>
</button>
</template>

<script>
/**
* Button 按钮
* @displayName Best Button
*/
export default {

name: 'BtButton',

props: {
/**
* 类型
* `default,primary,info,warn`
*/
type: {
type: String,
default: 'default'
},
/**
* 是否圆形按钮
*/
circle: {
type: Boolean,
default: false
},
/**
* 图标类名
*/
icon: {
type: String,
default: ''
},
/**
* 是否禁用
*/
disabled: {
type: Boolean,
default: false
}
},

methods: {
/**
* Click 事件
*
* @event click
* @type {object}
*/
handleClick (event) {
this.$emit('click', event)
}
}

}
</script>

button 的组件实现并不复杂,这里用一个很简单的例子来说明添加一个组件开发的过程(牛x的同学可以考虑打造 cli 来快速新建一个组件的模板)

将组件添加到全局入口文件和配置文件

我们将我们开发完的 button 组件注册到 src/index.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Button from '~/button'

const components = [
Button,
]

const install = function (Vue, opts = {}) {
components.forEach(component => {
Vue.component(component.name, component)
})
}

export default {
install,
Button,
}

将开发完成的 button 组件注册到全局的入口文件,提供全量引入的方式。

对于组件的配置文件 componets.json:

1
2
3
4
{
"button": "./packages/button",
"best-ui.common": "./src"
}

配置 webapck 在构建多个组件文件的过程中每个组件文件对应 entry 的路径 和 output 的文件名。

编写 d.ts 类型声明文件

types/button.d.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { BestUIComponent } from './components'

export type ButtonType = 'default' | 'primary' | 'info' | 'warn'

export declare class BTButton extends BestUIComponent {

type?: ButtonType

circle?: boolean

icon?: string

disabled?: boolean
}

types/components.d.ts:

1
2
3
4
5
import Vue from 'vue'

export declare class BestUIComponent extends Vue {
static install (vue: typeof Vue): void
}

编写单元测试

待补充 !-_-!

组件样式开发

Vue

BEM 样式编写规范

BEM代表 “块(block), 元素(element), 修饰符(modifier)”

以 Button 的组件为例子,我们看下怎么理解 BEM 的规范

对于 Best UI, 前缀统一使用 bt 进行命名,所以 button 组件在 Best UI 的样式命名 为 bt-button,这里 bt-button 表示一个 block

当 bt-button 下有其他的关联元素,比如标签,这时候可以命名为 bt-button__label

通常一个 button 有不同的修饰类型,一个 button 可以为 primary 类型,也可以为 info 类型,或者 warning 类型,这种就可以用修饰符来命名 bt-button–primary

我们看下 packages/theme/button.scss 的实现

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
@charset "UTF-8";
@import "common/var";
@import "mixins/_button";
@import "mixins/mixins";
@import "mixins/utils";

@include b(button) {
display: inline-block;
cursor: pointer;
background: $--button-default-background-color;
border: $--border-base;
border-color: $--button-default-border-color;
color: $--button-default-font-color;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
box-shadow: $--button-box-shadow;
outline: none;
margin: 0;
font-weight: $--button-font-weight;
transition: .1s;

@include utils-user-select(none);

& + & {
margin-left: 10px;
}

@include button-size($--button-padding-vertical, $--button-padding-horizontal, $--button-font-size, $--button-border-radius);

&:hover,
&:focus {
color: $--color-primary;
border-color: $--color-primary-light-7;
background-color: $--color-primary-light-9;
box-shadow: $--button-modifier-box-shadow;
}

&:active {
color: mix($--color-black, $--color-primary, $--button-active-shade-percent);
border-color: mix($--color-black, $--color-primary, $--button-active-shade-percent);
outline: none;
}

@include when(circle) {
border-radius: $--border-radius-circle;
padding: 10px;
font-size: 0;
& > * {
font-size: $--button-font-size;
}
}

@include m(primary) {
@include button-variant($--button-primary-font-color, $--button-primary-background-color, $--button-primary-border-color);
}

@include m(info) {
@include button-variant($--button-info-font-color, $--button-info-background-color, $--button-info-border-color);
}

@include m(warn) {
@include button-variant($--button-warn-font-color, $--button-warn-background-color, $--button-warn-border-color);
}

}

这里咋一看,可能会觉得这 sass 写得也太复杂了吧!笔者一开始看 Element 的时候就有这种感觉,因为这里都是参考了 Element 的规范,我们这里对这里的 sass 写法做一些详细分析。

Sass 公用方法分析

(1) common/var.scss 这个文件主要是对一些颜色,字体大小,边框等等样式变量进行定义,方便主题的统一修改替换

(2) 对于 mixins/utils.scss 这个主要是一些工具的 sass 函数,比如:清除浮动,文字溢出处理,垂直居中等等

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
@mixin utils-user-select($value) {
-moz-user-select: $value;
-webkit-user-select: $value;
-ms-user-select: $value;
}

@mixin utils-clearfix {
$selector: &;

@at-root {
#{$selector}::before,
#{$selector}::after {
display: table;
content: "";
}
#{$selector}::after {
clear: both;
}
}
}

@mixin utils-vertical-center {
$selector: &;

@at-root {
#{$selector}::after {
display: inline-block;
content: "";
height: 100%;
vertical-align: middle;
}
}
}

@mixin utils-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

(3) 我们重点看下 mixins/mixins.scss

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
@import "function"; /* 配置 $namespace, $element-separator, $modifier-separator, $state-prefix */
@import "../common/var";

/* BEM */

// block
@mixin b($block) {
$B: $namespace+'-'+$block !global; // $B is the global var

.#{$B} {
@content;
}
}

// element
@mixin e($element) {
$E: $element !global; // $E is the global var
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}

@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}

// modifier
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}

@at-root {
#{$currentSelector} {
@content;
}
}
}

@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}

这里 b 函数,e 函数,m 函数,when 函数分别用于对类名进行封装

比如我们要对 button 命名一个 bt-button,这时候可以用

1
2
3
@include b(button) {
...
}

当我们需要命名 bt-button__label,则写法如下:

1
2
3
4
5
@include b(button) {
@include e(label) {
...
}
}

需要对 bt-button 进行修饰得到 bt-button–primary 的时候:

1
2
3
4
5
@include b(button) {
@include m(primary) {
...
}
}

需要标识状态的类名可以使用 when 函数,比如:是否为圆形按钮,is-circle

1
2
3
4
5
@include b(button) {
@include when(circle) {
...
}
}

通过这种方式高效利用 sass 的强大语法,结合 BEM 的规范让我们书写样式的时候井然有序,简洁明了(前提是已经了解 sass 的语法)。

Vue 组件开发技巧

使用 Vue 框架实现一个组件库,不像我们平时在业务层级可以引入一些库比如 vuex,vue-router。我们需要追求更加简洁,尽量确保不引入多余的库。

跨层级的组件通信

假如我们有 A 组件,然后 A 组件可以通过 slot 插槽嵌入 B 组件,B 组件又可以通过 slot 嵌入 C 组件,这时候 C 组件想向 A 组件进行跨级通信简单通过 props 和 emit 要传递可能会显得比较繁琐。

于是,我们可以自己实现一个 dispatch 和 broadcast 的功能

PS: 这里主要参考 iView 的实现

src/mixins/emitter.js:

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
export default {
methods: {
/**
* @desc 向上遍历找寻对应组件名称的父组件分发事件
* @param {*} componentName
* @param {*} eventName
* @param {*} params
*/
dispatch (componentName, eventName, params) {
let parent = this.$parent || this.$root
let name = parent.$options.name

while (parent && (!name || name !== componentName)) {
parent = parent.$parent

if (parent) {
name = parent.$options.name
}
}
if (parent) {
parent.$emit(eventName, params)
}
},
/**
* @desc 向下便利找寻对应组件名称的子组件分发事件
* @param {*} componentName
* @param {*} eventName
* @param {*} params
*/
broadcast (componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name

if (name === componentName) {
child.$emit(eventName, params)
} else {
this.broadcast.apply(child, [componentName, eventName, params])
}
})
}
}
}

当我们的 C 组件需要向 A 组件进行通信的时候,我们这时候可以使用 dispatch,在 C 组件中向上寻找 A 组件,通过 name 属性找到 A 组件,然后传递对应的事件名称和参数。

当我们 A 组件需要向 C 组件进行通信,我们可以使用 broadcast 广播向下寻找 C 组件,并通知对应的事件和参数。

其他技巧等待缓慢补充

待补充 !-_-!

发布 Best UI

完成了以上的分析,这时候 Best UI 的大概的结构和实现已经出来了,那我们怎么去发布我们的库呢?

其实很简单,只要我们配置好对应的 package.json 文件就 ok 了

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
{
"name": "@douku/best-ui",
"version": "0.1.5",
"private": false,
"main": "lib/best-ui.common.js",
"files": [
"lib",
"src",
"packages",
"types"
],
"typings": "types/index.d.ts",
"scripts": {
"watch": "node ./build/bin/watch.js",
"build:component": "node ./build/bin/build-component.js",
"build": "node ./build/bin/build.js",
"build:all": "npm run build & npm run build:component & npm run build:css",
"build:css": "gulp build --gulpfile ./gulpfile.js",
"lint": "eslint --fix --ext .js,.vue src packages"
},
"repository": {
"type": "git",
"url": "git@github.com:DouKu/best-ui.git"
},
"keywords": [
"best",
"vue",
"components"
],
"bugs": {
"url": "https://github.com/DouKu/best-ui/issues"
},
"license": "MIT",
"unpkg": "lib/index.js",
"style": "lib/theme/index.css",
"dependencies": {
"async-validator": "^3.1.0",
"vue": "^2.6.10"
},
"peerDependencies": {
"vue": "^2.6.10"
},
"devDependencies": {
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"pre-push": "npm run lint"
}
}
}
  • name: 组件库的包名称
  • main: 全局加载的入口文件
  • files: 需要发布的文件列表
  • typings: 类型声明文件位置
  • peerDependencies: 这里简单来说就是如果你安装了 best-ui, 那你最好也安装 vue

使用 Best UI

全局引入

1
2
3
4
5
import Vue from 'vue';
import BestUI from '@douku/best-ui';
import '@douku/best-ui/lib/theme/index.css';

Vue.use(BestUI);

按需加载

.babelrc:

1
2
3
4
5
6
7
8
9
10
11
{
"plugins": [
[
"component",
{
"libraryName": "@douku/best-ui",
"styleLibraryName": "theme"
}
]
]
}

js 使用:

1
2
3
4
5
6
7
8
import { Button } from '@douku/best-ui';

Vue.component(Button.name, Button);

new Vue({
el: '#app',
render: h => h(App)
});

这里简单说一下 babel-plugins-component 实现按需加载的原理, 其实就是:

1
import { Button } from '@douku/best-ui';

转换为:

1
2
const button = require('@douku/best-ui/lib/button.js');
require('@douku/best-ui/lib/theme/button.css');

这样就不难理解为什么能过实现按需加载了!

结尾

今天是 2019 的最后一天了,回看博客,今年自己写的文章比去年多了一些,希望明年能写出更多。

保持热爱前端的态度,不断折腾,追求极致!

源码地址:Best UI

半成品文档地址:Best UI

Vue源码解析之Watcher原理

Vue

对于用户定义的 Watche 属性官方描述如下:

一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。

接着看下文档对 $watch 的用法描述:

观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。

初始化过程

挺久没更新文章了, 回顾一下 Vue 的 initState 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
// 响应式处理
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

在 Vue 实例化执行 _init 的时候执行 initState, 这时候 initState 执行 initWatch 把 vm 实例和 opts 选项参数中的 watch 属性传递给 initWatch 方法。

1
2
3
4
5
6
7
8
9
10
11
12
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}

initWatch 方法其实就是遍历用户定义的 watch 属性, 接着调用 createWatcher 进行 User Watcher 的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}

createWatcher 其实就是调用 vm 实例的 $watch 方法, $watch 方法我们刚才从官方文档的描述中可以知道, 就是观察 Vue 实例变化的一个表达式或计算属性函数。

$watch 是在执行 stateMixin 的时候定义在 Vue 的原型方法

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.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}

从 $watch 的实现可以看出

  • options.user = true, 表明这是一个 user watcher, 本质上也是通过 new Watcher 来实例化得到一个 user watcher.

  • 当 options.immediate 为 true, 则会在实例化当前的 user watcher 后立即调用执行 cb 函数, 也就是用户定义的数据变化响应执行函数. 最后会返回一个取消观察函数,用来停止触发回调。

1
2
3
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()

创建 User Watcher

new Watcher 的时候执行 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
52
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}

这里 this.getter 为 parsePath 函数执行后返回的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}

返回的函数主要是用于对当前的 vm 上的数据属性进行获取, 从而触发对应监听数据响应式对象的 getter.

由于当前的 watcher 不是 computed watcher, 所以不需要延后计算, 于是 user watcher 主要调用 this.get 方法

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
get () {
debugger
// 把当前的渲染watcher赋值给Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// 执行在mountComponet中传入的updateComponent
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
debugger
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

把当前的 user watcher 赋值给 Dep.target, 然后执行 this.getter, 触发定义监听的响应式数据依赖收集当前的 user watcher, 当 deep 为 true, 则会调用 traverse 对响应式数据进行深度遍历并触发对应的响应式数据的 getter, 从而达到深度监听子属性的功能.

数据变化触发响应

当监听的响应式数据发生变化时, 对应的 setter 方法执行, 接着触发 被收集的 user watcher 的 update 方法

1
2
3
4
5
6
7
8
9
10
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

update 方法这里执行 queueWatcher 方法在下一个 tick 后对视图进行更新, 而在对视图进行更新过程中, flushSchedulerQueue 会触发 user watcher 的 run 方法,

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
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

最后执行了用户定义 watch 属性传入的 handler 函数, 这里为: this.cb.call(this.vm, value, oldValue).

到这里已经大概了解了整个 user watcher, 从定义, 初始化, 数据监听依赖收集, 更新触发响应函数执行的过程。

Vue源码浅析之Computed初始化

Vue

什么是计算属性

官方文档描述如下:

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:

1
2
3
<div id="example">
{{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理。

所以,对于任何复杂逻辑,你都应当使用计算属性。

个人理解如下:

computed 主要的用途是把一个或多个变量进行计算处理, 得到计算后的结果, 其能够监听当前所依赖的变量的变更, 并重新计算变更的结果, 进而触发视图进行渲染更新。

这次就来探索一下 computed 的原理实现。

Computed 初始化 Get 过程

在实例化 Vue 的过程中, initState 函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
// 响应式处理
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

这里判断我们的 opts 可选参数是否传入 computed 属性, 若有则执行 initComputed 进行初始化。

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
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()

for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}

if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}

initComputed 先定义了一个 watchers 用于保存当前 computed 对象各个计算属性所以对应的 computed watcher。这个 computed watcher 用途我们后续会介绍。把用户定义的 computed 属性对应的 getter 函数进行获取。

接着, 就根据计算属性的 key 值实例化一个相对应的 computed watcher, 这个 watcher 为啥叫 computed watcher, 因为我们看到 Watcher 的构造函数传参有 computedWatcherOptions = { lazy: true }。表示这是一个 computed watcher, 用于计算属性监听所使用, 和之前说过的渲染 watcher 有所区别。computed watcher 的构造函数还会传入当前的 vm 实例, computed 属性中对应的 getter。

这时候我们看下 new Watcher 的实现。

实例化 computed 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
52
53
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
// lazy 为 true
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}

此时 this.lazy 为 true, 然后会把用户定义的 getter 传入的并保存在当前 computed watcher 的 this.getter中。

defineComputed 实现

接下来我们需要关注一下英文注释:

component-defined computed properties are already defined on the
component prototype. We only need to define computed properties defined
at instantiation here.

这句话其实就是介绍, 对于子组件来说, 其声明的 computed 属性已经被定义在当前组件的原型中, 这时候 key in vm 其实会为 true。

为什么已经定义在子组件中呢?

我们可以了解到, 在对子组件在初始化的过程中, 会通过 Vue.extend 来获取组件的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 继承组件定义
const Sub = function VueComponent (options) {
// 执行Vue.prototype._init方法
this._init(options)
}
// 继承父组件Vue的原型
Sub.prototype = Object.create(Super.prototype)
// 拦截重置构造函数
Sub.prototype.constructor = Sub

...省略

if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}

Vue.extend 的方法其实在进行 initState 的 initComputed 调用前, 已经在获取子组件的构造器的时候就调用了 initComputed 对 computed 进行挂载初始化了。

defineComputed 的实现如下:

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
export function defineComputed (
target: any, // Sub原型
key: string,
userDef: Object | Function // computed getter/setter
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

defineComputed 其实通过 Object.defineProperty 为当前的 Sub 原型定义对应计算属性的 getter/setter, 在原型上定义主要是为了给多个组件能够共享调用 createComputedGetter 这个 getter 的优化点。

createComputedGetter 实现

接下来主要看下 createComputedGetter 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
1
2
3
4
evaluate () {
this.value = this.get()
this.dirty = false
}

createComputedGetter 返回一个函数, 这个函数 computedGetter 就是每个 computed 属性所对应的 getter 函数, 当 computed 的属性被访问时, 比如在渲染过程中属性被访问, 会触发 computedGetter 的执行, 该函数其实就是触发当前 computed watcher 的 evaluate 或者 depend 方法执行, 一开始 watcher.dirty 为 true, 原因是 dirty 的初始值其实就是我们传入的 computedWatcherOptions = { lazy: true } 的 lazy。于是执行 watcher.evaluate() 这时候会调用 this.get() 进行求值。

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
get () {
// 把当前的watcher, 渲染watcher 或者 computed watcher 赋值给Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// 执行在mountComponet中传入的updateComponent
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

我们之前已经了解,pushTarget(this) 其实就是把当前的 computed watcher 赋值给 Dep.target, 接着执行 this.getter 计算 computed 属性对应的数值。

这里需要特别注意一点, this.getter 为用户给 computed 属性定义的 getter 方法, 此方法执行会触发这个方法所依赖的响应式数据的 getter 的执行。

这时候需要特别关注,当前的 Dep.target 为此时 computed 属性对应的 computed watcher, 而触发响应式数据的 getter 的执行则会使得响应式数据对象对应的 dep 收集 Dep.target, 也就是此时的 computed watcher。当前的 computed watcher 会被 push 到响应式对象的 dep.subs 的数组中, 这里其实就是当前的 computed watcher 订阅了所依赖的响应式数据的变化。

最后执行完成后调用 popTarget 把原始的 watcher, 比如渲染 watcher 恢复重新赋值给 Dep.target。

this.get() 执行完成后则把 this.dirty 置为 false。

回到 computedGetter, 如果处于渲染过程中, 也就是 Dep.target 不为空, 则继续执行了 watcher.depend

1
2
3
4
5
6
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}

当前的 computed watcher 已经订阅了所依赖的响应式数据的变化, 于是 computed watcher 的 deps 为所有依赖的响应式数据对应的 Dep。于是执行 dep.depend, 把当前的 Dep.target 也就是当前的渲染 watcher, push 到每个响应式数据对象所对应的 dep.subs 中。当前的渲染 watcher 订阅了 computed watcher 所依赖的数据的变化, 用于依赖数据更新触发视图 update 渲染。完成后, 然后返回通过 evaluate 计算得到的值。

这里整个 computed 的 get 求值就已经完成了。

Computed 变更 Set 过程

当 computed 属性所依赖的响应式数据发生变更后, 则响应式数据会触发其对应的 setter 执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}

set 的执行我们之前有了解过, 这里主要看两个点

  • newVal === value || (newVal !== newVal && value !== value)
  • dep.notify()

第一个是 set 的触发会对比当前更新的值是否发生变更, 如果没有变更则直接 return 不往下执行触发视图更新。

第二个则是执行了当前响应式数据对象对应的 dep 的 notify 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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()
}
}

因为在 get 过程中, 计算属性对应的 computed watcher 订阅了响应式数据的 dep, 这时候会通知 computed watcher 进行 update, 对应的渲染 watcher 也订阅了响应式数据的 dep, 也会执行 update。

computed watcher 的 update 会先执行, 然后执行渲染 watcher 的 update

1
2
3
4
5
6
7
8
9
10
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

computed watcher 执行 update 把其对应的 dirty 属性置为 true, 接着执行渲染 watcher 的update, 渲染 watcher 的 update 执行了 queueWatcher 让视图在下一个 tick 进行更新。

于是视图进行 flushSchedulerQueue 更新, 渲染 watcher 的更新会触发 computed 属性的 getter, 所以就再次执行 computedGetter 的执行, computed watcher 的 evaluate 和 depend 再次执行, 重新依赖收集所依赖的响应式数据, 接着返回数据更新后的新的 computed 值, 接着视图触发渲染更新。

这里已经完成了整个 computed 属性的初始化的解析以及当 computed 属性所依赖的数据发生变更后的重新计算依赖和数值的过程。

Vue源码浅析之nextTick

Vue

nextTicke 的作用

官方文档描述如下:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

从这个描述我们可以知道,nextTick 方法用于在视图渲染更新结束后进行回调方法执行, 目的是保证我们在回调方法中操作的 DOM 是已经完成视图更新渲染。

回顾响应式更新派发

我们已经知道当数据发送变更, 会触发响应式数据对象的 setter 方法, 接着订阅该数据更新的渲染 watcher 执行 queueWatcher, queueWatcher 的实现就是使用 nextTick(flushSchedulerQueue) 来实现视图的异步更新。

nextTick 的实现

nextTick 的源码定义在 core/util/next-tick.js 中

我们先看一下这个模块定义的全局变量

1
2
3
4
5
6
7
8
9
10
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

// 是否使用微任务标识
export let isUsingMicroTask = false
// 收集回调函数
const callbacks = []
// Promise 的判断标识
let pending = false

这里需要关注一下 callbacks 这个数组, 它的作用是为了收集每次调用 nextTick 方法对应的回调函数, 保证每个回调函数有序执行。

我们先看下 nextTick 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

nextTick 实现很简单,如果是通过回调函数的形式来调用, 则把传入的回调函数 push 到 callbacks 这个数组中, 被 push 的回调函数会被封装为一个匿名函数, 这个匿名对每个回调函数进行 try/catch 的异常捕获, 目的是为了保证当某个回调函数发生异常时, 不影响 js 的进一步执行。如果是使用 Promise 的方式进行 nextTick 进行调用, 则会 new 一个 Promsie 对象并把 resolve 赋值给 _resolve 以便链式调用。

一开始 pending 为 false, 所以会执行 timerFunc。

这里我们看下 timerFunc 是怎么实现的

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
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

timerFunc 会根据不同浏览器的原生异步任务支持情况来选择对应的异步任务创建类型, 这里简单列举这里的异步任务类型:

  • Promise 微任务
  • MutationObserver 微任务
  • setImmediate 宏任务
  • setTimeout 宏任务

timerFunc 本质就是创建一个异步任务来执行 flushCallbacks 这个函数。目的是为了让传入 nextTick 的回调函数是异步执行的。

1
2
3
4
5
6
7
8
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

flushCallbacks 其实就是遍历 callbacks 数组里面的回调函数并依次执行。

对于用户调用 Vue.nextTick 暴露的 api 接口传入对应的回调函数, 其执行总会是在视图完成渲染更新后才进行, 因为当数据发生变更, 触发渲染 watcher 执行 queueWatcher, 这时候 vue 内部调用 nextTick 先保证 flushSchedulerQueue 这个方法是先被 push 到 callbacks 这个回调数组。

举个例子

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
<template>
<div>
<p ref="dom">{{msg}}</p>
<button @click="handleChange">change</button>
</div>
</template>

<script>
export default {
data () {
return {
msg: 'vue'
}
},
methods: {
handleChange () {
this.$nextTick(() => {
console.log(this.refs.dom.msg, 'pre')
})
this.msg = 'vue-nextTick'
console.log(this.refs.dom.msg, 'common');
this.$nextTick(() => {
console.log(this.refs.dom.msg, 'after')
})
}
}
}
</script>

这时候的输出结果如下:

1
2
3
vue common
vue pre
vue-nextTick after

这里我们简单来看下对应的函数执行顺序, 我们把第一个 nextTick 的回调函数取名 preTick, 第二个叫 afterTick

同步代码执行:

callbacks.push(preTick)
callbacks.push(flushSchedulerQueue) // 修改 msg 的值所触发
console.log(this.refs.dom.msg, ‘common’) // 输出 vue common
callbacks.push(afterTick)

同步代码执行完成后, 执行 flushCallbacks 遍历执行回调函数:

preTick // 输出 vue pre
flushSchedulerQueue // 视图进行数据更新渲染, this.refs.dom.msg 为 vue-nextTick
afterTick // 输出 vue-nextTick after

到这里已经完成对 nextTick 的分析,其实 nextTick 的实现并不复杂, 如果已经深入了解浏览器 js 的执行机制可能会更容易理解。

Vue源码浅析之更新派发

Vue

getter 就是为了追踪依赖, 对依赖进行收集。而 setter 则是通知变化, 让视图进行更新。

之前已经探索了 vue 在 触发响应式对象的 getter 时候, 通过把组件对应的渲染 watcher 进行依赖收集, 这些 watcher 最终会被收集到 dep.subs 的数组中。

回顾 defineReactive 源码

那当数据发生变更,这时候会触发响应式对象的 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
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

...省略

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { ...省略 },
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

可以看到, 响应式对象的 setter 其实先获取当前的数据变更, 判断数据是否发生变更, 然后会对变更的数据进行 observe 响应式对象初始化, 当然前提是 newVal 是一个非 Vnode 类型的对象。接着这里调用当前响应式对象初始化过程中已经实例化的 dep 的 notify 方法。

而这时候 dep 的 subs 数组其实已经把当前订阅这个数据对象变更的渲染 watcher 收集起来了。

那我们看下 notify 的实现

更新派发过程

1
2
3
4
5
6
7
8
9
10
11
12
13
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()
}
}

这里其实就是对 subs 的订阅者 watcher 进行遍历, 并调用 watcher 的 update 方法。 通俗一点来说, 就是通知 subs 里面的 watcher 去执行 update 方法, 从而进行视图更新。

我们看下 Watcher 实现的 update 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

run 方法下面会详细分析。

这里比较关键的是先关注 queueWatcher 这个函数

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
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

这里通过全局的 has 对象来保证每次同一个 Watcher 只能被添加一次到 queue 这个队列。全局的 index 用于记录当前队列遍历的下标。一开始 flushing 和 waiting 都为 false, 所以执行 queue.push(watcher), 接着通过 nextTick 来异步执行 flushSchedulerQueue。至于 nextTick 怎么实现以后再单独探索, 反正就先认为是用来实现异步更新的机制。

当同步的代码执行完成了, 就会执行 nextTicke 的回调方法 flushSchedulerQueue

flushSchedulerQueue 分析

这里我们重点分析一下这个 flushSchedulerQueue 方法

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
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)

// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}

// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()

resetSchedulerState()

// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}

flashing 的标记为被置为 true

对全局保存 watcher 的这个 queue 队列进行排序

排序的注释翻译如下:

  • 组件的创建从父到子进行, 所以组件的 watcher 也是先进行父级的创建, 再到子级的创建, 对应 watcher 的执行也需要保证从父到子
  • 用户自定义的 watcher 优先于组件渲染 watcher 的创建
  • 如果一个组件在父组件的 watcher 执行期间被 destroyed, 则这个子级对应的 watcher 会被跳过执行

执行在 new Watcher 时候传入的 before 函数进行 hook 的执行

1
2
3
4
5
6
7
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

watcher.run 进行视图更新

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
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 把当前的渲染watcher赋值给Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// 执行在mountComponet中传入的updateComponent
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

run 方法其实调用 this.get, 而 get 这里有个 pushTarget 会导致 Dep.target 变更为当前的 watcher, 然后执行 this.getter => updateComponet => _update => render 触发组件重新渲染。

用户自定义 watcher

run 方法执行同时会判断如果当前的 watcher 是属于用户自定义的 watcher, 则这时候会执行用户定义的回调方法, 而这个用户定义的回调方法很可能会再次进行数据更新修改, 从而再次触发 queueWatcher 的执行。

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
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

而这时候由于 flashing 为true, 所以 queueWatcher 会进入 else 的逻辑, 然后从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。把 watcher 按照 id的插入到队列中,因此 queue 的长度发送了变化。

恢复状态

resetSchedulerState 方法是用于把一些逻辑流程执行的变量进行重置为初始值。

1
2
3
4
5
6
7
8
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}

小结一下

组件的数据更新触发视图更新,通过 getter 把订阅当前响应式对象数据的渲染 watcher 进行收集, 并保存到 dep 的 subs 中, 当数据进行更新触发 setter 的时候先是遍历 subs 执行每个 watcher 的 update 方法, 把当前的所有 watcher 保存到一个队列的结构中, 在同步的代码执行完成后, 通过 nextTick 来异步执行 flushSchedulerQueue 进行队列的遍历, 然后执行对应的 watcher 的 run 方法进行组件 patch 更新以及对应的响应对象的依赖重新收集。

Vue源码浅析之依赖收集

Vue

响应式对象Getter/Setter

前一篇文章讲了响应式对象的初始化,其中我们了解到 defineReactive 最终就是通过 Object.defineProperty 为数据对象定义 getter/setter, 我们看下官方文档对 getter/setter 的解释:

getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化

我们可以知道 getter 其实就是为了追踪依赖, 对依赖进行收集。而 setter 则是通知变化, 让视图进行更新.

这次我们先了解一下 getter 如何进行依赖收集。

回顾 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 function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 实例化一个 Dep 对象
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) {
// 收集当前的渲染watcher,作为订阅者,方便set的时候进行触发更新
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
...省略
}
})
}

defineReactive 在函数开始执行时实例化了一个 Dep 对象, 然后在 getter 函数中判断 Dep.target 是否存在, 如果存在则会执行 dep.depend()。

这时候我们看到这里会有疑问, 首先 Dep 是什么? Dep.target 又是什么? dep.depend() 发挥了什么作用?

那接下来我们一个个来探索。

Dep

我们先看下官方文档:

每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

其实 Dep 就是用来收集当前组件的渲染 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
/**
* Dep的作用是建立起数据和watcher之间的桥梁
*/
export default class Dep {
// 静态属性
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = [] // 订阅当前数据变化的watcher
}

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

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

depend () {
if (Dep.target) {
// 调用watcher.addDep
Dep.target.addDep(this)
}
}

notify () {
...省略
}
}

// 全局的watcher,同一时间只能有一个watcher被计算
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
// 把父级的watcher保存到栈结构中
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
// 恢复父级的watcher
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

在 src/core/observer/dep 中的代码定义了 Dep 这个类, 其构造函数初始化了一个 uid 和 subs 的空数组。接着我们看下 depend 这个函数, 其实现非常简单, 就是判断 Dep.target 是否存在, 存在则执行 Dep.target.addDep(this), 那这时候我们得要先知道 Dep.target 是什么东东。

Dep.target 是一个全局的渲染 watcher, 而且其保证同一时间只能有一个 watcher 被计算, 所以在 pushTarget 就是对把传入的渲染 watcher 赋值给 Dep.target, 并通过一个数组实现栈结构来保存当前的渲染 watcher, 为啥需要通过栈结构来保存这些渲染 watcher, 因为 Vue 在 mountComponent 每个组件初始化过程中会实例化一个渲染 Watcher, 组件的创建初始化是先父后子递归执行, 所以这里是为了保证父级的渲染 watcher 能够等子组件初始化完成后进行恢复, 恢复方法对应 popTarget。

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
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {

...省略

let updateComponent
/* istanbul ignore if */
... 省略

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

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

return vm
}

看到这里,可能会有疑问, Dep.target 是什么时候进行赋值为当前的渲染 watcher 的呢?

Watcher

mountComponent 实例化了渲染 watcher, 我们看下 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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
export default class Watcher {
...省略
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {

this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}

this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
...省略
}
this.value = this.lazy
? undefined
: this.get()
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 把当前的渲染watcher赋值给Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// 执行在mountComponet中传入的updateComponent
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
...省略
}

Watcher 的构造函数初始化数据后,把传入的 expOrFn 回调函数进行函数转换, 并赋值给当前实例的 getter。最后执行了 this.get 函数, get 函数里面调用了 pushTarget(this) 把当前的渲染 watcher 实例赋值了 Dep.target, 看到这里就觉得 Vue 的设计模式是真的很巧妙。

depend 函数其实就是把当前的 Dep 实例传参执行当前渲染 watcher 的 addDep 方法。

1
2
3
4
5
6
7
8
9
10
11
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)) {
// 把当前的watcher添加到dep的subs中,也就是当前的watcher作为订阅者
dep.addSub(this)
}
}
}

其功能就是把当前传入的 dep 实例 和 id 分布记录到 newDeps 和 newDepIds 的数组中, 然后再调用 dep 实例自身的 addSub, 把当前的渲染 watcher 实例添加到 dep 实例中的 subs 数组, 其实就是把当前的渲染 watcher 订阅到这个组件的数据对象持有的 dep 的 subs 中, 目的是为后续数据变化时候能通知到哪些 subs 做准备。

现在我们已经大概了解整个依赖收集的过程了, 这里还有一个问题, 就是我们一开始说的 getter 什么时候能够触发执行呢?

我们刚才已经看了 mountComponent 的源码, 其中 updateComponent 执行了 vm._render() 方法, 而 _render 会执行传入的 render 方法, 这时候会解析传入的数据, 从而触发对应数据对象的 getter 方法。

Vue源码浅析之响应式对象初始化

Vue

响应式对象简介

这里引用官方文档的解释:

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

根据上面的解释, 那这次我们来探索一下这个响应式对象的初始化过程, 也就是 Vue 如何把实例的 data 选项进行遍历并如何使用 Object.defineProperty 把属性转为 getter/setter。

初始化过程

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>{{user.name}}</div>
</template>
<script>
export default {
data () {
return {
user: {
name: ''
}
}
}
}
</script>

假设我们用这个简单的示例来定义一个Vue的单文件组件, 这个组件在初始化的过程中会进行组件的注册创建, 通过 Vue.extend 获取组件的构造器, 最后把定义好的 options 传参并实例化 vm 对象, 这时候构造器的执行就会调用 _init 方法, 在 _init 方法中执行了 initState 方法, initState 方法执行了 initProps, initMethods, initData, initComputed, initWatch 等等方法。

那这次我们通过 initData 来一起探索下 data 属性是怎么转化定义 getter/setter, 然后这个过程又做了什么事情。

响应式对象初始化 – initData

我们来看下 initData 的源码:

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
function initData (vm: Component) {
let data = vm.$options.data
// 把传入的 options 的 data 对象赋值到 vm._data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}

...省略

// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 判断props和data的属性命名是否冲突
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// vue实例代理属性
proxy(vm, `_data`, key)
}
}
// observe data 监测数据变化
observe(data, true /* asRootData */)
}

我们简单这看下, 一开始先判断传入的 options 的 data 属性是否为函数类型, 如果是则执行 getData 方法执行 data 函数获取对应的数据对象, 接着获取这个数据对象的 keys 并进行遍历, 这里遍历的目的有两个:

  • 对 data 和 props 进行属性命名校验检测
  • 代理 vm 实例的属性获取到 vm._data, 这也就是我们为什么可以通过 this.user.name 获取到我们定义的数据对象的属性值。

看完这里, 最后来到 observe 方法, 把 data 作为参数传入执行。

响应式对象初始化 – observe

observe 主要是用来监测数据变化, 也就是初始化响应式对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
// 非Vnode的对象类型才执行监听器初始化
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 对于已经挂载监听器的value直接返回
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 把value传入Observer进行进行监听器实例化
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

这里可以很清楚看到, observer 通过判断当前的数据对象需要满足为非Vnode的对象类型才进行函数进一步执行, 接着这里通过判断当前的数据对象是否已经拥有 ob 的属性, 如果有就直接获取返回 value.ob, 没有则实例化一个 Observer 并传入数据对象。

到这里我们可以大致看出, 这个函数最后返回的 ob 其实就是响应式对象。它使得传入的数据对象会增加 ob 属性, 我们看下 Observer 是什么东东。

响应式对象初始化 – Observer

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
/**
* Observer 用于给对象的属性添加getter和setter,用于进行依赖收集和更新下发
*/
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()
this.vmCount = 0
// 把实例对象添加到数据对象中的__ob__
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 数组类型遍历再次调用observe
this.observeArray(value)
} else {
// 遍历对象调用defineReactive
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])
}
}
}

Observer 的够着函数通过 def 函数, 给传入的 value 这个数据对象定义 ob 属性, 而对应这个属性的值就当前这个 Observer 的实例化对象。 def 函数的功能我们可以看下:

1
2
3
4
5
6
7
8
9
10
11
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

很简单, 就是调用 Object.defineProperty 来为 value 这个数据对象定义 ob 属性, 这里需要注意的是为什么要通过 Object.defineProperty, 而不直接通过 value.ob = this ? 先不揭晓答案, 我们只需要先注意这里 def 的调用中 !!enumerable 等同于 false, 也就是 ob 属性在 value 数据对象中是不可枚举的。

继续回到构造函数, 先判断 当前 value 是否为数组, 如果为数组就调用 this.observeArray(value) 来遍历value, 并对遍历的每一项进行递归调用 observe, 目的是保证数组的每一项都能初始化响应式对象。

如果不为数组, 那也就是 value 为对象类型, 调用 walk 方法来遍历 value 的属性。 这时候, 我们会发现如果我们刚才通过 value.ob = this 来定义 value 的 ob 属性, 会导致 ob 是可枚举, 这里遍历可以枚举出 ob, 但是 ob 我们没必要对其进行响应式对象初始化, 所以也就是为什么要通过 Object.defineProperty 把 ob 定义为不可枚举的属性。遍历过程其实就是把当前 value 的每个属性的值传参进行 defineReactive 调用。

响应式对象初始化 – 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
59
60
61
62
63
/**
* 定义一个响应式对象,动态添加getter和setter
*/
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

我们刚才调用了 defineReactive 传入 value 和 keys[i], 这时候会执行 val = obj[key], 于是 val 这时候其实就是 value[key], 也就是数据对象的子属性, 这时候我们发现又会递归调用 observe 来对子属性进行响应式对象的初始化, 当然前提是子属性为非vnode的对象类型。 接着就对 value 的各个属性进行 getter/setter 的定义了, 具体 getter 和 setter 的实现我们这里不展开, 所以响应式对象的遍历初始化就到此结束了。

Vue源码浅析之异步组件注册

Vue

Vue的异步组件注册

Vue官方文档提供注册异步组件的方式有三种:

  1. 工厂函数执行 resolve 回调
  2. 工厂函数中返回Promise
  3. 工厂函数返回一个配置化组件对象

工厂函数执行 resolve 回调

我们看下 Vue 官方文档提供的示例:

1
2
3
4
5
6
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包, 这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})

简单说明一下, 这个示例调用 Vue 的静态方法 component 实现组件注册, 需要了解下 Vue.component 的大致实现

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
// 此时type为component
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
// 是否为对象
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 记录当前Vue的全局components, filters, directives对应的声明映射
this.options[type + 's'][id] = definition
return definition
}
}

先判断传入的 definition 也就是我们的工厂函数, 是否为对象, 都说是工厂函数了, 那肯定不为对象, 于是这里不调用 this.options._base.extend(definition) 来获取组件的构造函数, 而是直接把当前的 definition(工厂函数) 保存到 this.options.components 的 ‘async-webpack-example’ 属性值中, 并返回definition。

接下来发生什么事情呢?
其实刚才我们只是调用了 Vue.component 注册一个异步组件, 但是我们最终是通过 new Vue 实例来实现页面的渲染。这里大致浏览一下渲染的过程:

Run:

  • new Vue执行构造函数
  • 构造函数 执行 this._init, 在 initMixin 执行的时候定义 Vue.prototype._init
  • $mount执行, 在 web/runtime/index.js 中已经进行定义 Vue.prototype.$mount
  • 执行 core/instance/lifecycle.js 中的 mountComponent
  • 实例化渲染Watcher, 并传入 updateComponent(通过 Watcher 实例对象的 getter 触发vm._update, 而至于怎么触发先忽略, 会另外讲解)
  • vm._update 触发 vm._render(renderMixin 时定义在 Vue.prototype._render) 执行
  • 在 vm.$options 中获取 render 函数并执行, 使得传入的 vm.$createElement(在 initRender 中定义在vm中)执行, vm.$createElement也就是平时书写的 h => h(App)这个h函数。
  • vm.$createElement = createElement
  • createComponent 通过 resolveAsset 查询当前组件是否正常注册

所以我们现在以及进入到 createComponent 这个函数了, 看下这里异步组件具体的实现逻辑:

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
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component, // vm实例
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {

// 在init初始化的时候赋值Vue
const baseCtor = context.$options._base

// Ctor当前为异步组件的工厂函数, 所以此步骤不执行
if (isObject(Ctor)) {
// 获取构造器, 对于非全局注册的组件使用
Ctor = baseCtor.extend(Ctor)
}

// async component
let asyncFactory
// 如果Ctro.cid为undefined, 则说明h会是异步组件注册
// 原因是没有调用过 Vue.extend 进行组件构造函数转换获取
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
// 解析异步组件
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
// Ctor为undefined则直接创建并返回异步组件的占位符组件Vnode
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

...此处省略不分析的代码

// 安装组件的钩子
installComponentHooks(data)

// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }, // 组件对象componentOptions
asyncFactory
)

return vnode
}

从源码我们可以看出, 异步组件不执行组件构造器的转换获取, 而是执行 resolveAsyncComponent 来获取返回的组件构造器。由于该过程是异步请求组件, 所以我们看下 resolveAsyncComponent 的实现

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
// 定义在render.js中的全局变量, 用于记录当前正在渲染的vm实例
import { currentRenderingInstance } from 'core/instance/render'

export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
// 高级异步组件使用
if (isTrue(factory.error) && isDef(factory.errorComp)) {...先省略}

if (isDef(factory.resolved)) {
return factory.resolved
}
// 获取当前正在渲染的vm实例
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
// already pending
factory.owners.push(owner)
}

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {...省略}

// 执行该逻辑
if (owner && !isDef(factory.owners)) {
const owners = factory.owners = [owner]
// 用于标记是否
let sync = true

...省略
const forceRender = (renderCompleted: boolean) => { ...省略 }

// once让被once包装的任何函数的其中一个只执行一次
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})

const reject = once(reason => { ...省略 })

// 执行工厂函数, 比如webpack获取异步组件资源
const res = factory(resolve, reject)

...省略

sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}

resolveAsyncComponent 传入异步组件工厂函数和 baseCtor(也就是Vue.extend), 先获取当前渲染的vm实例接着标记sync为true, 表示当前为执行同步代码阶段, 定义 resolve 和 reject 函数(忽略不分析), 此时我们可以发现 resolve 和 reject 都被 once 函数所封装, 目的是让被 once 包装的任何函数的其中一个只执行一次, 保证 resolve 和 reject 两者只能择一并只执行一次。OK, 接着来到 factory 的执行, 其实就是执行官方示例中传入的工厂函数, 这时候发起异步组件的请求。同步代码继续执行, sync置位false, 表示当前的同步代码执行完毕, 然后返回undefined

这里可能会问怎么会返回undefined, 因为我们传入的工厂函数没有loading属性, 然后当前的 factory 也没有 resolved 属性。

接着回到 createComponent 的代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
// 解析异步组件
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
// Ctor为undefined则直接创建并返回异步组件的占位符组件Vnode
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

因为刚才说 resolveAsyncComponent 执行返回了undefined, 所以执行 createAsyncPlaceholder 创建注释vnode

这里可能还会问为什么要创建一个注释vnode, 提前揭晓答案:

因为先要返回一个占位的 vnode, 等待异步请求加载后执行 forceUpdate 重新渲染, 然后这个节点会被更新渲染成组件的节点。

那继续, 刚才答案说了, 当异步组件请求完成后, 则执行 resolve 并传入对应的异步组件, 这时候 factory.resolved 被赋值为 ensureCtor 执行的返回结果, 就是一个组件构造器, 然后这时候 sync 为 false, 所以执行 forceRender, 而 forceRender 其实就是调用 vm.$forceUpdate 实现如下:

1
2
3
4
5
6
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}

$forceUpdate 执行渲染 watcher 的 update 方法, 于是我们又会执行 createComponent 的方法, 执行 resolveAsyncComponent, 这时候 factory.resolved 已经定义过了, 于是直接返回 factory.resolved 的组件构造器。 于是就执行 createComponent 的后续组件的渲染和 patch 逻辑了。组件渲染和 patch 这里先不展开。

于是整个异步组件的流程就结束了。

工厂函数中返回Promise

先看下官网文档提供的示例:

1
2
3
4
5
Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)

由上面的示例, 可以看到当调用Vue.component的时候, definition为一个会返回 Promise 的函数, 与工厂函数执行 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
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
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {

...省略

// 执行工厂函数, 比如webpack获取异步组件资源
const res = factory(resolve, reject)
if (isObject(res)) {
// 为Promise对象, import('./async-component')
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
res.component.then(resolve, reject)

if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}

if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}

if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}

sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}

主要不同点在于执行完 factory 工厂函数, 这时候我们的工厂函数会返回一个 Promise, 所以 res.then(resolve, reject) 会执行, 接下来的过程也是等待异步组件请求完成, 然后执行 resolve 函数, 接着执行 forceRender 然后返回组件构造器。

这里 Promise 写法的异步组件注册过程和执行回调函数没有太大的区别。

工厂函数返回一个配置化组件对象

同样, 看下官网示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})

从上面的示例可以看到, 工厂函数在执行成功后会返回一个配置对象, 这个对象的5个属性我们都可以从官方文档的注释了解到各自的作用。那我们看一下这种方式和前面提到的两种方式的区别在哪里.

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
// 高级异步组件使用
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}

if (isDef(factory.resolved)) {
return factory.resolved
}

...已了解过,省略

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}

if (owner && !isDef(factory.owners)) {
const owners = factory.owners = [owner]
let sync = true
let timerLoading = null
let timerTimeout = null

;(owner: any).$on('hook:destroyed', () => remove(owners, owner))

const forceRender = (renderCompleted: boolean) => {...省略}
// once让被once包装的任何函数的其中一个只执行一次
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})

const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})

// 执行工厂函数,比如webpack获取异步组件资源
const res = factory(resolve, reject)
if (isObject(res)) {
// 为Promise对象, import('./async-component')
if (isPromise(res)) {
...省略
} else if (isPromise(res.component)) {
res.component.then(resolve, reject)

if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}

if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}

if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}

sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}

渲染过程同样来到 resolveAsyncComponent, 一开始判断 factory.error 是否为 true, 当然一开始肯定是 false 的, 不进入该逻辑, 接着同样执行到 const res = factory(resolve, reject) 的执行, 因为我们刚才说了我们的工厂函数返回了一个异步组件配置对象, 于是 res 就是我们定义该工厂函数返回的对象, 这时候 isObject(res) 为 true, isPromise(res) 为 false, isPromise(res.component) 为 true, 接着判断 res.error 是否有定义, 于是在 factory 定义扩展了 errorComp, errorComp是通过 ensureCtor 来对 res.error 的定义组件转化为组件的构造器, loading 也是一样的逻辑, 在 factory 扩展 loadingComp 组件构造器。

接着, 这时候需要特别注意, 当我们定义的 res.delay 为 0, 则直接把 factory.loading 置为 true, 因为这里影响到 resolveAsyncComponent 的返回值。

1
2
3
return factory.loading
? factory.loadingComp
: factory.resolved

当 factory.loading 为 true, 会返回 loadingComp, 使得 createComponet 的时候不是创建一个注释vnode, 而是直接执行 loadingComp 的渲染。

如果我们的 res.delay 不为0, 则会启用一个计时器, 先同步返回 undefined 触发注释节点创建, 在一定的时间后执行 factory.loading = true 和 forceRender(false), 条件是组件没有加载完成以及没有出错 reject, 接着执行把注释vnode 替换为加载过程组件 loadingComp 的渲染。

而 res.timeout 主要用来计时, 当在 res.timeout 的时间内, 如果当前的 factory.resolved 为 undefined, 则说明异步组件加载已经超时了, 于是会调用 reject 方法, reject 其实就是调用 forceRender 来执行 errorComp 的渲染。

OK, 当我们的组件加载完成了, 执行了 resolve 方法, factory.resloved 置为 true, 调用 forceRender 来把注释节点或者是 loadingComp 的节点替换渲染为加载完成的组件。

到此, 我们已经了解三种异步组件的注册过程了。

小结一下

异步组件的渲染本质上其实就是执行2次或者2次以上的渲染, 先把当前组件渲染为注释节点, 当组件加载成功后, 通过 forceRender 执行重新渲染。或者是渲染为注释节点, 然后再渲染为loading节点, 在渲染为请求完成的组件。

这里需要注意的是 forceRender 的执行, forceRender 用于强制执行当前的节点重新渲染, 至于整个渲染过程是怎么样的后续文章有机会的话。。。再讲解吧。

本人语文表达能力有限, 只是突发奇想为了把自己了解到的过程用自己的话语表达出来, 如果有什么错误的地方望多多包涵。