GithubHelp home page GithubHelp logo

thinking-in-deep's People

Contributors

jarweb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

thinking-in-deep's Issues

preact 源码阅读

代码版本

  • preact:10.4.4

应用入口

function MyApp (props) {
  return (
  	<div className="wrap">
      <div className="inner">hi</div>
    </div>
  )
}
render(<MyApp />, root)

经过 babel-transform-jsx 编译后

// 将 jsx 的 dsl 代码转成 createElement 函数调用
// createElement(type, props, children, ...)

function MyApp (props) {
  return createElement(
    'div', // type
    {className: 'wrap'}, // props
    createElement( // children
    	'div',
      {className: 'inner'},
      'hi'
    )
  )
}
render(createElement(MyApp, null, undefined), root)

type 的类型

  • function:class 组件,function 组件,内建组件( 如 Fragment 等)都是 function 类型
  • string:html tag / text (如 : div )

props 上有什么

  • css className ,style
  • key,ref
  • html attribute

children

  • createElement 函数的

整体流程

// vnode
constructor: undefined
key: null
props: {}
ref: null
type: ƒ App() // 组件/dom tag
_children: null
_component: null
_depth: 0
_dom: null
_nextDom: undefined
_original: {props: {}, key: null, ref: null, _children: null, type: ƒ,}
_parent: null

// parentDom => dom root

// 入口 render
function render(vnode, parentDom, replaceNode) {
  ...
  // 包裹一层 fragment
  vnode = createElement(Fragment, null, [vnode])
  ...
  
  let commitQueue = []
  diff(....)
  commitRoot(commitQueue, vnode)
}


function diff(
	parentDom, // dom root
	newVNode, // fragment
	oldVNode, // {}
	globalContext, // {}
	isSvg, // false
	excessDomChildren, // null
	commitQueue, // []
	oldDom, // {}
	isHydrating // false
) {
  ...
  // class 组件
  if ('prototype' in newType && newType.prototype.render) {
  	// 实例化
    newVNode._component = c = new newType(newProps, componentContext);
  } 
  // 函数组件
  else {
  	// 通过 Component 进行实例化,所以类组件和函数组件是没啥区别的,函数组件的没有定义生命函数
  	// fragment 也是函数组件
    newVNode._component = c = new Component(newProps, componentContext);
    c.constructor = newType;
    c.render = (props, state, context) {
      return this.constructor(props, context);
    };
  }
	
  // 往 c 上挂着 props/state/context/_dirty/_rendeCallback 等
  
  // 类组件,执行生命函数 getDerivedStateFromProps(newprops,newstate),得到的返回值合并到 state 上
  
  // 首次渲染阶段
  // 类组件,执行生命函数 componentWillMount,把 componentDidMount 存到 _rendeCallback 数组里
  // 执行组件的 render 函数
  c.render(c.props, c.state, c.context)
  // 当 vnode 是组件/内置组件,执行 diffChildren(...)
  // 当 vnode 是 dom tag,执行 diffElementNodes(...)
  
  // 更新阶段
  // 类组件,执行 componentWillReceiveProps
  // 执行 shouldComponentUpdate
  // 将当前组件存到 commitQueue 里。等待递归子组件后,再拿出来执行其他生命函数
  // 执行 c.componentWillUpdate
  // 执行组件的 render 函数
  // 执行 c.getSnapshotBeforeUpdate(oldProps, oldState)
  // diffChildren(...)
}


function diffChildren(
	parentDom, // dom root
	renderResult, // 父组件 render 函数的返回值,是一个数组,元素是 vnode
	newParentVNode, // 父组件的 vnode
	oldParentVNode, // {}
	globalContext, // {}
	isSvg, // false
	excessDomChildren, // null
	commitQueue, // []
	oldDom, // {}
	isHydrating // false
) {
	// 遍历 renderResult 数组,根据元素的 vnode 类型进行不同的处理
	childVNode = renderResult[i]
	// 如果是 null/boolean,统一转成 null
	// 如果是 string/number,创建新的 vnode,createVNode
	// 如果是 数组,包一层 fragment
	// 如果 vnode._dom / vnode._component 存在,创建新的 vnode
	
	// 绑定 vnode 的 _parent 和 _depth。父 vnode 和 当前 vnode 的层级
	
	// 从 oldParentVNode 里得到 oldChildren ,进行新旧自组件 vnode 的调和。更新阶段
  ...
  newDom = diff(...)
  ...
  // 当前 vnode 的父 parentDom 插入当前 vnode 的 dom
}

function diffElementNodes(
	dom, // undefined
	newVNode, // vnode dom tag 类型
	oldVNode, // {}
	globalContext, // {}
	isSvg, // false
	excessDomChildren, // null
	commitQueue, // []
	isHydrating // false
) { 
	// 首次渲染
	// 创建 dom/text
	// diffProps(...), 绑定 class ,style,dom 属性,事件
	// diffChildren(...)
}


// 首次渲染
App 组件
	<Main></Main>

Main 组件
	<div>hi</div>
	
fragment vnode
App vnode
Main vnode
div vnode
hi vnode

render
 	diff fragment vnode
 		diffChildren App vnode
    	diff App vnode, App 组件实例化,执行生命函数
    		diffChildren Main vnode
    			diff Main vnode,Main 组件实例化,执行生命函数
    				diffChildren div vnode
    					diff div vnode
    						diffElement div vnode, div dom 创建,属性/事件挂载
    							diffChildren div vnode
    								diff text vnode
    									diffElement text vnode,text dom 创建
    										diffChildren text vnode, div append text。root append div。一层层往根 append
    										递归结束,一层层返回...
...
commitRoot,执行剩余的生命函数和 callback


// 类组件生命函数执行
实例化
父 getDerivedStateFromProps
父 componentWillMount,旧的,后面会移除
父 render
子 getDerivedStateFromProps
子 componentWillMount
子 render
子 componentDidMount
父 componentDidMount

// 更新阶段生命函数
父 getDerivedStateFromProps
父 componentWillReceiveProps,旧的,会被移除
父 shouldComponentUpdate
父 componentWillUpdate
父 render
子 getSnapshotBeforeUpdate,在子组件 diff 之前执行
子 getDerivedStateFromProps
子 componentWillReceiveProps,旧的,会被移除
子 shouldComponentUpdate
子 componentWillUpdate
子 render
子 getSnapshotBeforeUpdate,在子组件 diff 之前执行。dom 还没更新。返回值将作为第三参数传给 componentDidUpdate
子 componentDidUpdate
父 componentDidUpdate

// 卸载阶段
unapply ref
父 componentWillUnmout
子 componentWillUnmout


// setState
// 合并更新 state 为 this._nextstate
// 执行 enqueueRender(this) // this 就是当前 setstate 的组件实例

// 组件实例
context: {}
handle: ƒ () // 事件
props: {path: "/", url: "/", matches: {}} // props
state: {count: 1} // 旧的 state
_dirty: false
_force: false
_globalContext: {}
_nextState: {count: 2} // 新的 state
_parentDom: div#app
_renderCallbacks: []
_vnode: {props: {}, key: undefined, ref: undefined, __k: null, type: ƒ, } // 组件对应的 vnode

function enqueueRender(c) {
  // 标记当前组件实例为 _dirty: true
  // 将需要更新的组件存到 rerenderQueue 数组中
  // 执行 defer(process)
  
 // 该组件第一次触发更新时,就会调用 defer(process)
 // 后续的重复 setState 都不会再进入下面逻辑。后续重复 setState 只会直接更新 state,而渲染要等到当前宏任务执行完
  if (
		(!c._dirty &&
			(c._dirty = true) &&
			rerenderQueue.push(c) &&
			!rerenderCount++) ||
		prevDebounce !== options.debounceRendering
	) {
		prevDebounce = options.debounceRendering;
		(prevDebounce || defer)(process);
	}
}

// 在当前宏任务后的微任务中执行更新 process,从而达到批量更新的效果
const defer =
	typeof Promise == 'function'
		? Promise.prototype.then.bind(Promise.resolve())
		: setTimeout;
		
function process() {
	let queue;
	while ((rerenderCount = rerenderQueue.length)) {
		// 将所有需要更新的组件实例排序,从层级小到大,即父到子
		queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth);
		rerenderQueue = [];
		queue.some(c => {
			if (c._dirty) renderComponent(c);
		});
	}
}

// 最外一个脏组件开始更新
function renderComponent(component) {
  diff(...)
  commitRoot(...)
  updateParentDomPointers(...)
}


// hook
function getHookState(index, type) {
	if (options._hook) {
		options._hook(currentComponent, index, currentHook || type);
	}
	currentHook = 0;

	// 初始化组件实例上的 __hooks 属性
	const hooks =
		currentComponent.__hooks ||
		(currentComponent.__hooks = {
			_list: [],
			_pendingEffects: []
		});
		
	// 新的 hook
	if (index >= hooks._list.length) {
		hooks._list.push({});
	}
	
	// 已存在的 hook,直接返回。当组件多次渲染时,hook 不会重复创建
	return hooks._list[index]; // 返回当前的 hook
}


// useState
function useState(initialState) {
	currentHook = 1; // 全局属性
	return useReducer(invokeOrReturn, initialState);
}

// useReducer
function useReducer(reducer, initialState, init) {
	const hookState = getHookState(currentIndex++, 2);
	hookState._reducer = reducer;
	
	// 首次添加,每个 hook 里维护一个组件实例,来判断这个 hook 是否已经维护
	if (!hookState._component) {
		hookState._component = currentComponent;

		// [count, set]
		hookState._value = [
			!init ? invokeOrReturn(undefined, initialState) : init(initialState),

			action => {
				// 计算新的 state
				const nextValue = hookState._reducer(hookState._value[0], action);
				if (hookState._value[0] !== nextValue) {
					// 更新 state
					hookState._value[0] = nextValue;
					// 通过 setState 来触发渲染
					hookState._component.setState({});
				}
			}
		];
	}

	return hookState._value;
}


// useEffect
function useEffect(callback, args) {
	const state = getHookState(currentIndex++, 3);
	// 当依赖变化,会重新存储 useEffect callback
	if (!options._skipEffects && argsChanged(state._args, args)) {
		state._value = callback;
		state._args = args;
		// 当前 hook 所属的组件实例
		currentComponent.__hooks._pendingEffects.push(state);
	}
}

function useLayoutEffect(callback, args) {
	const state = getHookState(currentIndex++, 4);
	if (!options._skipEffects && argsChanged(state._args, args)) {
		state._value = callback;
		state._args = args;

		// 与 useEffect 的区别,执行的时机不一样。在 didmount 时执行
		currentComponent._renderCallbacks.push(state);
	}
}

  
// options,通过 options 上的钩子,在组件的不同生命函数中执行,来达到函数组件也有生命周期
diffed: vnode => {} // 所有组件 diff 完成后执行,在 commitRoot 前
unmount: vnode => {} // 组件卸载时执行
_catchError: ƒ _catchError(error, vnode) // 组件出错时执行
_commit: (vnode, commitQueue) => {} // 组件 didMount 时执行,即 commitRoot
_render: vnode => {} // 组件 render 前执行


// render,在组件 render 前执行
options._render = vnode => {
	// 执行上一次 render
	if (oldBeforeRender) oldBeforeRender(vnode);

	currentComponent = vnode._component;
	currentIndex = 0;

	const hooks = currentComponent.__hooks;
	if (hooks) {
		// 每次执行时,先清除 useeffect 请求副作用
		hooks._pendingEffects.forEach(invokeCleanup);
		// 执行 useeffect
		hooks._pendingEffects.forEach(invokeEffect);
		hooks._pendingEffects = [];
	}
}

function invokeEffect(hook) {
	hook._cleanup = hook._value(); // 副作用清除存到 _cleanup 上
}

function invokeCleanup(hook) {
	if (typeof hook._cleanup == 'function') hook._cleanup();
}

// 在 diff 递归结束后执行,所有组件都 diff 完成。但 commitRoot 之前
options.diffed = vnode => {
	if (oldAfterDiff) oldAfterDiff(vnode);

	const c = vnode._component;
	if (c && c.__hooks && c.__hooks._pendingEffects.length) {
		// 下一帧的 raf 中执行
		// 也是执行 c.__hooks._pendingEffects 里的 effect
		afterPaint(afterPaintEffects.push(c));
	}
};

// 在 commitRoot 时执行,即 dom 插入后。didmount 执行前
// 可认为是函数组件的 didmount 钩子
options._commit = (vnode, commitQueue) => {
	commitQueue.some(component => {
		try {
			// useLayoutEffect 的执行时机
			component._renderCallbacks.forEach(invokeCleanup);
			component._renderCallbacks = component._renderCallbacks.filter(cb =>
				cb._value ? invokeEffect(cb) : true
			);
		} catch (e) {
			commitQueue.some(c => {
				if (c._renderCallbacks) c._renderCallbacks = [];
			});
			commitQueue = [];
			options._catchError(e, component._vnode);
		}
	});

	if (oldCommit) oldCommit(vnode, commitQueue);
};

// 清除副作用
options.unmount = vnode => {
	if (oldBeforeUnmount) oldBeforeUnmount(vnode);

	const c = vnode._component;
	if (c && c.__hooks) {
		try {
			c.__hooks._list.forEach(invokeCleanup);
		} catch (e) {
			options._catchError(e, c._vnode);
		}
	}
}

// hook 对应的组件实例
constructor: ƒ HookDemo()
context: {}
props: {}
render: ƒ doRender(props, state, context)
state: {}
__hooks: {
  _list: [
    {
    	_component: Component {} // 组件实例
    	_reducer: ƒ invokeOrReturn(arg, f) // 
    	_value: (2) [0, ƒ] // [count, set] // 每次 set 后就执行当前组件的 setState 来触发渲染。所以多个更新会合并渲染
    },
    {
      _args: []
			_value: ƒ () // useEffect 的回调函数
    }
  ]
_pendingEffects: [
  {
    _args: []
		_value: ƒ ()
  }
]
__proto__: Object
}
_dirty: false
_globalContext: {}
_nextState: {}
_parentDom: div.page
_renderCallbacks: []
_vnode: {props: {}, key: null, ref: null, _children: null, type: ƒ,}


// 分组
useState, useReduce
useEffect
useLayoutEffect, useImperativeHandle
useRef, useMemo, useCallback
useContext
useErrorBoundary


// hook 的生命周期顺序
函数组件实例化
// getDerivedStateFromProps
// componentWillMount
options._render // hook useEffect
render
// 子 diff
options.diffed // hook useEffect,下一帧 raf 中执行
options._commit // hook useLayoutEffect
// componentDidMount


// ref
当该组件内的所有子组件都递归完成后,把子组件的dom插入到父的dom内后
进行 ref  dom 实例的绑定
ref 绑定在 compnentDidMount 之前全部完成
更新阶段,在 ref 绑定之前会进行组件的移除(如果存在)

// context
const MyContext = createContext('a')

<MyContext.Provider value="b">
  <Demo />
</MyContext.Provider>

<MyContext.Consumer>
  {value => (
      <div>{value}</div>
  )}
</MyContext.Consumer>

// 当 diff 到 Provider 组件时,Provider 的 vnode 结构:
constructor: undefined
key: undefined
props: {value: "b", children: {}} // value 是 Provider 接收的数据
ref: undefined
type: Provider(props) {...}
	_contextRef: {_id: "__cC0", _defaultValue: "a", Consumer: ƒ, Provider: ƒ} // Provider 函数的属性
_children: null

// Provider 组件也是函数组件,也是通过 Component 来实例化
// 得到的实例是:
constructor: Provider(props) {...}
context: {}
props: {value: "b", children: {}} // props.value 是 Consumer 组件要拿到的值
render: ƒ doRender(props, state, context) // render 返回子组件 Demo

globalContext = assign(assign({}, globalContext), c.getChildContext()) // 返回当前的组件实例和 contextid 的映射对象

// globalContext
__cC0: Component {...} // 通过 id 拿到对应的组件实例

this.getChildContext = () => {
  ctx[context._id] = this;
  return ctx;
}

// globalContext 会通过 diff 传给子组件,然后挂到子组件的实例上
c.context = componentContext;
c._globalContext = globalContext;

// 当 diff 到 Consumer 组件时,Consumer 组件的 vnode:
constructor: undefined
key: null
props: {children: ƒ}
ref: null
type: ƒ Consumer(props, context)
   contextType: {_id: "__cC0", _defaultValue: "a", Consumer: ƒ, Provider: ƒ} // Consumer 函数上的属性
                  
// 通过 diff 传入的 globalContext 和 contextType 上的 _id 可以拿到 Provider 组件实例的 props。
let componentContext = tmp // componentContext 就是 props.value
  ? provider
    ? provider.props.value
    : tmp._defaultValue
  : globalContext;
                  
// 当 consumner 组件实例化时,c = new Component(newProps, componentContext) // value 作为 context 参数传入给 Component 进行实例化
context: "b"
props: {children: ƒ}

// Provider 组件实例收集了 Consumer 组件的实例。当 provider 上的 context value 变化时,会触发 consumer 组件实例更新
if (provider) provider.sub(c)
                  
c.context = componentContext; // Consumer 实例的 context 
c._globalContext = globalContext;

// render,即 Consumer 函数执行。就是一个 render props
c.render(c.props, c.state, c.context)

// Consumer 组件定义
Consumer(props, context) {
  return props.children(context);
}

// context 是如何更新的?
// 当 Provider 组件的 props 更新时,会触发 Provider 组件的更新,会执行 shouldComponentUpdate 生命函数
this.shouldComponentUpdate = _props => {
  if (this.props.value !== _props.value) { // 浅比较,有性能优化
    // 之前 Provider 组件实例收集的 Consumer 组件实例
    subs.some(c => {
      c.context = _props.value; // 更新 context 的值
      enqueueRender(c); // 触发渲染。当 context 频繁更新时,也会合并渲染
    });
  }
}
// vnode
constructor: undefined
key: null
props: {}
ref: null
type: ƒ App() // 组件函数/类
_children: null // 子组件 vnode,组件实例化后 render 的返回值
_component: null // 组件实例
_depth: 0 // 层级
_dom: null // dom 实例
_nextDom: undefined // 新的 dom 实例
_original: {props: {…}, key: null, ref: null, _children: null, type: ƒ, …} // vnode 备份
_parent: null // 父组件 vnode,子组件 vnode 通过 _parent 一层层往上连接父 vnode
// 组件实例
base: div.page // dom 实例
constructor: ƒ Fragment(props)
context: {} // context value
props: {children: Array(1)}
render: ƒ doRender(props, state, context) // render 函数
state: {}
_dirty: false // 是否需要更新
_force: false // 是否强制更新
_globalContext: {} // 组件实例和 context id 的映射
_nextState: {}
_parentDom: div#app // 父的 dom 实例,父创建好了才创建子的 dom 实例
_renderCallbacks: [] // render 后的生命函数/callback
_vnode: {props: {…}, key: null, ref: null, _children: Array(1), type: ƒ, …} // 对应的 vnode

image-20200630160442884

