write once, run everywhere
一次编写,四处运行就是跨端的真谛。因为前端当下需要处理的场景实在是太多了:android
、ios
、pc
、小程序,甚至智能手表、车载电视等,当某几个场景非常相似的时候,我们希望能够用最少的开发成本来达到最好的效果,而不是每个端都需要一套单独的人力来进行维护,所以跨端技术就诞生了。
那么在跨端方案百花齐放的今天,比如现在最为人们所熟知的react native
、flutter
、electron
等,他们之间有没有什么共同的特点,而我们又是否能够找到其中的本质,就是今天这篇文章想讲述的问题。
其实,浏览器本就是一个跨端实现方案,因为你只需要输入网址,就能在任何端的浏览器上打开你的网页。那么,如果我们把浏览器嵌入 app
中,再将地址栏等内容隐藏掉,是不是就能将我们的网页嵌入原生 app
了。而这个嵌入 app
的浏览器,我们把它称之为 webview
,所以只要某个端支持 webview
,那么它就能使用这种方案跨端。
同时这也是开发成本最小的一种方案,因为这实际上就是在写前端界面,和我们开发普通的网页并没有太大区别。
典型的代表是 react-native
,它的开发语言选择了 js
,使用的语法和 react
完全一致,其实也可以说它就是 react
,这就是我们的框架层。而不同于一般 react
应用,它需要借助原生的能力来进行渲染,组件最终都会被渲染为原生组件,这可以给用户带来比较好的体验。
这种方案和上面的区别就是,它并没有直接借用原生能力去渲染组件,而是利用了更底层的渲染能力,自己去渲染组件。这种方式显然链路会比上述方案的链路跟短,那么性能也就会更好,同时在保证多端渲染一致性上也会比上一种方案更加可靠。这类框架的典型例子就是 flutter
。
众所周知,在最近几年有一个东西变得非常火爆:小程序,现在许多大厂都有一套自己的小程序实现,但相互之间还是有不小差异的,通常可以借助 taro
,remax
这类框架实现一套代码,多端运行的效果,这也算是一种另类的跨端,它的实现方式还是比较有意思的,我们后面会展开细讲。
rn
包含三个线程:
js
代码。在大多数情况下,react native
使用的js
引擎是JSC(JavaScriptCore) ,在使用 chrome
调试时,所有的 js
代码都运行在 chrome
中,并且通过 websocket
与原生代码通信。此时的运行环境是v8
。yoga
去实现的,这是一个基于flexbox
的跨平台布局引擎。shadow thread
会维护一个 shadow tree
来计算我们的各个组件在 native
页面的实际布局,然后通过 bridge
通知native thread
渲染 ui
。native
启动一个原生界面,比如android
会起一个新的activity
来承载rn
,并做一些初始化的操作。js
引擎,运行 js
代码,此时的流程和 react
的启动流程就非常相似了,我们先简单观察调用栈,
是不是看见了一些非常熟悉的函数名,在上一讲的基本原理中已经提到过了,这里我们就不再赘述。同时再看一下FiberNode
的结构,也和react
的保持一致,只不过我们在js
层是无法拿到真实结点的,所以stateNode
只是一个代号。
3 . js
线程通知shadow thread
。在react
中,走到createInstance
以后我们就可以直接调用createElement
来创建真实结点了,但是在rn
中我们没办法做到这一步,所以我们会通知native
层让它来帮助我们创建一个对应的真实结点。
4 . shadow thread
计算布局,通知native Thread
创建原生组件。
5 . native
在界面上渲染原生组件,呈现给用户。
比如某个时候,用户点击了屏幕上的一个按钮触发了一个点击事件,此时界面需要进行相应的更新操作。
native
获取到了点击事件,传给了js thread
js thread
根据 react
代码进行相应的处理,比如处理 onClick
函数,触发了 setState
。react
的更新流程一样,触发了 setState
之后会进行 diff
,找到需要更新的结点shadow thread
shadow thread
计算布局之后通知 native thread
进行真正的渲染。我们上述说的通知,都是通过 bridge
实现的,bridge
本身是用实现C++
的,就像一座桥一样,将各个模块关联起来,整个通信是一个「异步」的过程。这样做好处就是各自之间不会有阻塞关系,比如 不会native thread
因为js thread
而阻塞渲染,给用户良好的体验。但是这种「异步」也存在一个比较明显的问题:因为通信过程花费的时间比较长,所以在一些时效性要求较高场景上体验较差。
比如长列表快速滚动的时候或者需要做一些跟手的动画,整个过程是这样的:
native thread
监听到了滚动事件,发送消息通知js thread
js thread
处理滚动事件,如果需要修改 state
需要经过一层js diff
,拿到最终需要更新的结点js thread
通知 shadow thread
shadow thread
通知 native
渲染当用户操作过快的时候,就会导致界面来不及更新,进而导致在快速滑动的时候会出现白屏、卡顿的现象。
我们很容易看出,这是由rn
的架构引出的问题,其实小程序的架构也会有这个问题,所以在rn
和小程序上出现一些需要频繁通信的场景时,就会导致页面非常差,流畅度降低。那么如果想解决这个问题,势必要从架构上去进行修改。
那么既然我们知道了rn
是如何实现的跨端,那么我们就可以来探究一下它本质上是在干什么。首先,跨端可以分为「逻辑跨端」和「渲染跨端」。
「逻辑跨端」通常通过 vm
来实现,例如利用 v8
引擎,我们就能在各个平台上运行我们的 js
代码,实现「逻辑跨端」。
那么第二个问题就是「渲染跨端」,我们把业务代码的实现抽象为开发层,比如 react-native
中我们写的 react
代码就属于开发层,再把具体要渲染的端称为渲染层。作为开发层来说,我一定知道我想要的ui
长什么样,但是我没有能力去渲染到界面上,所以当我声明了一个组件之后,我们需要考虑的问题是如何把我想要什么告诉渲染层。
就像这样的关系,那么我们最直观的方式肯定是我能够实现一种通信方式,在开发层将消息通知到各个系统,再由各个系统自己去调用对应的 api
来实现最终的渲染。
function render() {
if(A) {
message.sendA('render', { type: 'View' })
}
if(B) {
message.sendB('render', { type: 'View' })
}
if(C) {
message.sendC('render', { type: 'View' })
}
}
比如这样,我就能通过判断平台来通知对应的端去渲染View
组件。这一部分的工作就是跨端框架需要帮助我们做的,它可以把这一步放到 JS
层,也可以把这一步放到c++
层。我们应该把这部分工作尽量往底层放,也就是我们可以对各个平台的 api
进行一层封装,上层只负责调用封装的 api
,再由这一层封装层去调用真正的 api
。因为这样可以复用更多的逻辑,否则像上文中我们在 JS
层去发送消息给不同的平台,我们就需要在A\B\C三个平台写三个不同方法去渲染组件。
但是,归根结底就是,一定有一个地方是通过判断不同平台来调用具体实现,也就是下面这样
有一个地方会对系统进行判断,再通过某种通信方式通知到对应的端,最后执行真正的方法。其实,所有跨端相关操作其实都在做上图中的这些事情。所有的跨端也可以总结为下面这句话:
「我知道我想要什么,但是我没有能力去渲染,我要通知有能力渲染的人来帮助我渲染」
比如hybrid
跨端方案中,webview
其实就充当了桥接层的角色,createElement
,appendChild
等api
就是给我们封装好的跨平台api
,底层最终调用到了什么地方,又是如何渲染到界面上的细节都被屏蔽掉了。所以我们利用这些api
就能很轻松的实现跨端开发,写一个网页,只要能够加载 webview
的地方,我们的代码就能跑在这个上面。
又比如flutter
的方案通过研发一个自渲染的引擎来实现跨端,这种思路是不是相当于另外一个浏览器?但是不同的点在于 flutter
是一个非常新的东西,而 webview
需要遵循大量的 w3c
规范和背负一堆历史包袱。flutter
并没有什么历史包袱,所以它能够从架构,设计的方面去做的更好更快,能够做更多的事情。
对于跨端来说,如何屏蔽好各端的细节至关重要,比如针对某个端特有的api
如何处理,如何保证渲染细节上各个端始终保持一致。如果一个跨端框架能够让开发者的代码里面不出现 isIos
、isAndroid
的字眼,或者是为了兼容各种奇怪的渲染而产生的非常诡异的hack
方式。那我认为它绝对是一个真正成功的框架。
但是按我经验而言,先后写过的 h5
、rn
、小程序,他们都没有真正做到这一点,所以项目里面会出现为了解决不同端不一致问题而出现的各种奇奇怪怪的代码。而这个问题其实也是非常难解决的,因为各端的差异还是比较大的,所以说很难去完全屏蔽这些细节。
比如说h5
中磨人的垂直居中问题,我相信只要开发过移动端页面的都会遇见,就不用我多说了。
为什么大家其实本质上都是在干一件事情,却出现了这么多的解决方案?其实大家都觉得某些框架没能很好的解决某个问题,所以想自己去造一套。其中可能很多开发者最关心的就是性能问题,比如:
rn
因为架构上的原因导致某些场景性能差,所以它就想办法从架构上去进行修改。flutter
直接自己搞了一套渲染引擎,同时选用支持AOT
的dart
作为开发语言。但是其实我们在选择框架的时候性能并不是唯一因素,开发体验、框架生态这些也都是关键因素,我个人感受是,目前rn
的生态还是比其他的要好,所以在开发过程中你想要的东西基本都有。
ok,说了这么多,对于跨端部分的内容其实我想说的已经说的差不多了,还记得上文提到的 Taro、Uni-app 一类跨小程序方案么。为什么说它是另类的跨端,因为它其实并没有实际跨端,只是为了解决各个小程序语法之间不兼容的问题。但是它又确实是一个跨端解决方案,因为它符合 「write once, run everything。」
下面我们先来了解下小程序的背景。
小程序是各个app
厂商对外开放的一种能力。通过厂商提供的框架,就能在他们的app
中运行自己的小程序,借助各大app
的流量来开展自己的业务。同时作为厂商如果能吸引到更多的人加入到开发者大军中来,也能给app
带来给多的流量,这可以看作一个双赢的业务。那么最终呈现在app
中的页面是以什么方式进行渲染的呢?其实还是通过webview
,但是会嵌入一些原生的组件在里面以提供更好的用户体验,比如video
组件其实并不是h5 video
,而是native video
。
那么到了这里,我们就可以来谈一谈关于小程序跨端的东西了。关于小程序跨端,核心并不是真正意义上的跨端,虽然小程序也做到了跨端,例如一份代码其实是可以跑在android
和Ios
上的,但是实际上这和hybrid
跨端十分相似。
在这里我想说的其实是,市面上现在有非常多的小程序:字节小程序、百度小程序、微信小程序、支付宝小程序等等等等。虽然他们的dsl
十分相似,但是终归还是有所不同,那么就意味着如果我想在多个app
上去开展我的业务,我是否需要维护多套十分相似的代码?我又能否通过一套代码能够跑在各种小程序上?
想通过一套代码跑在多个小程序上,和想通过一套代码跑在多个端,这两件事到底是不是一件事呢?我们再回到这张图
这些平台是否可以对应上不同的小程序?
再回到那句话:「我知道我想要什么,但是我没有能力去渲染,我要通知有能力渲染的人来帮助我渲染。」
现在来理一下我们的需求:
那么从这句话来看:「我」代表了什么,「有能力渲染的人」又代表了什么?
第二个很容易对应上,「有能力渲染的人」就是小程序本身,只有它才能帮助我们把内容真正渲染到界面上。
而「我」又是什么呢?其实这个「我」可以是很多东西,不过这里我们的需求是想用react
进行开发,所以我们回想一下第一讲中react
的核心流程,当它拿到vdom
的时候,是不是就已经知道【我想要什么】了?所以我们把react
拿到vdom
之前的流程搬过来,这样就能获取到「我知道我想要什么」的信息,但是「我没有能力去渲染」,因为这不是web
,没有dom api
,所以我需要通知小程序来帮助我渲染,我还可以根据不同的端来通知不同的小程序帮助我渲染。
所以整个流程就是下面这样的:
前面三个流程都在我们的js
层,也就是开发层,我们写的代码经历一遍完整的 react
流程之后,会将最后的结果给到各个小程序,然后再走小程序自己的内部流程,将其真正的渲染到界面上。
采用这种做法的典型例子有remax
、taro3
,他们宣称用真正的react
去开发小程序,其实并没有错,因为真的是把react
的整套东西都搬了过来,和react
并无差异。我们用taro
写一个非常简单的例子来看一下:
import { Component } from 'react'
import { View, Text, Button } from '@tarojs/components'
import './index.css'
export default class Index extends Component {
state = {
random: Math.random()
}
componentWillMount () { }
componentDidMount () { }
componentWillUnmount () { }
componentDidShow () { }
componentDidHide () { }
handleClick = () => {
debugger;
console.log("Math.random()", Math.random());
this.setState({random: Math.random()})
}
render () {
return (
<View className='index'>
<Text>Hello world! {this.state.random}</Text>
<Button onClick={this.handleClick}>click</Button>
</View>
)
}
}
这是一个用taro
写的组件,把它编译到字节小程序之后是这样的效果:
根据我们之前的分析,在最后生成的文件中,一定包含了一个「小程序渲染器」。它接受的data
就是整个ui
结构,然后通过小程序的渲染能力渲染到界面上,我们去dist
文件中找一下,就能找到一个base.ttm
l的文件,里面的内容是这样的
<template name="taro_tmpl">
<block tt:for="{{root.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</template>
<template name="tmpl_0_catch-view">
<view hover-class="{{i.hoverClass===undefined?'none':i.hoverClass}}" hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}" hover-start-time="{{i.hoverStartTime===undefined?50:i.hoverStartTime}}" hover-stay-time="{{i.hoverStayTime===undefined?400:i.hoverStayTime}}" animation="{{i.animation}}" bindtouchstart="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongpress="eh" bindanimationstart="eh" bindanimationiteration="eh" bindanimationend="eh" bindtransitionend="eh" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" catchtouchmove="eh" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</view>
</template>
<template name="tmpl_0_static-view">
<view hover-class="{{i.hoverClass===undefined?'none':i.hoverClass}}" hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}" hover-start-time="{{i.hoverStartTime===undefined?50:i.hoverStartTime}}" hover-stay-time="{{i.hoverStayTime===undefined?400:i.hoverStayTime}}" animation="{{i.animation}}" style="{{i.st}}" class="{{i.cl}}" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</view>
</template>
<template name="tmpl_0_pure-view">
<view style="{{i.st}}" class="{{i.cl}}" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</view>
</template>
<template name="tmpl_0_view">
<view hover-class="{{i.hoverClass===undefined?'none':i.hoverClass}}" hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}" hover-start-time="{{i.hoverStartTime===undefined?50:i.hoverStartTime}}" hover-stay-time="{{i.hoverStayTime===undefined?400:i.hoverStayTime}}" animation="{{i.animation}}" bindtouchstart="eh" bindtouchmove="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongpress="eh" bindanimationstart="eh" bindanimationiteration="eh" bindanimationend="eh" bindtransitionend="eh" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</view>
</template>
<template name="tmpl_0_static-text">
<text selectable="{{i.selectable===undefined?false:i.selectable}}" space="{{i.space}}" decode="{{i.decode===undefined?false:i.decode}}" style="{{i.st}}" class="{{i.cl}}" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</text>
</template>
<template name="tmpl_0_text">
<text selectable="{{i.selectable===undefined?false:i.selectable}}" space="{{i.space}}" decode="{{i.decode===undefined?false:i.decode}}" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</text>
</template>
<template name="tmpl_0_button">
<button size="{{i.size===undefined?'default':i.size}}" type="{{i.type}}" plain="{{i.plain===undefined?false:i.plain}}" disabled="{{i.disabled}}" loading="{{i.loading===undefined?false:i.loading}}" form-type="{{i.formType}}" open-type="{{i.openType}}" hover-class="{{i.hoverClass===undefined?'button-hover':i.hoverClass}}" hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}" hover-start-time="{{i.hoverStartTime===undefined?20:i.hoverStartTime}}" hover-stay-time="{{i.hoverStayTime===undefined?70:i.hoverStayTime}}" name="{{i.name}}" bindtouchstart="eh" bindtouchmove="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongpress="eh" bindgetphonenumber="eh" data-channel="{{i.dataChannel}}" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</button>
</template>
<template name="tmpl_0_scroll-view">
<scroll-view scroll-x="{{i.scrollX===undefined?false:i.scrollX}}" scroll-y="{{i.scrollY===undefined?false:i.scrollY}}" upper-threshold="{{i.upperThreshold===undefined?50:i.upperThreshold}}" lower-threshold="{{i.lowerThreshold===undefined?50:i.lowerThreshold}}" scroll-top="{{i.scrollTop}}" scroll-left="{{i.scrollLeft}}" scroll-into-view="{{i.scrollIntoView}}" scroll-with-animation="{{i.scrollWithAnimation===undefined?false:i.scrollWithAnimation}}" enable-back-to-top="{{i.enableBackToTop===undefined?false:i.enableBackToTop}}" bindscrolltoupper="eh" bindscrolltolower="eh" bindscroll="eh" bindtouchstart="eh" bindtouchmove="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongpress="eh" bindanimationstart="eh" bindanimationiteration="eh" bindanimationend="eh" bindtransitionend="eh" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</scroll-view>
</template>
<template name="tmpl_0_static-image">
<image src="{{i.src}}" mode="{{i.mode===undefined?'scaleToFill':i.mode}}" lazy-load="{{i.lazyLoad===undefined?false:i.lazyLoad}}" style="{{i.st}}" class="{{i.cl}}" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</image>
</template>
<template name="tmpl_0_image">
<image src="{{i.src}}" mode="{{i.mode===undefined?'scaleToFill':i.mode}}" lazy-load="{{i.lazyLoad===undefined?false:i.lazyLoad}}" binderror="eh" bindload="eh" bindtouchstart="eh" bindtouchmove="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongpress="eh" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid}}">
<block tt:for="{{i.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</image>
</template>
<template name="tmpl_0_#text" data="{{i:i}}">
<block>{{i.v}}</block>
</template>
<template name="tmpl_0_container">
<template is="{{'tmpl_0_' + i.nn}}" data="{{i:i}}" />
</template>
从名字可以看出,这是用于渲染各种组件的template
,所以当我们拿到react
传递过来的data
时,将其传给template
,template
就能根据对应的组件名采用不同的模版进行渲染。随后再用一个for
循环将其子组件进行递归渲染,完成整个页面的渲染。这个就可以理解为我们针对不同端写的不同渲染器,如果我们编译到wx
小程序,这里面的内容是会不同的。
总之,「在」**react**
「对其处理完之后,会把数据」**setData**
「传递给「「小程序」」,小程序再用之前写好的各种」**template**
「将其渲染到页面上。」
下面这张图就是经过react
处理之后,能够拿到页面的数据,将其传递给小程序之后,就能递归渲染出来。
那么这样的架构有什么问题呢,可以很明显的看到会走两遍diff
,为什么会走两遍diff
呢?因为在react
层为了获取到我想要什么这个信息,我们必须走一遍diff
,这样才能将最后得到的data
交给小程序。
而交给小程序之后,小程序对于之前的流程是无感知的,所以它为了得到需要更新什么这个信息,也需要过一遍diff
,或者通过一些其他的方式来拿到这个信息(并没有深入了解过小程序的渲染流程,所以不确定是否是通过diff
拿到的),所以这一整套流程就会走两遍diff
。
为什么我们不能将两次diff
合并为一次?因为小程序的渲染对开发者而言就是个黑盒,我们不能干扰到其内部流程。如果我们能够直接对接小程序的渲染sdk
,那么其实根本没必要走两遍diff
,因为前置的 react
的diff
我们已经能够知道需要更新什么内容。
这个问题的本质和普通意义上的跨端框架没有太大的区别,开发层也就是 react
知道自己需要什么东西,但是它没有能力去渲染到界面上,所以需要通过小程序充当渲染层来渲染到真正的界面上。这种开发方式有一种用 react
去写 vue
的意思,但是为什么会出现这种诡异的开发方式,如果这个 vue
做的足够好的话,谁又想去这样折腾?
其实还有一个小问题,wx
的template
是无法支持递归调用的,也就导致了我们想用template
递归渲染data
内容是无法实现的,那么这个问题要如何解决呢..我们看一下上面的代码在wx
小程序中编译出来的结果:
我们可以看到各种template
之间多了0、1、2、3这种标号..就是为了解决无法递归调用的问题,提前多造几个名字不同功能相同的template
,不就能跨过递归调用的限制了么...
上述的这些跨端都是通过某种架构方式去实现的,那如果我们粗暴一点的想,我能不能直接把一套代码通过编译的方式去编译到不同的平台。比如我把js
代码编译成java
代码、object-c
代码,其实,个人感觉也不是不行,但是因为这些的差异实在太大,所以在写js
代码的时候,可能需要非常强的约束性、规范性,把开发者限制在某个区域内,才能很好的编译过去。也就是说,从js
到java
其实是一个自由度高到自由度低的一个过程,肯定是无法完全一一对应上的,并且由于开发方式、语法完全不一样,所以想通过编译的方式将js
编译到ios
和android
上去还是比较难的,但是对于小程序来说,尝试把jsx
编译到template
似乎是一个可行的方案,实际上,taro1/2 都是这么干的。不过从jsx
到template
也是一个自由度从高到低的一个过程,所以是没办法绝对完美地将把所有语法都编译到template
...
这里可以给大家分享一个很有意思的例子,最近很火的 SolidJS 框架也支持用 JSX
写代码,但是它完全没有react
这么重的runtime
,因为它的JSX
最终会被编译成一些原生的操作...我们看一个简单的例子:https://playground.solidjs.com/
在 react
语境下,我们在input
框里面输入内容的时候,上面的文案应该跟着改变,但是实际上并没有。这是因为这个东西最后被编译完之后是一些原生的操作,它其实只会运行一遍,最后你触发的各种click
并不会导致函数重新运行,而是直接通过原生操作操作到对应的DOM
上来修改视图,也就导致了上面问题的产生。
其实我觉得这样挺反人类的,虽然是JSX
的语法,但是却缺少了最核心的东西:函数式的思维。(还不如写template
)。
提到跨端,可能很多人第一个想到的东西就是 virtual dom
,因为它是对于ui
的抽象,脱离了平台,所以可能很多人会觉得virtual dom
和跨平台已经是绑定在一起的东西了。但是其实个人感觉并不是。
首先我们回想一下,我们之前说到的跨平台的本质是什么?开发层知道自己想要什么,然后告诉渲染层自己想要什么,就这么简单。那对于react-native
来说,是通过virtual dom
来判断自己需要更新什么结点的吗?其实并不是,单靠一个virtual dom
还不足以获取到这个信息,必须还要加上diff
,所以是virtual dom
+diff
获取到了自己想要什么的信息,再通过通信的方式告诉native
去更新真正的结点。
所以virtual dom
在这个里面只扮演了一个获取方法的角色,是通过virtual dom
+diff
这个方法拿到了我们想要的东西。换言之,我们也可以通过其他的方法来拿到我们想要什么。比如之前分享的san
框架,这是一个没有virtual dom
的框架,但是它为什么能够跨平台,我们先不管它内部是如何实现的,但是在更新阶段,如果它在某个时刻调用了 createElement
,那么它一定是知道了:自己想要什么。对应上跨端的内容,这个时候就能通过某种手段去告诉native
,渲染某个东西。
「所以,当我们通过其他手段获取到了:我们想要什么这个信息之后,就能通知」**native**
「去渲染真正的内容。」
那么vdom
的优势在于什么地方?我认为主要是下面两个:
jsx
新时代,函数式编程思想template
获取更多优化信息,又能够支持 jsx
首先,jsx
简直开创了一个新时代,让我们能够以函数式编程思想去写ui
,之前谁能想到一个切图仔还能用这样的方式去写ui
。
其次,我们知道,vue
虽然是使用的template
作为dsl
,但是实际上我们也是可以写jsx
的,jsx
所提供的灵活能力是template
无法比拟的。而之所以能够同时支持template
和jsx
其实就是因为vdom
的存在,如果vue
不引入vdom
,是没办法说去支持jsx
的语法的,或者说,是没办法去支持真正的jsx
。
还是那句话,跨端就是:「我知道我想要什么,但是我没有能力去渲染,我要通知有能力渲染的人来帮助我渲染。」
他们的本质都非常简单,但是细节却非常难处理,同时对于目前市面上的多种跨端框架,也需要大家根据自己的项目去权衡利弊选择一个最有方案,毕竟目前没有一个框架能完全吊打所有其他框架,适合自己的才是最好的。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/G_2eBJyrr6FHm7WjI_MIPw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。