没有足够的数据
wupengyu 赞了文章 · 2020-03-18
随着 Web 技术和移动设备的快速发展,Hybrid 技术已经成为一种最主流最常见的方案。一套好的 Hybrid架构方案 能让 App 既能拥有极致的体验和性能,同时也能拥有 Web技术 灵活的开发模式、跨平台能力以及热更新机制,想想是不是都鸡冻不已。。😄。本系列文章是公司在这方面实践的一个总结,包含了原理解析、方案选型与实现、实践优化等方面。
大家可以到github上和我进行讨论哈!
Hybrid App,俗称混合应用,即混合了 Native技术 与 Web技术 进行开发的移动应用。现在比较流行的混合方案主要有三种,主要是在UI渲染机制上的不同:
以上的三种方案,其实同样都是基于 JSBridge 完成的通讯层,第二三种方案,其实可以看做是在方案一的基础上,继续通过不同的新技术进一步提高了应用的混合程度。因此,JSBridge 也是整个混合应用最关键的部分,例如我们在设置微信分享时用到的 JS-SDK,wx对象 便是我们最常见的 JSBridge:
任何技术方案的选型,其实都应该基于使用场景和现有条件。基于公司现有情况的几点考虑,在方案一上进一步优化,更加适合我们的需求。
因此,如何既能利用 H5 强大的开发和迭代能力,又能赋予 H5 强大的底层能力和用户体验,同时能复用现有的成熟 Native组件,便成为了我们最大的需求点 -- 一套完整又强大的 Hybrid技术架构方案。😠
Hybrid App的本质,其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,其实这里也可以理解为我们需要一套跨语言通讯方案,来完成 Native(Java/Objective-c/...) 与 JavaScript 的通讯。这个方案就是我们所说的 JSBridge,而实现的关键,便是作为容器的 WebView,一切的原理都是基于 WebView 的机制。
基于 WebView 的机制和开放的 API, 实现这个功能有三种常见的方案:
第二三种机制的原理是类似的,都是通过对 WebView 信息冒泡传递的拦截,从而达到通讯的,接下来我们主要从 原理-定制协议-拦截协议-参数传递-回调机制 5个方面详细阐述下第三种方案 -- URL拦截方案。
在 WebView 中发出的网络请求,客户端都能进行监听和捕获
我们需要制定一套URL Scheme规则,通常我们的请求会带有对应的协议开头,例如常见的 https://xxx.com 或者 file://1.jpg,代表着不同的含义。我们这里可以将协议类型的请求定制为:
xxcommand://xxxx?param1=1¶m2=2
这里有几个需要注意点的是:
(1) xxcommand:// 只是一种规则,可以根据业务进行制定,使其具有含义,例如我们定义 xxcommand:// 为公司所有App系通用,为通用工具协议:
xxcommand://getProxy?h=1
而定义 xxapp:// 为每个App单独的业务协议。
xxapp://openCamera?h=2
不同的协议头代表着不同的含义,这样便能清楚知道每个协议的适用范围。
(2) 这里不要使用 location.href 发送,因为其自身机制有个问题是同时并发多次请求会被合并成为一次,导致协议被忽略,而并发协议其实是非常常见的功能。我们会使用创建 iframe 发送请求的方式。
(3) 通常考虑到安全性,需要在客户端中设置域名白名单或者限制,避免公司内部业务协议被第三方直接调用。
客户端可以通过 API 对 WebView 发出的请求进行拦截:
当解析到请求 URL 头为制定的协议时,便不发起对应的资源请求,而是解析参数,并进行相关功能或者方法的调用,完成协议功能的映射。
由于协议的本质其实是发送请求,这属于一个异步的过程,因此我们便需要处理对应的回调机制。这里我们采用的方式是JS的事件系统,这里我们会用到 window.addEventListener
和 window.dispatchEvent
这两个基础API;
通过事件的机制,会让开发更符合我们前端的习惯,例如当你需要监听客户端的通知时,同样只需要在通过 addEventListener
进行监听即可。
Tips: 这里有一点需要注意的是,应该避免事件的多次重复绑定,因此当唯一标识重置时,需要removeEventListener
对应的事件。
由于 WebView 对 URL 会有长度的限制,因此常规的通过 search参数 进行传递的方式便具有一个问题,既 当需要传递的参数过长时,可能会导致被截断,例如传递base64或者传递大量数据时。
因此我们需要制定新的参数传递规则,我们使用的是函数调用的方式。这里的原理主要是基于:
Native 可以直接调用 JS 方法并直接获取函数的返回值。
我们只需要对每条协议标记一个唯一标识,并把参数存入参数池中,到时客户端再通过该唯一标识从参数池中获取对应的参数即可。
由于 Native 可以算作 H5 的宿主,因此拥有更大的权限,上面也提到了 Native 可以通过 WebView API直接执行 Js 代码。这样的权限也就让这个方向的通讯变得十分的便捷。
// Swift
webview.stringByEvaluatingJavaScriptFromString("alert('NativeCall')")
// 调用js中的JSBridge.trigger方法
// 该方法的弊端是无法获取函数返回值;
webView.loadUrl("javascript:JSBridge.trigger('NativeCall')")
Tips: 当系统低于4.4时,evaluateJavascript 是无法使用的,因此单纯的使用 loadUrl 无法获取 JS 返回值,这时我们需要使用前面提到的 prompt 的方法进行兼容,让 H5端 通过 prompt 进行数据的发送,客户端进行拦截并获取数据。
// 4.4+后使用该方法便可调用并获取函数返回值;
mWebView.evaluateJavascript("javascript:JSBridge.trigger('NativeCall')", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
基于上面的原理,我们已经明白 JSBridge 最基础的原理,并且能实现 Native <=> H5 的双向通讯机制了。
接下来,我们来理下代码上需要的资源。实现这套方案,从上图可以看出,其实可以分为两个部分:
我们这里的做法是,将这两部分一起封装成一个 Native SDK,由客户端统一引入。客户端在初始化一个 WebView 打开页面时,如果页面地址在白名单中,会直接在 HTML 的头部注入对应的 bridge.js。这样的做法有以下的好处:
这里有一点需要注意的是,协议的调用,一定是需要确保执行在bridge.js 成功注入后。由于客户端的注入行为属于一个附加的异步行为,从H5方很难去捕捉准确的完成时机,因此这里需要通过客户端监听页面完成后,基于上面的回调机制通知 H5端,页面中即可通过window.addEventListener('bridgeReady', e => {})
进行初始化。
将 H5 接入 App 中通常有两种方式:
(1) 在线H5,这是最常见的一种方式。我们只需要将H5代码部署到服务器上,只要把对应的 URL地址 给到客户端,用 WebView 打开该URL,即可嵌入。该方式的好处在于:
但相对的,这种方式也有对应的缺点:
通常,这种方式更适用在一些比较轻量级的页面上,例如一些帮助页、提示页、使用攻略等页面。这些页面的特点是功能性不强,不太需要复杂的功能协议,且不需要离线使用。在一些第三方页面接入上,也会使用这种方式,例如我们的页面调用微信JS-SDK。
(2) 内置包H5,这是一种本地化的嵌入方式,我们需要将代码进行打包后下发到客户端,并由客户端直接解压到本地储存中。通常我们运用在一些比较大和比较重要的模块上。其优点是:
但同时,它的劣势也十分明显:
这两种接入方式均有自己的优缺点,应该根据不同场景进行选择。
本文主要解析了现在Hybrid App的发展现状和其基础原理,包含了
只有在了解了其最本质的实现原理后,才能对这套方案进行实现以及进一步的优化。接下来,我们将基于上面的理论,继续探讨如何把这套方案的真正代码实现以及方案优化方案,请继续 第二篇实战篇。欢迎大家一起讨论!更多文章内容请到github。感谢!😊
查看原文随着 Web 技术和移动设备的快速发展,Hybrid 技术已经成为一种最主流最常见的方案。一套好的 Hybrid架构方案 能让 App 既能拥有极致的体验和性能,同时也能拥有 Web技术 灵活的开发模式、跨平台能力以及热更新机制,想想是不是都鸡冻不已。。😄。本系列文章是公司在这...
赞 180 收藏 143 评论 21
wupengyu 发布了文章 · 2020-01-20
简单来说,就是virtual dom & react diff。
我们都知道在前端开发中,js运行很快,dom操作很慢,而react充分利用了这个前提。在react中render的执行结果是树形结构的javascript对象,当数据(state || props)发生变化时,会生成一个新的树形结构的javascript对象,这两个javascript对象我们可以称之为virtual dom。然后对比两个virtual dom,找出最小的有变化的点,这个对比的过程我们称之为react diff,将这个变化的部分(patch)加入到一个队列中,最终批量更新这些patch到dom中。
关于提升性能,很多人说virtual dom可以提升性能,这一说法实际上是很片面的。因为我们知道,直接操作dom是非常耗费性能的,但是即使我们用了react,最终依然要去操作真实的dom。而react帮我们做的事情就是尽量用最佳的方式有操作dom。如果是首次渲染,virtual dom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。
react本身的优势在于react diff算法和批处理策略。react在页面更新之前,提前计算好了如何进行更新和渲染DOM,实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。所以,在这个过程中React帮助我们"提升了性能"。
所以,我更倾向于说,virtual dom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。
我们在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写,第二种是直接使用React.createElement编写。实际上,上面两种写法是等价的,jsx只是为React.createElemen方法的语法糖,最终所有的jsx都会被babel转换成React.createElement。
但是请注意,babel在编译时会判断jsx中组件的首字母,当首字母为小写时,其被认定为原生dom标签,createElement的第一个变量被编译为字符串。当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象。
在react16中,废弃了三个will属性componentWillMount,componentWillReceiveProps,comonentWillUpdate,但是目前还未删除,react17计划会删除,同时通过UNSAFF_前缀向前兼容。
在 React 中,我们可以将其生命周期分为三个阶段。
react diff会帮助我们计算出virtual dom中真正变化的部分,并只针对该部分进行实际dom操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染。传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3)。react diff基于一下三个策略实现了O(n)的算法复杂度。
基于以上三个前提策略,React分别对tree diff、component diff以及element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。
首先说一下element diff的过程。比如有老的集合(A,B,C,D)和新的集合(B,A,D,C),我们考虑在不增加空间复杂度的情况下如何以O(n)的时间复杂度找出老集合中需要移动的元素。
在react里的思路是这样的,遍历新集合,初始化lastIndex=0(代表访问过的老集合中最右侧的位置),表达式为max(prev.mountIndex, lastIndex),如果当前节点在老集合中的位置即(prev.mountIndex)比lastIndex大说明当前访问节点在老集合中就比上一个节点位置靠后则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。
部分源码为
var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
var prevChild = prevChildren && prevChildren[name]; // 老节点
var nextChild = nextChildren[name]; // 新节点
if (prevChild === nextChild) { // 如果新节点存在老节点集合里
// 移动节点
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) { // 如果不存在在
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// 删除节点
this._unmountChild(prevChild);
}
// 初始化并创建节点
this._mountChildAtIndex(
nextChild, nextIndex, transaction, context
);
}
nextIndex++;
}
// 移动节点
moveChild: function(child, toIndex, lastIndex) {
if (child._mountIndex < lastIndex) {
this.prepareToManageChildren();
enqueueMove(this, child._mountIndex, toIndex);
}
}
React Fiber是React对核心算法的一次重新实现。
在协调阶段阶段,以前由于是采用的递归的遍历方式,这种也被称为Stack Reconciler,主要是为了区别Fiber Reconciler取的一个名字。这种方式有一个特点: 一旦任务开始进行,就无法中断,那么js将一直占用主线程,一直要等到整棵virtual dom树计算完成之后,才能把执行权交给渲染引擎,那么这就会导致一些用户交互、动画等任务无法立即得到处理,就会有卡顿,非常的影响用户体验。
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值时,用户会感觉到卡顿。1秒60帧,所以每一帧分到的时间是1000/60 ≈ 16ms。所以我们书写代码时力求不让一帧的工作量超过 16ms。如果任意一个步骤所占用的时间过长,超过16ms了之后,用户就能看到卡顿。
简单来说就是时间分片 + 链表结构。而fiber就是维护每一个分片的数据结构。
Fiber利用分片的思想,把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,在每个小片执行完之后,就把控制权交还给React负责任务协调的模块,如果有紧急任务就去优先处理,如果没有就继续更新,这样就给其他任务一个执行的机会,唯一的线程就不会一直被独占。
因此,在组件更新时有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。所以 React Fiber把一个更新过程分为两个阶段:
高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身不是React API的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
我理解的高阶组件是,将组件以参数的方式传递给另外一个函数,在该函数中,对组件进行包装,封装了一些公用的组件逻辑,实现组件的逻辑复用,该函数被称为高阶组件。但是请注意,高阶组件不应修改传入的组件行为。
属性代理
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
反向继承
function hoc(ComponentClass) {
return class HOC extends ComponentClass {
render() {
if (this.state.success) {
return super.render()
}
return <div>Loading...</div>
}
}
}
export default class ComponentClass extends Component {
state = {
success: false,
data: null
};
async componentDidMount() {
const result = await fetch(...请求);
this.setState({
success: true,
data: result.data
});
}
render() {
return <div>主要内容</div>
}
}
术语 “render prop” 是指一种技术,用于使用一个值为函数的 prop 在 React 组件之间的代码共享。
带有渲染属性(Render Props)的组件需要一个返回 React 元素并调用它的函数,而不是实现自己的渲染逻辑。
我理解的渲染属性是,提供渲染页面的props给子组件,共享可以共享子组件的状态,复用子组件的状态,并告诉子组件如何进行渲染。
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// 与 HOC 不同,我们可以使用具有 render prop 的普通组件来共享代码
class Mouse extends React.Component {
static propTypes = {
render: PropTypes.func.isRequired
}
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
const App = React.createClass({
render() {
return (
<div style={{ height: '100%' }}>
<Mouse render={({ x, y }) => (
// render prop 给了我们所需要的 state 来渲染我们想要的
<h1>The mouse position is ({x}, {y})</h1>
)}/>
</div>
)
}
})
ReactDOM.render(<App/>, document.getElementById('app'))
React Hooks 是 React 16.7.0-alpha 版本推出的新特性,它可以让你在不编写class的情况下使用state以及其他的 React特性。React Hooks要解决的问题是状态共享,是继render-props和hoc之后的第三种状态共享方案,不会产生JSX嵌套地狱问题。这个状态指的是状态逻辑,所以称为状态逻辑复用会更恰当,因为只共享数据处理逻辑,不会共享数据本身。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
super代表父类的构造函数,javascript规定如果子类不调用super是不允许在子类中使用this的,这不是React的限制,而是javaScript的限制,同时你也必须给super传入props,否则React.Component就没法初始化this.props
在 React 的类组件中,当我们把事件处理函数引用作为回调传递过去,事件处理程序方法会丢失其隐式绑定的上下文。当事件被触发并且处理程序被调用时,this的值会回退到默认绑定,即值为 undefined,这是因为类声明和原型方法是以严格模式运行。
SyntheticEvent是react合成事件的基类,定义了合成事件的基础公共属性和方法。react会根据当前的事件类型来使用不同的合成事件对象,比如鼠标单机事件 - SyntheticMouseEvent,焦点事件-SyntheticFocusEvent等,但是都是继承自SyntheticEvent。在合成事件中主要做了以下三件事情。
组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange等,给document上添加事件addEventListener,并指定统一的事件处理程序dispatchEvent。
通过virtual dom的props属性拿到要注册的事件名,回调函数,通过listenTo方法使用原生的addEventListener进行事件绑定。
事件存储,就是把react组件内的所有事件统一的存放到一个二级map对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。先查找事件名,然后找对对应的组件id相对应的事件。如下图:
由执行机制看,setState本身并不是异步的,而是在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后再执行,这个过程给人一种异步的假象。
ReactComponent.prototype.setState = function(partialState, callback) {
// 将setState事务放进队列中
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
enqueueSetState: function (publicInstance, partialState) {
// 获取当前组件的instance
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 将要更新的state放入一个数组里
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// 将要更新的component instance也放在一个队列里
enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
// 如果没有处于批量创建/更新组件的阶段,则处理update state事务
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果正处于批量创建/更新组件的过程,将当前的组件放在dirtyComponents数组中
dirtyComponents.push(component);
}
这里的partialState可以传object,也可以传function,它会产生新的state以一种Object.assgine()的方式跟旧的state进行合并。
由这段代码可以看到,当前如果正处于创建/更新组件的过程,就不会立刻去更新组件,而是先把当前的组件放在dirtyComponent里,所以不是每一次的setState都会更新组件。这段代码就解释了我们常听说的:setState是一个异步的过程,它会集齐一批需要更新的组件然后一起更新。而batchingStrategy 又是个什么东西呢?
ReactDefaultBatchingStrategy.js
var ReactDefaultBatchingStrategy = {
// 用于标记当前是否出于批量更新
isBatchingUpdates: false,
// 当调用这个方法时,正式开始批量更新
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// 如果当前事务正在更新过程在中,则调用callback,既enqueueUpdate
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
// 否则执行更新事务
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
前端路由的原理思路大致上都是相同的,即实现在无刷新页面的条件下切换显示不同的页面。而前端路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。目前实现前端路由有两种方式:
路径中hash值改变,并不会引起页面刷新,同时我们可以通过hashchange事件,监听hash的变化,从而实现我们根据不同的hash值展示和隐藏不同UI显示的功能,进而实现前端路由。
HTML5的History接口,History对象是一个底层接口,不继承于任何的接口。History接口允许我们操作浏览器会话历史记录。
而history的pushState和repalce方法可以实现改变当前页面显示的url,但都不会刷新页面。
未完待续~
参考文档:
react生命周期详解
React diff
react 16新特性
react fiber1
react fiber2
react hooks
react 事件机制
setState机制1
setState机制2
react-router原理
集合
简单来说,就是virtual dom & react diff。我们都知道在前端开发中,js运行很快,dom操作很慢,而react充分利用了这个前提。在react中render的执行结果是树形结构的javascript对象,当数据(state || props)发生变化时,会生成一个新的树形结构的javascript对象,...
赞 0 收藏 0 评论 0
wupengyu 关注了专栏 · 2020-01-14
基础知识、算法、原理、项目、面试。公众号code秘密花园
关注 5211
wupengyu 关注了用户 · 2020-01-14
Reading makes a full man, conference a ready man, and writing an exact man.
Reading makes a full man, conference a ready man, and writing an exact man.
关注 2324
wupengyu 发布了文章 · 2019-12-30
9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。
React Hooks是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
说是新的特性,但是与其他的版本的迭代不同,它不只是加一点api减一点api的改变。而是完整的一套解决方案。
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
这是React官网关于react使用的第一个例子,使用Hooks的方式实现了一个计数器功能。
useState是什么?在这里我们认为useState就是一个Hook。
下面介绍 React 2个最常用的钩子,useState,useEffect。
这个Hook,应该是我们使用最多的一个Hook了。通过在函数组件里调用它来给组件添加一些内部 state。
useState方法会返回一个包含2个值的数组,第一个值是当前状态。第二个值更新这个状态的函数,它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。我们一般会通过解构的方式获取其值。
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
这里我们将内部状态count初始化为0,在count发生变化时,会引起组件的会重新渲染。同时,我们通过解构的方式,给该状态进行了赋值。
<button onClick={() => setCount(count + 1)}>
Click me
</button>
直接调用useState返回值的第二个参数,即可完成更新。
我在写React ,类组件的时候,最不愿意写的就是生命周期方法,一方面是因为生命周期方法比较多,另一方面其实也比较容易出bug。比如说,我们想要实现一个监听props的某个值的变化,进而进行一些特殊操作,就可能需要两种生命周期的方法配合。但是在Hooks里,我们只需要用到useEffect就可以实现了。
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 类似于componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 更新文档的标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect方法接受2个参数,第一个参数是一个函数,是在第一次渲染以及之后更新渲染之后会进行的副作用,强调一点,该函数可以有返回值,但是该返回值必须是一个函数,会在组件被销毁时执行。
第二个参数是可选的,是一个数组,数组中存放的是需要监听的属性。即当数组中的属性发生变化时,第一个参数的函数会被调用,如果是空数组,则在第一次渲染时会被调用。
基于以上两个最主要的Hook,我们基本上可以满足于我们大部分的需求。但是如果想要对组件进行优化,则需要另外两个Hook。useCallback和useMemo。
useCallback接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
useMemo把创建函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
基于以上的Hook我们基本上可以完成大部分的功能了,除了上文重点介绍的useState和useEffect,react还给我们提供来很多有用的hooks useContext useReducer useRef useImperativeMethods useMutationEffect useLayoutEffect,这里就不一一介绍了,如果想要使用Hooks,这些api也是需要了解的,具体使用可以参考下面的文档。
9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。
赞 6 收藏 5 评论 0
wupengyu 赞了文章 · 2019-11-19
不论是寒冬还是暖冬,找工作之前都需要做好充足的准备,面试的时候才能做到游刃有余。此文是把我最近找工作准备的以及笔试面试中涉及到的手写题做一个总结。给自己,也给需要的同学。
CSS是前端必须要掌握的技能之一。一般面试也都会从CSS开始。所以CSS问题答的好坏会直接影响你在面试官心中的形象。
本文主要介绍面试中常会遇到的CSS问题及给出建议性的答案。
盒模型感觉是刚学前端的时候就会接触到的问题。元素都是按照盒模型的规则布局在页面中的。盒模型由 margin + border + padding + content
四个属性组成,分为两种:W3C的标准盒模型和IE盒模型。
W3C的标准盒模型
width = content
,不包含 border + padding
IE盒模型
width = border + padding + content
相互转换
二者之间可以通过CSS3的 box-sizing
属性来转换。
box-sizing: content-box
是W3C盒模型
box-sizing: border-box
是IE盒模型
垂直居中的方法,如果全写出来,有10多种。面试的时候一般都会说比较常用的几种。flex
、position + transform
、position + 负margin
是最常见的三种情况。
<div class="outer">
<div class="inner"></div>
</div>
方法一:flex
.outer{
display: flex;
justify-content: center;
align-items: center
}
方法二: position + transform, inner宽高未知
.outer{
position:relative;
}
.inner{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
方法三:position + 负margin, inner宽高已知
.outer{
position: relative;
}
.inner{
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -50px;
margin-top: -50px;
}
方法四:设置各个方向的距离都是0,再将margin设为auto,也可以实现,前提是inner宽高已知
.outer {
position: relative;
}
.inner {
width: 100px;
height: 100px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
三栏布局是很常见的一种页面布局方式。左右固定,中间自适应。实现方式有很多种方法。
第一种:flex
<div class="container">
<div class="left">left</div>
<div class="main">main</div>
<div class="right">right</div>
</div>
.container{
display: flex;
}
.left{
flex-basis:200px;
background: green;
}
.main{
flex: 1;
background: red;
}
.right{
flex-basis:200px;
background: green;
}
第二种:position + margin
<div class="container">
<div class="left">left</div>
<div class="right">right</div>
<div class="main">main</div>
</div>
body,html{
padding: 0;
margin: 0;
}
.left,.right{
position: absolute;
top: 0;
background: red;
}
.left{
left: 0;
width: 200px;
}
.right{
right: 0;
width: 200px;
}
.main{
margin: 0 200px ;
background: green;
}
第三种:float + margin
<div class="container">
<div class="left">left</div>
<div class="right">right</div>
<div class="main">main</div>
</div>
body,html{
padding:0;
margin: 0;
}
.left{
float:left;
width:200px;
background:red;
}
.main{
margin:0 200px;
background: green;
}
.right{
float:right;
width:200px;
background:red;
}
CSS基本选择器包含ID选择器、类选择器、标签选择器、通配符选择器。
正常情况下,一般都能答出!important > 行内样式 > ID选择器 > 类选择器 > 标签选择器 > 通配符选择器
。
但如果这几种选择器同时作用于一个元素时,该元素最后应用哪个样式呢?这就涉及到权重计算的问题。
关于权重计算,有两种不同的计算方式:一种是以二进制的规则计算,一种是以1,10,100,1000这种的计算方式。我更倾向于二进制的这种方式。
各选择器权值:
比较方式:
如果层级相同,继续往后比较,如果层级不同,层级高的权重大,不论低层级有多少个选择器。
BFC的全称为 Block Formatting Context
,也就是块级格式化上下文的意思。
以下方式都会创建BFC:
column-span 为 all 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)。
BFC布局规则:
BFC能解决的问题:
清除浮动主要是为了防止父元素塌陷。清除浮动的方法有很多,常用的是 clearfix
伪类。
方法一:clearfix
<div class="outer clearfix">
<div class="inner">inner</div>
</div>
.outer{
background: blue;
}
.inner{
width: 100px;
height: 100px;
background: red;
float: left;
}
.clearfix:after{
content: "";
display: block;
height: 0;
clear:both;
visibility: hidden;
}
方法二:额外加一个div,clear:both
<div class="container">
<div class="inner"></div>
<div class="clear"></div>
</div>
.container{
background: blue;
}
.inner {
width: 100px;
height: 100px;
background: red;
float: left;
}
.clear{
clear:both;
}
方法三:触发父盒子BFC,overflow:hidden
<div class="outer">
<div class="inner">inner</div>
</div>
.outer{
background: blue;
overflow: hidden;
}
.inner {
width: 100px;
height: 100px;
background: red;
float: left;
}
flex
布局现在已经很普及的在用了。垂直居中用 flex
实现起来很简单。关于 flex
的属性也不是很多,父容器和子容器各6个,一共12个,比较好记。
下面是我复习flex属性时的一张导图。
position属性的重要性应该没啥可说的了。想必谁都回答的上来。
absolute
绝对定位,相对于 static
定位以外的第一个父元素进行定位。relative
相对定位,相对于其自身正常位置进行定位。fixed
固定定位,相对于浏览器窗口进行定位。static
默认值。没有定位,元素出现在正常的流中。inherit
规定应该从父元素继承 position 属性的值。但是要注意一个问题,absolute
是相对于父元素的哪个属性进行定位的?通过下面的例子我们来看一看。
.container{
position: relative;
width: 30px;
height: 30px;
margin: 20px;
border: 10px solid red;
padding: 10px;
background: blue;
}
.inner {
position: absolute;
width: 10px;
height: 10px;
top: 0;
left: 0;
background: pink;
}
从上图可以看出,是相对于 static
定位以外的第一个父元素的 padding
来定位的。
CSS3中新增了一个 position:sticky
属性,该属性的作用类似 position:relative
和 position:fixed
的结合。元素在跨越特定阈值前为相对定位,之后为固定定位。必须指定 top, right, bottom
或 left
四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。但 sticky
尚在实验性阶段。
方法1:利用CSS3的vw单位
vw
会把视口的宽度平均分为100份
.square {
width: 10vw;
height: 10vw;
background: red;
}
方法2:利用margin或者padding的百分比计算是参照父元素的width属性
.square {
width: 10%;
padding-bottom: 10%;
height: 0; // 防止内容撑开多余的高度
background: red;
}
方法1: 利用border属性
利用盒模型的 border
属性上下左右边框交界处会呈现出平滑的斜线这个特点,通过设置不同的上下左右边框宽度或者颜色即可得到三角形或者梯形。
.triangle {
height:0;
width:0;
border-color:red blue green pink;
border-style:solid;
border-width:30px;
}
如果想实现其中的任一个三角形,把其他方向上的 border-color
都设置成透明即可。
.triangle {
height:0;
width:0;
border-color:red transparent transparent transparent;
border-style:solid;
border-width:30px;
}
方法二: 利用CSS3的clip-path属性
不了解 clip-path
属性的可以先看看 MDN
上的介绍:chip-path
.triangle {
width: 30px;
height: 30px;
background: red;
clip-path: polygon(0px 0px, 0px 30px, 30px 0px); // 将坐标(0,0),(0,30),(30,0)连成一个三角形
transform: rotate(225deg); // 旋转225,变成下三角
}
有错误之处还请小伙伴们及时指出,以免误人子弟。想看往期内容,翻到页面最上面有链接~
查看原文不论是寒冬还是暖冬,找工作之前都需要做好充足的准备,面试的时候才能做到游刃有余。此文是把我最近找工作准备的以及笔试面试中涉及到的手写题做一个总结。给自己,也给需要的同学。
赞 145 收藏 111 评论 3
wupengyu 赞了文章 · 2019-11-11
欲实现的水印平铺的效果图如下:
从图上看,应该做到以下几点:
文字在X和Y方向上进行平铺;
文字进行了一定的角度的旋转;
水印作为背景,其z-index位置应位于页面内容底部, 即不能覆盖页面主内容;
平铺的水印应能随窗口大小改变进行自适应。
思路:
首先我们先在canvas上绘制如下图所示一小块画布:
var tpl = '<canvas id = "watermark" width = "160px" height = "100px" style="display:none;"></canvas>';
将单一水印绘制在canvas画布里,然后将canvas节点插入到需要平铺水印的容器里,例如这里将canvas标签插入到body里面。
$(document.body).append(tpl);
这里简要说明:canvas标签width height属性,若不进行指定,则会有个默认的大小(300px * 150px),并且,这块画布的大小使用外部css文件设定宽高是无效的。
下面开始在canvas里面绘制单一水印:
var cw = $('#watermark')[0];
var ctx = cw.getContext("2d"); //返回一个用于在画布上绘图的环境
ctx.cearRect(0,0,160,100); //绘制之前画布清除
ctx.font="20px 黑体";
ctx.rotate(-20*Math.PI/180);
ctx.fillStyle = "rgba(100,100,100,0.1)";
ctx.fillText("465dd92381", -20, 80);
ctx.rotate('20*Math.PI/180'); //坐标系还原
实现了一小块画布的绘制以后,再建一个canvas画布(id为repeat-watermark):
var tplr = '<canvas id = "repeat-watermark"></canvas>';
$(document.body).append(tplr);
为整块画布设定样式:
#repeat-watermark{
position:fixed;
z-index:-1;
top:0;
background: #fff;
}
z-index的值可以根据需要调整,使其位于页面底部平铺。
另作一点说明:位于水印上层的页面如果想让水印始终可见,可以将页面中使用的颜色使用rgba设置。
下面将前面绘制的id为watermark的canvas 在当前的canvas(id为repeat-watermark)里采用createPattern方法进行平铺:
var crw = $('#repeat-watermark')[0];
crw.width = $(document).width();
crw.height = $(document).height();
ctxr = crw.getContent("2d");
ctxr.clearRect(0,0,crw.width,crw.height); //清除整个画布
var pat = ctxr.createPattern(cw, "repeat"); //在指定的方向上重复指定的元素
ctxr.fillStyle = pat;
ctxr.fillRect(0, 0, crw.width, crw.height);
此时还有一个问题,由于水印绘制只随着页面进行了一次加载,因而当窗口改变大小时,页面背景水印不会跟随改变进行填充或者裁剪,而是会出现空白,因此,将上述绘制水印封装为draw方法,然后添加以下事件:
$(window).resize(function(){
var w = $(document).width();
var h = $(document).height();
self.draw(w, h);
});
下面附上源码:
'use strict';
require('./watermark.css');
var Watermark = function(container, options) {
var self = this;
self.opt = {
docWidth: $(document).width(),
docHeight: $(document).height(),
fontStyle: "20px 黑体", //水印字体设置
rotateAngle: -20 * Math.PI / 180, //水印字体倾斜角度设置
fontColor: "rgba(100, 100, 100, 0.1)", //水印字体颜色设置
firstLinePositionX: -20, //canvas第一行文字起始X坐标
firstLinePositionY: 80, //Y
SecondLinePositionX: 0, //canvas第二行文字起始X坐标
SecondLinePositionY: 70 //Y
};
$.extend(self.opt, options);
self.render(container);
self.draw(self.opt.docWidth, self.opt.docHeight);
self.events();
};
Watermark.prototype = {
render: function(d) {
var self = this;
d.append(tpl);
},
draw: function(docWidth, docHeight) {
var self = this;
var cw = $('#watermark')[0];
var crw = $('#repeat-watermark')[0];
crw.width = docWidth;
crw.height = docHeight;
var ctx = cw.getContext("2d");
//清除小画布
ctx.clearRect(0, 0, 160, 100);
ctx.font = self.opt.fontStyle;
//文字倾斜角度
ctx.rotate(self.opt.rotateAngle);
ctx.fillStyle = self.opt.fontColor;
//第一行文字
ctx.fillText(self.opt.watermark, self.opt.firstLinePositionX, self.opt.firstLinePositionY);
//第二行文字
//ctx.fillText(window.watermark.mobile, self.opt.SecondLinePositionX, self.opt.SecondLinePositionY);
//坐标系还原
ctx.rotate(-self.opt.rotateAngle);
var ctxr = crw.getContext("2d");
//清除整个画布
ctxr.clearRect(0, 0, crw.width, crw.height);
//平铺--重复小块的canvas
var pat = ctxr.createPattern(cw, "repeat");
ctxr.fillStyle = pat;
ctxr.fillRect(0, 0, crw.width, crw.height);
},
events: function() {
var self = this;
$(window).resize(function() {
var w = $(document).width();
var h = $(document).height();
self.draw(w, h);
});
}
};
var tpl = '<canvas id = "watermark" width = "160px" height = "100px" style="display:none;"></canvas>' + '<canvas id = "repeat-watermark"></canvas>';
module.exports = Watermark;
查看原文欲实现的水印平铺的效果图如下: 从图上看,应该做到以下几点: 文字在X和Y方向上进行平铺; 文字进行了一定的角度的旋转; 水印作为背景,其z-index位置应位于页面内容底部, 即不能覆盖页面主内容; 平铺的水印应能随窗口大小改变进行自适应。 思路: 首先我们先在c...
赞 9 收藏 13 评论 1
查看全部 个人动态 →
(゚∀゚ )
暂时没有
注册于 2015-08-22
个人主页被 2.5k 人浏览
推荐关注