// setState 总是批量的异步的?
setState 会直接更新 state,当渲染总是在当前宏任务后的微任务中更新
当两个 setTimeout 进行 setstate, 这些渲染更新会分成两次
当在 一个 setTimeout 里多次 setstate,那么渲染更新是批量的
 react 有很大的不同

认识 tree shaking?

认识 tree shaking ?

  • rollup 可以做到很好的 tree shaking 效果。相比 webpack,体积会小很多。
    • rollup 会对代码进行分析优化
    • 库开发打包尽量使用 rollup
  • webpack 是根据 babel 7 在编译时给代码插入一些标记,如 */\*#__PURE__\*/*,当代码在压缩过程中,压缩工具会找到这些标记的代码,然后移除掉相关。但效果没有 rollup 好,会有一些 webpack 的运行时代码打包进去。
  • js tree shaking 的实现前提
    • ECMAScript 6 模块加载是静态的,可以通过静态解析语法树来推导整个依赖树。与CommonJS 模块不同
    • 只能在顶层出现
    • 模块名只能是常量,不能改,不可变
    • 静态分析不执行代码,只从字面量上对代码进行分析
  • 为了达到 tree shaking 效果,需要怎么做?
    • 避免 babel 转换 es module,因为 tree shaking 是在 es module 的静态依赖分析上实现的。通过配置 module:false
    • 让 webpack 尽量读取 esmodule 的第三方包的入口文件
      • 库作者需要提供不同模块的打包,同时在 package.json 中表明入口读取
        • main, jsnext, module, umd, cjs
    • 多写纯函数,避免副作用产生。tree shaking 无法优化含副作用的代码
    • 当文件中有副作用,且 tree shaking 移除这些代码没问题,可以在 package.json 中 配置 sideEffects: false,来告知 webpack 可以进行 tree shaking
      • sideEffect: false,可以理解为,没有除顶层模块以外的代码交互。
      • 副作用怎么理解?
        • 一个函数会、或可能会对函数外部变量产生影响的行为。
    • 导入需要的函数,import {xxx} from 'xxx',不要全部导入
    • scope hosting 提升作用域也可以很好的进行 tree shaking。
      • 将不同模块的代码一到同一个作用域下,然后过滤掉没使用的代码,同时也减少作用域查找和内存消耗
    • 手动添加 pure flag 可以做到 tree shaking
  • 什么代码会被移除?
    • 无使用变量/函数,只写入不读的对象
    • 不能执行的分支
  • 什么代码不会被移除?
    • 产生有副作用的代码不能被移除
    • 类中的无用代码没办法处理
    • iife 里的无用代码不能处理
      • 由于 babel 会把类用 iife 包裹起来,这样导致没用到的类不能进行 tree shaking 掉。可以设置 babelrc presets:[["env",{"loose": false}]]来启用宽松的编译模式,这样不会用 iife 的模式来编译类
  • webpack 中 tree shaking 的问题
    • 可以看这篇文章 https://juejin.im/post/5a5652d8f265da3e497ff3de
    • babel 编译会产生副作用,uglify 也会产生副作用,同时不会分析代码优化
    • 应用先进行 tree shaking ,再通过 babel/uglify 是可以解决这个问题的,但依赖的库不行。
  • 现在很多库的开发都是只有 umd/cjs 的产物。
    • 例如 lodash,可以通过 import clone from 'lodash/clone'来避免整个包引入
    • antd 也是。可以通过 babel-import 插件来按需引入。原理是通过修改我们代码的引入方式。如:import {Button} from 'antd' 在编译时被改成import Button from 'antd/lib/button'
    • rollup 的提案是在 package.json 里增加一个 module 字段表示这个入口时 es module 的代码。让应用直接使用 es module 的代码来编译。
      • 但这会产生另外的问题,很多应用构建未来提升编译速度,会忽略 node_module 的代码,导致浏览器会出现兼容性问题。所以库还要对代码进行编译,但保持 es module
      • 所以库的打包,还是建议使用 rollup
        • 比 webpack 更好的 tree shaking
        • 支持到处 es 模块
      • 也可以使用 babel 来导出 es 模块,但 babel 不能 tree shaking。babel + webpack

preact-router 源码阅读

版本

  • preact-router:3.2.1

大概原理

// Router 组件
<Router history={...} defaultUrl={...} onChange={...}>
	<A path="/a" />
  <B path="/b" />		
  <C path="/c" />
</Router>

class Router extends Component {
  contructor(props) {
    super(props)
    this.state = {
    url: getCurrentUrlFromLocation(props.history) // 维护当前 url,通过 url 匹配相应的视图组件
  }
  }
  
  shouldComponentUpdate() {...} // 优化性能,避免重复渲染
  
  componentDidMount() {
  	// history 监听变化,触发路由跳转
  	// 这里是关键,相关逻辑在 history 库
  	// 通过 history 库,很容易实现一个简单前端路由
  	// browser router:监听 popstate 事件
  	// hash router:监听 hashchange 事件
  	this.unlisten = this.props.history.listen((location) => {
			const url = getUrlFromLocation(location)
			this.routeTo(url)
    })
  }
  
  componentWillUnmount() {
    this.unlisten()
  }
  
  routeTo(url) {
    this.setState({url}) // 通过 setstate 来触发视图渲染
  }
  
  getMatchingChildren(children, url) {
    // 通过 url 找到 children 数组里匹配的组件
    const matches = exec(...) // 匹配函数
    const newProps = {url, matches}
    cloneElement(vnode, newProps)
    return vnode
  }
  
  render() {
    let active = this.getMatchingChildren(this.props.children, this.state.url)
    if (url !=== this.prevUrl) {
    	this.preUrl = url
      this.props.onChange(...)
    }
    return active
  }
}

function getCurrentUrlFromLocation(history) {
  // 从 history.location / history.getCurrentLocation() / window.location 上批接出简单的 url
  url = pathname + search
}


// 触发 url 改变的方法
// 都是封装过的方法,与原生 history 的方法不一样,不会触发浏览器刷新
// 组件型
Link 组件
// 方法调用型
history.push(...)
history.replace(...)
history.go(n)
history.goBack()
history.goForward

// Link 组件
a 标签,监听 click 事件,阻止默认事件,通过 history.replace/push 来改变 url
 a 标签带有 target 为非 _self 值,Link 组件会失效

// Redirect 组件
<Redirect from="*" to="/bar" />
其实就是在 willmount 的时候切换到指定的路由
自身 render 返回值是 null

popstate、hashchange 事件

  • popstate
    • 当 history.back() / history.forward() / history.go(n) n 不为 0,n为0 会强制刷新页面 / 点击浏览器前进后台/页面加载(chrome/safari)时,会触发 popstate 事件,url 会改变,页面会刷新,会请求服务器
      • 当触发 popstate 时,相当于刷新整个页面,Router 组件会重新渲染
    • 当 history.pushState() / history.replaceState() 时,不会触发 popstate。但 url 会改变,页面不会刷新,不会请求服务器
      • 当 spa 需要试图跳转时,会通过 history.push(path, state) / history.replace(path, state),其实就是上面原生 history 方法的封装。当 url 改变时,通过新的 url setState 来触发视图渲染。这种情况下浏览器也不会刷新,所以也不会请求服务器
  • hashchange
    • 当 hash 变化时,会触发 hashchange 事件,url 会改变,页面不会刷新
      • url 改变时,通过新的 url setState 来触发视图渲染

browser router 和 hash router 的区别

  • browser router
    • H5 的特性,有一定的兼容性问题
    • 需要每个前端路由都有后端路由相对应
      • 首次访问时,该路由会请求到服务器,服务器需要针对所有的页面路由都返回同一个 html。然后有前端来根据 url 显示相应的视图。后续的路由切换都是在前端完成的,不会发起服务请求。
      • 所以除了资源放 cdn,还需要 nginx 配置一下转发
  • hash router
    • 兼容性相对好些
    • 不需要服务端做路由转发,直接放 cdn 就好了
    • 页面分享/callback 定向/支付/sdk签注,可能会丢失 hash 部分
      • 需要做一层转换,将 hash 换成 search,页面打开时再根据 search 转成相应的 hash

webpack 模块加载分析

// 简单版的 webpack runtime,有很多 runtime 函数没有加入的
// modules 是所有的模块集合,通过 url 为 key,模块函数为 value

(function (modules) {
  // The module cache
  var installedModules = {};

  // 根据 moduleId 返回对应的模块对象
  function require(moduleId) {
    // 缓存中是否存在
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    // 构建初始对象
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false, // 是否已缓存
      exports: {}
    };

    // 在所有的模块里找到相应模块,传入一些参数执行
    modules[moduleId].call(module.exports, module, module.exports, require);
    // 执行完后,标记该模块已缓存
    module.l = true;
    // 返回该模块出去
    return module.exports;
  }

  // 将所有模块挂到 require 函数上
  require.m = modules;
  // 将缓存的模块挂到 require 函数上
  require.c = installedModules;
  // 定义一个设置 getter 的函数,并挂到 require 函数上
  require.d = function (exports, name, getter) {
    if (!require.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };
  // define __esModule on exports
  require.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  require.t = function (value, mode) {
    if (mode & 1) value = require(value);
    if (mode & 8) return value;
    if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    require.r(ns);

    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if (mode & 2 && typeof value != 'string') for (var key in value) require.d(ns, key, function (key) { return value[key]; }.bind(null, key));
    return ns;
  };

  // getDefaultExport function for compatibility with non-harmony modules
  require.n = function (module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    require.d(getter, 'a', getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  require.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // __webpack_public_path__
  require.p = "";

  // Load entry module and return exports
  return require(require.s = "./index.js"); // 执行入口文件
})({
  "./a.js": (function (module, export, require) {
    eval("
      require.r(export);
      require.d(export, \"add\", function() { return add; });
      var bjs = require(\"./b.js\");
      function add(a, b) {return Object(bjs[\"sum\"])() + a + b};
    ");
  }),
  "./b.js": (function (module, export, require) {
    eval("
      require.r(export);
      require.d(export, \"sum\", function() { return sum; });
      function sum() {return 100}
    ");
  }),
  "./index.js": (function (module, export, require) {
    eval("
      require.r(export);
      var ajs = require(\"./a.js\");
      Object(ajs[\"add\"])(1, 2)
    ");
  })
});
          
                          
// 构建初始对象
// {
//   i: "./index.js", // url 就是 moduleId
//   l: false,
//   exports: { }
// }
// 
// modules[moduleId].call(module.exports, module, module.exports, require);
// modules[moduleId] 是一个包装过的函数
// 
// function (module, export , require) {
//   eval("
//     require.r(export);
//     var ajs = require(\"./a.js\");
//     Object(ajs[\"add\"])(1, 2)
//  ");
// }
// 该函数绑定 this 为初始的模块对象,这里是空对象 {}
// 传入参数为:module, module.exports, require
//   eval("
//     require.r(export); // 为模块对象定义一个 esModule 的 key
//     var ajs = require(\"./a.js\"); // 加载当前模块的依赖 ajs,会一直加载依赖的依赖
//     Object(ajs[\"add\"])(1, 2) // 执行模块
//  ");
// 
// var ajs = require(\"./a.js\");
// eval("
//     require.r(export ); // 为模块对象定义一个 esModule 的 key
//     require.d(export, \"add\", function() { return add; }); // 为模块的 add 属性定义一个 getter 属性,当模块被调用时,读取模块上的 add
//     var bjs = require(\"./b.js\"); // 加载当前的模块依赖 bjs
//     function add(a, b) {return Object(bjs[\"sum\"])() + a + b};
// ");
// 
// var bjs = require(\"./b.js\");
// eval("
//     require.r(export ); // // 为模块对象定义一个 esModule 的 key
//     require.d(export, \"sum\", function() { return sum; }); // 为模块的 add 属性定义一个 getter 属性
//     这是最后的依赖模块,没有依赖了,然后回去执行入口模块的逻辑
//     function sum() { return 100 } 
// ");
// 复杂版的 runtime
(function (modules) {
	// 这个函数很重要,用来加载初始化模块的
	// 所有后续加载的模块都是通过 window["webpackJsonp"].push(...) 来实现的
	// 这个 push 函数就是 webpackJsonpCallback
  // 当 push 一个 data 时,其实就是执行,维护一个当前 chunk 的数组,数组的每个元素都是模块函数,通过索引来查找,索引是模块的 id,所以该数组不是连续的
	// data:是什么?push([['chunkid'],{该 chunk 包含的所有模块}, ['入口模块id',‘依赖’]])
	function webpackJsonpCallback(data) {
		var chunkIds = data[0];
		var moreModules = data[1];
		var executeModules = data[2];

		// add "moreModules" to the modules object,
		// then flag all "chunkIds" as loaded and fire callback
		var moduleId, chunkId, i = 0, resolves = [];
		for (; i < chunkIds.length; i++) {
			chunkId = chunkIds[i];
			if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
				resolves.push(installedChunks[chunkId][0]);
			}
			installedChunks[chunkId] = 0;
		}
		for (moduleId in moreModules) {
			if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
				modules[moduleId] = moreModules[moduleId];
			}
		}
		if (parentJsonpFunction) parentJsonpFunction(data);

		while (resolves.length) {
			resolves.shift()();
		}

		// add entry modules from loaded chunk to deferred list
		deferredModules.push.apply(deferredModules, executeModules || []);
		// run deferred modules when all chunks ready
		return checkDeferredModules();
	};

	function checkDeferredModules() {
		var result;
		for (var i = 0; i < deferredModules.length; i++) {
			var deferredModule = deferredModules[i];
			var fulfilled = true;
			for (var j = 1; j < deferredModule.length; j++) {
				var depId = deferredModule[j];
				if (installedChunks[depId] !== 0) fulfilled = false;
			}
			if (fulfilled) {
				deferredModules.splice(i--, 1);
				result = webpackRequire(webpackRequire.s = deferredModule[0]);
			}
		}
		return result;
	}

	// The module cache
	var installedModules = {};
	// object to store loaded and loading chunks
	// undefined = chunk not loaded, null = chunk preloaded/prefetched
	// Promise = chunk loading, 0 = chunk loaded
	var installedChunks = {
		"runtime": 0
	};
	var deferredModules = [];

	// script path function
	function jsonpScriptSrc(chunkId) {
		return webpackRequire.p + "static/js/" + ({}[chunkId] || chunkId) + "." + { "1": "bbc47b9f", "2": "9b6415cc", "3": "a9a988f4" }[chunkId] + ".chunk.js"
	}

	// The require function
	function webpackRequire(moduleId) {
		// Check if module is in cache
		if (installedModules[moduleId]) {
			return installedModules[moduleId].exports;

		}
		// Create a new module (and put it into the cache)
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}

		};

		// Execute the module function
		modules[moduleId].call(module.exports, module, module.exports, webpackRequire);

		// Flag the module as loaded
		module.l = true;

		// Return the exports of the module
		return module.exports;
	}

	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	webpackRequire.e = function requireEnsure(chunkId) {
		var promises = [];
		// JSONP chunk loading for javascript
		var installedChunkData = installedChunks[chunkId];
		if (installedChunkData !== 0) { // 0 means "already installed".

			// a Promise means "currently loading".
			if (installedChunkData) {
				promises.push(installedChunkData[2]);

			} else {
				// setup Promise in chunk cache
				var promise = new Promise(function (resolve, reject) {
					installedChunkData = installedChunks[chunkId] = [resolve, reject];

				});
				promises.push(installedChunkData[2] = promise);

				// start chunk loading
				var script = document.createElement('script');
				var onScriptComplete;

				script.charset = 'utf-8';
				script.timeout = 120;
				if (webpackRequire.nc) {
					script.setAttribute("nonce", webpackRequire.nc);

				}
				script.src = jsonpScriptSrc(chunkId);

				// create error before stack unwound to get useful stacktrace later
				var error = new Error();
				onScriptComplete = function (event) {
					// avoid mem leaks in IE.
					script.onerror = script.onload = null;
					clearTimeout(timeout);
					var chunk = installedChunks[chunkId];
					if (chunk !== 0) {
						if (chunk) {
							var errorType = event && (event.type === 'load' ? 'missing' : event.type);
							var realSrc = event && event.target && event.target.src;
							error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
							error.name = 'ChunkLoadError';
							error.type = errorType;
							error.request = realSrc;
							chunk[1](error);
						}
						installedChunks[chunkId] = undefined;
					}
				};
				var timeout = setTimeout(function () {
					onScriptComplete({ type: 'timeout', target: script });
				}, 120000);
				script.onerror = script.onload = onScriptComplete;
				document.head.appendChild(script);
			}
		}
		return Promise.all(promises);
	};

	// expose the modules object (__webpack_modules__)
	webpackRequire.m = modules;

	// expose the module cache
	webpackRequire.c = installedModules;

	// define getter function for harmony exports
	webpackRequire.d = function (exports, name, getter) {
		if (!webpackRequire.o(exports, name)) {
			Object.defineProperty(exports, name, { enumerable: true, get: getter });
		}
	};

	// define __esModule on exports
	webpackRequire.r = function (exports) {
		if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
		}
		Object.defineProperty(exports, '__esModule', { value: true });
	};

	// create a fake namespace object
	// mode & 1: value is a module id, require it
	// mode & 2: merge all properties of value into the ns
	// mode & 4: return value when already ns object
	// mode & 8|1: behave like require
	webpackRequire.t = function (value, mode) {
		if (mode & 1) value = webpackRequire(value);
		if (mode & 8) return value;
		if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
		var ns = Object.create(null);
		webpackRequire.r(ns);
		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
		if (mode & 2 && typeof value != 'string') for (var key in value) webpackRequire.d(ns, key, function (key) { return value[key]; }.bind(null, key));
		return ns;
	};

	// getDefaultExport function for compatibility with non-harmony modules
	webpackRequire.n = function (module) {
		var getter = module && module.__esModule ?
			function getDefault() { return module['default']; } :
			function getModuleExports() { return module; };
		webpackRequire.d(getter, 'a', getter);
		return getter;
	};

	// Object.prototype.hasOwnProperty.call
	webpackRequire.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

	// __webpack_public_path__
	webpackRequire.p = "/static/app/";

	// on error function for async loading
	webpackRequire.oe = function (err) { console.error(err); throw err; };

        // push 其实就是 runtime 里的 webpackJsonpCallback
	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
	jsonpArray.push = webpackJsonpCallback;
	jsonpArray = jsonpArray.slice();
	for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
	var parentJsonpFunction = oldJsonpFunction;

	// run deferred modules from other chunks
	checkDeferredModules();
})([]);
// 一般打包出来的模块形式:
// polyfill.js // dll
// vender.js // dll
// inline runtime 
// jsonp chunk // 通过 jsonp 形式加载的
// jsonp chunk
// ...

模块数组

  • 索引是模块 id,值的模块函数,该数组是不连续的
  • 应用的所有模块都在这个数组上
  • 当入口执行时,需要用到的依赖会在这个数组里,通过 require 传给入口函数
  • 每个文件就是一个模块
  • 开发环境中热更新是通过替换这个数组上相应索引的值来实现的。即 webpack 的热更新是模块级别的。没有 diff-patch

image-20200527165646696

一个异步加载示例

// jsonp 加载的模块
window["webpackJsonp"].push([
	["offLine"], // 当前的模块名称
	{
                // 依赖
		"./src/assets/onLineTest.txt": (function (module, exports, __webpack_require__) {
			module.exports = __webpack_require__.p + "static/media/onLineTest.d41d8cd9.txt";
		}),
                // 模块源码
		"./src/utils/offLine.ts": (function (module, exports, __webpack_require__) {
			function testOnLine() {}

			if (window.navigator) {
				setTimeout(function () {
					testOnLine()
				}, 2000);
			}
		})
	},
	[["./src/utils/offLine.ts", "runtime"]]
]);

复杂的异步加载示例

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
	["guard_pay"], // 当前模块名称
	{
    // react-dom 依赖
		"./node_modules/react-dom/index.js": (function (module, exports, __webpack_require__) {
			module.exports = (__webpack_require__("dll-reference vendor_6f16b710b6ec15e318df"))(495);
		}),
		// react 依赖
		"./node_modules/react/index.js": (function (module, exports, __webpack_require__) {
			module.exports = (__webpack_require__("dll-reference vendor_6f16b710b6ec15e318df"))(163);
		}),

		// .... 忽略很多依赖

		"./src/utils/throttle.ts": (function (module, __webpack_exports__, __webpack_require__) {
			__webpack_require__.r(__webpack_exports__);
			__webpack_require__.d(__webpack_exports__, "default", function () { return throttle; });

			function throttle(fn, wait) { var timer; return function () { if (timer) return; fn.apply(void 0, arguments); timer = setTimeout(function () { clearTimeout(timer); timer = null; }, wait || 500); }; }
		}),

		// .... 忽略很多依赖

		"./src/apps/GuardPay.tsx": function (module, __webpack_exports__, __webpack_require__) {
			__webpack_require__.r(__webpack_exports__);
			// 入口文件的依赖
			var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./node_modules/react/index.js");
			var react__WEBPACK_IMPORTED_MODULE_1___default = __webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);
			var react_dom__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("./node_modules/react-dom/index.js");
			var react_dom__WEBPACK_IMPORTED_MODULE_2___default = __webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_2__);
			var _ErrorBoundary__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("./src/ErrorBoundary.tsx");
			var _pages_VipCenter_GuardPay__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__("./src/pages/VipCenter/GuardPay/index.tsx");
			var _hooks_useDebug__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__("./src/hooks/useDebug/index.ts");
			var _hooks_useDocumentTitle__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__("./src/hooks/useDocumentTitle/index.ts");
			// app 组件
			var App = Object(_utils_checkNetWork__WEBPACK_IMPORTED_MODULE_3__["default"])(
				function () {
					Object(_hooks_useDocumentTitle__WEBPACK_IMPORTED_MODULE_9__["default"])('title');
					Object(_hooks_useDebug__WEBPACK_IMPORTED_MODULE_8__["default"])();

					return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div",
						{ className: _pages_VipCenter_GuardPay_style_module_scss__WEBPACK_IMPORTED_MODULE_5___default.a.page },
						react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(_pages_VipCenter_GuardPay__WEBPACK_IMPORTED_MODULE_7__["default"], null));
				}
			);
      
      // render dom	
react_dom__WEBPACK_IMPORTED_MODULE_2___default.a.render(react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(_ErrorBoundary__WEBPACK_IMPORTED_MODULE_6__["default"], null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(App, null)), document.getElementById('root'));
		}),

    // 入口文件,一般标记为 0 
		0: (function (module, exports, __webpack_require__) {
			module.exports = __webpack_require__("./src/apps/GuardPay.tsx");
		}),

		"dll-reference lib_e4f1cebcccf7c26ba350": (function (module, exports) {
			module.exports = lib_e4f1cebcccf7c26ba350;
		}),

		"dll-reference polyfill_73fb46439efc9a918d08": (function (module, exports) {
			module.exports = polyfill_73fb46439efc9a918d08;
		}),

		"dll-reference vendor_6f16b710b6ec15e318df": (function (module, exports) {
			module.exports = vendor_6f16b710b6ec15e318df;
		})
	}, 
	[[0, "runtime", "babelHelper", 0]]
]);

关于业务开发者出路的思考

业务开发的特点

  • 迭代快
  • 需求变化不定

对开发者的要求

  • 开发效率
  • 开发流程标准化
  • 开发工作台、协作平台、流水线工具
  • 开发质量
  • 检查工具
  • 项目约定
  • code review
  • 开发者能力
  • 技术实践
  • 解决方案、架构沉淀总结
  • 造轮子
  • 产品思维
  • 与设计师沟通用户体验
  • 与产品经理沟通需求目的
  • 为什么要做这个需求?有没有更好的方案?有没更友好的界面表现?
  • 数据驱动,ab 测试
  • 商业闭环,流量闭环

关于方案沉淀

  • 经验的提炼,一类问题的解决方法
  • 例如:
    • cli、开发工作台
    • 接口平台
    • 项目管理
    • 发布平台
    • 监控平台
    • 代理平台
    • 低代码,可视化搭建
      • 云凤碟、飞冰等
    • 中后台标准化,运营活动
    • 面向组件的发布模式
      • 组件区块开发维护
      • 组件区块动态策略发布
    • 组件库
    • 框架
    • 工具库
    • sdk
      • jsbsdk,埋点,上报,错误/性能收集
    • 架构
      • spa,app shell,ssr,progress ssr
      • 微前端,serverless,bff
    • 接口通信
      • restfull,grathql
    • ....

不能在业务中一直做重复的事情,需要站在业务对开发者的要求的角度,看自己能在业务开发的同时能做哪些方面的深入沉淀

webpack 热更新分析

webpack 热更新分析

  • webpack-dev-server hot + hotModuleReplacePlugin + module.hot.accept

    • 需要有 module.hot.accept 才会进行热更,不然执行的是兜底方式 location.reload()
  • 热更新过程?

    • 监听到有更新,生成 xxx.hot-update.json 和 xxx.hot-update.js
    • 通过 hot-update.json 告知 socket client 更新的 hash 和 moduleId。然后 socket client 通过 jsonp 形式请求 hot-update.js,里面是更新的模块。
    • 这些新的模块 eval 后会挂到一个全局的模块管理数组上,然后通过 webpackrequire 执行相关模块就可以达到热更新效果了
    • webpack 会重新编译文件并发消息通知浏览器,浏览器在 check 之后触发 webpackHotUpdateCallback
  • socket 通信过程?

    • 服务端 hot-middleware 发送 sync 告知浏览器 hash
    • 服务端 hot-middleware 发送 build 告知浏览器 hash ,表示编译完成
    • 浏览器收到 hash 向服务器请求 hot-update.json
    • 服务端返回 hash 和 moduleId
    • 浏览器通过 jsonp 发起更新代码 hot-update.js 文件请求
    • 浏览器更换执行模块代码进行热更新
  • 拿到 hot-update.js 后是怎样更新的?

    • webpackHotUpdate("main", {
      	"./node_modules/css-loader/dist/cjs.js?!./node_modules/postcss-loader/src/index.js?!./src/index.css": (function (module, exports, __webpack_require__) {
      		var ___CSS_LOADER_API_IMPORT___ = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js");
      		exports = ___CSS_LOADER_API_IMPORT___(false);
      
      		// 这是一整个 module,不是某个代码片段,看上去是按一个个文件来的
      		exports.push([module.i, "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n    color: yellow;\n}\n", ""]);
      
      		module.exports = exports;
      	})
      })
    • function webpackHotUpdateCallback(chunkId, moreModules) {
      		hotAddUpdateChunk(chunkId, moreModules);
      		if (parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules);
      	}
      
      function hotAddUpdateChunk(chunkId, moreModules) {
      		if (!hotAvailableFilesMap[chunkId] || !hotRequestedFilesMap[chunkId])
      			return;
      		hotRequestedFilesMap[chunkId] = false;
      		for (var moduleId in moreModules) {
      			if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      				hotUpdate[moduleId] = moreModules[moduleId];
      			}
      		}
      		if (--hotWaitingFiles === 0 && hotChunksLoading === 0) {
      			hotUpdateDownloaded();
      		}
      	}
      	
      function hotUpdateDownloaded() {
      		hotSetStatus("ready");
      		var deferred = hotDeferred;
      		hotDeferred = null;
      		if (!deferred) return;
      		if (hotApplyOnUpdate) {
      			// Wrap deferred object in Promise to mark it as a well-handled Promise to
      			// avoid triggering uncaught exception warning in Chrome.
      			// See https://bugs.chromium.org/p/chromium/issues/detail?id=465666
      			Promise.resolve()
      				.then(function() {
                                            // 热更逻辑
      					return hotApply(hotApplyOnUpdate);
      				})
      				.then(
      					function(result) {
      						deferred.resolve(result);
      					},
      					function(err) {
      						deferred.reject(err);
      					}
      				);
      		} else {
      			var outdatedModules = [];
      			for (var id in hotUpdate) {
      				if (Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
      					outdatedModules.push(toModuleId(id));
      				}
      			}
      			deferred.resolve(outdatedModules);
      		}
      	}
      	
      
      // 更新的逻辑在这里,在 hotModuleReplacementRuntime.js 里
      function hotApply(options) {...}
      
      // 简单来说:
      // 将 hotUpdate 上对应的模块挂到 modules 对象上
      appliedUpdate[moduleId] = hotUpdate[moduleId];
      modules[moduleId] = appliedUpdate[moduleId];
      // 删除旧的模块
      // 执行 module.hot.accept 回调函数队列,因为可以注册多个 accept 函数
      cb = module.hot._acceptedDependencies[dependency]
      cb(moduleOutdatedDependencies)
  • 一些相关的库?

    • webpack-dev-server
      • 负责启动服务,和前置的准备工作
      • 实例化 webpack compiler,进行构建
      • 有 server 和 client 两部分的代码,client 的代码就是 socket 的客户端部分,需要在 entry 里配置打包到一起
      • 启动 express 本地服务,和 websocket,监听 webpack 的编译结果,然后通过 socket 通知浏览器,浏览器再根据 socket 拿到的相关信息进行静态文件请求
    • webpack-dev-middleware
      • 负责本地文件的编译,输出,监听
    • hotModuleReplacementPlugin
      • webpack 在浏览器进行热更新逻辑的代码,会打包到 bundle runtime 里

怎么做前端工程化?

什么是前端工程化

  • 工具自动化、经验规范化、标准化、流程化、集成化

为什么需要工程化?

  • 为了效率、成本、质量、协作、降低风险

前端开发的一般流程

  • 1、需求文档
    • 需求文档平台
    • 介入需求,优化需求,底层技术抽象,技术架构拓展性,技术架构分层
  • 2、需求评审
    • 优化需求,需求讨论
  • 3、技术调研
    • 技术讨论,demo 实现,方案确定
  • 4、项目初始化
    • 自动化
    • 项目初始化脚手架
    • 项目初始化平台,可视化界面操作
    • 打包编译工具、代码风格检查、目录设计、业务框架、项目模版、通用 sdk 注入、技术栈、代码提交规范、通用包依赖下载、性能优化、代码拆分、环境变量
  • 5、 接口平台
    • 自动化
    • 平台化
    • 接口文档阅读
    • 服务端表设计、自动生成字段、自动导入接口平台、导出 ts 类型、导出 js 请求模版代码
    • 接口 mock
    • 接口测自动化试
    • 环境代理
  • 6、代码编写、构建编译、data mock、用例自测、代码提交、code review
  • 7、 前后联调
  • 8、测试环境/预发环境打包发布
    • 发布平台
    • 环境区分
  • 9、 测试、ui 验收
  • 10、bugfix、ui 修改
  • 11、产品验收
  • 12、分支合并、code review
  • 13、生产打包发布、回滚、cdn 缓存处理
    • 发布平台
  • 14、远程调试、代理、监控
    • 代理平台
    • 监控平台
      • 错误、性能、埋点
      • 链路追踪
    • 用户出现问题,找到用户的链路 id,模拟用户的操作回放,复现问题场景,代理线上到本地代码,结合 sourcemap 进行代码调试

需要哪些工具平台?

  • 项目管理平台
    • 管理项目,创建项目,项目配置
    • 可以看作是 cli 工具的可视化
  • 接口平台
  • 发布平台
    • 发布平台、项目管理平台、代理平台合在一起,形成开发工作台
  • 代理平台
  • 监控平台
  • 业务埋点平台

在前端工程化里做性能优化

  • 代码压缩、混淆、拆分、合并、tree shaking

  • 预加载,preload、prefetch、dns-preconnect

  • 非阻塞加载 css js

  • 懒加载,首屏资源最小化

  • 缓存持久化、稳定化:通用框架、polyfill、common bundle,利用 v8 优化:热运行、script streaming

  • 图片压缩、字体压缩、字体图标、css sprite、base 64

  • 骨架屏注入

  • pwa service worker 缓存处理

  • prepack 代码预处理

svelte 源码阅读

分析流程

// 项目代码
// app.svelte
<script>
	import { onMount } from 'svelte'
	import Demo from './Demo.svelte'
	export let name;
	let count = 0

	const handle = () => {
		count += 1
	}
  
  // 可以写多个
  onMount(() => console.log('mount'))
	onMount(() => console.log('mount'))
</script>
<style>
	h1 {
		color: purple;
	}
</style>

<h1>Helloza {name}! {count}</h1>
<button on:click={handle}>click</button>
<Demo msg={count} />

// demo.svelte
<script>
  // export 表示,接收外表的 props
	export let msg;
</script>

<h1>Hello {msg}!</h1>



// 编译阶段
// code is input,svelt-loader 进行对 svelte 文件编译
// 每个 svelte 文件,执行一次这个方法
// source 是文件的源码字符串
preprocess(source, ...).then(processed => {
  // ...
  compile(processed.toString(), compileoptions)
})

// 主要作用是根据 option 对 source 做预处理
async function preprocess(source, preprocessor, options) {
  // 如果有 preprocessor, 根据 preprocessor 对 source 进行处理
  // 返回一个对象
  return {
    code: source,
    dependencies: ...,
    toString() {
      return source
    }
  }
}

function compile(source, options = {}) {
  // 构建一个 stat 实例,主要是保存编译速度之类
  // 对 source 进行 parse 为 ast
  const ast = parse$2(source, options)
  // 进行组件实例化,组件类里面处理了大量逻辑
  component = new Component(ast, source, ...
  // 对组件进行浏览器端代码生成,区分 ssr/dom
  // 通过 dom 函数处理,得到的是一个 function create_fragment(ctx){...} 函数的声明字符串
  // 也就是说 js 是一段字符串,最后会将这段字符串与项目源码一起输出到 thunk 里
  js = ssr ? ssr(component, options) : dom(component, options)
  // 返回一个对象
  res = component.generate(js)
  return res
}

function parse$2(template, options = {}) {
  // parser 实例化
  const parser = new Parser$2(template, options)
  // parser 对象结构为:
  {
    stack:[
      {start, end, type, children}
    ],
    css: [
      {start,end,attributes,children, content}
    ],
    js: [
      {start, end, context, content}
    ],
    html: {start, end, type, children}
    meta_tags,
    template,
    filename,
    customeElement,
  }
  // 返回一个对象
  return {
    html: parser.html,
    css: parser.css[0],
    instance: js context is default [0],
    module: js context is module [0]
  }
}

class Parser$2 {
  // 对模版进行 ast 编译,分离 css/js/html 等。
}

// 很复杂
class Component {}

// 生成运行时代码片段
function dom(component, options) {
  // 构造一个 renderer 实例
  const renderer = new Renderer(component, options)
  // 构造一个 builder 实例,之后完这个 builder 里添加各种代码片段
  const builder = new CodeBuilder() 
  // 各种处理,往 builder 里插入代码字符串片段
  ...
  // 返回这个 builder
  return builder.toString()
}

//////////// dom 生成的函数片段 start /////////
// 根据实际情况,生产的片段可能不一样
function create_fragment(ctx) {
  var h1, t0, t1, t2, t3, current;

  var demo = new Demo({ props: { msg: "aa" } });

  return {
    c() {
      h1 = element("h1");
      t0 = text("Helloza ");
      t1 = text([✂129 - 133]);
      t2 = text("!");
      t3 = space();
      demo.$$.fragment.c();
      attr(h1, "class", "svelte-i7qo5m");
      dispose = listen(button, "click", [✂348-354])
    },

    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, t0);
      append(h1, t1);
      append(h1, t2);
      insert(target, t3, anchor);
      mount_component(demo, target, anchor);
      current = true;
    },

    p(changed, ctx) {
      if (!current || changed.name) {
        set_data(t1, [✂129 - 133]);
      }
    },

    i(local) {
      if (current) return;
      transition_in(demo.$$.fragment, local);

      current = true;
    },

    o(local) {
      transition_out(demo.$$.fragment, local);
      current = false;
    },

    d(detaching) {
      if (detaching) {
        detach(h1);
        detach(t3);
      }

      destroy_component(demo, detaching);
    }
  };
}

function instance($$self, $$props, $$invalidate) {
  [✂10 - 60] // 是一个占位符???

  $$self.$set = $$props => {
    if ('name' in $$props) $$invalidate('name', name = $$props.name);
  };

  return { name };
}

class App extends SvelteComponent {
  constructor(options) {
    super();
    // 执行 init 方法
    init(this, options, instance, create_fragment, safe_not_equal, ["name"]);
  }
}
//////////// dom 生成的函数片段 end /////////


res = component.generate(js)
// 的到的对象结构是:
{
  js: {
    code // 这里包含着我们自己也的代码,以及 dom 函数生成的运行时代码,就是上面的代码片段。最后这些代码经过 babal 编译后,输出到文件 zhunk 中
    map // soucemap 实例
  }
  css:{
    code // css 代码
    map // soucemap 实例
  }
  ast, // parse 生成的 ast 对象
  warnings: [],
  // 代码里所有的声明
  // eg. in demo.svelte file
  // export let name
  // <div>{name}</div>
  vars: [ 
    {
      name: 'name',
      injected: false,
      module: false,
      mutated: false,
      reassigned: false,
      referenced: true,
      writable: false
    }
  ]
  stat // 编译的统计
}



// 运行时流程
// 入口是
const app = new App({
  target: document.body,
  props: {name: 'hhha'}
})

// 编译后的 App 组件
new App extends SvelteComponent {
  constructor(options) {
    super();
    // 执行 init 方法
    init(this, options, instance, create_fragment, safe_not_equal, ["name"]);
  }
}

// svelte 没有虚拟 dom,首次渲染时,从父一直往子插入到 html 文档中。更新阶段,直接更新 dom
function init(component, options, instance, create_fragment, not_equal$$1, prop_names) {
  // 保存当前组件的实例
  // 初始化 component.$$ 属性,往这个属性上挂载一些属性:
  const $$ = component.$$ = {
    fragment,
    ctx,
    props,
    update,
    bound,
    on_mount: [],
    on_destroy: [],
    before_render: [],
    after_render: [],
    context,
    callbacks,
    dirty
  }
	
	// 执行 instance 函数,得到当前组件需要的 props, 挂到 ctx 上。这里需要注意 cb 参数,这个函数会将所有的反应数据进行包装。更新数据操作会执行这个 cb 函数。
	$$.ctx = instance(component,prosp,cb) || props
  // 执行 所有的 $$.before_render 数组里存的函数,首次 $$.before_render 数组 为空
  // 执行 create_fragment 函数,拿到一个对象,可以看上面的例子
  $$.fragment = create_fragment($$.ctx)
	// 执行 $$.fragment.c(),该函数会创建当前组件的所有 dom 元素,挂载 html 属性到 dom 上,绑定组件的事件。同时执行子组件的 $$.fragment.c() 函数。子组件的实例有了吗?子组件的实例化在父组件执行 create_fragment 函数的时候,就进行实例创建了。这里其实是从父到子,一直执行各自的 create_fragment 函数。生成各自的 dom 和属性挂载。但还没有插入到 html 文档上。
  // 执行顺序是:父创建dom -》子创建dom ... -〉挂载子属性 -》子事件绑定到 dom -》挂载父属性 -〉 父事件绑定到 dom
  // 执行 mount_component 函数, 该函数会从父一直递归到子组件,把前面生成好的 dom + 属性 插入到父中。
	// 流程是:父插入到 root -》子插入到父 ... 一直到完成为止。这时候所有的 dom 都插入到html 文档了。首次渲染相当于结束
  mount_component(component, options.target, options.anchor)
	// 更新操作,首次渲染基本可以忽略这个函数
	flush()
}

// 看一下 init 函数的 instance 参数
// 编译后,有这个东西 [✂10-249✂],是原来代码的展位符??
function instance($$self, $$props, $$invalidate) {
  let {name} = $$props
  let count = 0
  // 可以看出,事件触发 state 更新时,会先执行 $$invalidate 函数, 该函数会执行 make_dirty 方法,从而触发更新
  const handle = () => {
    $$invalidate('count', count += 1)
  }
  setTimeout(() => {
    $$invalidate('count', count += 1)
  }, 3000)
  // 组件实例的 $set 方法, 某些方式的更新数据会用到这个方法。例如:调用子的 $set 方法,来更新子的 props
  $$self.$set = $$props => {
    if ('name' in $$props) $$invalidate('name', name = $$props.name)
  }
  return {name, count, handle}
}

// 首次渲染阶段,不断递归这个函数
function mount_component(component, target, anchor) {
  // 执行 $$.fragment.m 函数。该函数会将之前创建好的 dom 插入到 html 文档上,然后递归执行 mount_component 函数。从父到子一直插入到 html 文档上
  // 递归完后,执行 after_render 函数,该函数执行时,会执行该组件的 onMount 生命函数,svelte 的生命函数一个组件可以写多个。同时收集该组件的 onMount 函数返回值,等组件卸载时执行。可以用来卸载时清除副作用。
  // onMount 函数执行顺序: 子组件 -》 父组件
}

// 首次渲染生命函数:
 beforeUpdate -》子 beforeUpdate -》父 afterUpdate -  afterUpdate -  onMount -》子 onMount
// 更新阶段生命函数
 beforeUpdate -》子 beforeUpdate -》子 afterUpdate -》子 onDestroy -  afterUpdate -》父 onDestroy

// 更新阶段
// 每个反应式的属性都会被 $$invalidate 函数包裹:事件、ajax、异步 等
// $$invalidate 执行时,触发 make_dirty
function make_dirty(component, key) {
  // 缓存需要更新的组件实例 push in dirty_components
  // 执行 schedule_update
}

function schedule_update() {
  // 异步执行 flush,在本次事件循环后紧跟的微任务队列中执行
}

function flush() {
  // 如果有 dirty_components.length,循环拿到当前组件,循环执行 update 函数
  // 如果有 binding_callbacks ,执行 binding_callbacks
  // 如果有 render_callbacks,执行 render_callbacks
  // 如果有 flush_callbacks,执行 flush_callbacks
}

function update($$) {
  // 执行生命函数 before_render
  // 执行 $$.fragment.p 函数,更新所有的的 state
  $$.fragment.p($$.dirty, $$.ctx)
  // 收集 after_render
}

// 每次更新所有的反应式数据
// 同时更新子组件的 props 
p(changed, ctx) {
    if (!current || changed.name) {
      // 更新数据
       set_data(t1, [✂280-284]);
    }

    if (!current || changed.count) {
       set_data(t3, [✂288-293]);
    }

    if (!current || changed.hi) {
       set_data(t5, [✂296-298]);
    }

    var demo_changes = {};
  	// 更新子的 props
    if (changed.count) demo_changes.msg = [✂357-362];
    demo.$set(demo_changes);
  
 		// 当有显示隐藏某些片段时,会执行该片段的 p/c/m 函数
    if ([✂706-719]) {
      if (if_block) {
        // 执行片段的 p,进行数据更新或片段更新,这是一个递归操作。不断对片段里的片段或数据进行更新
        if_block.p(changed, ctx);
        transition_in(if_block, 1);
      } else {
        // create_if_block 这个函数很重要。当有 if/else 的模版逻辑时。都会编译生成这个函数代码。所以编译后的代码量会很大。这个函数的逻辑与 create_fragment 函数的逻辑相似,该很熟生成子组件的实例。然后返回一个包含操作函数的对象。{c,m,o,i,o,d}
        if_block = create_if_block(ctx);
        // 执行片段的 c,进行 dom 元素生成和属性挂载
        if_block.c();
        transition_in(if_block, 1);
        // 执行片段的 m,进行 dom 插入
        if_block.m(if_block_anchor.parentNode, if_block_anchor);
      }
    } else if (if_block) {
      group_outros();
      transition_out(if_block, 1, () => {
        if_block = null;
      });
      check_outros();
    }
}

// $set 其实也是调用 $$invalidate
$$self.$set = $$props => {
  if ('name' in $$props) $$invalidate('name', name = $$props.name);
};

svelte 总结

  • 没有 Virtual DOM 。享受不到 Virtual DOM 带来的诸多好处,比如基于 render function 的组件的强大抽象能力,基于 vdom 做测试,服务端/原生渲染亲和性等等。
  • 核心**在于『通过静态编译减少框架运行时的代码量』
  • 首次渲染阶段,从父开始一层层插入当前的父中。父插入 root -》 子插入父
  • 更新阶段,收集需要更新的组件,更新该组件的数据。当组件节点变化时(删除/变更),该组件的父组件会对该子组件的 dom 进行卸载,然后重新生成新的 dom 并插入到当前父中。
  • 简单的 demo 生成的代码量非常小,当模版中大量判断渲染显示隐藏的逻辑时,生成的代码量很大。包含的运行时代码也越多。
  • 性能上也和 vanilla JS 相差无几。内存占用比 vdom 类框架少。

webpack 的优化手段

webpack 构建速度优化

  • 高版本的 webpack/node
  • cache-loader,thread-loader,happy-pack,hard-source,babel-cache
  • terser-webpack-plugin parallel,cache
  • loader:exclude/include 减少 loader 范围
  • resolve
    • modules:减少查找,结合 pnp
    • mainFields: 减少查找,优先采用那种入口文件
    • extensions:减少查找,优先匹配哪种文件类型
    • noparse:减少编译
    • ignorepluns:减少编译
    • alias:减少查找
  • performance
    • bail
    • mode
    • hints error
  • stats
    • error true,减少 stats 输出
  • 减少 plugin(开发环境)
    • 不要 chunk split
    • 不要压缩
    • 减少 babel 插件
    • sourcemap 类型:cheap-module-eval-source-map,不生成列信息,快
  • dev-server,
    • hmr 增量编译,entry 注入 socket 客户端,开启 hot,配置 hotModuleReplacePlugin,编写入口 module.hot.accept
    • hot
    • react-hot-loader
    • 开启 inline
    • cache 开启缓存
  • 动态路由,减少编译代码
  • 指明 target,node 不会打包官方模块
  • watch 配置
    • 忽略监控 node_module 或其他
    • 降低编译频率,默认 300ms
    • 延长轮训间隔,默认 1000ms
  • ts 检查交给 vscode
  • eslint 交给 vscode

webpack 应用性能优化

  • mode production
  • img 压缩
  • css 压缩,删除无用,提取关键 css,inline 关键 css
  • js
    • dll 分包/使用 cdn
    • externals 使用 cdn 包,或者构建好的 dll
    • scope hoisting:代码更新,速度更快
      • 减少函数声明,减少作用域创建的开销,减少内存开销
      • 原理:将多个模块合并到一个函数里,但为了不造成代码的冗余,只有被使用一次的模块才会合并,必须是 es module 机制
      • moduleConcatentenationPlugin
    • chunk split
    • 动态 import
    • 压缩
    • babel 的优化插件
    • inline runtime/某些 js
    • sourcemap:不开启,或者 hidden-source-map,生成最详细的 sourcemap,但不会暴露源码,可上传到监控平台
    • tree shaking:module false
    • preload/prefetch/async/defer
  • 稳定的 hash
  • pwa:workbox
  • 分析减少包体积
  • output 设置 crossorigin:anonymous 避免带上 cookie

如何提高工作效率?

如何提高工作效率?

  • 任务排序
    • 每天下班整理今天的任务完成情况,安排明天的任务排序
  • 写代码先想清楚
    • 功能拆分,模块拆分,接口设计,抽象函数,画图辅助
    • 想清楚要实现什么?怎么调用?是否耦合?是否易拓展?
  • 拒绝开会、聊天
    • 手机飞行模式,协作工具免打扰
    • 番茄工作法
  • 好的身体状态
    • 健身,坐姿,椅子,屏幕
    • 茶,咖啡,白水
    • 戒糖
    • 好的饮食
  • 降噪耳机
    • 必备

前端架构分层思考

前端架构分层思考

image-20200905115749168

  • 请求通信层
    • 理解:把服务端比作是数据库,那请求通信层就是数据库驱动。请求层提供相关的通用的 api,通过通用 api 来封装请求的接口。
    • 功能:http 规范、接口请求、缓存、sw、轮训、并串行、超时、取消、分页、错误处理 ...
  • 存储层
    • 数据实体:与服务端的一致
    • 持久化同步:同步到本地持久化存储
  • 模型层
    • 增删改查:对实体进行操作的 action,可复用
  • 视图控制层
    • 与视图一一对应,处理副作用、业务逻辑、接口通信、错误监听等。
    • 无需考虑复用
  • 视图层
    • 保持简单
  • 应用控制层
    • 应用生命周期
    • 应用配置
    • 插件配置
  • 通用模块
    • 组件库
    • hook 库
    • 函数库
    • 插件库
    • 中间件库

可能建立好团队的3要素

建立好团队的3要素

  • 人才
    • 各方面技术的人才,取长补短
    • 价值观与团队价值观一致
    • 人品
      • 诚实。别满嘴跑火车,不知道哪句能信,打嘴炮,不干活,搞办公室政治,内耗大。
      • 责任感。答应了就要做到,做好
      • 靠谱。凡事有着落有交代
      • 底线。做事情要有底线,不会为了利益不择手段
      • 协作。能与人协作沟通,上下级,别的团队
      • 踏实。耐心专研技术,别眼高手低
      • 其他的性格小缺点不太重要
    • 潜力
      • 如何体现一个人有潜力?
      • 现在学得不错,对会的都学的挺深入,会学,基础扎实,知道,关注,有规划
        • 考察基础扎不扎实
        • 考察会不会学习,科学学习,科学上网,自己解决问题,会总结形成方案。
        • 考察学的深不深,对会的东西的专研程度。可以看出是否踏实、是否热爱、是否能突破。
        • 考察关注,是否关注技术变化,新技术知道哪些,对新技术的态度是什么。
  • 文化/价值观
    • 员工办公的幸福感
      • 福利、环境、专注办公、自由办公
    • 员工需要自己的生活。重视员工生活
    • 团队协作需要相互理解尊重,不是交朋友,不是称兄道弟
    • 员工需要学习进步,分享,得到鼓励
    • 开会的原则
      • 重点、时限、人员、主题,能不开会就不要开会,工具化辅助代替开会,开会前问题收集、头脑风暴,不要在会议上头脑风暴/提出问题
    • 有规划,不滥用加班
    • 不打鸡血/画饼,不喊口号,不形式主义
    • 有原则
    • 鼓励/奖励、批评/惩罚
    • 数据驱动,哪些功能,哪些手段,数据如何
  • 工具
    • 工具来帮助人步调一致的做事情
    • 个人习惯,早上任务制定,晚上任务 checkout
    • 工作资源排期,每个人都可以看到你做了啥,在做啥,管理者可以知道有哪些资源可以使用
    • 任务拆分,项目进度
    • 目标透明,需求池,
    • 公司 wiki,经验沉淀,系统介绍,产品文档,运营手册,开发文档
      • 整理业务的架构流程图
      • 整理工具的使用
      • 整理技术债
      • 整理分享内容
      • 整理常见的坑/兼容性
      • 整理约束与规范
      • 整理技术对接的流程与约定
      • 整理迭代流程
      • 整理基础服务,通用接口
      • 整理服务,在维护的项目
      • 运营人员的工具使用说明
      • 运营的经验沉淀,做过哪些,哪些有成果,哪些没成果,原因是什么
      • 权限申请
      • 新人入职资料

代码如何拆分?

代码拆分的好处?

  • 有利于缓存的稳定性
    • 代码拆的越细粒度,越不容易因为版本更新造成的缓存失效
    • 多项目或多页面共享缓存文件
  • 有利于加载优化
    • 体积小的文件加载速度快,虽然可能带来请求数的增加,结合 http2 或懒加载预加载可以解决
    • 有利于首屏优化,使首屏资源最小化,非首屏资源懒加载
  • 结合 service worker 做资源缓存

如何设计代码拆分?

  • dll chunk
    • 例如:框架 react、react-dom
    • 统一框架版本,统一升级
    • 每个项目都能共享到文件缓存
    • 客户端还可以该 chunk 进行拦截,直接返回离线包
  • polyfill chunk
    • 统一客户端的环境兼容性
    • 针对每个客户端提供 n 个版本的 polyfill 包,直接注入 webview
    • 针对浏览器提供业务规定的版本
    • webpack 注入,避免项目中手动引入
  • 路由 chunk
    • 按路由拆分,懒加载
  • 其他,使用 import()
    • 独立的 sdk
    • 某些体积大的库或组件
    • 复杂业务的弹窗
    • 需要交互触发的界面
    • 独立的模块、楼层、区块
    • 非首屏的容器组件
  • 思考
    • 需要经常分析代码中哪些包比较大,哪些代码可以单独抽出来延迟加载,可以在 code review 或重构中做这些工作。当然最好是开发者时刻有拆包意识,而不是一味写代码
    • common 库是否有必要抽出来?
      • 如果整个库引入,这应该将这些库都合在一起,做成一个 common chunk
      • 如果只是用了其中的某些方法,就别抽出来了,因为 common 包很难做到缓存稳定。每次打包,进行 tree shaking 后,都可能往里面加入一些小函数。还不如把这些函数直接放到该页面的 chunk 里。
      • 针对异步 async 的代码,抽取一个 common chunk 意义不大
    • 原则:尽可能拆 chunk,尽可能保持 chunk 的缓存稳定,容易变化的代码合在一个 chunk

在 webpack 中配置

// optimization
// 抽出 runtime,提升文件稳定性
runtimeChunk: {
   name: 'runtime'
},
// webpack 会自动处理分割代码,包括初始化 initial 或异步 async 的
// splitchunks... chunks:“all”,
cacheGroups: {
    // 从 main 里抽 lodash,优先级比 vender 高
     lodash: {
          test: (module) => {
              return /lodash/.test(module.resource)
          },
            chunks: 'all', // 从全部代码里抽
            name: 'lodash',
            priority: 30,
      },
     // 从 main 里抽 moment,优先级比 vender 高
      moment: {
            test: (module) => {
              return /moment/.test(module.resource)
            },
            chunks: 'all',
            name: 'moment',
            priority: 40,
      },
      // 剩余的全部 node_module 的代码都抽到一个 vender 里
      vender: {
            test: /[\\/]node_modules[\\/]/,
            chunks: 'all',
            name: 'vender',
            priority: 20,
            enforce: true
      },
      // 精确抽取 entry 定义的 sdk chunk,这个代码如果不依赖其他库的话,可以直接执行
      sdk: {
            name: 'sdk',
            priority: 10,
            chunks:(chunk) {
              return chunk.name === 'sdk';
            }
      },
      // 抽去整个项目的所有css打到一起,通过非阻塞加载 css 文件
      styles: {
            name: 'styles',
            test: /\.less|css$/,
            chunks: 'all',
            enforce: true
      }
}

chunk

设计一个简单的增量更新方案

设计一个简单的增量更新方案

diff

html

  • head 中 inline config json
    • config 中有版本号
    • config 说明哪些文件使用缓存,哪些文件需要增量更新
    • config 是在 webpack 构建时,根据后台设置和 manifest inline 到 html
    • 每次发版都构建 config,发布资源到 cdn,刷新 cdn 缓存

流程

  • 页面加载
    • 比对页面 head 中 config 和 localstorage 中的 config
      • 根据相应策略进行代码加载
      • 保存 config 数据到 localstorage
    • 首次加载缓存 diff 包,后续加载从 localstorage 中读取
      • 通过 loadscript 方法加载 diff 包
      • 存储到 localstorage
    • 全量加载
      • 场景
        • 当 localstorage 中没有 config,即首次加载
        • 当 config 中版本比 localstorage 中版本大于 20
        • 当 config 中显示要求全量更新
        • 当 localstorage 中有全量更新标示
      • 通过动态创建 script/link 的形式加载 cdn 上的资源
      • 并在 head 中 通过 link preload 来提升加载优先级,及预加载资源
      • 同时请求 localstorage 中的全量更新标示
      • widow onload 事件触发后,调用 sdk 的 loadscript 方法,通过 ajax 的形式加载 config 中列出的所有的文件。保存资源内容到 localstorage
    • 增量加载
      • 场景
        • 当 config 中版本比 localstorage 中的版本在 20 内
      • 直接运行 localstorage 中的代码,显示界面
      • widow onload 事件触发后,调用 sdk 的 loadpatch 方法,通过 ajax 的形式,带上 localstorage 中 config 的版本,请求整个应用的 patch
        • patch json 会不会过大?
      • localstorage 中的内容和 patch 合并
      • 更新 localstorage
      • 再次打开页面将看到之前增量更新/交互提示用户网站有新的版本

sdk 接口

  • loadscript
  • loadpatch
  • mergepatch
    • 当 mergepatch 出错时,存储全量更新标记
  • localstorage api
  • hashscript
    • 对 localstorage 中的代码进行 hash 生成
  • checkscript
    • 比对 patch json 中的 hash 和 localstorage 中的 hash
    • 当 hash 比对出错时,存储全量更新标记
  • markforce
    • 标记全量更新标示
    • 同时请求数据收集接口

webpack plugin

  • inline 注入 config 数据

  • 构建完成后

    • 携带应用名(唯一)参数,请求配置后台,配置后台返回计算好的版本号,配置后台也会更新应用版本号

    • 根据版本号决定是否通知增量更新服务

    • 把资源发到 cdn

      • 需要等到增量服务完成通知?

配置后台

  • 更新应用版本号
  • 配置应用 config
  • 获取应用最近 20 个版本的 patch

diff 服务

  • 获取前 20 个版本的源码与当前源码进行 diff
  • 保存 diff 的 patch
  • 通知构建平台发布资源,刷新 cdn

依赖

多页项目构建的一个小小好处

小项目如何管理?

  • 例如:一堆小的运营活动项目,每次开发这种活动项目都会有一种重复劳动的感觉,而且没有沉淀,容易工作疲劳。
  • 解决办法:
    • 所有运营活动项目放到一个 git 项目里管理,通过 webpack 多页构建,输入不同的项目入口来打包不同的项目。
// example
|--app
|--|--projectA
|-----|--components // 业务组件
|-----|--assets
|-----|--index.jsx
|-----|--style.module.scss
|-----|--routes.ts
|--|--projectB
|--|--projectC
|--components // 通用组件、通用业务组件
|--utils // 通用工具函数
|--assets // 通用图片、css、字体等

// dev
npm run dev
// build
npm run build --project projectA
  • 注意
    • 不是所有项目都需要路由配置,根据项目需要
    • 通用组件等改动需要兼容,需要有测试用例,或者直接开发新的版本
  • 好处
    • 免去每个项目都要初始化项目,安装依赖、配置环境、配置打包等繁琐工作,可以直接进入项目开发
    • 重用以前的项目组件,可以做到开发中沉淀,提高开发效率,降低工作疲劳
    • 可以提炼通用、工具库、业务流程抽象,方便把这些通用的东西抽出独立打包成 npm 包
    • 减少项目切换,有时这个项目修修 bug,那个项目改改需求,很容易在多项目中切来切去。打开一堆 vscode
    • 这种模式还可以用到其他中小型项目中,不仅仅是运营活动 h5
  • 缺点
    • 通用的代码被修改,可能使其他项出现 bug 的风险。所以通用的组件等需要做好测试
    • 项目不宜太大,避免初次构建速度过慢

面向组件的应用发布模式设计

面向组件的应用发布的模式

架构设计

image-20191230134708917

image-20200628123828958

模式介绍

  • 通过接口动态显示页面中的组件。可以做到组件级别的发布,不同于页面即应用的模式。组件通过解耦组合来形成页面界面。
  • 开发者负责开发组件、组合组件,形成组件物料市场。
  • 产品经理/运营同事负责配置页面、发布页面。通过组件级别的数据分析、ab 测试,快速验证组件组合的效果。
  • 每个页面都由组件积木搭建而成,可以方便的、细粒度的、精确的、数据化的替换积木。达到快速上下架组件或定制基于用户角度的个性化界面显示。
  • 同时沉淀组件物料市场,某些业务场景的需求,产品经理可以在已有的物料上发布页面。快速验证需求的可行性。

请求流程

  • 优化前

image-20200628103549703

  • 优化后
    • 框架离线化
    • 资源预加载
    • 组件缓存 cdn

image-20200628103442721

接口设计

  • getPageConfig

    • 入参:pageId

    • 出参:json

    • {
        pageId: xxx,
        ...
        chunks: [
          {
            chunkUrl: xxx, // 组件地址
            chunkData: xxx, // 组件数据,来源于后台配置/业务接口,首屏数据尽量放到这个接口里。非首屏数据放到组件内部惰性请求
          },
          {
            chunkUrl: xxx,
            chunkData: xxx,
          }
          ...
        ]
      }
    • 实现组件动态化

      • 由产品/运营根据数据来决策发布组件
      • 组件 ab 测试,版本,环境
    • 每个页面都会请求这个接口,会存在请求量过大的情况,如何优化?

      • 缓存 redis
      • 负载均衡
    • image-20200628141451498

组件设计

  • 数据来源
    • 组件外部 chunkData
    • 组件内部接口请求
    • 组件需要同时满足这2种数据注入方式
  • 组件状态
    • 数据加载中
    • 无数据占位
    • 出错占位
  • 埋点,版本,环境

管理后台

  • 组件物料市场
    • 组件组合,组件管理,组件预览
  • 页面管理
    • 组件组合,组件数据配置,组件上下架,组件版本,页面配置,页面预览,环境管理,组件 ab 数据

svelte 源码阅读

分析流程

// 项目代码
// app.svelte
<script>
	import { onMount } from 'svelte'
	import Demo from './Demo.svelte'
	export let name;
	let count = 0

	const handle = () => {
		count += 1
	}
  
  // 可以写多个
  onMount(() => console.log('mount'))
	onMount(() => console.log('mount'))
</script>
<style>
	h1 {
		color: purple;
	}
</style>

<h1>Helloza {name}! {count}</h1>
<button on:click={handle}>click</button>
<Demo msg={count} />

// demo.svelte
<script>
  // export 表示,接收外表的 props
	export let msg;
</script>

<h1>Hello {msg}!</h1>



// 编译阶段
// code is input,svelt-loader 进行对 svelte 文件编译
// 每个 svelte 文件,执行一次这个方法
// source 是文件的源码字符串
preprocess(source, ...).then(processed => {
  // ...
  compile(processed.toString(), compileoptions)
})

// 主要作用是根据 option 对 source 做预处理
async function preprocess(source, preprocessor, options) {
  // 如果有 preprocessor, 根据 preprocessor 对 source 进行处理
  // 返回一个对象
  return {
    code: source,
    dependencies: ...,
    toString() {
      return source
    }
  }
}

function compile(source, options = {}) {
  // 构建一个 stat 实例,主要是保存编译速度之类
  // 对 source 进行 parse 为 ast
  const ast = parse$2(source, options)
  // 进行组件实例化,组件类里面处理了大量逻辑
  component = new Component(ast, source, ...
  // 对组件进行浏览器端代码生成,区分 ssr/dom
  // 通过 dom 函数处理,得到的是一个 function create_fragment(ctx){...} 函数的声明字符串
  // 也就是说 js 是一段字符串,最后会将这段字符串与项目源码一起输出到 thunk 里
  js = ssr ? ssr(component, options) : dom(component, options)
  // 返回一个对象
  res = component.generate(js)
  return res
}

function parse$2(template, options = {}) {
  // parser 实例化
  const parser = new Parser$2(template, options)
  // parser 对象结构为:
  {
    stack:[
      {start, end, type, children}
    ],
    css: [
      {start,end,attributes,children, content}
    ],
    js: [
      {start, end, context, content}
    ],
    html: {start, end, type, children}
    meta_tags,
    template,
    filename,
    customeElement,
  }
  // 返回一个对象
  return {
    html: parser.html,
    css: parser.css[0],
    instance: js context is default [0],
    module: js context is module [0]
  }
}

class Parser$2 {
  // 对模版进行 ast 编译,分离 css/js/html 等。
}

// 很复杂
class Component {}

// 生成运行时代码片段
function dom(component, options) {
  // 构造一个 renderer 实例
  const renderer = new Renderer(component, options)
  // 构造一个 builder 实例,之后完这个 builder 里添加各种代码片段
  const builder = new CodeBuilder() 
  // 各种处理,往 builder 里插入代码字符串片段
  ...
  // 返回这个 builder
  return builder.toString()
}

//////////// dom 生成的函数片段 start /////////
// 根据实际情况,生产的片段可能不一样
function create_fragment(ctx) {
  var h1, t0, t1, t2, t3, current;

  var demo = new Demo({ props: { msg: "aa" } });

  return {
    c() {
      h1 = element("h1");
      t0 = text("Helloza ");
      t1 = text([✂129 - 133]);
      t2 = text("!");
      t3 = space();
      demo.$$.fragment.c();
      attr(h1, "class", "svelte-i7qo5m");
      dispose = listen(button, "click", [✂348-354])
    },

    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, t0);
      append(h1, t1);
      append(h1, t2);
      insert(target, t3, anchor);
      mount_component(demo, target, anchor);
      current = true;
    },

    p(changed, ctx) {
      if (!current || changed.name) {
        set_data(t1, [✂129 - 133]);
      }
    },

    i(local) {
      if (current) return;
      transition_in(demo.$$.fragment, local);

      current = true;
    },

    o(local) {
      transition_out(demo.$$.fragment, local);
      current = false;
    },

    d(detaching) {
      if (detaching) {
        detach(h1);
        detach(t3);
      }

      destroy_component(demo, detaching);
    }
  };
}

function instance($$self, $$props, $$invalidate) {
  [✂10 - 60] // 是一个占位符???

  $$self.$set = $$props => {
    if ('name' in $$props) $$invalidate('name', name = $$props.name);
  };

  return { name };
}

class App extends SvelteComponent {
  constructor(options) {
    super();
    // 执行 init 方法
    init(this, options, instance, create_fragment, safe_not_equal, ["name"]);
  }
}
//////////// dom 生成的函数片段 end /////////


res = component.generate(js)
// 的到的对象结构是:
{
  js: {
    code // 这里包含着我们自己也的代码,以及 dom 函数生成的运行时代码,就是上面的代码片段。最后这些代码经过 babal 编译后,输出到文件 zhunk 中
    map // soucemap 实例
  }
  css:{
    code // css 代码
    map // soucemap 实例
  }
  ast, // parse 生成的 ast 对象
  warnings: [],
  // 代码里所有的声明
  // eg. in demo.svelte file
  // export let name
  // <div>{name}</div>
  vars: [ 
    {
      name: 'name',
      injected: false,
      module: false,
      mutated: false,
      reassigned: false,
      referenced: true,
      writable: false
    }
  ]
  stat // 编译的统计
}



// 运行时流程
// 入口是
const app = new App({
  target: document.body,
  props: {name: 'hhha'}
})

// 编译后的 App 组件
new App extends SvelteComponent {
  constructor(options) {
    super();
    // 执行 init 方法
    init(this, options, instance, create_fragment, safe_not_equal, ["name"]);
  }
}

// svelte 没有虚拟 dom,首次渲染时,从父一直往子插入到 html 文档中。更新阶段,直接更新 dom
function init(component, options, instance, create_fragment, not_equal$$1, prop_names) {
  // 保存当前组件的实例
  // 初始化 component.$$ 属性,往这个属性上挂载一些属性:
  const $$ = component.$$ = {
    fragment,
    ctx,
    props,
    update,
    bound,
    on_mount: [],
    on_destroy: [],
    before_render: [],
    after_render: [],
    context,
    callbacks,
    dirty
  }
	
	// 执行 instance 函数,得到当前组件需要的 props, 挂到 ctx 上。这里需要注意 cb 参数,这个函数会将所有的反应数据进行包装。更新数据操作会执行这个 cb 函数。
	$$.ctx = instance(component,prosp,cb) || props
  // 执行 所有的 $$.before_render 数组里存的函数,首次 $$.before_render 数组 为空
  // 执行 create_fragment 函数,拿到一个对象,可以看上面的例子
  $$.fragment = create_fragment($$.ctx)
	// 执行 $$.fragment.c(),该函数会创建当前组件的所有 dom 元素,挂载 html 属性到 dom 上,绑定组件的事件。同时执行子组件的 $$.fragment.c() 函数。子组件的实例有了吗?子组件的实例化在父组件执行 create_fragment 函数的时候,就进行实例创建了。这里其实是从父到子,一直执行各自的 create_fragment 函数。生成各自的 dom 和属性挂载。但还没有插入到 html 文档上。
  // 执行顺序是:父创建dom -》子创建dom ... -〉挂载子属性 -》子事件绑定到 dom -》挂载父属性 -〉 父事件绑定到 dom
  // 执行 mount_component 函数, 该函数会从父一直递归到子组件,把前面生成好的 dom + 属性 插入到父中。
	// 流程是:父插入到 root -》子插入到父 ... 一直到完成为止。这时候所有的 dom 都插入到html 文档了。首次渲染相当于结束
  mount_component(component, options.target, options.anchor)
	// 更新操作,首次渲染基本可以忽略这个函数
	flush()
}

// 看一下 init 函数的 instance 参数
// 编译后,有这个东西 [✂10-249✂],是原来代码的展位符??
function instance($$self, $$props, $$invalidate) {
  let {name} = $$props
  let count = 0
  // 可以看出,事件触发 state 更新时,会先执行 $$invalidate 函数, 该函数会执行 make_dirty 方法,从而触发更新
  const handle = () => {
    $$invalidate('count', count += 1)
  }
  setTimeout(() => {
    $$invalidate('count', count += 1)
  }, 3000)
  // 组件实例的 $set 方法, 某些方式的更新数据会用到这个方法。例如:调用子的 $set 方法,来更新子的 props
  $$self.$set = $$props => {
    if ('name' in $$props) $$invalidate('name', name = $$props.name)
  }
  return {name, count, handle}
}

// 首次渲染阶段,不断递归这个函数
function mount_component(component, target, anchor) {
  // 执行 $$.fragment.m 函数。该函数会将之前创建好的 dom 插入到 html 文档上,然后递归执行 mount_component 函数。从父到子一直插入到 html 文档上
  // 递归完后,执行 after_render 函数,该函数执行时,会执行该组件的 onMount 生命函数,svelte 的生命函数一个组件可以写多个。同时收集该组件的 onMount 函数返回值,等组件卸载时执行。可以用来卸载时清除副作用。
  // onMount 函数执行顺序: 子组件 -》 父组件
}

// 首次渲染生命函数:
 beforeUpdate -》子 beforeUpdate -》父 afterUpdate -  afterUpdate -  onMount -》子 onMount
// 更新阶段生命函数
 beforeUpdate -》子 beforeUpdate -》子 afterUpdate -》子 onDestroy -  afterUpdate -》父 onDestroy

// 更新阶段
// 每个反应式的属性都会被 $$invalidate 函数包裹:事件、ajax、异步 等
// $$invalidate 执行时,触发 make_dirty
function make_dirty(component, key) {
  // 缓存需要更新的组件实例 push in dirty_components
  // 执行 schedule_update
}

function schedule_update() {
  // 异步执行 flush,在本次事件循环后紧跟的微任务队列中执行
}

function flush() {
  // 如果有 dirty_components.length,循环拿到当前组件,循环执行 update 函数
  // 如果有 binding_callbacks ,执行 binding_callbacks
  // 如果有 render_callbacks,执行 render_callbacks
  // 如果有 flush_callbacks,执行 flush_callbacks
}

function update($$) {
  // 执行生命函数 before_render
  // 执行 $$.fragment.p 函数,更新所有的的 state
  $$.fragment.p($$.dirty, $$.ctx)
  // 收集 after_render
}

// 每次更新所有的反应式数据
// 同时更新子组件的 props 
p(changed, ctx) {
    if (!current || changed.name) {
      // 更新数据
       set_data(t1, [✂280-284]);
    }

    if (!current || changed.count) {
       set_data(t3, [✂288-293]);
    }

    if (!current || changed.hi) {
       set_data(t5, [✂296-298]);
    }

    var demo_changes = {};
  	// 更新子的 props
    if (changed.count) demo_changes.msg = [✂357-362];
    demo.$set(demo_changes);
  
 		// 当有显示隐藏某些片段时,会执行该片段的 p/c/m 函数
    if ([✂706-719]) {
      if (if_block) {
        // 执行片段的 p,进行数据更新或片段更新,这是一个递归操作。不断对片段里的片段或数据进行更新
        if_block.p(changed, ctx);
        transition_in(if_block, 1);
      } else {
        // create_if_block 这个函数很重要。当有 if/else 的模版逻辑时。都会编译生成这个函数代码。所以编译后的代码量会很大。这个函数的逻辑与 create_fragment 函数的逻辑相似,该很熟生成子组件的实例。然后返回一个包含操作函数的对象。{c,m,o,i,o,d}
        if_block = create_if_block(ctx);
        // 执行片段的 c,进行 dom 元素生成和属性挂载
        if_block.c();
        transition_in(if_block, 1);
        // 执行片段的 m,进行 dom 插入
        if_block.m(if_block_anchor.parentNode, if_block_anchor);
      }
    } else if (if_block) {
      group_outros();
      transition_out(if_block, 1, () => {
        if_block = null;
      });
      check_outros();
    }
}

// $set 其实也是调用 $$invalidate
$$self.$set = $$props => {
  if ('name' in $$props) $$invalidate('name', name = $$props.name);
};

总结

  • 没有 Virtual DOM 。享受不到 Virtual DOM 带来的诸多好处,比如基于 render function 的组件的强大抽象能力,基于 vdom 做测试,服务端/原生渲染亲和性等等。
  • 核心**在于『通过静态编译减少框架运行时的代码量』
  • 首次渲染阶段,从父开始一层层插入当前的父中。父插入 root -》 子插入父
  • 更新阶段,收集需要更新的组件,更新该组件的数据。当组件节点变化时(删除/变更),该组件的父组件会对该子组件的 dom 进行卸载,然后重新生成新的 dom 并插入到当前父中。
  • 简单的 demo 生成的代码量非常小,当模版中大量判断渲染显示隐藏的逻辑时,生成的代码量很大。包含的运行时代码也越多。
  • 性能上也和 vanilla JS 相差无几。内存占用比 vdom 类框架少。

库开发需要考虑 polyfill 吗?

库开发需要考虑 polyfill 吗?

  • 为什么会有这样的疑问?
    • 因为现在大部分的应用默认是不对 node_modules 进行 babel 转换的。所以当库没有进行 polyfill,而应用又没有对 node_modules 进行 babel 转换,这就有可能出现浏览器兼容性错误。
    • 库的开发者一般都会为了保持库的性能和减小体积,一般不会做 polyfill。
    • 假设每个库都做了 polyfill,这可能会导致有 polyfill 重复。同时还可能存在 polyfill 命名冲突问题。而且库的 polyfill 也不一定满足应用的需求。
    • 但如果应用对 node_modules 进行 babel 转换,可能会出现因重复转换而出现错误。同时也会造成打包构建时间变长。
  • 所以我认为的结论是?
    • 现代前端应用应该进行构建转换编译
      • 代码合并、混淆、压缩、tree shaking 都需要经过构建工具,所以这个步骤不应该少
      • 前端构建还能促进前端往着工程化、标准化、专业化发展
      • 如果还像以前一样在html script 里编写代码,这不利于前端发展,会出现以前前端模块混乱、命名冲突、难维护等问题
    • 应用程序应该转换 node_modules 的代码,这可能会是的构建时间变长。但长期来看,能促进构建工具的发展。同时能整体控制应用的 polyfill,使得 polyfill 是精简的、符合应用需求的。
    • 库应该以聚焦在精简、高性能的程序开发。库方法导出时还需要考虑是否能 tree shaking。
    • 构建工具需要聚焦提升构建速度、去除库的 polyfill、针对已转换的代码能有好的处理。
  • 详情可以看这里的讨论 w3ctag/polyfills#6

重新认识 babel

babel 的整体流程

  • input string -> @babel/parser parser -> AST -> transformer[s] -> AST -> @babel/generator -> output string

babel

babel 的核心库

  • @babel/parser

    • 解析 js 代码为 ast
      • 词法分析,语法分析
    • babelparse.parse(code, options)
    • babelparse.parseExpression(code, options)
  • @babel/core

    • 修改 ast,增删改

      • babel.transform
      • babel.transformSync
      • babel.transformAsync
      • babel.transformFile
      • babel.transformFileSync
      • babel.transformFileAsync
      • babel.transformFromAst
      • babel.transformFromAstSync
      • babel.transformFromAstAsync
     const babel = require('babel-core')
     babel.transformFile('./demo.js', {
       presets: [],
       plugins: []
     }, function(err, result) {
       console.log(result)
     })
    
  • @babel/traverse

  • @babel/generator

    • 将 ast 转换成 js 代码
    • generate(ast, options, code)

babel 其他库

  • @babel/cli

    • 命令工具
  • @babel/types

    • used to validate, build and change AST nodes
  • @babel/polyfill

    • core-js 和 regenerator-runtime 的封装

    • 用于模拟完整的 es2015+ 的环境(不包括第四阶段的提议)

    • 在入口顶部引入

    • 建议在应用中使用,而不是在库开发中使用

    • @babel/polyfill 是一个大而全的方案,除非是不考虑体积性能的项目,一般不建议使用。

    • 可以使用 @babel/preset-env useBuildIn 来代替,或者从 core-js 中手动引入需要的 polyfill

      • 当 useBuildIn:usage,需要安装 @babel/polyfill
      • 当 useBuildIn:entry,安装 @babel/polyfill 同时入口顶部引入
      • 当 useBuildIn:false,安装 @babel/polyfill 同时入口顶部引入
      • 需要结合 transform-runtime
    • babel 7.4 不建议使用了,直接用 core-js 和 regenerator-runtime

     import "core-js/stable";
     import "regenerator-runtime/runtime";
    • @babel/polyfill 里包含了什么?

    • image-20200330160624464

    • 了解下 core-js ?

      • 几乎实现了所有的 polyfill,但是没有实现 generator,所以需要 regenerator-runtime 来解决 generator 的兼容性

      image-20200330161703572

    • 了解 regenerator-runtime ?

      • 实现 generator 函数的兼容性
  • @babel/runtime

    • 包含 Babel modular runtime helpers 和 regenerator-runtime

    • 作为 @babel/plugin-transform-runtime 的 runtime 依赖。

    • 简单来说:

      • babel 会在源码中插入一些 helper 来解决兼容性问题

      • @babel/plugin-transform-runtime 能识别这些 helper,然后改成从 @babel/runtime 里引入的方式,来达到减少代码体积

        // code
        class Circle {}
        
        // turn into
        function _classCallCheck(instance, Constructor) {
          // this is a helper func
        }
        var Circle = function Circle() {
          _classCallCheck(this, Circle);
        };
        
        // use @babel/plugin-transform-runtime
        var _classCallCheck = require("@babel/runtime/helpers/classCallCheck"); // helper from babel-runtime
        
        var Circle = function Circle() {
          _classCallCheck(this, Circle);
        };
    • 将 babel 插入到源码中的一些 helper 提取到一个独立的模块中引入,以减少代码重复来减少 polyfill 的体积

    • 同时还可以避免某些全局污染,例如:promise、set、map 等,@babel/runtime 会将他们重新命名,避免全局污染

    • 具体做了什么?

      • @babel/runtime/regenerator当您使用生成器/异步功能(可通过该regenerator选项切换)时自动需要。
      • 根据项目使用,必要时按需引入 core-js 里的函数,而不是假设用户会对其进行填充(可通过该corejs选项切换)
      • 删除内嵌的Babel 的 helper 函数,并在@babel/runtime/helpers中引入对应模块(可通过该helpers选项切换)。
      {
        "plugins": [
          [
            "@babel/plugin-transform-runtime",
            {
              "absoluteRuntime": false,
              "corejs": false, // 推荐 corejs 3
              "helpers": true, // 通过独立模块来替换 inline 的 helper
              "regenerator": true, // 修改名称,避免全局命名污染
              "useESModules": false,
              "version": "7.0.0-beta.0"
            }
          ]
        ]
      }
      
      // 代码调用
      require("@babel/core").transform("code", {
        plugins: ["@babel/plugin-transform-runtime"],
      })
      
      // corejs option
      false	npm install --save @babel/runtime
      2	npm install --save @babel/runtime-corejs2
      3	npm install --save @babel/runtime-corejs
    

    image-20200330163744491

    • @babel/runtime-corejs-2 和 @babel/runtime-corejs-3 有什么不同?

      • @babel/runtime-corejs-2
        • 内置类型
        • 类型的静态方法
      • @babel/runtime-corejs-3
        • 内置类型
        • 类型的静态方法
        • 类型的实例方法
  • @babel/register

    • 实时编译,适用于开发阶段,多用在 node 应用,做实时编译

    • 如何使用?

      // register.js
      require('babel-register)
      require('./my-demo.js')
      
      // run:
      node register.js
      
      // 或者 npm script
      'build': 'babel-node register.js'
    • babel-node vs babel-cli

      • babel-node 提供一个支持ES6的REPL环境,能直接运行 es6 代码
  • @babel/template

  • @babel/helpers

  • @babel/core-frame

babel preset

  • preset 是 plugin 的集合

  • @babel/preset-env

    • 包含了以下 plugin
      • es5
      • es2015
      • es2016
      • es2017
      • es2018
    • 不包含实验性的 plugin
      • 可以通过 结合 stage-x 来覆盖
      • stage-x 包含了不同阶段的试验性 plugin,但是 babel 7 已经不支持 preset-stage-x 了,只能将这些 preset 里的 plugin 手动一一配置到 plugin 里
    • 所以 preset-env + 实验性特性的覆盖面是最广的。实验性特性要一一添加,没有 preset 可以便捷添加
  • @babel/preset-react

  • @babel/preset-typescript

  • @babel/preset-flow

  • 如何开发自己的 preset?

    module.exports = () => ({
      presets: [
        require("@babel/preset-env"),
      ],
      plugins: [
        [require("@babel/plugin-proposal-class-properties"), { loose: true }],
        require("@babel/plugin-proposal-object-rest-spread"),
      ],
    });
  • preset 的执行顺序?

    • Presets: ['a', 'b', 'c'],执行顺序是从右到左 c,b,a
    • 所以配置 babelrc 时要注意顺序
{
  "presets": [
    "env"
  ],
  "plugins": [
    ["transform-runtime", {
      "helpers": true,
      "polyfill": true,
      "regenerator": true,
      "moduleName": "babel-runtime"
    }]
  ]
}

兼容性怎么做?

  • 直接引入 babel-polyfill
    • 缺点:很大,冗余
    • 适合后台类项目,不要在库中使用
  • 从 core-js 和 regenerator-runtime 中安实际需要手动引入
    • 缺点:麻烦,容易漏掉
    • 但体积小
  • @babel/preset-env
    • useBuildIn:usage,按需引入 helper
    • 指定适配的 browserlist
    • @babel/plugin-transform-runtime,抽离重用 helper,缩小体积
    • 使用 runtime-corejs 3
    • 需要安装 @babel/runtime-corejs3
  • polyfill.io 服务
    • 自建兼容性服务,通过 ua 来返回相应的 polyfill
    • 可以做到无冗余,但 ua 有时候不可信,需要有兜底方案

测试

// input
class A {
  constructor(name) {
    this.name = name
    console.log(new.target)
  }
  get() {
    return 'a'
  }
  static say() {
    return 'hi'
  }
}

Promise.resolve().then(() => {
  console.log(1)
}).finally(() => {
  console.log(2)
});

[1,2].map(item => item + 1);

'x'.padStart(2, 'a');

let m = new Map();

let s = new Set();

[1,2].includes(item => item === 1);
// demo 1:usage + runtime-corejs3
module.exports = {
  presets: [
    ['@babel/preset-env', {
      'debug': false,
      'useBuiltIns': 'usage',
      'modules': false,
      targets: {
        "node": "current",
        browsers: ['last 2 versions']
      }
    }],
  ],
  plugins: [
    ["@babel/plugin-transform-runtime", { 
      corejs: 3 ,
      helpers: true
    }],
  ]
}

// output
import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _Set from "@babel/runtime-corejs3/core-js-stable/set";
import _Map from "@babel/runtime-corejs3/core-js-stable/map";
import _padStartInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/pad-start";
import _mapInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/map";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise"; // 有重复引入
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";
import "core-js/modules/es7.promise.finally";
import "core-js/modules/es6.function.name";
import _classCallCheck from "@babel/runtime-corejs3/helpers/classCallCheck";
import _createClass from "@babel/runtime-corejs3/helpers/createClass";

var _context, _context2, _context3;

var A =
/*#__PURE__*/
function () {
  function A(name) {
    _classCallCheck(this, A);

    this.name = name;
    console.log(this instanceof A ? this.constructor : void 0);
  }

  _createClass(A, [{
    key: "get",
    value: function get() {
      return 'a';
    }
  }], [{
    key: "say",
    value: function say() {
      return 'hi';
    }
  }]);

  return A;
}();

_Promise.resolve().then(function () {
  console.log(1);
}).finally(function () {
  console.log(2);
});

_mapInstanceProperty(_context = [1, 2]).call(_context, function (item) {
  return item + 1;
});

_padStartInstanceProperty(_context2 = 'x').call(_context2, 2, 'a');

var m = new _Map();
var s = new _Set();

_includesInstanceProperty(_context3 = [1, 2]).call(_context3, function (item) { // 实例方法兼容
  return item === 1;
});


// 总结
1、存在重复引入
2、从 core-js  @babel/runtime-corejs3 中引入
3、可以对实例方法进行 helper
// demo2: usage + runtime-corejs2
module.exports = {
  presets: [
    ['@babel/preset-env', {
      'debug': false,
      'useBuiltIns': 'usage',
      'modules': false,
      targets: {
        "node": "current",
        browsers: ['last 2 versions']
      }
    }],
  ],
  plugins: [
    ["@babel/plugin-transform-runtime", { 
      corejs: 2 ,
      helpers: true
    }],
  ]
}

// output
import "core-js/modules/es7.array.includes";
import "core-js/modules/es6.string.includes";
import _Set from "@babel/runtime-corejs2/core-js/set";
import _Map from "@babel/runtime-corejs2/core-js/map";
import "core-js/modules/es7.string.pad-start";
import _Promise from "@babel/runtime-corejs2/core-js/promise";
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";
import "core-js/modules/es7.promise.finally";
import "core-js/modules/es6.function.name";
import _classCallCheck from "@babel/runtime-corejs2/helpers/classCallCheck";
import _createClass from "@babel/runtime-corejs2/helpers/createClass";

var A =
/*#__PURE__*/
function () {
  function A(name) {
    _classCallCheck(this, A);

    this.name = name;
    console.log(this instanceof A ? this.constructor : void 0);
  }

  _createClass(A, [{
    key: "get",
    value: function get() {
      return 'a';
    }
  }], [{
    key: "say",
    value: function say() {
      return 'hi';
    }
  }]);

  return A;
}();

_Promise.resolve().then(function () {
  console.log(1);
}).finally(function () {
  console.log(2);
});

[1, 2].map(function (item) {
  return item + 1;
});
'x'.padStart(2, 'a');
var m = new _Map();
var s = new _Set();
[1, 2].includes(function (item) {
  return item === 1;
});

// 总结
1、重复引入
2、从 core-js  @babel/runtime-corejs2 中引入
3@babel/runtime-corejs2 无法实现实例方法兼容
4、core-js 可以实例方法兼容
// demo 3: usage + @babel/runtime
module.exports = {
  presets: [
    ['@babel/preset-env', {
      'debug': false,
      'useBuiltIns': 'usage',
      'modules': false,
      targets: {
        "node": "current",
        browsers: ['last 2 versions']
      }
    }],
  ],
  plugins: [
    ["@babel/plugin-transform-runtime", {  // 默认从 @babel/runtime 里导入
      helpers: true 
    }],
  ]
}

// output
import "core-js/modules/es7.array.includes";
import "core-js/modules/es6.string.includes";
import "core-js/modules/es6.set";
import "core-js/modules/web.dom.iterable";
import "core-js/modules/es6.array.iterator";
import "core-js/modules/es6.string.iterator";
import "core-js/modules/es6.map"; // 直接覆盖内置类型
import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";
import "core-js/modules/es7.promise.finally";
import "core-js/modules/es6.function.name";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; // 从 @babel/runtime 里导入
import _createClass from "@babel/runtime/helpers/createClass";

var A =
/*#__PURE__*/
function () {
  function A(name) {
    _classCallCheck(this, A);

    this.name = name;
    console.log(this instanceof A ? this.constructor : void 0);
  }

  _createClass(A, [{
    key: "get",
    value: function get() {
      return 'a';
    }
  }], [{
    key: "say",
    value: function say() {
      return 'hi';
    }
  }]);

  return A;
}();

Promise.resolve().then(function () {
  console.log(1);
}).finally(function () {
  console.log(2);
});
[1, 2].map(function (item) {
  return item + 1;
});
'x'.padStart(2, 'a');
var m = new Map();
var s = new Set();
[1, 2].includes(function (item) {
  return item === 1;
});

// 总结
1、覆盖内置类型
2、从 core-js 中引入
// demo 4: 'useBuiltIns': false + babel/runtime-corejs3
module.exports = {
  presets: [
    ['@babel/preset-env', {
      'debug': false,
      'useBuiltIns': false,
      'modules': false,
      targets: {
        "node": "current",
        browsers: ['last 2 versions']
      }
    }],
  ],
  plugins: [
    ["@babel/plugin-transform-runtime", { 
      corejs: 3 ,
      helpers: true
    }],
  ]
}

// output
import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _Set from "@babel/runtime-corejs3/core-js-stable/set";
import _Map from "@babel/runtime-corejs3/core-js-stable/map";
import _padStartInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/pad-start";
import _mapInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/map";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
import _classCallCheck from "@babel/runtime-corejs3/helpers/classCallCheck";
import _createClass from "@babel/runtime-corejs3/helpers/createClass";

var _context, _context2, _context3;

var A =
/*#__PURE__*/
function () {
  function A(name) {
    _classCallCheck(this, A);

    this.name = name;
    console.log(this instanceof A ? this.constructor : void 0);
  }

  _createClass(A, [{
    key: "get",
    value: function get() {
      return 'a';
    }
  }], [{
    key: "say",
    value: function say() {
      return 'hi';
    }
  }]);

  return A;
}();

_Promise.resolve().then(function () {
  console.log(1);
}).finally(function () {
  console.log(2);
});

_mapInstanceProperty(_context = [1, 2]).call(_context, function (item) {
  return item + 1;
});

_padStartInstanceProperty(_context2 = 'x').call(_context2, 2, 'a');

var m = new _Map();
var s = new _Set();

_includesInstanceProperty(_context3 = [1, 2]).call(_context3, function (item) {
  return item === 1;
});

// 总结
1、全部从 runtime-corejs3 中导出
2、无覆盖,无命名冲突
3、没有 useBuiltIns 也能插入 helper???

babel 的未来?

react router 源码阅读

版本:5.2.0

使用/接口设计

<Router>
	<Switch>
		<Route exact path="/"> <Home /> </Route>
    <Route path="/about"> <About /> </Route>
		<Route path="/list"> <List /> </Route>
		<Route path="*">
			<Redirect
        to={{
          pathname: "/404",
          state: { msg: 'notfount' }
        }}
      />
		</Route>
	</Switch>
</Router>

<Switch>
  <Route path="/" exact children={<Home />} />
  <Route path="/about" children={<About/ >} />
  <Route path="/list" children={<List />} />
  
  <Route
    path="/about"
    render={props => (
      <About {...props} routes={about.routes} /> // routes 子路由嵌套
    )}
  />
</Switch>

// router
BrowserRouter
HashRouter
// 或者使用 Router + custom history

// hook
let history = useHistory()
let location = useLocation()
let match = useRouteMatch({
  path: to,
  exact: activeOnlyWhenExact
})
let {id} = useParams()

// hoc
withRouter

// 切换
<Link to="/about">about</Link>
history.replace()
history.push()

// Switch 组件,只显示一个路由组件

源码阅读

// Router 组件

props {
  history
  children
}

class Router extends Component {
  contructor(props) {
    super(props)
    this.state = {
      location: props.hisory.location // 通过 location 来区分视图
    }
    
    this.unlisten = props.history.listen(location => {
      this.setState({location}) 
    })
  }
  
  render() {
    // 将一些值通过 context 传给 Route 组件
    return (
    	<RouterContext.Provider value={{history, location, match, state, staticContext}}>
      	<HistoryContext.Provider value={{history}}>
      		{this.props.children}
      	</HistoryContext.Provider>
    	</RouterContext.Provider>
    )
  }
}


// Switch 组件
props {
  location
  children
}

class Switch extends Component {
  render () {
    return (
    	<RouterContext.Consumer>
      {context => {
        // 返回匹配到的组件,通过 matchPath 函数匹配 props.children 里 path 一致的组件
      }}
     </RouterContext.Consumer>
    )
  }
}

// Route 组件
props {
  path
  exact
  location
  strict
  sensitive
  render // render props
  children // 子组件
  component
}

class Route extends Component {
  render() {
    return (
    	<RouterContext.Consumer>
      {context => {
       	// ...
       	return (
       		<RouterContext.Provider value={props}> // props 是 context,location,match 的合并
      			// 子组件是:
      			component/children/children(props)/render/render(props)
      		</RouterContext.Provider>
       	)
      }}
      </RouterContext.Consumer>
    )
  }
}

// Rdirect 组件
在组件 didmount/didUpdate 的时候进行路由切换
通过 push/replace 来进行
render null

// hook
// 都是通过 historyContext, RouterContext 上获取 context 的值来实现的
function useHistory() {
  return useContext(HistoryContext);
}

了解一下 useContext

const MyContext = createContext(null)

// provider, parent
<MyContext.Provider value={data}>
	<Demo />
</MyContext.Provider>

// 1、consumer, children, Demo
const data = useContext(MyContext)

// 2、consumber
<MyContext.Consumer>
{data => ...}
</MyContext.Consumer>

// 简单例子:通过 context 来实现一个状态管理
const StoreContext = createContext(null)
const [count, set] = useState(0)
const state = initState
const updateState = (nextState) => {
    Object.assign(state, nextState)
  	setState(count+1)
  }
}

<StoreContext.Provider value={{state, updateState}}>
	<Demo />
</StoreContext.Provider>

const {state, updateState} = useContext(StoreContext)

react v16.13.1 源码阅读

reactElement

$$typeof: Symbol(react.element)
key: null
props:
	children:
    $$typeof: Symbol(react.element)
    key: null
    props: {}
    ref: null
    type: ƒ App()
    _owner: null
    _store: {validated: true}
    _self: null
    _source: {fileName: "/Users/renjiazheng/Jar/jar-github/read-source/read-source/src/index.js", lineNumber: 9, columnNumber: 5}
    __proto__: Object
    __proto__: Object
ref: null
type: Symbol(react.strict_mode)
_owner: null
_store: {validated: false}


const Demo = (props) => {
	return (<div className="demo">demo, {props.data}</div>)
}

const App = () => {
	return (
    	<div className="app">
      	<Demo data={'hi'} />
      </div>
    )
}

// ==> jsx to react element
var Demo = function Demo(props) {
  // type: html tag/comp/内建组件
  // props: props/config/ref/key 等
  // children: 后面的参数都是 children
  return React.createElement("div", {className: 'demo'}, "demo, ", props.data);
};

var App = function App() {
  return React.createElement("div", {className: 'app'}, React.createElement(Demo, {
    data: 'hi'
}));
  
// 所有的 jsx 都会被转成 createElement,最终导出的就只有一个 App 函数,进入 render
  • 函数组件/类组件的 render 返回值就是 react element
  • type
    • String,表示原生 dom,称为 hostcomponent
    • class,classscomponent 本身
    • function,functioncomponent 本身
    • symbol,如:
      • symbol(react.strict_mode)
      • symbol(react.portal)
      • symbol(react.profile)
      • symbol(react.fragment)
      • symbol(react.provider)
      • symbol(react.context)
      • symbol(react.forward_ref)
      • symbol(react.suspense)
      • symbol(react.memo)
      • symbol(react.lazy)
      • ...
  • $$typeof
    • ReactElememt 函数创建的 element 都是 symbol(react.element)
  • props
    • 接收的 props 和 children

FiberRoot

function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  this.current = null; // fiber tree 的根节点
  this.containerInfo = containerInfo; // root dom
  this.hydrate = hydrate; // 是否要与以后的 dom 融合
  
  this.pendingChildren = null;
  this.pingCache = null;
  this.finishedExpirationTime = NoWork;
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.callbackNode = null;
  this.callbackPriority = NoPriority;
  this.firstPendingTime = NoWork;
  this.firstSuspendedTime = NoWork;
  this.lastSuspendedTime = NoWork;
  this.nextKnownPendingLevel = NoWork;
  this.lastPingedTime = NoWork;
  this.lastExpiredTime = NoWork;

  this.interactionThreadID = tracing.unstable_getThreadID();
  this.memoizedInteractions = new Set();
  this.pendingInteractionMap = new Map();
}

FiberNode

function FiberNode(tag, pendingProps, key, mode) {
  this.tag = tag; // 不同的组件类型
  this.key = key; // react element 的 key
  this.elementType = null; // react element 的 type
  this.type = null; // 异步组件resolved之后返回的内容,一般是`function`或者`class`
  this.stateNode = null; // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)

  this.return = null; // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  this.child = null; // 指向自己的第一个子节点
  this.sibling = null; // 兄弟节点的return指向同一个父节点
  
  this.index = 0;
  this.ref = null; // ref
  
  this.pendingProps = pendingProps; // 新的 props
  this.memoizedProps = null; // 旧的 props
  this.memoizedState = null; // 新旧 state
  
  this.updateQueue = null; // 该Fiber对应的组件产生的Update会存放在这个队列里面
  
  this.dependencies = null;
  this.mode = mode; // Effects 表示这个子树是否默认是异步渲染的

  this.effectTag = NoEffect; // 用来记录Side Effect
  this.nextEffect = null; // 用来快速查找下一个side effect
  this.firstEffect = null; // 第一个side effect
  this.lastEffect = null; // 最后一个side effect
  
  this.expirationTime = NoWork; // 过期时间
  this.childExpirationTime = NoWork; // 快速确定子树中是否有不在等待的变化
  this.alternate = null; // `current <==> workInProgress` 在渲染完成之后他们会交换位置,类似于备份,方便重用
}

tag

  • fiber node 类型
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class,未知类型
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5; // html tag
export const HostText = 6; // text
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;

effectTags

  • 副作用类型,标记每个更新的类型,作相应的处理
export const NoEffect = /*              */ 0b00000000000;
export const PerformedWork = /*         */ 0b00000000001;

// You can change the rest (and add more).
export const Placement = /*             */ 0b00000000010; // 插入/移到
export const Update = /*                */ 0b00000000100; // props 更新
export const PlacementAndUpdate = /*    */ 0b00000000110;
export const Deletion = /*              */ 0b00000001000;
export const ContentReset = /*          */ 0b00000010000;
export const Callback = /*              */ 0b00000100000;
export const DidCapture = /*            */ 0b00001000000;
export const Ref = /*                   */ 0b00010000000;
export const Snapshot = /*              */ 0b00100000000;

// Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /*   */ 0b00110100100;

// Union of all host effects
export const HostEffectMask = /*        */ 0b00111111111;

export const Incomplete = /*            */ 0b01000000000;
export const ShouldCapture = /*         */ 0b10000000000;

update & updateQueue

  • 调用更新函数会生成 update 对象,update 对象会维护在对应组件的 fiber node 上
// 通过链表来存储所有的 update
export type Update<State> = {
  // 更新的过期时间
  expirationTime: ExpirationTime,

  // export const UpdateState = 0;
  // export const ReplaceState = 1;
  // export const ForceUpdate = 2;
  // export const CaptureUpdate = 3;
  // 指定更新的类型,值为以上几种
  tag: 0 | 1 | 2 | 3,
  // 更新内容,比如`setState`接收的第一个参数,函数或者一个 state 对象
  payload: any,
  // 对应的回调,`setState`,`render`都有
  callback: (() => mixed) | null,

  // 指向下一个更新
  next: Update<State> | null,
  // 指向下一个`side effect`
  nextEffect: Update<State> | null,
};

// 一个普通的对象
export type UpdateQueue<State> = {
  // 每次操作完更新之后的`state`,当更新阶段,basestate 存的是 state
  baseState: State,

  // 队列中的第一个`Update`
  firstUpdate: Update<State> | null,
  // 队列中的最后一个`Update`
  lastUpdate: Update<State> | null,

  // 第一个捕获类型的`Update`
  firstCapturedUpdate: Update<State> | null,
  // 最后一个捕获类型的`Update`
  lastCapturedUpdate: Update<State> | null,

  // 第一个`side effect`
  firstEffect: Update<State> | null,
  // 最后一个`side effect`
  lastEffect: Update<State> | null,

  // 第一个和最后一个捕获产生的`side effect`
  firstCapturedEffect: Update<State> | null,
  lastCapturedEffect: Update<State> | null,
};

渲染阶段

  • render(element, container, callback)

    • return legacyRenderSubtreeIntoContainer(null, element, container, false, callback)

      • root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate)

        • 创建 fiber root,如果不是 hydrate 模式,移除 dom root 内的节点
        • new FiberRootNode,创建 fiber root 实例
        • 同时参加 hostFiber,createFiber(HostRoot, null, null, mode),即 fiber tree 的根节点
        • 将 hostFiber 赋给 root.current,hostFiber.stateNode = root,stateNode 对应着 fiber root
      • 封装 render 传入的 callback

      • updateContainer(children, fiberRoot, parentComponent, callback)

        • scheduleWork(current$1, expirationTime) ,即执行 scheduleUpdateOnFiber 函数

          • performSyncWorkOnRoot(root),同步的任务处理,还有 performConcurrentWorkOnRoot 模式的

            • workLoopSync(),执行该函数直到完成,执行不同组件的初始化,生成 fiber node

              • workInProgress = performUnitOfWork(workInProgress),循环执行该函数,直到 workInProgress === null 退出循环
                • next = beginWork(current, unitOfWork, renderExpirationTime​$1)
                  • 根据 fiber.tag 的类型来处理不同情况:
                  • IndeterminateComponent,不确定什么类型,mountIndeterminateComponent
                    • 当作 class component 或者 func component 处理
                  • LazyComponent,mountLazyComponent
                    • refineResolvedLazyComponent,检查组件是否 resolve
                    • 当组件没有 resolve,会被抛出 throw
                    • 当组件 resolve 之后,按照组件的 tag,进行相应的处理
                  • FunctionComponent,updateFunctionComponent
                    • renderWithHooks(current, workInProgress, Component, nextProps, context, renderExpirationTime)
                    • 注入 ReactCurrentDispatcher,给 hook 提供通信
                    • var children = Component(props, secondArg),执行函数组件得到 react element
                    • 函数组件执行,这里可能多次执行
                    • reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime),调和子组件
                  • ClassComponent,updateClassComponent
                    • constructClassInstance(workInProgress, Component, nextProps)
                    • var instance = new ctor(props, context),组件实例化
                    • mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime)
                    • applyDerivedStateFromProps,执行 getDerivedStateFromProps
                    • callComponentWillMount,执行 componentWillMount
                    • finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime)
                    • reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime),调和子组件, 即 vdom diff 算法的过程,最后在 fiber tree 上,生成子的 fiber node
                    • 根据 element 的 $$ typeof,string/number,array,interator 进行相应的处理
                    • reconcileSingleElement
                    • 如果是首次渲染,直接创建 fiber node,createFiberFromFragment(element.props.children, returnFiber.mode, expirationTime, element.key)
                    • 如果是更新阶段
                    • 如果 $$typeof 是 react.element
                    • fragment:返回 children 的 fiber node,并删除兄弟节点
                    • reconcileSingleTextNode
                    • createFiberFromText
                    • reconcileChildrenArray
                    • reconcileChildrenIterator
                  • HostRoot,updateHostRoot
                  • HostComponent,dom 类型,如 div 等,updateHostComponent
                  • HostText,updateHostText
                  • SuspenseComponent,updateSuspenseComponent
                  • HostPortal,updatePortalComponent
                  • ForwardRef,updateForwardRef
                  • Fragment,updateFragment
                  • Mode,updateMode
                  • Profiler,updateProfiler
                  • ContextProvider,updateContextProvider
                  • ContextConsumer,updateContextConsumer
                  • MemoComponent,updateMemoComponent
                  • SimpleMemoComponent,updateSimpleMemoComponent
                  • IncompleteClassComponent,mountIncompleteClassComponent
                  • SuspenseListComponent,updateSuspenseListComponent
                • next = completeUnitOfWork(unitOfWork),当 next === null 即叶子结点时,执行 completeUnitOfWork
                  • next = completeWork(current, workInProgress, renderExpirationTime$1)
                    • HostComponent,updateHostComponent
                    • prepareUpdate
                    • diffProperties(domElement, type, oldProps, newProps, rootContainerInstance)
                    • 样式属性,dom 属性
                    • 事件绑定,ensureListeningTo(rootContainerElement, propKey)
                    • legacyListenToEvent(registrationName, doc),事件都是绑定到 doc 上的。相关事件也会绑定,比如只绑定了 input onchange,其他*["blur", "change", "click", "focus", "input", "keydown", "keyup", "selectionchange"] 也一起进行绑定*
                    • legacyListenToTopLevelEvent
                    • trapCapturedEvent/trapBubbledEvent
                    • trapEventForPluginEventSystem
                    • 分为4种事件监听回调
                    • DiscreteEvent,dispatchDiscreteEvent
                    • UserBlockingEvent,dispatchUserBlockingUpdate
                    • ContinuousEvent,dispatchEvent
                    • 其他,dispatchEvent
                    • addEventCaptureListener(container, rawEventName, listener)
                    • addEventBubbleListener(container, rawEventName, listener)
                    • createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress)
                    • var domElement = createElement(type, props, rootContainerInstance, parentNamespace)
                    • precacheFiberNode(internalInstanceHandle, domElement)
                    • updateFiberProps(domElement, props),事件回调处理
                    • node[internalEventHandlersKey] = props,事件回调对象会挂到 dom node 上,如:input {onFocus: ƒ, onChange: ƒ}
                    • appendAllChildren(instance, workInProgress, false, false)
                    • HostText,updateHostText
            • finishSyncRender,对 fiber node 生成 dom 片段,插入 dom

              • commitRoot,commitRootImpl,

              • commitBeforeMutationEffects

                • commitBeforeMutationLifeCycles
                • flushPassiveEffects
                  • commitPassiveHookEffects
                    • commitHookEffectListUnmount,执行 hook 的 clean。hook 存在 fiber node 的 lasteffect/firsteffect 上
                    • commitHookEffectListMount,执行 hook 的 create
                  • flushSyncCallbackQueue
              • commitMutationEffects

                • commitResetTextContent,清空 innertext
                • commitDetachRef,设置 ref current 为 null
                • commitPlacement,插入到父节点
                  • insertOrAppendPlacementNodeIntoContainer
                  • insertOrAppendPlacementNode
                • commitWork
                  • FunctionComponent,ForwardRef,SimpleMemoComponent
                    • commitHookEffectListUnmount,限制性 effect cleanup
                  • commitUpdate
                    • updateFiberProps,更新事件回调
                    • updateProperties
                    • updateDOMProperties,更新 dom 属性,不同的表单类型处理不一样
                  • commitTextUpdate
                  • commitSuspenseComponent
                    • hideOrUnhideAllChildren,通过 css display none 来显示隐藏节点
                • commitDeletion
                  • unmountHostComponents
                    • commitNestedUnmounts
                    • commitUnmount
                    • removeChildFromContainer
                    • removeChild
                    • commitUnmount
                    • onCommitUnmount
                    • safelyCallDestroy,hook destory 清除副作用
                    • safelyDetachRef,重置 ref
                    • safelyCallComponentWillUnmount,执行 unmount 函数
                  • detachFiber,重置 fiber node 的 alternate 属性
              • commitLayoutEffects,执行生命周期函数

                • commitLifeCycles
                  • func comp,commitHookEffectListMount
                    • 执行 useEffect 的 create 函数,同时得到 destory
                    • effect.destory = create()
                  • class comp
                    • instance.componentDidMount(),执行 didmount
                    • instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate), 执行 didupdate
                    • commitUpdateQueue(finishedWork, updateQueue, instance)
                  • hostcomponent
                    • commitMount(_instance2, type, props),dom focus
                  • HostRoot
                    • commitUpdateQueue
                  • SuspenseComponent
                    • commitSuspenseHydrationCallbacks
                • commitAttachRef
                  • 给 fiber.ref 赋值 fiber.stateNode, stateNode 就是 dom 实例
              • flushSyncCallbackQueue,执行 render 传入的 callback

  • 总结

    • beginWork 阶段
      • 组件实例化,执行部分生命周期函数
      • reconcileChildren 调和子组件,生成子组件的 fiber node,子组件实例化,执行自身生命周期函数
      • completeUnitOfWork 对 html tag/text 节点进行 dom 创建,属性挂载/事件绑定
    • finishSyncRender 阶段
      • dom 片段生成,执行剩余生命周期函数

更新阶段

  • setstate

    • var fiber = get(inst),根据 instance 找到对应的 fiber node

    • enqueueSetState,创建一个 update,将 update 存到链表中的 sharedQueue.pending

    • scheduleWork,即 scheduleUpdateOnFiber,进入循环更新

    • // 为更新创建对应的 update 对象
      callback: null
      expirationTime: 1073741823 // 过期时间
      next: // 通过链表链接其他的 update
        callback: null
        expirationTime: 1073741823
        next: {expirationTime: 1073741823, suspenseConfig: null, tag: 0, payload: {}, callback: null,}
        payload: {count: 3} // setstate({count: 3})
        priority: 98
        suspenseConfig: null
        tag: 0
        __proto__: Object
      payload: {count: 3} // setstate 的 state
      priority: 98 // 优先级
      suspenseConfig: null
      tag: 0
      
      
      // fiber 上的 updatequeue 对象
      updateQueue:
        baseState: {count: 1} // 最初的 state,更新后的 state 会更新这里
        effects: null
        baseQueue: { // 已处理的 update 链
          callback: null
          expirationTime: 1073741823
          next: {}
          payload: {count: 2}
          priority: 97
          suspenseConfig: null
          tag: 0
        }
        shared: {
          pending: { // shared.pending,等待处理的 update
            callback: null
            expirationTime: 1073741823
            next: {
              callback: null
              expirationTime: 1073741823
              next: {expirationTime: 1073741823, suspenseConfig: null, tag: 0, payload: {}, callback: null,}
              payload: {count: 3} // 第一个 setstate
              priority: 98
              suspenseConfig: null
              tag: 0
            }
            payload: {count: 4} // 并列的第二个 setstate,会批量更新
            priority: 98
            suspenseConfig: null
            tag: 0
          }
        }
       
      • 只有一个节点时,update 对象的 next 执行自身。循环链表

      • payload,setstate 传入的对象

      • 当连续 setstate 时

        • 正常情况:

          • 每个 setstate 会创建一个 update 存到 shared.pending 对象上,然后通过循环链表 next 来连起所有的 update,等到事件回调执行完才去进行调度更新,达到批量更新的效果
        • 事件循环情况:

          • 当 setState 被推到事件循环中时,由于事件循环的机制,所有的任务都是分开执行的,所以每个 setstate 会执行一次调度更新

          • const handle = () => {
              // 两个 setTimeout 的任务是在事件循坏中分开执行的,当事件 handle 执行完后,不会有调度更新,当一个 settimeout task 执行时,会进行一次更新
              setTimeout(() => {
                // 里面的 setstate 也是逐个更新的,不是批量的,为什么?因为 excutionContext 是 nocontext。默认情况下就是每个更新动作都会触发一次调度更新。react 针对合成事件/生命函数进行了优化,阻止了调度更新。把调度更新移到了合成事件/生命函数执行完之后。使得一次合成事件执行,里面即使有多个更新动作,也只在最后才触发一次调度更新。
                this.setState({ 
                  count: 0
                })
                this.setState({
                  count: 1
                })
              }, 0)
              
              setTimeout(() => {
                this.setState({
                  count: 0
                })
              }, 0)
            }
  • forceupdate

    • var fiber = get(inst),根据 instance 找到对应的 fiber
    • enqueueForceUpdate,创建 update,挂在 updateQueue 在当前的 fiber
    • update.tag = ForceUpdate
    • scheduleWork
  • replaceState

    • 跟 setState、forceUpdate 一样的逻辑
    • update.tag = ReplaceState
    • scheduleWork
  • useState

    • dispatchAction
      • scheduleWork
  • 总结

    • 触发更新动作,创建 update 存到对应的 fiber.updateQueue.shared.pending.payload 上,当多个更新触发时,会通过 next 指针形成链表。

    • 每个 update 都是由更新动作触发而创建的。forceupdate/replacestate/setstate/usestate 的调用都会转化为一个个 update 对象,最后形成 update 链表

    • 多个 setState 触发会如何?

      • 多个 setstate 在一个事件回调里面,需要等到事件回调执行完成。setstate 触发 scheduleWork 但不会进行 performSyncWorkOnRoot。而是等到事件回调执行完成。
      • 执行 ensureRootIsScheduled 来将一个个 performSyncWorkOnRoot 存进一个数组里。
      • 等事件回调执行完,会通过 flushSyncCallbackQueueImpl 方法将数组里的 performSyncWorkOnRoot 取出执行。这样就进入更新调度了

事件系统

  • 事件是如何绑定的?

    • p 标签上有 onclick 事件,p 标签的父是 div

      • 在 div 对应的 fiber 调和子节点的时候生成 p 的 fiber,p 的事件还存在 p fiber.pendingProps 上。beginwork 完成后会将 fiber.pendingProps 上的属性存到 fiber.memoizedProps 上。即事件回调存到了 fiber.memoizedProps 上

      • 在 completeWork 时,创建 fiber.stateNode。然后在 updateFiberProps 函数中 fiber.memoizedProps 赋给 fiber.stateNode

      • // 事件回调存在 fiber.stateNode 上,不管是函数组件还是类组件
        fiber.stateNode: {
          handleClick: f,
          handleChange: f,
        }
    • 渲染阶段,在 finalizeInitialChildren 函数里进行

      • setInitialProperties,对某些标签的事件进行绑定,如:load,error,reset,submit,toggle,invalid 等,对表单元素会做很多额外的封装

      • setInitialDOMProperties,设置 dom style,innerhtml,innertext,通过 ensureListeningTo 进行事件绑定等。这里的事件绑定是代理到 dom root/document 的

      • ensureListeningTo,除了绑定对应的事件,还有其他相同组的事件,

        • 如:绑定的是 onchange,但 react 同时还会绑定 ["blur", "change", "click", "focus", "input", "keydown", "keyup", "selectionchange"]

        • react 会将事件回调进行封装。比如,onchange = () => {console.log('hi')}。这个事件回调是存在 fiber.stateNode 上的

        • 在事件绑定的时候,通过 dispatchDiscreteEvent 返回事件回调。即事件回调是被 dispatchDiscreteEvent 封装了

        • // null,click,1,body
          listener = dispatchDiscreteEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container)
          
          discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, container, nativeEvent)
          
          
          dispatchEvent
            var blockedOn = attemptToDispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent); // 回去对应的 dom 实例,即 fiber.stateNode
            queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, container, nativeEvent)
        • discreteUpdates

  • 事件是如何执行的?

    • 事件会代理在 dom root 上。当事件触发时,dom root 会监听到事件触发,然后执行事件回调
    • 每个事件回调都会被 dispatchEvent 封装,通过 runWithPriority 进行调度执行

image-20200602230333548

hook

  • hook 是如何存储的?
const App = () => {
  const [a, seta] = useState(11) // hook 1
  const ref = useRef('ref') // hook 2
  const [b, setb] = useState(22) // hook 3
  useEffect(() => { // hook 4
    console.log('effect')
    return () => {
      console.log('clean')
    }
  }, [])
  
  return (<div>app</div>)
}

// 当前 fiber node
...
elementType: ƒ App()
memoizedState: {
  baseQueue: null
  baseState: 11
  memoizedState: 11 // hook 1
  next: {
    baseQueue: null
    baseState: null
    memoizedState: {current: div.App} // hook 2
    next: {
      baseQueue: null
      baseState: 22
      memoizedState: 22 // hook 3
      next: { // hook 4, effect hook 没有 memoizedState
        create: () => {}  // effect
        deps: null
        destroy: () => { console.log('clean'); } // effect cleanup
        next: {tag: 5, deps: null, next: {}, create: ƒ, destroy: ƒ}
        tag: 5
      }
      queue: {
      	dispatch: ƒ () // setb
        lastRenderedReducer: ƒ basicStateReducer(state, action)
        lastRenderedState: 22
        pending: null
      }
    }
    queue: null
    }
  }
  queue: {
    dispatch: ƒ () // seta
    lastRenderedReducer: ƒ basicStateReducer(state, action)
    lastRenderedState: 11
    pending:
  }
}
  • hook 是如何执行的?

    • useState

      • mountState(initialState)

        • 创建一个空的 hook 对象,计算 initialstate,因为有可能是函数,将 state 存到 hook.baseState 上,同时初始化 hook.queue, 绑定 hook.dispatcher

        • hook: {
            baseQueue: null
            baseState: null
            memoizedState: null
            next: null
            queue: null
          }
          hook.memoizedState = hook.baseState = initialState
          hook.queue = {
            pending: null,
            dispatch: null,
            lastRenderedReducer: basicStateReducer,
            lastRenderedState: initialState
          }
          dispatch = hook.queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
          
          // [a, seta] = useState(1)
          return [hook.memoizedState, dispatch]
      • dispatchAction

        • scheduleWork
      seta(33)
      dispatchAction(fiber, queue, action)
      
      // 参数:
      action: 33
      queue: {
        dispatch: ƒ ()
        lastRenderedReducer: ƒ basicStateReducer(state, action)
        lastRenderedState: 11
        pending: {
          action: 33
          eagerReducer: ƒ basicStateReducer(state, action)
          eagerState: 33
          expirationTime: 1073741823
          next: {expirationTime: 1073741823, suspenseConfig: null, action: 33, eagerState: 33, eagerReducer: ƒ,}
          priority: 98
        }
      }
      
      然后创建一个 update
      接着开始调度更新:scheduleWork(fiber, expirationTime) // 所有更新最终都是结果 scheduleWork 来调度
    • useRef

      • mountRef(initialValue)

        • 创建一个空的 hook 对象,创建 ref 对象,将 ref 对象存到 hook 上

        • hook:{
            baseQueue: null
            baseState: null
            memoizedState: null
            next: null
            queue: null
          }
          
          ref: {
            current: initialvalue
          }
          
          hook.memoizedState = ref
          
    • useEffect

      • mountEffect(create, deps)

        • mountEffectImpl(Update | Passive, Passive$1, create, deps)

        • 创建一个 hook,标记 effectTag = fiberEffectTag,将 effect 挂到 hook.memoizedState 上

        • hook: {
            baseQueue: null
            baseState: null
            memoizedState: {tag: 5, destroy: undefined, deps: null, next: {…}, create: ƒ}
            next: null
            queue: null
          }
          
    • useCallback

      • mountCallback(callback, deps)
        • hook.memoizedState = [callback, nextDeps]
    • useContext

      • readContext(callback, deps)

      • // 首个 context
        lastContextDependency = contextItem;
        currentlyRenderingFiber.dependencies = { // 在 fiber dependencies 上存储 context 信息
          expirationTime: NoWork,
          firstContext: {
            context: context,
            observedBits: deps,
            next: null
          },
          responders: null
        };
        
        // 新增的 context
        lastContextDependency = lastContextDependency.next = {
            context: context,
            observedBits: deps,
            next: null
          }
        
    • useLayoutEffect

      • mountLayoutEffect(create, deps)

        • mountEffectImpl

        • mountEffectImpl(Update | Passive, Passive$1, create, deps) // useEffect
          mountEffectImpl(Update, Layout, create, deps) // useLayoutEffect
          
    • useReducer

      • mountReducer,和 usestate 的逻辑差不多
    • 当组件多长执行时,只是重复创建 hook ,相当于 hook 的初始化,不会进行 hook 的执行。hook 还没有和 fiber 相关联

    • 但 seta 的时候,会进行 dispatch 当前的 fiber 和当前的 hook.queue

context

  • context 存在哪里?也是 fiber.stateNode 上
  • todo

ref

  • todo
react element
$$typeof: Symbol(react.element)
key: null
props: {className: "App", children: Array(3)}
ref: {current: "hiref"}
type: "div"
_owner: FiberNode {tag: 2, key: null, stateNode: null, elementType: ƒ, type: ƒ, …}
_store: {validated: false}

concurrent mode

  • todo

整体的 fiber tree

class DemoCls extends React.Component {
  state = {
    count: 1
  }

handle = () => {
    this.setState({
      count: 3
    })
  }

  render() {
    return (
      <div onClick={this.handle}>class, {this.state.count}</div>
    )
  }
}

function App() {
  const [a, seta] = useState(11)
  const ref = useRef('hiref')
  const [b, setb] = useState(22)
  
  useEffect(() => {
		console.log('effect')
    return () => {
      console.log('clean')
    }
  })

  const handle = () => {
    console.log(ref)
  }

  const handleChange = () => {
    console.log('input')
  }

  const handleFocus = () => {
    console.log('focus')
  }

  return (
    <div>
      <p 
        onClick={handle} 
        style={{color: '#fff'}} 
        className="test" 
        id="test" 
        key="test"
        ref={ref}
      >
        say <span>hi</span> haaa!
      </p>

      <input 
        id="inpuid" 
        onFocus={handleFocus} 
        onChange={handleChange} 
      />

      <DemoCls />
    </div>
  );
}

render(<App />, root)


fiberRoot {
  containerInfo: 指向 dom root 
  current: 指向 root fiber,fiber tree 的根节点
}

hostFiber { // fiber tree 的根节点
  alternate: 自身
  stateNode: 指向 fiberRoot
  type: null
  child: 指向 App  fiber
}

App fiber {
  alternate: 指向自身
  stateNode: null
  type: f App() // 组件
  elemenType: f App() // 组件
  return: 指向 host fiber
  sibling:null
  child:指向 div fiber
  firstEffect: 需要更新的 fiber
  lastEffect: 需要更新的 fiber
  nextEffect: null
  memoizedProps: {}
  memoizedState: {
    baseQueue: null
    baseState: 11
    memoizedState: 11
    queue: {
      dispatch: f // seta
      lastRenderedReducer: f
      lastRenderedState: 11
      pending: null
    }
    next: {
      baseQueue: null
      baseState: null
      memoizedState: {
        current: f
      }
      queue: null
      next: {
        baseQueue: null
        baseState: 22
        memoizedState: 22
        queue: {
          dispatch: f // setb
          lastRenderedReducer: f
          lastRenderedState: 22
          pending: null
        }
        next: {
          baseQueue: null
          baseState: null
          memoizedState: {
            create: f
            deps: null
            destroy: f
            next: {}
          }
          queue: null
        }
      }
    }
  }
  pendingProps: {}
  ref: null
  updateQueue: {
    lastEffect: { // useEffect
      create: () => {...}
      deps: null
      destroy: () => {...} // clean up
      next: {...} // next useEffect
    }
  }
}

div fiber {
  alternate: 自身
	stateNode: dom div
	type: "div"
	elementType: "div"
	return: 指向 app fiber
	sibling:null
	child:指向 p fiber,第一个 child
	ref: null
	key: null
	firstEffect: 需要更新的 fiber
  lastEffect: 需要更新的 fiber
  nextEffect: null
  memoizedProps: 指向 children 组件,react element,不是 fiber,3个 children。p、input、democl
  memoizedState: null
  prndingProps: 指向 children 组件,react element,不是 fiber,3个 children
}

p fiber {
  alternate: 自身
  stateNode:dom p
  type: "p"
  elementType: "p"
  return: 指向 div fiber
  sibling:指向 input fiber
  child:指向 say fiber
  ref:{current: f}
  key: "test"
  fistEffect: null
  lastEffect: null
  nextEffect: null
  memoizedProps: {
    children: ["say", {...}, "haaa!"] // react element
    className: "test"
    id: "test"
    onClick: f
    style: {color: "#fff"}
  },
  memoizedState: null
  pendingProps: {
    children: ["say", {...}, "haaa!"] // react element
    className: "test"
    id: "test"
    onClick: f
    style: {color: "#fff"}
  }
}

say fiber {
  alternate: null
  stateNode:text say
  type: null
  elementType: null
  return: 指向 p fiber
  sibling:指向 span fiber
  child:null
  ref:null
  key: null
  fistEffect: null
  lastEffect: null
  nextEffect: null
}

...

Democls fiber {
  alternate: 自身
  stateNode: Democls fiber
  type: class Democls
  elementType: class Democls
  return: 指向 div fiber
  sibling: null
  child: 指向 div fiber
  memoizedState: {count: 1}
  memoizedProps: {}
  ...
  updateQueue: {
    baseQueue: {
      callback: null
      next: {...}
      pauload: {count: 2} // 还没处理的 update
    }
    baseState: {count: 1} // 已完成的 update
    effects: null
    shared: {pending: null}
  }
}

image-20200603132011686

  • 递归流程

    • 从 hostFiber 开始,一直递归第一个子节点
    • 直到子节点为空,返回上一个含兄弟节点的父节点,从兄弟节点开始递归第一个子节点
    • 直到返回到 hostFiber
  • 渲染流程

    • 从入口 render 开始
    • 先创建 fiberRoot,同时创建 hostFiber
    • 然后进入 updateContainer 函数,在这里创建 update,同时通过 enqueueUpdate 函数把 render 入口的 react element 存到 hostfiber.updateQueue.shared.pending.payload
      • enqueueUpdate(fiber, update),这个函数专门是将更新存在对应的 fiber 上的
    • 设置完 update 就开始继续调度 scheduleWork(fiber, expTime),计算优先级,根据过期时间和优先级,继续不同策略的调度
      • expirationTime === Sync,performSyncWorkOnRoot,同步渲染
      • ensureRootIsScheduled,performConcurrentWorkOnRoot,异步渲染
      • ???同步渲染/异步渲染有什么区别???
    • 在 performSyncWorkOnRoot 循环执行 workLoopSync,直到完成,这时证棵 fiber tree 就构建完成了。然后执行 finishSyncRender(root) 进行渲染 dom
    • workLoopSync 里根据 react element 循环创建对应的 fiber
      • beginWork(current, unitOfWork, expirationTime)
        • 根据 fiber.tag 执行不同的创建 fiber 方式
        • 比如是 hostRoot,即 hostFiber,会拿到 fiber.updateQueue.shared.pending.payload 上的 react element,即 render 的入口组件。一般是 react element 或者 react 的内置组件。如 react.strict_mode。然后进入调和子组件,reconcileChildren
        • 比如是函数组件,直接执行函数组件,执行过程中,会将 hook,props,state 等存到 fiber 上
      • 当到达一个树分支的叶子节点,执行 completeUnitOfWork
        • 根据 fiber.tag 进行相应的 dom 生成,一般是 hostComponent(即 dom 节点,如 div)/hostText (文本)
        • 渲染阶段
          • createInstance,创建相应的 dom 节点
            • updateFiberProps,存储事件回调
          • appendAllChildren,插入到父节点中
          • finalizeInitialChildren,初始化,dom 节点的属性,和事件绑定
          • markRef,ref 绑定 dom 实例
        • 更新阶段
          • updateHostComponen,进行属性更新,事件绑定
    • 调和子组件是什么过程?reconcileChildren
      • 首次渲染,会直接创建对应的 fiber
        • createFiberFromFragment
        • createFiberFromElement
        • createFiberFromText
        • createFiberFromTypeAndProps
        • createFiber
      • 更新阶段
        • reconcileSingleElement,只有一个节点
          • 如果不是 fragment,复用该节点的第一个子节点的 fiber,删除其兄弟节点,重置 ref
          • 如果是 fragment,删除该节点的兄弟节点,返回字节点
        • reconcileSingleTextNode
          • 删除该节点的兄弟节点,复用第一个子节点
      • 数组节点,即两个新旧数组比较,key 的作用,fiber.index 记录着原数组的长度
        • 如果旧的 fiber 存在
          • 循环新的数组,通过 key 优化
            • 通过第一个 oldfiber 和第一个 element,得到一个新 fiber
            • 如果数组里的节点是 string/number,执行 updateTextNode
            • 如果是对象,updateElement/updateFragment
            • 如果旧的节点为空,创建新的节点
        • 如果新数组比就数组小,删除旧数组的其他节点
        • 如果旧的 fiber 不存在
          • 如果旧的数组比新的数组小,直接创建新数组的其他节点的 fiber
        • 通过 key 复用 fiber
    • 循环执行完 workLoopSync 后,进行 finishSyncRender
      • commitBeforeMutationEffects,插入 dom 前,循环遍历 fiber tree 上的 nextEffect,执行一些生命周期函数
        • commitBeforeMutationLifeCycles
          • 执行 getSnapshotBeforeUpdate
        • flushPassiveEffects
          • commitHookEffectListUnmount
          • commitHookEffectListMount,执行 hook
      • commitMutationEffects
        • commitUpdate
          • updateFiberProps
          • updateProperties,updateDOMProperties,更新 dom 属性
        • commitTextUpdate
        • commitPlacement
          • insertOrAppendPlacementNode/insertOrAppendPlacementNodeIntoContainer
          • 将 fiber.stateNode 插入到 dom root 上
      • commitLayoutEffects
        • commitLifeCycles
          • commitHookEffectListMount,hook effect
          • componentDidMount/componentDidUpdate
          • commitUpdateQueue,执行 render 传入的 callback
        • commitAttachRef,绑定 ref
  • 流程回顾

    • // 渲染阶段
      scheduleUpdateOnFiber
        performSyncWorkOnRoot
          workLoopSync
            performUnitOfWork
              beginWork // 通过父 fiber 生成 child element 相应的 fiber,循环完,最终会生成整颗 fiber tree
                /updateFunctionComponent // 函数组件执行,hook dispatcher 绑定
                /updateClassComponent // 类组件执行,相关生命函数执行
                /updateHostRoot
                /updateHostComponent
                /updateHostText
                   reconcileChildren // child element to fiber
            completeUnitOfWork
              completeWork // 给每个 fiber 创建 dom 实例,同时存到对应 fiber.stateNode。循环将子的 dom 实例插入到父的 dom 实例中。然后初始化每个 fiber.statenode 的 dom 属性、事件等
          finishSyncRender // 如果是组件,执行相关的生命函数、hook。将 hostfiber.stateNode 插入到 dom root。最后执行 callback
    • hastFiber,对 updateQueue.shared.pending.payload 上的 react element(即 app 组件) 生成对应的 fiber

    • 然后通过 app fiber 去 reconcileChildren 调合 children element 的到 children fiber。最终完成整颗 fiber tree

    • 在 completeWork 的时候将 instance 存到 fiber.stateNode

    • fiber tree 创建后,会进行 finishSyncRender 生成每个 fiber 对应的 stateNode

  • 整体流程分为:

    • performUnitOfWork = beginWork + reconcilerChildren + completeUnitOfWork
      • beginWork: 组件实例化,生命函数
        • constructor
        • getDeriveStateFromProps
        • shouldComponentUpdate
        • render
      • reconcilerChildren:子 element 对应的 fiber 创建,diff 算法
      • completeUnitOfWork:dom 实例化,属性设置/更新,事件注册,dom 插入形成片段
      • performUnitOfWork 过程可以被中断/恢复/重来。因为这个阶段没有副作用。会出现生命函数执行多次
    • finishSyncRender = commitRoot
      • dom 插入,属性更新,生命函数。这个阶段不能中断,含有副作用。
        • getSnapshotBeforeUpdate // 可以读到 dom
        • componentDidMount
        • componentDidUpdate
    • about fiber:

概览图

image-20200824154805801

image-20200824182042035

image-20200824154901823

image-20200824155010668

image-20200824155029271

image-20200824175834090

image-20200825125206187

image-20200825125236968

image-20200825125308903

image-20200825131424774

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.