SegmentFault 前端漫步最新的文章
2018-05-27T20:35:56+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
借助 workbox 将网站升级成 PWA
https://segmentfault.com/a/1190000015050724
2018-05-27T20:35:56+08:00
2018-05-27T20:35:56+08:00
hugo_seth
https://segmentfault.com/u/hugo_seth
21
<p>PWA(Progressive Web Apps)是谷歌近几年一直在推进的 web 应用新模型。PWA 借助 Service Worker 缓存网站的静态资源,甚至是网络请求,使网站在离线时也能访问。并且我们能够为网站指定一个图标添加在手机桌面,实现点击桌面图标即可访问网站。</p>
<h2><a href="https://link.segmentfault.com/?enc=V8h0NnEoKXmdMkXm%2BeUSdA%3D%3D.opPoXg5q%2B2MAIeSRiMZzGWsb2RfpBaUjbpsTYVdIkNiqeoxSrgfpKttQzi35x5cAGWfHblCFqovvxxuk73j08uchR6GIcCN0IahyXBh6ZkI%3D" rel="nofollow">Web App Manifest</a></h2>
<p><code>Web App Manifest</code> 是一个 <code>JSON</code> 文件,它用来定义网站添加到桌面的图标以及从桌面图标进入网站时的一系列行为,如:启动样式,全屏主题等。</p>
<p>先创建 <code>manifest.json</code>:</p>
<pre><code>{
"name": "blog-pwa",
"short_name": "blog-pwa",
"icons": [
{
"src": "/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}</code></pre>
<p>将文件引入:</p>
<pre><code><link rel=manifest href=/manifest.json></code></pre>
<p>我们可以从开发者工具上看我们的配置:</p>
<p><img src="/img/bVbbjfJ?w=1128&h=1152" alt="图片描述" title="图片描述"></p>
<p><code>icons</code> 属性定义了添加到桌面的图标, <code>display: standalone</code> 表示我们要从桌面全屏启动,<code>theme_color": "#4DBA87</code> 是全屏启动时手机顶部状态栏的背景色,<code>background_color": "#000000</code> 是启动页的背景色,启动页目前不能定制,默认由 <code>background_color</code> 加 <code>icon</code> 加 <code>name</code> 组合而成。</p>
<p><code>Web App Manifest</code>很简单,只要照着文档每个属性看一遍就行。</p>
<h2><a href="https://link.segmentfault.com/?enc=K7HqhH6KbY4uW1fYjf4jAw%3D%3D.Otosp9szJ0CVzimk9DAflZJNerM%2BFggTOqJWqq0Aa0089UhKm5rK9UVnQpIeGXkaVsU5B%2B4HGTCQ3ZmGZWF2fQBJWG3gOnpIvLY0kmqoDKROOMsOe7d8j%2FQ1mcFKUMgZ" rel="nofollow">Service Worker</a></h2>
<p><code>Service Worker</code> 是浏览器在后台独立于网页运行的脚本。是它让 PWA 拥有极快的访问速度和离线运行能力。</p>
<p>那它是如何做到的呢?我们一步步来看。</p>
<h3>注册 <code>Service Worker</code>
</h3>
<pre><code>if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(registration => {
console.log(
'ServiceWorker registration successful with scope: ',
registration.scope
)
})
.catch(err => {
console.log('ServiceWorker registration failed: ', err)
})
}</code></pre>
<p>需要注意的是,<code>Service Worker</code> 脚本除了域名为 <code>localhost</code> 时能运行在 <code>http</code> 协议下以外,只能运行 <code>https</code> 协议下。</p>
<h3>安装</h3>
<pre><code>const CACHE_NAME = 'cache-v1'
const DATA_CACHE_NAME = 'data-cache-v1'
const PRE_CACHE = ['/index.html', '/css/app.css', '/js/app.js']
self.addEventListener('install', e => {
console.log('[ServiceWorker] Install')
e.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(PRE_CACHE)
})
)
})</code></pre>
<p>在安装的时候预缓存网站的静态资源,任何资源路径出错都会造成 <code>Service Worker</code> 安装失败。</p>
<h3>代理请求</h3>
<pre><code>self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(response => {
if (response) {
return response
}
const fetchRequest = e.request.clone()
return fetch(fetchRequest).then(response => {
// Check if we received a valid response
if (!response || response.status !== 200) {
return response
}
const responseToCache = response.clone()
caches.open(DATA_CACHE_NAME).then(cache => {
cache.put(e.request, responseToCache)
})
return response
})
})
)
})</code></pre>
<p>安装成功后,<code>Service Worker</code> 就可以监听网站的所有请求,匹配到缓存时直接返回,未匹配到时请求服务器,服务器成功返回时添加到缓存。</p>
<h3>更新</h3>
<p>现在网站的 <code>Service Worker</code> 已经可以正常工作了,那如何更新它呢?</p>
<p>我们只需要修改 <code>Service Worker</code> 文件就可以更新它。当我们每次访问网站时都会去下载这个文件,当发现文件不一致时,就会安装这个新 <code>Service Worker</code> ,安装成功后,它将进入等待阶段。当我们关闭窗口重新导航到网站时(刷新网页不行),新 <code>Service Worker</code> 将开始控制网站。旧 <code>Service Worker</code> 终止工作并触发 <code>activate</code> 事件:</p>
<pre><code>self.addEventListener('activate', e => {
e.waitUntil(
caches.keys().then(keyList => {
return Promise.all(
keyList.map(key => {
if (key !== CACHE_NAME && key !== DATA_CACHE_NAME) {
console.log('[ServiceWorker] Removing old cache', key)
return caches.delete(key)
}
})
)
})
)
})</code></pre>
<p>在其卸载时一定要删除旧缓存,不然我们的网站永远无法更新。</p>
<p>上面只简单讲了 <code>Service Worker</code> 如何工作。我们会发现有很多问题需要我们进一步解决:</p>
<ol>
<li>预缓存的静态资源修改后在下一次发版本时的文件名都不一样,手动写死太低效,最好每次都自动生成资源文件名。</li>
<li>缓存资源是以硬编码字符串判断是否有效,这样每次发版本都需要手动修改,才能更新缓存。并且每次都是全量更新。能否以文件的粒度进行资源缓存呢?</li>
<li>请求代理没有区分静态资源和动态接口。已经缓存的动态接口也会一直返回缓存,无法请求新数据。</li>
</ol>
<p>上面只列出了三个明显的问题,还有很多问题是没有考虑到的。如果让我们自己来解决这些问题,不仅是工作量很大,而且也很难写出生产环境可用的 <code>Service Worker</code>。</p>
<h2><a href="https://link.segmentfault.com/?enc=hO%2BZt%2B7sIntc9NKBt6EgZQ%3D%3D.1BZ2iA3cy9DwE8p%2B0rhp1yoVNs3DP3WR2hGhTpCYDqBA6sMCO%2B3qDJZEtH5exq%2Bv" rel="nofollow">workbox</a></h2>
<p>既然如此,我们最好是站在巨人的肩膀上,这个巨人就是谷歌。workbox 是由谷歌浏览器团队发布,用来协助创建 PWA 应用的 <code>JavaScript</code> 库。当然直接用 <code>workbox</code> 还是太复杂了,谷歌还很贴心的发布了一个 <code>webpack</code> 插件,能够自动生成 <code>Service Worker</code> 和 静态资源列表 - <a href="https://link.segmentfault.com/?enc=PtI%2FzjNJisVZwpBzxzrspw%3D%3D.2EV4cZgw9G9FT5RSvIJLVy9KEZLb0XIYJjuqILfCb9DYsadl7DGu8m6PekyieR3ZteVb0EbqDrJCX0ggz4CwCAQ4dtY8nvBCIoW6Nw2aLB8%3D" rel="nofollow">workbox-webpack-plugin</a>。</p>
<p>只需简单一步就能生成生产环境可用的 <code>Service Worker</code> :</p>
<pre><code>const { GenerateSW } = require('workbox-webpack-plugin')
new GenerateSW()</code></pre>
<p>打包一下:</p>
<p><img src="/img/bVbbjuu?w=3028&h=1318" alt="图片描述" title="图片描述"></p>
<p>还能说什么呢?谷歌大法好!当然这只是最简单的可用版本,其实这里有一个最严重的问题不知道有没人发现,那就是 <code>importScripts</code> 引用的是谷歌域名下的 cdn ,这让我们墙内的网站怎么用,所以我们需要把这个问题解决并自定义一些配置增强 <code>Service Worker</code> 的能力:</p>
<pre><code>new GenerateSW({
importWorkboxFrom: 'local',
skipWaiting: true,
clientsClaim: true,
runtimeCaching: [
{
// To match cross-origin requests, use a RegExp that matches
// the start of the origin:
urlPattern: new RegExp('^https://api'),
handler: 'staleWhileRevalidate',
options: {
// Configure which responses are considered cacheable.
cacheableResponse: {
statuses: [200]
}
}
},
{
urlPattern: new RegExp('^https://cdn'),
// Apply a network-first strategy.
handler: 'networkFirst',
options: {
// Fall back to the cache after 2 seconds.
networkTimeoutSeconds: 2,
cacheableResponse: {
statuses: [200]
}
}
}
]
})
</code></pre>
<p>首先 <code>importWorkboxFrom</code> 我们指定从本地引入,这样插件就会将 <code>workbox</code> 所有源文件下载到本地,墙内开发者的福音。上面提到过新 <code>Service Worker</code> 安装成功后需要进入等待阶段,<code>skipWaiting: true</code> 将使其跳过等待,安装成功后立即接管网站,注意这个要和 <code>clientsClaim</code> 一起设置为 <code>true</code>。<code>runtimeCaching</code> 顾名思义是配置运行时如何缓存请求的,这里只说一点,缓存跨域请求时 <code>urlPattern</code> 的值必须为 <code>^</code> 开头的正则表达式,其它的配置看文档都能得到详细的介绍。</p>
<p>再打包一次:</p>
<p><img src="/img/bVbbjwz?w=2014&h=1386" alt="图片描述" title="图片描述"></p>
<p>现在我们就可以将打包好的代码部署到网站上了,<a href="https://link.segmentfault.com/?enc=xunAMwGmzaoEtrvIBppzpw%3D%3D.ilER858vup%2F1%2FsjxtnqtVHDZFSNw3rxO7Bex2AyD5C40BvXjQjMC8QszbmgUmWnC" rel="nofollow">源码在这</a>,最后再上几张图:</p>
<p><img src="/img/bVbbjwQ?w=3282&h=1722" alt="图片描述" title="图片描述"></p>
<p><img src="/img/remote/1460000015067445?w=272&h=480" alt="动图" title="动图"></p>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=%2F2esuNw38MrX4XFezM0zew%3D%3D.r2vj4YzAiJ7dl0WlunR9Nv2kqs9MecjsH%2BiOOpYuPKsrDXR0GtB6Ck2qmubLg3xuhbhd1ww6BZWEHmpPZnVL5FfnoSaixl7vqJNh4wlSmuA%3D" rel="nofollow">Web App Manifest</a></p>
<p><a href="https://link.segmentfault.com/?enc=L18pYvBg1%2FlUuzOYapu9Ww%3D%3D.o6rnW3TRI8hMJnXXpohxojqNBX9j3wiGqi5n1kuoMS3WGHdPmFMd9JWCjdTFjWpcR%2F4x5bT0ikLvquEf4c%2FwHe5%2BzNKfJ6bPqd19T3KD%2BFJuAaCk1v4Utf2Rq9jPJYCZ" rel="nofollow">服务工作线程:简介</a></p>
<p><a href="https://link.segmentfault.com/?enc=9IFWHZAtOCWl4kSnQtlfaQ%3D%3D.ZPqV1Bjpza0OUeHzg58TfBbRcmy6tvxWYOuRCytvehbQUsVNsv%2BwSCB4%2FzAVADkLlWqGutEjkREq1iiqkJ6YBm40RcfuiO6CDzDii0v94tkqqKZRkLwq3Y%2FYRovQuDQO" rel="nofollow">服务工作线程生命周期</a></p>
<p><a href="https://link.segmentfault.com/?enc=192iKVnxo1Tw45THHYUczw%3D%3D.6UfgA8LFLZiGoWQAEjwMkdOCgqNFClTo1V2mtyB%2BvcYRs7eyggqfeGh2aE25QpTDIXiiKK2SEf00qYMFs8y2oGgtyY%2F6mW7tC9MsNl9Oxoc%3D" rel="nofollow">workbox-webpack-plugin</a></p>
MVVM 中的动态数据绑定
https://segmentfault.com/a/1190000014710466
2018-05-03T14:51:02+08:00
2018-05-03T14:51:02+08:00
hugo_seth
https://segmentfault.com/u/hugo_seth
1
<p><a href="https://segmentfault.com/a/1190000014695931">上一篇文章</a>我们了解了怎样实现一个简单模板引擎。但这个模板引擎只适合静态模板,因为它是将模板整体编译成字符串进行全量替换。如果每次数据改变都进行一次替换,会有两个最主要的问题:</p>
<ol>
<li>性能差。<code>DOM</code> 操作本身就非常大的开销,更别说每一次都替换这么大的量。</li>
<li>破坏事件绑定。这个是最麻烦的,如果我们没有给解绑移除 <code>DOM</code> 绑定的事件,还会造成内存泄露。而且每一次替换都要重新绑定事件。</li>
</ol>
<p>因此,没有人会将这种模板引擎用来编译动态模板。那我们如何编译动态模板呢?</p>
<p>回答这个问题之前,我们先要了解前端的世界何时出现了动态模板:它是由 MVVM 框架带来的,动态模板是 MVVM 框架的视图层(view)。我们知道的 MVVM 框架有 <code>knockout.js</code>、<code>angular.js</code>、<code>avalon</code> 和 <code>vue</code>。</p>
<p>对于这些框架,大部分人最熟悉的应该就是 <code>vue</code>,所以我下面也是以 <code>vue 1.0</code> 作为参考,来实现一个功能更简单的动态模板引擎。它是框架自带的一个功能,让框架能够响应数据的改变。从而刷新页面。</p>
<p>MVVM 动态模板的特点是能最小化刷新:哪个变量改变了,与之相关的节点才会更新。这样我们就能避免上面提到的静态模板的两大问题。</p>
<p>要实现最小化刷新,我们要将模板中的每个绑定都收集起来。这个收集工作是框架在完成第一次渲染前就已经完成了,每个绑定都会生成一个 <code>Directive</code> 实例:</p>
<pre><code>class Directive {
constructor(vm, el, exp, update) {
this.vm = vm
this.el = el
this.exp = exp
this.update = update
this.watchers = []
this.get = getEvaluationFn(exp).bind(this, vm.$data)
this.bind()
}
}
function getEvaluationFn(exp) {
return new Function('data', 'with(data) { return ' + exp + '}')
}</code></pre>
<p>我们知道,每个绑定都由指令和指令值(指令值可能是表达式,可能是语句,也可能就是一个变量,还可能是框架自定义的语法)构成,每种指令都有对应的刷新函数(<code>update</code>)。如节点值的绑定的刷新函数是:</p>
<pre><code>function updateTextNode() {
const value = this.get()
this.el.nodeValue = value
console.log(this.exp + ' updated: ' + value)
}</code></pre>
<p>有了刷新函数,那如何做到在数据改变时调用刷新函数更新节点的值呢?我们就还要将每个指令里的相关变量都跟这个 <code>Directive</code> 实例关联起来。我们用一个 <code>$binding</code> 对象来记录,它的键是变量,值是 <code>Binding</code> 实例:</p>
<pre><code>class Binding {
constructor() {
this.subs = []
}
addChild(key) {
return this[key] || new Binding()
}
addSub(watcher) {
this.subs.push(watcher)
}
}</code></pre>
<p>那上面的 <code>subs</code> 里添加的为什么不是 <code>Directive</code> 实例呢,而是 <code>watcher</code> 呢?它其实是 <code>Watcher</code> 的实例,这是为了以后能够实现 <code>$watch</code> 方法提前引入的概念,<code>Watcher</code> 实例的 <code>cb</code> 既可以是指令的刷新函数,也可以是 <code>$watch</code> 方法的回调函数:</p>
<pre><code>class Watcher {
constructor(vm, path, cb, ctx) {
this.id = ++uid
this.vm = vm
this.path = path
this.cb = cb
this.ctx = ctx || vm
this.addDep()
}
}</code></pre>
<pre><code>class Directive {
bind() {
this.watchers.push(new Watcher(this.vm, this.exp, this.update, this))
}
}</code></pre>
<p>我们先考虑最简单的情况,指令值就是一个变量,根据上面的思路,我们就可以写出最简单的实现了,代码就不贴了,<a href="https://link.segmentfault.com/?enc=pPPJouXN4cQKGtol2K1C0Q%3D%3D.PeyJVbjwfggJZX9uBDQZ8dkp2WkphLyxIZwzQ5rw9iyJ1trBqGnVG%2FlzSTzruAKQdCphFE%2Fd5sxNEQWS1fDiADH4DkWJYeBj%2F4XP5CowfMlURmXFnaTeatIJWCjBt0YM" rel="nofollow">有兴趣的直接看源码</a>。</p>
<pre><code><div id="app">
<h1>MVVM</h1>
<p>
<span>My name is {{name.first}}-{{name.last }},</span>{{age}} years old
</p>
</div>
<script src="../dist/eve.js"></script>
<script>
const app = new Eve({
el: '#app',
data: {
name: {
first: 'hugo',
last: 'seth'
},
age: 1
}
})
console.log(app)
</script></code></pre>
<p><img src="/img/bV9Oyr?w=808&h=560" alt="图片描述" title="图片描述"></p>
<p>上面实现的动态模板是在我们假定了指令值是最简单的变量的情况下实现的。那要是把上面的模板改为下面这样呢?</p>
<pre><code><h1>MVVM</h1>
<p>
<span>My name is {{name.first}}-{{name.last }},</span>{{'age: ' + age}} years old
</p>
<p>salary: {{ salary.toLocaleString() }}</p></code></pre>
<p>那我们上面的实现有一些数据就不能动态刷新了,原因很简单,就是我们是直接将 <code>'age: ' + age</code> 和 <code>Directive</code> 实例关联,而我们修改的只是 <code>age</code>,自然就找不到对应的实例了。那我们如何解决呢?</p>
<p>首先想到的肯定是按照现有的实现来扩展,让它支持模板插值是表达式的情况。已有的实现是直接解析得到变量,那我们就继续想办法直接解析表达式得到变量。像 <code>'age: ' + age</code> 这种表达式直接解析出 <code>age</code> 其实不难。但 <code>salary.toLocaleString()</code> 这种就不好做了,要是 <code>salary.toLocaleString().slice(1)</code> 这种可以说是没办法解析了。</p>
<p>既然这条路行不通,其实我们是有更简单的方法。既然我们都已经将 <code>data</code> 进行了代理,那我们就可以在 <code>get</code> 获取变量值时进行依赖收集。因为我们本来就会运行 <code>Directive</code> 实例的求值函数进行初始值的替换,这就会触发变量的 <code>get</code> 。具体的代码怎么写就不说了,<a href="https://link.segmentfault.com/?enc=gMJ%2FXLurCyXFWxQ2evLoLg%3D%3D.NsV%2ByV1uk0j6W87OGSLJgUJVKN%2B2vihtvFbO1zAUbfnHJVwoDie8vkhmyv68e7gGS87haEuO6rADucLvn%2BOID35wKCi9ORrfLOLIqmT6ffT3vWgISeQFhYm8JhSVG40S" rel="nofollow">详细的修改</a>和<a href="https://link.segmentfault.com/?enc=%2BK%2BH2LEnUp16tj3qgT%2Bznw%3D%3D.SSTtA4aLOHGkHOPMYc4W8bW0uTlgu1oiIOo2o%2BKN8Fkl%2B4Zsm6N80MKvsnzzyGfk5h5PlB7R8%2FfIGUm1KBiV22N30%2BSQGBEpMM4u85ErslQ%3D" rel="nofollow">支持表达式的源码</a>。</p>
<p><img src="/img/bV9O7X?w=808&h=560" alt="图片描述" title="图片描述"></p>
<p>当然现在只实现动态模板最简单的插值指令。还有一些更复杂的指令如:<code>if</code> 和 <code>for</code> 的实现方式,下次有机会再分享。</p>
<h3>思考题</h3>
<p>在最后的实现下,我们把模板改为下面这样(虽然很少会有人这样写),就会出现重复的 <code>Watcher</code> 实例,该如何解决这个问题?</p>
<pre><code><h1>MVVM</h1>
<p>
hello,<span>My name is {{name.first + '-' + name.last }}</span>
</p></code></pre>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=hd1IBGaexX0w4GLWf7soHg%3D%3D.NX%2Fk%2F0ntIGPWm%2Fw6hkIpK4RqM46osNY1KWiNPAQBhBWUnC1M3n%2FvgEhDV5E4Msa7" rel="nofollow">vue早期源码学习系列之四:如何实现动态数据绑定</a></p>
实现一个简单的模板引擎
https://segmentfault.com/a/1190000014695931
2018-05-02T17:53:45+08:00
2018-05-02T17:53:45+08:00
hugo_seth
https://segmentfault.com/u/hugo_seth
3
<p>对现在的前端来说,模板是非常熟悉的概念。毕竟现在三大框架那么火,不会用框架还能叫前端吗?,而框架是必定有模板的。那我们写的模板是如何转换成 HTML 显示在网页上的呢?</p>
<p>我们先从简单的说起,静态模板一般用于需要 SEO 且页面数据是动态的网页。由前端编写好静态模板,后端负责将动态的数据和静态模板交给模板引擎,最终编译成 HTML 字符串返回给浏览器。这种时候我们用到的模板引擎可能是远古的 jsp,或是现在用的比较多的 pug(原来叫 jade)、ejs。</p>
<p>模板引擎做的就是编译模板的工作。它说白了就是一个函数:将模板字符串转换成 HTML 字符串。</p>
<p>我们先写一个最简单的静态模板编译函数:</p>
<h2>正则替换</h2>
<p>我们的模板和数据如下:</p>
<pre><code>const tpl = '<p>hello,我是{{name}},职业:{{job}}<p>'
const data = {
name: 'hugo',
job: 'FE'
}</code></pre>
<p>那我们想到的最简单的办法就是正则替换,当然我们别忘了要把前缀加上,<code>name</code> 要转换成 <code>data.name</code></p>
<pre><code>function compile(tpl, data) {
const regex = /\{\{([^}]*)\}\}/g
const string = tpl.trim().replace(regex, function(match, $1) {
if ($1) {
return data[$1]
} else {
return ''
}
})
console.log(string) // <p>hello,我是hugo,职业:FE<p>
}
compile(tpl, data)</code></pre>
<p>上面的编译函数在例子中是可以工作的,但要是我把模板和数据改一下呢?</p>
<pre><code>const tpl = '<p>hello,我是{{name}},年龄:{{info.age}}<p>'
const data = {
name: 'hugo',
info: {
age: 26
}
}</code></pre>
<p>这个时候控制台打印的就是:</p>
<pre><code><p>hello,我是hugo,年龄:undefined<p></code></pre>
<p>因为 <code>data["info.age"]</code> 的值是 <code>undefined</code> 。所以我们还要处理正则匹配到的字符串,这个时候再用正则已经非常不好做了。既然这样,不如就直接全改用字符串匹配:</p>
<h2>字符串解析</h2>
<pre><code>function compile(tpl) {
let string = ''
tpl = tpl.trim()
while (tpl) {
const start = tpl.indexOf('{{')
const end = tpl.indexOf('}}')
if (start > -1 && end > -1) {
if (start > 0) {
string += JSON.stringify(tpl.slice(0, start))
}
string += '+ data.' + tpl.slice(start + 2, end).trim() + ' +'
tpl = tpl.slice(end + 2)
} else {
string += JSON.stringify(tpl)
tpl = ''
}
}
console.log(string)
// "<p>hello,我是"+ data.name +",年龄:"+ data.info.age +"<p>"
return new Function('data', 'return ' + string)
}
compile(tpl)(data) // <p>hello,我是hugo,年龄:26<p></code></pre>
<p>这样我们新的编译函数就可以处理 <code>{{info.age}}</code> 这种嵌套属性的情况了。上面的 <code>JSON.stringify</code> 作用是给字符串的两端加上 <code>"</code>,然后转义字符串中的特殊字符。</p>
<p>虽然我们解决了嵌套属性的问题,但又面临更困难的问题,就是怎样让模板里插值支持像 <code>{{ '名字是: ' + name }}</code> 这样表达式。在这种情况下,我们是很难在每个正确的地方加 <code>data.</code> 前缀的,因为前缀只能加上变量前,而表达式里可能还有字符串。</p>
<h2>使用 <code>with</code> 语句</h2>
<p>我们考虑最简单的处理方式,也就是不加前缀了,使用 <code>with</code> 语句指定变量的作用域。所以我们只要编译后返回一个函数,在这个函数内使用 <code>with</code> 语句指定作用域,函数再返回 HTML 字符串。在下面的例子中,我使用的是 <code>ejs</code> 模板的语法:</p>
<pre><code>const tpl = `<p>hello,我的<%= '名字是: ' + name %>,年龄:<%= info.age %><p>`
const data = {
name: 'hugo',
info: {
age: 26
}
}</code></pre>
<pre><code>function compile(tpl) {
const ret = []
tpl = tpl.trim()
ret.push('var _data_ = [];')
ret.push('with(data) {')
while (tpl) {
let start = tpl.indexOf('<%=')
const end = tpl.indexOf('%>')
if (start > -1 && end > -1) {
if (start > 0) {
ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
}
ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
tpl = tpl.slice(end + 2)
} else {
ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
tpl = ''
}
}
ret.push('}')
ret.push('return _data_.join("")')
return new Function('data', ret.join('\n'))
}
const fn = compile(tpl)
fn(data)
// <p>hello,我的名字是: hugo,年龄:26<p></code></pre>
<p>上面的编译函数将模板根据模板语法 <code><%=%></code> 分割成各个部分放入数组中,再将数组中的元素由换行符连接,成为 <code>new Function</code> 的函数体,生成的函数如下:</p>
<pre><code>function(data/*``*/) {
var _data_ = [];
with(data) {
_data_.push("<p>hello,我的");
_data_.push('名字是: ' + name);
_data_.push(",年龄:");
_data_.push(info.age);
_data_.push("<p>");
}
return _data_.join("")
}</code></pre>
<p>我们再将 <code>data</code> 作为参数传入这个函数就可以得到期望的 HTML 字符串。</p>
<p>现在我们已经实现了能够编译插值是表达式的模板引擎。但我们还差一个非常重要的功能,那就是编译模板中的语句,如:<code>for</code> 循环和 <code>if</code> 语句。要实现编译语句的功能,我们必须将语句和插值区分开,因此要使用不同的模板语法:语句用 <code><% %></code>,插值则用<code><%= %></code>。那我们就可以将上面的编译函数稍微修改下,根据不同的语法分别处理,就可以支持模板语句了:</p>
<pre><code>const tpl = `
<p>hello,我是<%= name + '-seth' %>,年龄:<%= info.age %><p>
<% if (info.age > 18 && info.age < 28){ %>
<p>是个九零后中年人</p>
<% } %>
<h3>兴趣</h3>
<ul>
<% for (var i = 0; i < interests.length; i++) { %>
<li><%= interests[i] %></li>
<% } %>
</ul>
`</code></pre>
<pre><code>const data = {
name: 'hugo',
info: {
age: 26
},
interests: ['movie']
}</code></pre>
<pre><code>function compile(tpl) {
const ret = []
tpl = tpl.trim()
ret.push('var _data_ = [];')
ret.push('with(data) {')
while (tpl) {
let start = tpl.indexOf('<%')
const end = tpl.indexOf('%>')
if (start > -1 && end > -1) {
if (start > 0) {
ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
}
if (tpl.charAt(start + 2) === '=') {
ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
} else {
ret.push(tpl.slice(start + 2, end))
}
tpl = tpl.slice(end + 2)
} else {
ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
tpl = ''
}
}
ret.push('}')
ret.push('return _data_.join("")')
return new Function('data', ret.join('\n'))
}</code></pre>
<pre><code>const fn = compile(tpl)
fn(data)
// <p>hello,我的名字是: hugo,年龄:26<p>
// <p>是个九零后中年人</p>
// <h3>兴趣</h3>
// <ul>
// <li>movie</li>
// </ul>
</code></pre>
<p>这个修改后的编译函数没什么好解释的,就是根据不同的模板语法做不同的处理,最终返回的函数如下:</p>
<pre><code>function(data /*``*/ ) {
var _data_ = [];
with(data) {
_data_.push("<p>hello,我的");
_data_.push('名字是: ' + name);
_data_.push(",年龄:");
_data_.push(info.age);
_data_.push("<p>\n");
if (info.age > 18 && info.age < 28) {
_data_.push("\n <p>是个九零后中年人</p>\n");
}
_data_.push("\n<h3>兴趣</h3>\n<ul>\n ");
for (var i = 0; i < interests.length; i++) {
_data_.push("\n <li>");
_data_.push(interests[i]);
_data_.push("</li>\n ");
}
_data_.push("\n</ul>");
}
return _data_.join("")
}</code></pre>
<p>这样我们就已经完成了一个功能简单的模板引擎。</p>
彻底搞懂路由跳转:location 和 history 接口
https://segmentfault.com/a/1190000014120456
2018-04-01T19:30:12+08:00
2018-04-01T19:30:12+08:00
hugo_seth
https://segmentfault.com/u/hugo_seth
38
<p>在单页应用中,通常由前端来配置路由,根据不同的 url 显示不同的内容。想要知道这是如何做到的,首先得了解浏览器提供的两大 API:</p>
<ol>
<li>
<p><code>window.location</code></p>
<ul>
<li><code>location.href</code></li>
<li><code>location.hash</code></li>
<li><code>location.search</code></li>
<li><code>location.pathname</code></li>
</ul>
</li>
<li>
<p><code>window.history</code></p>
<ul>
<li><code>history.pushState()</code></li>
<li><code>history.replaceState()</code></li>
<li><code>history.go()</code></li>
<li><code>history.back()</code></li>
<li><code>history.forward()</code></li>
</ul>
</li>
</ol>
<h2>window.location</h2>
<p>我们先了解 location 对象,location 有很多的属性。我们可以通过改变其属性值修改页面的 url。我们在单页应用中需要做到的是改变 url 不刷新页面,location 接口提供以下两种方式可以做到:</p>
<ol>
<li>
<code>location.href</code> 赋值时只改变 url 的 hash<br><img src="/img/bV7pcG?w=968&h=378" alt="图片描述" title="图片描述">
</li>
<li>直接赋值 <code>location.hash</code><br><img src="/img/bV7pc1?w=944&h=440" alt="图片描述" title="图片描述">
</li>
</ol>
<p>而上面的列出其余两个属性 <code>location.search</code> 会直接刷新页面,这个就不解释了。但 <code>location.pathname</code> 照道理来说只改变 hash 应该是可以的,但实际上浏览器会编码这个属性值,所以无法直接赋带 # 号的值。</p>
<h2>window.history</h2>
<p><a href="https://link.segmentfault.com/?enc=tr59jwNIE77XNu7oC%2B2cYQ%3D%3D.0Vd7yaLpShiZmnLotsNtr6lIbaYFKW16ZEto812zBfD4fsWr0egrvh4HMiuokimHcZAExRaFP5Qagn%2B81mrqMQ%3D%3D" rel="nofollow">history 接口</a>是 HTML5 新增的,它有五个方法可以改变 url 而不刷新页面。</p>
<ol>
<li>
<code>history.pushState()</code><br><img src="/img/bV7pg8?w=936&h=434" alt="图片描述" title="图片描述">
</li>
<li>
<code>history.replaceState()</code><br><img src="/img/bV7phf?w=950&h=478" alt="图片描述" title="图片描述">
</li>
<li>
<code>history.go()</code><br><img src="/img/bV7phg?w=854&h=578" alt="图片描述" title="图片描述">
</li>
</ol>
<p>上面只演示了三个方法,因为 <code>history.back()</code> 等价于 <code>history.go(-1)</code>,<code>history.forward()</code> 则等价于 <code>history.go(1)</code>,这三个接口等同于浏览器界面的前进后退。</p>
<h2>如何监听 url 的变化</h2>
<p>现在我们已经知道如何不刷新页面改变页面的 url。虽然页面没刷新,但我们要改变页面显示的内容。这就需要 js 监听 url 的变化从而达到我们的目的。</p>
<p>我们有两个事件可以监听 url 的改变:</p>
<h3>hashchange</h3>
<p><code>hashchange</code> 事件能监听 url hash 的改变。</p>
<p>先要加上事件监听的代码:</p>
<pre><code>window.addEventListener('hashchange', function(e) {
console.log(e)
})</code></pre>
<p>然后就可以在页面的 console 里愉快的实验了:</p>
<p><img src="/img/bV7plH?w=970&h=974" alt="图片描述" title="图片描述"></p>
<p>从上图中我们可以知道不管是通过 location 接口直接改变 hash,还是通过 history 接口前进后退(只是 hash 改变的情况下),我们都可以监听到 url hash 的改变。但这个事件也只能监听 url hash 的变化。所以我们需要一个更强大的事件:<code>popstate</code>。</p>
<h3>popstate</h3>
<p><a href="https://link.segmentfault.com/?enc=iWnq3McWBZ9UineqQnA36w%3D%3D.n9XRMNyANlFkNV%2FS6%2B8TVqbIKhMjUCie20mmywIu3GMqH34UK7%2FceY6zuWq%2FaxrKYoY9fXWZ9D06YByAdFSirA%3D%3D" rel="nofollow">popstate</a> 事件能监听除 <code>history.pushState()</code> 和 <code>history.replaceState()</code> 外 url 的变化。</p>
<p>先加上事件监听的代码:</p>
<pre><code>window.addEventListener('popstate', function(e) {
console.log(e)
})</code></pre>
<p>然后又可以在页面的 console 里愉快的实验了:</p>
<p><img src="/img/bV7pow?w=970&h=750" alt="图片描述" title="图片描述"></p>
<p>其实不止 <code>history.pushState()</code> 和 <code>history.replaceState()</code> 对 url 的改变不会触发 <code>popstate</code> 事件,当这两个方法只改变 url hash 时也不会触发 <code>hashchange</code> 事件。</p>
<h2>hash 模式和 history 模式</h2>
<p>我们都知道单页应用的路由有两种模式:hash 和 history。如果我们在 hash 模式时不使用 <code>history.pushState()</code> 和 <code>history.replaceState()</code> 方法,我们就只需要在 <code>hashchange</code> 事件回调里编写 url 改变时的逻辑就行了。而 history 模式下,我们不仅要在 <code>popstate</code> 事件回调里处理 url 的变化,还需要分别在 <code>history.pushState()</code> 和 <code>history.replaceState()</code> 方法里处理 url 的变化。而且 history 模式还需要后端的配合,不然用户刷新页面就只有 404 可以看了?</p>
<p>所以 hash 模式下我们的工作其实是更简单的,但为什么现在都推荐用 history 模式呢?总不是 hash 模式下的 url 太丑了,毕竟这是个看脸的世界?</p>
<p>不过 <code>vue-router</code> 在浏览器支持 <code>pushState()</code> 时就算是 hash 模式下也是用 <code>history.pushState()</code> 来改变 url,不知道有没什么深意?还有待研究...</p>
【译】JavaScript 核心(第二版)
https://segmentfault.com/a/1190000012825172
2018-01-13T10:46:07+08:00
2018-01-13T10:46:07+08:00
hugo_seth
https://segmentfault.com/u/hugo_seth
6
<blockquote>原文:<a href="https://link.segmentfault.com/?enc=XwlWIZzAbHtutf2PDw3Ftg%3D%3D.bUjLF0JC%2Fgom8GsjeiQLJZZYM02Q%2B6dWC6SZlhdflUdQqu1v8KuA%2FGiVWZSMdPlVaTf%2BhLYJ%2BlWXZizTvg0ajdBwMr6YnXlHHJRL0zmxrRI%3D" rel="nofollow">JavaScript. The Core: 2nd Edition</a><br>作者:<a href="https://link.segmentfault.com/?enc=IWy7whGbytbWKhhyEQQgxw%3D%3D.NSYUTAlTSib3%2Bn1YzVo0Kvp%2BRLO3WklnOfKCR%2FYOL6U%3D" rel="nofollow">Dmitry Soshnikov</a>
</blockquote>
<p>文章其他语言版本:<a href="https://link.segmentfault.com/?enc=YdT4SMFUjWSbWYG1YdWLPw%3D%3D.ruEWkyytCfP%2B04s8pQy6Nb32Qc63fWw0q2D6eByStRb99bNL64CTgyM7mNJX7LCdSnCl2EkAx9rbu6DdWKXpbBuYLdmH9W3S9s3dxJVAslA%3D" rel="nofollow">俄语</a></p>
<p>这篇文章是 <a href="https://link.segmentfault.com/?enc=zBUb%2F5lAcHM4OZeXWZ86Vg%3D%3D.oL4VndMIg7vjX%2F5Oo4DHSIZ5LTstMhyYSyxH6loaP71Siluu8XTHX00%2FfewTinMXNe%2FwMTacN5dx4LEUR5ZEYw%3D%3D" rel="nofollow">JavaScript. The Core</a> 演讲的第二版,文章内容专注于 ECMAScript 编程语言和其运行时系统的核心组件。</p>
<p><strong>面向读者:</strong>有经验的开发者、专家</p>
<p><a href="https://link.segmentfault.com/?enc=RHcl0pLFe1fSjpG9vEGfzA%3D%3D.W0OQ%2F5dYHH2nMEVKrctCC7dg2BLAjUGaZVea1uORAMLqueRHG%2FmC6TOAK4G7XmJmBvp6qFqfYPfqNX3FpyRSuQ%3D%3D" rel="nofollow">文章第一版</a> 涵盖了 JS 语言通用的方面,该文章描述的抽象大多来自古老的 ES3 规范,也引用了一些 ES5 和 ES6( ES2015 )的变更。</p>
<p>从 ES2015 开始,规范更改了一些核心组件的描述和结构,引入了新的模型等等。所以这篇文章我将聚焦新的抽象,更新的术语和在规范版本更替中仍然维护并保持一致的非常基本的 JS 结构。</p>
<p>文章涵盖 ES2017+ 运行时系统的内容。</p>
<blockquote>
<strong>注释:</strong>最新 <a href="https://link.segmentfault.com/?enc=T%2FB4OrwPlhD8E2jZEkEuuA%3D%3D.HM9J92eEC3P5xRCPxwvr5G5cZFGLKONWpJiIyKRvIEE%3D" rel="nofollow">ECMAScript 规范</a> 版本可以在 TC-39 网站上查看。</blockquote>
<p>我将从对象的概念开始讲起,它是 ECMAScript 的根本。</p>
<h3>对象</h3>
<p>ECMAScript 是一门面向对象、基于原型进行组织的编程语言,且它的核心抽象为对象的概念。</p>
<blockquote>
<strong>定义1:对象:</strong>对象是属性的集合并且有一个原型(prototype)对象。原型的值为一个对象或 <code>null</code> 。</blockquote>
<p>我们来看一个基本的对象示例。对象的原型可通过内部的 <code>[[Prototype]]</code> 属性引用,在用户代码层面则是暴露在 <code>__proto__</code> 属性上。</p>
<p>代码如下:</p>
<pre><code>let point = {
x: 10,
y: 20,
};</code></pre>
<p>上面的对象有两个显式的属性和一个隐藏的 <code>__proto__</code> 属性,它是 <em>point</em> 对象的原型引用:</p>
<p><img src="/img/bV0A1y?w=1369&h=392" alt="A basic object with a prototype" title="A basic object with a prototype"></p>
<blockquote>
<strong>注:</strong>对象也可能存储 symbol 。阅读这篇文章了解更多关于 symbol 的内容。</blockquote>
<p>原型对象用于实现动态分配机制的继承。我们先思考一下原型链概念,以便详细了解这个机制。</p>
<h3>原型</h3>
<p>所有对象在创建的时候都会得到原型。如果没有显式地设置原型,那么对象接收默认原型作为它们的继承对象。</p>
<blockquote>
<strong>定义2:原型:</strong>原型是一个代理对象,用来实现基于原型的继承。</blockquote>
<p>原型可以通过 <code>__proto__</code> 属性或 <code>Object.create</code> 方法显式的设置。</p>
<pre><code>// Base object.
let point = {
x: 10,
y: 20,
};
// Inherit from `point` object.
let point3D = {
z: 30,
__proto__: point,
};
console.log(
point3D.x, // 10, inherited
point3D.y, // 20, inherited
point3D.z // 30, own
);</code></pre>
<blockquote>
<strong>注:</strong>默认情况下,对象接收 <code>Object.prototype</code> 作为它们的继承对象。</blockquote>
<p>任何对象都可作为其它对象的原型,且原型本身可以有原型。如果对象的原型不为 <code>null</code> ,原型的原型不为 <code>null</code> ,以此类推,这就叫做原型链。</p>
<blockquote>
<strong>定义3:原型链:</strong>原型链是对象的有限链接,用来实现继承和共享属性。</blockquote>
<p><img src="/img/bV0FCc?w=1335&h=108" alt="Figure 2. A prototype chain" title="Figure 2. A prototype chain"></p>
<p>规则非常简单:如果对象自身没有一个属性,就会试图在原型上解析属性,然后原型的原型,直到查找完整个原型链。</p>
<p>技术上来说这个机制被称为动态分配或代理。</p>
<blockquote>
<strong>定义4:代理:</strong>一个在继承链上解析属性的机制。这个过程是在运行时发生的,因此也被叫做<strong>动态分配</strong>。</blockquote>
<blockquote>
<strong>注:</strong>与此相反的静态分配是在编译的时候解析引用的,动态分配则是在运行时。</blockquote>
<p>如果属性最终都没有在原型链上找到的话,那么返回 <code>undefined</code> 值。</p>
<pre><code>// An "empty" object.
let empty = {};
console.log(
// function, from default prototype
empty.toString,
// undefined
empty.x,
);</code></pre>
<p>从上面的代码可以知道,一个默认的对象实际上永远不为空--它总是从 <code>Object.prototype</code> 继承一些东西。如果想要创建一个无原型的字典(dictionary),我们必须明确地将原型设为 <code>null</code> :</p>
<pre><code>// Doesn't inherit from anything.
let dict = Object.create(null);
console.log(dict.toString); // undefined</code></pre>
<p>动态分配机制允许继承链完全可变,提供修改代理对象的能力:</p>
<pre><code>let protoA = {x: 10};
let protoB = {x: 20};
// Same as `let objectC = {__proto__: protoA};`:
let objectC = Object.create(protoA);
console.log(objectC.x); // 10
// Change the delegate:
Object.setPrototypeOf(objectC, protoB);
console.log(objectC.x); // 20</code></pre>
<blockquote>
<strong>注:</strong>即使 <code>__proto__</code> 现在是标准属性,并且在解释时使用易于理解,但实践时倾向使用操作原型的 API 方法,如 <code>Object.create</code>、<code>Object.getPrototypeOf</code>、<code>Object.setPrototypeOf</code> ,类似于反射(Reflect)模块。</blockquote>
<p>从上面 <code>Object.prototype</code> 示例我们知道同一个原型可以给多个对象共享。从这个原理出发,ECMAScript 实现了基于类的继承。我们看下示例,并且深入了解 JS 的 “类(class)” 抽象。</p>
<h3>类</h3>
<p>当多个对象共享同一个初始的状态和行为时,它们就形成了一个<em>类</em>。</p>
<blockquote>
<strong>定义5:类:</strong>一个类是一个正式的抽象集,它规定了对象的初始状态和行为。</blockquote>
<p>假如我们需要多个对象继承同一个原型,我们当然可以创建这个原型并显式的继承它:</p>
<pre><code>// Generic prototype for all letters.
let letter = {
getNumber() {
return this.number;
}
};
let a = {number: 1, __proto__: letter};
let b = {number: 2, __proto__: letter};
// ...
let z = {number: 26, __proto__: letter};
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);</code></pre>
<p>我们可以从下图看到这些关系:</p>
<p><img src="/img/bV0VDi?w=1205&h=1035" alt="Figure 3. A shared prototype" title="Figure 3. A shared prototype"></p>
<p>然而这明显很繁琐。类抽象正是服务这个目的 - 作为一个语法糖(和构造器在语义上所做的一样,但是是更友好的语法形式),它让我们使用更方便的模式创建那些对象:</p>
<pre><code>class Letter {
constructor(number) {
this.number = number;
}
getNumber() {
return this.number;
}
}
let a = new Letter(1);
let b = new Letter(2);
// ...
let z = new Letter(26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
</code></pre>
<blockquote>
<strong>注:</strong> ECMAScript 中基于类的继承是在基于原型的代理之上实现的。<p><strong>注:</strong>一个“类”只是理论上的抽象。技术上来说,它可以像 Java 或 C++ 一样通过静态分配来实现,也可以像 JavaScript、Python、Ruby 一样通过动态分配(代理)来实现。</p>
</blockquote>
<p>技术上来说一个“类”表示“构造函数 + 原型”的组合。因此构造函数创建对象并自动设置新创建实例的原型。这个原型存储在 <code><ConstructorFunction>.prototype</code> 属性上。</p>
<blockquote>
<strong>定义6:构造器:</strong>构造器是一个函数,它用来创建实例并自动设置它们的原型。</blockquote>
<p>我们可以显式的使用构造函数。此外,在类抽象引入之前,JS 开发者过去因为没有更好的选择而这样做(我们依然可以在互联网上找到大量这样的遗留代码):</p>
<pre><code>function Letter(number) {
this.number = number;
}
Letter.prototype.getNumber = function() {
return this.number;
};
let a = new Letter(1);
let b = new Letter(2);
// ...
let z = new Letter(26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);</code></pre>
<p>创建单级的构造函数非常简单,而从父类继承的模式则要求更多的模板代码。目前这些模板代码作为实现细节被隐藏,而这正是在我们创建 JavaScript 类时在底层所发生的。</p>
<blockquote>
<strong>注:</strong>构造函数就是类继承的实现细节。</blockquote>
<p>我们看一下对象和它们的类的关系:</p>
<p><img src="/img/bV00WJ?w=1779&h=1035" alt="Figure 4. A constructor and objects relationship" title="Figure 4. A constructor and objects relationship"></p>
<p>上图显示了每个对象都有一个关联的原型。就连构造函数(类)也有原型也就是 <code>Function.prototype</code> 。我们看到 a、b 和 c 是 Letter 的实例,它们的原型是 <code>Letter.prototype</code> 。</p>
<blockquote>
<strong>注:</strong>所有对象的实际原型总是 <code>__proto__ </code> 引用。构造函数显式声明的 <code>prototype </code> 属性只是一个指向它实例的原型的引用;实例的原型仍然是通过 <code>__proto__ </code> 引用得到。<a href="https://link.segmentfault.com/?enc=atSxJ3jF%2F7lJBWS0MnUm2w%3D%3D.AkxcdhF1MET5BH6XqFeItK67h4YLvQ7rSsWZvUlhkQ7A0av3W9o9IfQLL8g%2FUnZeQGHPSfO9Ulwd5TD6JipKWTGDJoZ8f2Hvoa6dtyrFcciSzbzCX7TtNb2G6MLaABIpsMaeKyyrmLtzEj0gRsNuZy8nUO1L12GehM5FJrHJyQsP8gBo9S6ZriEyvWFSSUWPE7FacUTw6bjZEKie%2Fp767w%3D%3D" rel="nofollow">点此链接详细了解</a>。</blockquote>
<p>你可以在文章 <a href="https://link.segmentfault.com/?enc=4OjpVUNfMAAmyflBWDi%2F2w%3D%3D.E31zIJNcVf4v7lOHUmakcQ4tY%2FfrudwfROGHRhIPRvy%2Bj5905U32H9LNDxGKZ6DdEeQZ5TRAgzQ2XGJMKl25FHuaL1zhdCKa%2F5Pkl9UYgFE%3D" rel="nofollow">ES3. 7.1 OOP: The general theory</a> 中找到关于 OPP 通用概念(包括基于类、基于原型等的详细介绍)的详细讨论。</p>
<p>现在我们已经了解了 ECMAScript 对象间的基本关系,让我们更深入的了解 JS 运行时系统。我们将会看到,几乎所有的东西都可以用对象表示。</p>
<h3>执行上下文</h3>
<p>为了执行 JS 代码并追踪其运行时的计算,ECMAScript 规范定义了<em>执行上下文(execution context)</em>的概念。逻辑上执行上下文是用<em>栈</em>来保持的(执行上下文栈我们一会就会看到),它与<em>调用栈(call stack)</em>的通用概念相对应。</p>
<blockquote>
<strong>定义7:执行上下文:</strong>执行上下文是一个规范策略,用于追踪代码的运行时计算。</blockquote>
<p>ECMAScript 代码有几种类型:全局代码、函数和 <code>eval</code> ;它们都在各自的执行上下文中运行。不同的代码类型及其适当的对象可能会影响执行上下文的结构:例如,<em>生成器函数(generator functions)</em>会将其<em>生成器对象(generator object)</em>保存在上下文中。</p>
<p>我们看一个递归函数调用:</p>
<pre><code>function recursive(flag) {
// Exit condition.
if (flag === 2) {
return;
}
// Call recursively.
recursive(++flag);
}
// Go.
recursive(0);</code></pre>
<p>当一个函数调用时,就创建一个新的<em>执行上下文</em>并把它压入栈 - 这时它就成了<em>活跃的执行上下文</em>。当函数返回时,其上下文就从栈中推出。</p>
<p>我们将调用另一个上下文的上下文称为<em>调用者(caller)</em>。被调用的上下文因此就叫做<em>被调用者(callee)</em>。在上面的例子中,<code>recursive </code> 函数同时承担着上述两者角色:调用者和被调用者 - 当递归地调用自身。</p>
<blockquote>
<strong>定义8:执行上下文栈:</strong>执行上下文栈是一个后进先出的结构,它用来维护控制流和执行顺序。</blockquote>
<p>在上面的例子中,我们对栈有“压入-推出”的修改:</p>
<p><img src="/img/bV1c6e?w=1716&h=414" alt="Figure 5. An execution context stack" title="Figure 5. An execution context stack"></p>
<p>我们可以看到,<em>全局上下文</em>一直都在栈的底部,它是在执行任何其他上下文之前创建的。</p>
<p>你可以在<a href="https://link.segmentfault.com/?enc=0zjfMqA2quegQhu0gkXHDg%3D%3D.0c1DJ1GIZcu1a2kMK%2BQiDz%2FP3q2dfVZJ8z4dcnVQjQlDBIrPENxFLOeFtCDJvC193QsWq26pcs3X3gH%2BptucWFW%2FWv%2Bk9Q8O%2Bbbuw%2FoMxV4%3D" rel="nofollow">这篇文章</a>中找到更多关于执行上下文的详细内容。</p>
<p>一般情况下,一个上下文中的代码会运行到结束,然而正如我们上面所提到的,一些对象 - 如<em>生成器</em>,可能会违反栈后进先出的顺序。一个生成器函数可能会挂起其运行上下文并在完成之前将其从栈中移除。当生成器再次激活时,其上下文恢复并再次被压入栈:</p>
<pre><code>function *gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value, // 1
g.next().value, // 2
);</code></pre>
<p>上面代码中的 <code>yield</code> 语句返回值给调用者并将上下文推出。第二次调用 <code>next</code> 时,相同的上下文再次被压入栈并恢复。这样的上下文会比创建它的调用者生命周期更长,因此违反了后进先出的结构。</p>
<blockquote>
<strong>注:</strong>你可以阅读<a href="https://link.segmentfault.com/?enc=2VJ1FO5HFOxTTc2wi5c3cw%3D%3D.PA3HEYO%2B1zdKUluUFh%2FiBm85S7o%2By383Q9iD9bpkwvUpkKnG5OHIlP%2BYOTxGywrIbHP22zhaDNAKXVvK%2BGbBdfN7ALC48%2FAqRXYk%2BsyTBTBI57brWuon3ieX8X4%2FFJBm" rel="nofollow">这篇文档</a>了解关于生成器和迭代器的更多内容。</blockquote>
<p>现在我们将讨论执行上下文的重要组成部分;特别是 ECMAScript 运行时如何管理变量的存储和代码中嵌套块创建的<em>作用域(scope)</em>。这是<em>词法环境(lexical environments)</em>的通用概念,它在 JS 中用来存储数据和解决“<em>函数参数问题(Funarg problem)</em>” - 和<em>闭包(closure)</em>的机制一起。</p>
<h3>环境</h3>
<p>每个执行上下文都有一个相关的<em>词法环境</em>。</p>
<blockquote>
<strong>定义9:词法环境:</strong>词法环境是用于定义上下文中出现的<em>标识符</em>与其值之间的关联的结构。每个环境都可以有一个指向其<em>可选父环境</em>的引用。</blockquote>
<p>所以,一个环境是在某个范围内定义的变量,函数和类的<em>存储</em>。</p>
<p>从技术上来说,一个环境是由一个<em>环境记录</em>(一个将标识符映射到值的实际存储表)和一个对父项(可能是 <code>null</code>)的引用这一对组成。</p>
<p>看代码:</p>
<pre><code>let x = 10;
let y = 20;
function foo(z) {
let x = 100;
return x + y + z;
}
foo(30); // 150</code></pre>
<p>上面代码的<em>全局上下文</em>和 <code>foo</code> 函数的上下文的环境结构如下图所示:</p>
<p><img src="/img/bV1hNs?w=1035&h=861" alt="Figure 6. An environment chain" title="Figure 6. An environment chain"></p>
<p>从逻辑上讲,这使我们想起上面讨论过的原型链。并且标识符解析的规则也非常相似:如果在自己的环境中找不到变量,则尝试在父级环境中、在父级父级中查找它,以此类推 - 直到整个环境链都查找完成。</p>
<blockquote>
<strong>定义10:标识符解析:</strong>在环境链中解析变量(绑定)的过程。 无法解析的绑定会导致 <code>ReferenceError</code> 。</blockquote>
<p>这就解释了:为什么变量 <code>x</code> 被解析为 <code>100</code>,而不是 <code>10</code> - 它是直接在 <code>foo</code> 自己的环境中找到;为什么我们可以访问参数 <code>z</code> - 它也只是存储在<em>激活环境</em>中;也是为什么我们可以访问变量 <code>y</code> - 它是在父级环境中找到的。</p>
<p>与原型类似,相同的父级环境可以由多个子环境共享:例如,两个全局函数共享相同的全局环境。</p>
<blockquote>
<strong>注:</strong>您可以在<a href="https://link.segmentfault.com/?enc=wya72ho4tgHEIO2qaNjR%2Bg%3D%3D.Tt3HnS0drlmxdENbhh3DsZ5iq078pi5mjoonxUCz%2Fq15oXvKDRRoTn1Lb4oGCWdFdgxoy82B5014Gv%2B88Z2THPLQrqQ5ns%2FO6q4pJXtR2a3XvC62LTxi0v9GpmRSBmPKx67lIfBoufwnBt780nvy5A%3D%3D" rel="nofollow">这篇文章</a>中获得有关词法环境的详细信息。</blockquote>
<p>环境记录因<em>类型</em>而异。有<strong>对象</strong>环境记录和<strong>声明式</strong>环境记录。在声明式记录之上还有<strong>函数</strong>环境记录和<strong>模块</strong>环境记录。每种类型的记录都有它的特性。但是,标识符解析的通用机制在所有环境中都是通用的,并且不依赖于记录的类型。</p>
<p>一个<em>对象环境记录</em>的例子就是<em>全局环境记录</em>。这种记录也有相关联的<em>绑定对象</em>,它可以存储记录中的一些属性,而不是全部,反之亦然(译者注:不同的可以看下面的示例代码)。绑定对象也可以通过 <code>this</code> 得到。</p>
<pre><code>// Legacy variables using `var`.
var x = 10;
// Modern variables using `let`.
let y = 20;
// Both are added to the environment record:
console.log(
x, // 10
y, // 20
);
// But only `x` is added to the "binding object".
// The binding object of the global environment
// is the global object, and equals to `this`:
console.log(
this.x, // 10
this.y, // undefined!
);
// Binding object can store a name which is not
// added to the environment record, since it's
// not a valid identifier:
this['not valid ID'] = 30;
**加粗文字**
console.log(
this['not valid ID'], // 30
);</code></pre>
<p>上述代码可以表示为下图:</p>
<p><img src="/img/bV1B3q?w=1332&h=901" alt="Figure 7. A binding object" title="Figure 7. A binding object"></p>
<p>需要注意的是,绑定对象的存在是为了兼容遗留的结构,例如 <code>var</code> 声明和<code>with</code> 语句,它们也将它们的对象作为绑定对象提供。这就是环境被表示为简单对象的历史原因。现在,环境模型更加优化,但其结果是,我们无法再将绑定作为属性访问(译者注:如上面的代码中我们不能通过 <code>this.y</code> 访问 <code>y</code> 的值)。</p>
<p>我们已经看到环境是如何通过父链接相关联的。现在我们将看到一个环境的生命周期如何比创造它的上下文环境的更久。这是我们即将讨论的闭包机制的基础。</p>
<h3>闭包</h3>
<p>ECMAScript中的函数是<em>头等的(first-class)</em>。这个概念是函数式编程的基础,这些方面也被 JavaScript 所支持。</p>
<blockquote>
<strong>定义11:头等函数:</strong>它是一种函数,其可以作为正常数据参与:存储在变量中,作为参数传递,或作为另一个函数的值返回。</blockquote>
<p>与头等函数概念相关的是所谓的“<a href="https://link.segmentfault.com/?enc=2%2FQC4Mru%2FoQkhLOHa5jZbQ%3D%3D.auhLWta9p8MKm0VcwHjrWYEJU7eHkXrDx2ll9tEpB2%2BkWyG%2FtvQxLWh%2Bx%2BLQDfTH" rel="nofollow">函参问题(Funarg problem)</a>”(或“一个函数参数的问题”)。当一个函数需要处理<em>自由变量</em>时,这个问题就会出现。</p>
<blockquote>
<strong>定义12:自由变量:</strong>一个既不是参数也不是自身函数的局部变量的变量。</blockquote>
<p>我们来看看函参问题,并看它在 ECMAScript 中是如何解决的。</p>
<p>考虑下面的代码片段:</p>
<pre><code>let x = 10;
function foo() {
console.log(x);
}
function bar(funArg) {
let x = 20;
funArg(); // 10, not 20!
}
// Pass `foo` as an argument to `bar`.
bar(foo);</code></pre>
<p>对于函数 <code>foo</code> 来说,<code>x</code> 是自由变量。当 <code>foo</code> 函数被激活时(通过<br><code>funArg</code> 参数) - 应该在哪里解析 <code>x</code> 的绑定?是创建函数的外部作用域还是调用函数的<em>调用者作用域</em>?正如我们所见,调用者即 <code>bar</code> 函数,也提供了 <code>x</code> 的绑定 - 值为 20 。</p>
<p>上面描述的用例被称为 <strong><em>downward funarg problem</em></strong>,即在确定绑定的正确环境时的模糊性:它应该是创建时的环境,还是调用时的环境?</p>
<p>这是通过使用静态作用域的协议来解决的,也就是创建时的作用域。</p>
<blockquote>
<strong>定义13:静态作用域:</strong>一种实现静态作用域的语言,仅仅通过查看源码就可以确定在哪个环境中解析绑定。</blockquote>
<p>静态作用域有时也被称为<em>词法作用域</em>,因此也就是<em>词法环境</em>的命名由来。</p>
<p>从技术上来说,静态作用域是通过捕获创建函数的环境来实现的。</p>
<blockquote>
<strong>注:</strong>您可以阅读<a href="https://link.segmentfault.com/?enc=IIharoqzka1LYQtaOw6pxQ%3D%3D.37Za0Oy4xVw8UGeiMtJaa0ypqM6Ok4UrqFz44jWSZ0hMS5dAYfOGdplLiE7hAxBh92rU0QKsboNbwmjafKH9mqsKJnXnU5v1QxYijTa3RGg%3D" rel="nofollow">链接文章</a>的了解静态和动态作用域。</blockquote>
<p>在我们的例子中,<code>foo</code> 函数捕获的环境是全局环境:</p>
<p><img src="/img/bV1K8N?w=1101&h=905" alt="Figure 8. A closure" title="Figure 8. A closure"></p>
<p>我们可以看到一个环境引用了一个函数,而这个函数又回引了环境。</p>
<blockquote>
<strong>定义14:闭包:</strong>闭包是<em>捕获定义环境</em>的函数。在将来此环境用于标识符解析。<p><strong>注:</strong>一个函数调用时是在全新的环境中激活,该环境存储<em>局部变量</em>和<em>参数</em>。激活环境的父环境被设置为函数的闭包环境,从而产生<em>词法作用域</em>语义。</p>
</blockquote>
<p>函参问题的第二个子类型被称为<strong><em>upward funarg problem</em></strong>。它们之间唯一的区别是捕捉环境的生命周期比创建它的环境更长。</p>
<p>我们看例子:</p>
<pre><code>function foo() {
let x = 10;
// Closure, capturing environment of `foo`.
function bar() {
return x;
}
// Upward funarg.
return bar;
}
let x = 20;
// Call to `foo` returns `bar` closure.
let bar = foo();
bar(); // 10, not 20!</code></pre>
<p>同样,从技术上来说,它与捕获定义环境的确切机制没有区别。只是这种情况下,如果没有闭包,<code>foo</code> 的激活环境就会被销毁。但是我们捕获了它,所以它不能被释放,并被保留 - 以支持静态作用域语义。</p>
<p>人们对闭包的理解通常是不完整的 - 开发人员通常考虑闭包仅仅依据 <em>upward funarg problem</em>(实际上是更合理)。但是,正如我们所看到的,<em>downward</em> 和 <em>upward funarg problem</em> 的技术机制是完全一样的 - 就是静态作用域的机制。</p>
<p>正如我们上面提到的,与原型类似,几个闭包可以共享相同的父环境。这允许它们访问和修改共享数据:</p>
<pre><code>function createCounter() {
let count = 0;
return {
increment() { count++; return count; },
decrement() { count--; return count; },
};
}
let counter = createCounter();
console.log(
counter.increment(), // 1
counter.decrement(), // 0
counter.increment(), // 1
);</code></pre>
<p>由于在包含 <code>count</code> 变量的作用域内创建了两个闭包:<code>increment</code> 和 <code>decrement</code> ,所以它们共享这个<em>父作用域</em>。也就是说,捕获总是“<em>通过引用</em>” 发生 - 意味着对整个父环境的引用被存储。</p>
<p><img src="/img/bV1PuQ?w=1794&h=773" alt="Figure 9. A shared environment" title="Figure 9. A shared environment"></p>
<p>有些语言可能捕获的是值,制作捕获的变量的副本,并且不允许在父作用域中更改它。但是,重复一遍,在 JS 中,它始终是对父范围的引用。</p>
<blockquote>
<strong>注:</strong>引擎的实现可能会优化这一步,而不会捕获整个环境。只捕获使用的自由变量,但它们仍然在父作用域中保持不变的可变数据。</blockquote>
<p>你可以在<a href="https://link.segmentfault.com/?enc=Ogd6GuKkOV5N%2BK%2B%2BgtFEIA%3D%3D.X%2BE0jA1TUtgtVmn%2Bt%2BQwXV2FlUZo56aByTns81AznZxBfBfQtS0OgmiFeU%2BuRJWDx3SB2u3j8N33zOMFk0gpfQ%3D%3D" rel="nofollow">链接文章</a>中找到有关闭包和函参问题的详细讨论。</p>
<p>所有的标识符都是静态的作用域。然而,在 ECMAScript 中有一个值是动态作用域的。那就是 <code>this</code> 的值。</p>
<h3>this</h3>
<p><code>this</code> 值是一个特殊的对象,它是动态地、隐式地传递给上下文中的代码。我们可以把它看作是一个隐含的额外参数,我们可以访问,但是不能修改。</p>
<p><code>this</code> 值的目的是为多个对象执行相同的代码。</p>
<blockquote>
<strong>定义15:this:</strong>一个隐式的上下文对象,可以从一个执行上下文的代码中访问 - 以便为多个对象执行相同的代码。</blockquote>
<p><code>this</code> 主要的用例是基于类的 OOP。一个实例方法(在原型上定义)存在于一个范例中,但在该类的所有实例中共享。</p>
<pre><code>class Point {
constructor(x, y) {
this._x = x;
this._y = y;
}
getX() {
return this._x;
}
getY() {
return this._y;
}
}
let p1 = new Point(1, 2);
let p2 = new Point(3, 4);
// Can access `getX`, and `getY` from
// both instances (they are passed as `this`).
console.log(
p1.getX(), // 1
p2.getX(), // 3
);</code></pre>
<p>当 <code>getX</code> 方法被激活时,会创建一个新的环境来存储局部变量和参数。另外,函数环境记录得到传递来的 <code>[[ThisValue]]</code> ,它是根据函数的调用方式动态绑定的。当用 <code>p1</code> 调用时,<code>this</code> 值恰好是 <code>p1</code> ,第二种情况下是 <code>p2</code> 。</p>
<p><code>this</code> 的另一个应用是<em>泛型接口函数</em>,它可以用在 <em>mixin</em> 或 <em>traits</em> 中。</p>
<p>在下面的例子中,<code>Movable</code> 接口包含泛型函数 <code>move</code> ,它期望这个 mixin 的用户实现 <code>_x</code> 和 <code>_y</code> 属性:</p>
<pre><code>// Generic Movable interface (mixin).
let Movable = {
/**
* This function is generic, and works with any
* object, which provides `_x`, and `_y` properties,
* regardless of the class of this object.
*/
move(x, y) {
this._x = x;
this._y = y;
},
};
let p1 = new Point(1, 2);
// Make `p1` movable.
Object.assign(p1, Movable);
// Can access `move` method.
p1.move(100, 200);
console.log(p1.getX()); // 100</code></pre>
<p>作为替代方案,mixin 也可以应用于原型级别,而不是像上例中每个实例做的那样。</p>
<p>为了展示 <code>this</code> 值的动态性,考虑下面例子,我们把这个例子留给读者来解决:</p>
<pre><code>function foo() {
return this;
}
let bar = {
foo,
baz() {
return this;
},
};
// `foo`
console.log(
foo(), // global or undefined
bar.foo(), // bar
(bar.foo)(), // bar
(bar.foo = bar.foo)(), // global
);
// `bar.baz`
console.log(bar.baz()); // bar
let savedBaz = bar.baz;
console.log(savedBaz()); // global</code></pre>
<p>因为只通过查看 <code>foo</code> 函数的源代码,我们不能知道它在特定的调用中 <code>this</code> 的值是什么,所以我们说 <code>this</code> 值是动态作用域。</p>
<blockquote>
<strong>注:</strong>您可以在<a href="https://link.segmentfault.com/?enc=0vylvqRkWUFS66cfZdsTOg%3D%3D.EyCKA59lH1t74xkmbE7KPtxK7n9rXKn4itV3T8t9%2BgSjUOh79UdUAnmqrG4Jn0eIt8qgiIGJ1JW8%2BPzwBZdZkQ%3D%3D" rel="nofollow">这篇文章</a>中得到关于如何确定 <code>this</code> 值的详细解释,以及为什么上面的代码是那样的结果。</blockquote>
<p><strong>箭头函数</strong>中 <code>this</code> 值比较特殊:其 <code>this</code> 是词法的(静态的),而不是动态的。即他们的函数环境记录不提供 <code>this</code> 值,它是从父环境中获取的。</p>
<pre><code>var x = 10;
let foo = {
x: 20,
// Dynamic `this`.
bar() {
return this.x;
},
// Lexical `this`.
baz: () => this.x,
qux() {
// Lexical this within the invocation.
let arrow = () => this.x;
return arrow();
},
};
console.log(
foo.bar(), // 20, from `foo`
foo.baz(), // 10, from global
foo.qux(), // 20, from `foo` and arrow
);</code></pre>
<p>就像我们所说的,在<em>全局上下文</em>,<code>this</code> 值是全局对象(<em>全局环境记录</em>的<em>绑定对象</em>)。以前只有一个全局对象。在当前版本的规范中,可能有多个全局对象,这是<em>代码领域(code realms)</em>的一部分。我们来讨论一下这个结构。</p>
<h3>领域</h3>
<p>在求值之前,所有 ECMAScript 代码都必须与一个领域相关联。从技术上来说,一个领域只是为一个上下文提供全局环境。</p>
<blockquote>
<strong>定义16:领域:</strong>代码领域是封装独立的全局环境的对象。</blockquote>
<p>当一个<em>执行上下文</em>被创建时,它与一个特定的代码领域相关联,这个代码领域为这个上下文提供了全局环境。该关联在未来将保持不变。</p>
<blockquote>
<strong>注:</strong>浏览器环境中的直接领域是 <code>iframe</code> 元素,正是它提供了一个自定义的全局环境。在 Node.js 中,它和<a href="https://link.segmentfault.com/?enc=mRJIGhl3UC8ZvJQEAeAJVw%3D%3D.TNufcX8sC%2F8d5GKfnX3BMefYxs%2BnS6Qk5kiVUZL60FQ%3D" rel="nofollow"> vm 模块</a>的沙箱类似。</blockquote>
<p>规范的当前版本没有提供显式创建领域的能力,但是它们可以由实现隐含地创建。不过有一个将这个API暴露给用户代码的<a href="https://link.segmentfault.com/?enc=14eB7SfnKR97BJgq4exlqQ%3D%3D.gP5JubTzB1xTkM70mozb4o6eU6E9b3YGgoVuRUpc8EPCiWglYEy%2FZsTsnZaWN3W0" rel="nofollow">提案</a>。</p>
<p>从逻辑上来说,堆栈中的每个上下文总是与其领域相关联:</p>
<p><img src="/img/bV1TSi?w=1160&h=571" alt="Figure 10. A context and realm association" title="Figure 10. A context and realm association"></p>
<p>现在我们正在接近 ECMAScript 运行时的全貌了。然而,我们仍然需要看到代码的入口点和初始化过程。这是由 <em>jobs(作业)</em> 和 <em>job queues(作业队列)</em> 机制管理的。</p>
<h3>Job</h3>
<p>有一些操作可以被推迟的,并在执行上下文堆栈上有可用点时立即执行。</p>
<blockquote>
<strong>定义17:Job:</strong> Job 是一个抽象操作,当没有其他 ECMAScript 计算正在进行时,该操作启动 ECMAScript 计算。</blockquote>
<p>Job 在 <strong><em>作业队列</em></strong> 中排队,在当前的规范版本中有两个作业队列 <strong><em>ScriptJobs</em></strong> 和 <strong><em>PromiseJobs</em></strong>。</p>
<p><em>ScriptJobs</em> 队列中的初始 job 是我们程序的主要入口 - 初始化已加载且求值的脚本:创建一个领域,创建一个全局上下文,并且与这个领域相关联,它被推入堆栈,全局代码被执行。</p>
<p>需要注意的是,<em>ScriptJobs</em> 队列管理着脚本和模块两者。</p>
<p>此外,这个上下文可以执行其他上下文,或使其他 jobs 到队列中排队。一个可以产生排队的 job 就是 promise。</p>
<p>如果没有正在运行的执行上下文,并且执行上下文堆栈为空,则 ECMAScript 实现会从作业队列中移除第一个 job,创建执行上下文并开始执行。</p>
<blockquote>
<strong>注:</strong>作业队列通常由被称为<strong><em>“事件循环”</em></strong>的抽象来处理。 <br> ECMAScript 标准没有指定事件循环,而是将其留给实现决定,但是你可以在<a href="https://gist.github.com/DmitrySoshnikov/26e54990e7df8c3ae7e6e149c87883e4">链接页面</a>找到一个教学示例。</blockquote>
<p>示例:</p>
<pre><code>// Enqueue a new promise on the PromiseJobs queue.
new Promise(resolve => setTimeout(() => resolve(10), 0))
.then(value => console.log(value));
// This log is executed earlier, since it's still a
// running context, and job cannot start executing first
console.log(20);
// Output: 20, 10</code></pre>
<blockquote>
<strong>注:</strong>你可以在<a href="https://link.segmentfault.com/?enc=AHLnZ4NRC7SVWDK808W8FQ%3D%3D.YgYSPyTQ4k16M%2B7NNGT%2BCSd8oYpB17K3DC5J1DkzfuFfOcF7LCQ0MdFLY66pMoTpCqdtG9HAG43SwwVX42om9fHGoU67tChsk%2BkpGysQm3T3mBVx521ObtTp3BWNsstX" rel="nofollow">链接文档</a>中阅读有关 promise 的更多信息。</blockquote>
<p><strong><em>async 函数</em></strong>可以等待(await) promise 执行,所以它们也使 promise 作业排队:</p>
<pre><code>async function later() {
return await Promise.resolve(10);
}
(async () => {
let data = await later();
console.log(data); // 10
})();
// Also happens earlier, since async execution
// is queued on the PromiseJobs queue.
console.log(20);
// Output: 20, 10</code></pre>
<blockquote>
<strong>注:</strong>更多 async 函数内容请<a href="https://link.segmentfault.com/?enc=1WxDXW%2BmvyFuGts78I9VTQ%3D%3D.622zE63owyNZViqNYrBbw617dj0g0uzIeonNitaU2yzWk1F8wzCtL%2B%2F0woIJnfkp9Cb9KS%2FajhwFzAD1Te40xVBsdFuF2a5TvBxvHdRrA%2Bsvw4k4009Lk%2B3%2Fo8RsiP5B" rel="nofollow">点击链接</a>。</blockquote>
<p>现在我们已经非常接近当前 JS 宇宙的最终画面。马上我们将看到我们讨论的所有组件的主要所有者 - 代理商(Agents)。</p>
<h3>Agent</h3>
<p>ECMAScript中的<em>并发(concurrency)</em>和<em>并行(parallelism)</em>是使用<em>代理模式(Agent pattern)</em>的实现的。代理模式非常接近<a href="https://link.segmentfault.com/?enc=l9gf8FfmLrLNGP2l2OsUmw%3D%3D.82QlL4%2BSFerhwYoVH2dR3q3C5w3bb4LxzZ9cfoqhv5p4IFv70HyXSynpZZaxOnuS" rel="nofollow">参与者模式(Actor pattern)</a> - 一个具有消息传递风格的轻量级进程。</p>
<blockquote>
<strong>定义18:Agent:</strong>代理是封装执行上下文堆栈、作业队列集和代码领域的抽象概念。</blockquote>
<p>依赖代理的实现可以在同一个线程上运行,也可以在单独的线程上运行。浏览器环境中的 <code>Worker</code> 代理是代理概念的一个例子。</p>
<p>代理的状态是相互隔离的,可以通过发送消息进行通信。一些数据可以在代理之间共享,例如 <code>SharedArrayBuffer</code> 。代理也可以组合成<em>代理集群</em>。</p>
<p>在下面的例子中,<code>index.html</code> 调用 <code>agent-smith.js</code> worker ,传递共享的内存块:</p>
<pre><code>// In the `index.html`:
// Shared data between this agent, and another worker.
let sharedHeap = new SharedArrayBuffer(16);
// Our view of the data.
let heapArray = new Int32Array(sharedHeap);
// Create a new agent (worker).
let agentSmith = new Worker('agent-smith.js');
agentSmith.onmessage = (message) => {
// Agent sends the index of the data it modified.
let modifiedIndex = message.data;
// Check the data is modified:
console.log(heapArray[modifiedIndex]); // 100
};
// Send the shared data to the agent.
agentSmith.postMessage(sharedHeap);</code></pre>
<p>worker 的代码如下:</p>
<pre><code>// agent-smith.js
/**
* Receive shared array buffer in this worker.
*/
onmessage = (message) => {
// Worker's view of the shared data.
let heapArray = new Int32Array(message.data);
let indexToModify = 1;
heapArray[indexToModify] = 100;
// Send the index as a message back.
postMessage(indexToModify);
};</code></pre>
<p>你可以在<a href="https://gist.github.com/DmitrySoshnikov/b75a2dbcdb60b18fd9f05b595135dc82">链接页面</a>得到示例的完整代码。</p>
<p>(需要注意的是,如果你在本地运行这个例子,请在 Firefox 中运行它,因为由于安全原因,Chrome 不允许从本地文件加载 web worker)</p>
<p>下图展示了 ECMAScript 运行时:</p>
<p><img src="/img/bV1Yxp?w=1802&h=995" alt="Figure 11. ECMAScript runtime" title="Figure 11. ECMAScript runtime"></p>
<p>如图所示,那就是在 ECMAScript 引擎下发生的事情!</p>
<p>现在文章到了结尾的时候。这是我们可以在概述文章中涵盖的 JS 核心的信息量。就像我们提到的,JS 代码可以被分组成模块,对象的属性可以被 <code>Proxy</code> 对象追踪等等。 - 许多用户级别的细节可以在 JavaScript 语言的不同文档中找到。</p>
<p>尽管我们试图表示一个 ECMAScript 程序本身的逻辑结构,希望能够澄清这些细节。如果你有任何问题,建议或反馈意见,我将一如既往地乐于在评论中讨论这些问题。</p>
<p>我要感谢 TC-39 的代表和规范编辑帮助澄清本文。该讨论可以在这个 <a href="https://link.segmentfault.com/?enc=RbsVq5q0c%2Bvops0t%2FDDQgA%3D%3D.7ypqplaFky4QeSAKCpuVy8xAsakKdHU8TubwHPFk0n2mC6dK0eJ1rH%2F5nioNEm1Trc%2F9wf8U9KcuaMwQWsI2hQ%3D%3D" rel="nofollow">Twitter 主题</a>中找到。</p>
<p>祝学习 ECMAScript 好运!</p>
<p><strong>Written by:</strong> Dmitry Soshnikov<br><strong>Published on:</strong> November 14th, 2017</p>
【译】Redux 还是 Mobx,让我来解决你的困惑!
https://segmentfault.com/a/1190000011148981
2017-09-13T15:27:56+08:00
2017-09-13T15:27:56+08:00
hugo_seth
https://segmentfault.com/u/hugo_seth
69
<blockquote>
<p>原文地址:<a href="https://link.segmentfault.com/?enc=9E8A6r5Sz3%2FTrn4lYeQMlQ%3D%3D.EJjasvocTqo16KwKnFXwMSvWAtN0im2uS3N3WrEtzrxz8%2FS%2F74azwZ0aDwB4z6PE2Y8VhvHnblX%2F3AsFO%2FVgLg%3D%3D" rel="nofollow">Redux or MobX: An attempt to dissolve the Confusion</a></p>
<p>原文作者:<a href="https://link.segmentfault.com/?enc=jx8KUyD5sl%2B7VWxIDRMRLw%3D%3D.YPI8rC4PC8R8FGRXt0ujuyWovcHR%2BvkBqDAPEGNgA08%3D" rel="nofollow">rwieruch</a></p>
</blockquote>
<p>我在去年大量的使用了 <a href="https://link.segmentfault.com/?enc=815hVK%2BEUHtQVSc3uicf4g%3D%3D.Y62TeEMiRpylf9kLdkYABuibvanGxcVLRHnniIN7BpUrQj3H7WRH07s4a%2BabqkGV" rel="nofollow">Redux</a>,但我最近都在使用 <a href="https://link.segmentfault.com/?enc=FrhKhgu2%2Fp8sMhK7w0bJdQ%3D%3D.Dbk%2FFdMHjIj0lFkh5sXoLSc%2FND9sRnSH8wPKUJCTV2k%3D" rel="nofollow">Mobx</a> 来做状态(state)管理。<a href="https://link.segmentfault.com/?enc=R1EDL8M0ZDN%2BL%2FE6jgOVWw%3D%3D.X1eDa1EN2KJz1By4cFJORcKvlTPoOn6c6L4atBR7W3ztiapxeJObiCvq2XnPkKiRDf%2B5iYgjvRR1Kx%2Br%2BkkNxZnUeeHGdAuMTnaq6N6HvM8%3D" rel="nofollow">似乎现在社区里关于该选什么来替代 Redux 很自然地成为了一件困惑的事</a>。开发者不确定该选择哪种解决方案。这个问题并不只是出现在 Redux 与 Mobx 上。无论何时,只要存在选择,人们就会好奇最好的解决问题的方式是什么。我现在写的这些是为了解决 Redux 和 Mobx 这两个状态管理库之间的困惑。</p>
<p>大部分的文章都用 React 来介绍 Mobx 和 Redux 的用法。但是在大部分情况下你都可以将 React 替换成 Angular 、 Vue 或其他。</p>
<p>在 2016 年年初的时候我用 <a href="https://link.segmentfault.com/?enc=tEtXWJj06yVdI8mqDIoUoQ%3D%3D.KVh5CXdIGxVjNuIZ6uBadPX1dXae%2FY%2B6yj5LpoWhd2qK1f9pN5jjpwUkWuOkIX7q" rel="nofollow">React + Redux</a> 写了一个相当大的应用。在我发现可以使用 Mobx 替代 Redux 时,我花时间将应用从 Redux 重构成了 Mobx 。现在我可以非常自在的使用它俩并且解释它俩的用法。</p>
<p>这篇文章将要讲什么呢?如果你不打算看这么长的文章(TLDR:<a href="https://link.segmentfault.com/?enc=8pLfmbHhytfI%2FL4kAtQzOg%3D%3D.LlWE2ov8X2eXbjOanImnDI9YvY3xkQHvxWzcOcBUH7QJ7HJ9eWkyoSysV4%2B%2FUhjJ" rel="nofollow">too long, didn't read(查看此链接请自备梯子)</a>),你可以看下目录。但我想给你更多细节:第一,我想简单地回顾状态管理库为我们解决了什么问题。毕竟我们写 React 时只用 <code>setState()</code> 或写其他 SPA 框架时用 <code>setState()</code> 类似的方法一样也可以做的不错。第二,我会大致的说下它们之间的相同之处和不同之处。第三,我会给 React 生态初学者指明怎样学习 React 的状态管理。友情提醒:在你深入 Mobx 和 Redux 之前,请先使用 <code>setState()</code> 。最后,如果你已经有一个使用了 Mobx 或 Redux 的应用,我将会就如何从其中一个状态管理库重构到另一个给你更多我的理解。</p>
<hr>
<h2>目录</h2>
<ul>
<li>我们要解决的是什么问题?</li>
<li>Mobx 和 Redux 的不同?</li>
<li>React 状态管理的学习曲线</li>
<li>尝试另一个状态管理方案?</li>
<li>最后思考</li>
</ul>
<h3>我们要解决的是什么问题?</h3>
<p>所有人都想在应用中使用状态管理。但它为我们解决了什么问题?很多人开始一个小应用时就已经引入一个状态管理库。所有人都在谈论 Mobx 和 Redux ,不是吗?但大部分应用在一开始的时候<a href="https://link.segmentfault.com/?enc=jZTwef7w8PBHPzhs31FoEw%3D%3D.plj%2FjUywOi8EW%2FDVsZlzVpasd2CmIVydDKOCxYf2zcVhNNZ3fHIGXGR2ux2INchv4yMyE9OQa4fGZrib%2BZb8FxgH%2FZ1qa51NvHxRa5PtZ3A%3D" rel="nofollow">并不需要大型的状态管理</a>。这甚至是危险的,因为这部分人将无法体验 Mobx 和 Redux 这些库所要解决的问题。</p>
<p>如今的现状是要用组件(components)来构建一个前端应用。组件有自己的内部状态。举个栗子,在 React 中上述的本地状态是用<code>this.state</code>和<code>setState()</code>来处理。但本地状态的状态管理在膨胀的应用中很快会变得混乱,因为:</p>
<ul>
<li>一个组件需要和另一个组件共享状态</li>
<li>一个组件需要改变另一个组件的状态</li>
</ul>
<p>到一定程度时,推算应用的状态将会变得越来越困难。它就会变成一个有很多状态对象并且在组件层级上互相修改状态的混乱应用。在大部分情况下,状态对象和状态的修改并没有必要绑定在一些组件上。<a href="https://link.segmentfault.com/?enc=PTOGXIO9hV3gYcoyJUFAeA%3D%3D.RKCLvUavU2FWoR3tyJksy1Gk%2FaUpHLk89TIxGP%2FqqHaqbbshbcV9yZGkj4NthlwVyljSAdFw2zY%2BXRyYdfXdbA%3D%3D" rel="nofollow">当你把状态提升时,它们可以通过组件树得到</a>。</p>
<p>所以,解决方案是引入状态管理库,比如:Mobx 或 Redux。它提供工具在某个地方保存状态、修改状态和更新状态。你可以从一个地方获得状态,一个地方修改它,一个地方得到它的更新。它遵循单一数据源的原则。这让我们更容易推断状态的值和状态的修改,因为它们与我们的组件是解耦的。</p>
<p>像 Redux 和 Mobx 这类状态管理库一般都有附带的工具,例如在 React 中使用的有 <a href="https://link.segmentfault.com/?enc=Bp2ktvNV3yTCON9%2BqEp3Ew%3D%3D.LQEWOW70gmfin9X2LhKsMKjbG6bj4KdAW3euv6PdtESbytf33iPKN0mElBCtuihR" rel="nofollow">react-redux</a> 和 <a href="https://link.segmentfault.com/?enc=NTPEwo4m4SFm%2BRgXdMze5g%3D%3D.21d%2BFd%2B7ByT43TRn5T4KaJlt5DUhgIPQ0D%2FM8%2BpwQ9Vckjew44TwtY0Hza0SWyxi" rel="nofollow">mobx-react</a>,它们使你的组件能够获得状态。一般情况下,这些组件被叫做容器组件(container components),或者说的更加确切的话,就是连接组件( connected components )。只要你将组件升级成连接组件,你就可以在组件层级的任何地方得到和更改状态。</p>
<h3>Mobx 和 Redux 的不同?</h3>
<p>在我们深入了解 Redux 和 Mobx 的不同之前,我想先谈谈它们之间的相同之处。</p>
<p>这两个库都是用来管理 JavaScript 应用的状态。它们并不一定要跟 React 绑定在一起,它们也可以在 AngularJs 和 VueJs 这些其他库里使用。但它们与<a href="https://link.segmentfault.com/?enc=cQ22pIA82mi1gG8Bt69bew%3D%3D.CUQSVj4fMKuRhSUfP0cJcZhSwpmIeZstjCjb1sFkrHgK7%2Bx1WJD3xThtH3PX%2BeIQLlIGQn2oxbNRL3Rx6oq9POyJXbK1HhlujrDYWsag%2FeE%3D" rel="nofollow"> React 的理念</a>结合得非常好。</p>
<p>如果你选择了其中一个状态管理方案,你不会感到被它锁定了。因为你可以在任何时候切换到另一个解决方案。你可以从 Mobx 换成 Redux 或从 Redux 换成 Mobx。我下面会展示如何能够做到。</p>
<p><a href="https://link.segmentfault.com/?enc=QDSBv4hWzEAp%2BWPANjJLbw%3D%3D.6S9SzyRiYMiSlLP0Brz6m3OTbq5wiYP3foGXjuofyKk%3D" rel="nofollow">Dan Abramov</a> 的 Redux 是从 <a href="https://link.segmentfault.com/?enc=aOQKhKRi9neAec%2F2kr10vw%3D%3D.5rcDbiyhrd3s2ggdhT2sQAD%2FfSJ16BJmJwd8bOMVVb%2FBqB0nYWgXagu9RXnfCvcNdCF7vaiAP3UxYuBgRlLJ%2BQ%3D%3D" rel="nofollow">flux 架构</a>派生出来的。和 flux 不同的是,Redux 用单一 store 而不是多个 store 来保存 state,另外,它用纯函数替代 dispatcher 来修改 state,如果你对 flux 不熟并且没接触过状态管理,不要被这段内容所烦恼。</p>
<p>Redux 被 FP(函数式编程)原则所影响。FP 可以在 JavaScript 中使用,但很多人有面向对象语言的背景,比如 Java。他们在刚开始的时候很难适应函数式编程的原则。这就是为什么对于初学者来说 Mobx 可能更加简单。</p>
<p>既然 Redux 拥抱 FP,那它使用的就是纯函数。一个接受输入并返回输出并且没有其他依赖的纯函数。一个纯函数在相同的输入下输出总是相同而且没有任何副作用。</p>
<pre><code>(state, action) => newState</code></pre>
<p>你的 Redux state 是不可变的,你应该总是返回一个新的 state 而不是修改原 state。你不应该执行 state 的修改或依据对象引用的更改。</p>
<pre><code>// don't do this in Redux, because it mutates the array
function addAuthor(state, action) {
return state.authors.push(action.author);
}
// stay immutable and always return a new object
function addAuthor(state, action) {
return [ ...state.authors, action.author ];
}</code></pre>
<p>最后,在 Redux 的习惯用法里,state 的格式是像数据库一样标准化的。实体之间只靠 id 互相引用,这是最佳实践。虽然不是每个人都这样做,你也可以使用 <a href="https://link.segmentfault.com/?enc=eREDsGnYlSwculeIff%2F5ug%3D%3D.KSEWwJ5uEPEgoe0zBAvnnFmK9soHOtFSTLMPEj%2F297aStNYOOK%2Fj4djfiGyb9MHA" rel="nofollow">normalizr</a> 来使 state 标准化。标准化的 state 让你能够保持一个扁平的 state 和保持实体为单一数据源。</p>
<pre><code>{
post: {
id: 'a',
authorId: 'b',
...
},
author: {
id: 'b',
postIds: ['a', ...],
...
}
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=IVOLtRA6Pc4E1m9gl%2F2CUg%3D%3D.gEFKMwz5aOTG%2Fk2b%2Bq9lZ%2Bosd6t7ihOJe39NFSm2jXc%3D" rel="nofollow">Michel Weststrate</a> 的 Mobx 则是受到面向对象编程和响应式编程的影响。它将 state 包装成可观察的对象,因此你的 state 就有了 Observable 的所有能力。state 数据可以只有普通的 setter 和 getter,但 observable 让我们能在数据改变的时候得到更新的值。</p>
<p>Mobx 的 state 是可变的,所以你直接的修改 state :</p>
<pre><code>function addAuthor(author) {
this.authors.push(author);
}</code></pre>
<p>除此之外,state 实体保持嵌套的数据结构来互相关联。你不必标准化 state,而是让它们保持嵌套。</p>
<pre><code>{
post: {
id: 'a',
...
author: {
id: 'b',
...
}
}
}</code></pre>
<h4>单 store 与多 stores</h4>
<p>在 Redux 中,你将所有的 state 都放在一个全局的 store。这个 store 对象就是你的单一数据源。另一方面,多个 reducers 允许你修改不可变的 state。</p>
<p>Mobx 则相反,它使用多 stores。和 Redux 的 reducers 类似,你可以在技术层面或领域进行分治。也许你想在不同的 stores 里保存你的领域实体,但仍然保持对视图中 state 的控制。毕竟你配置 state 是为了让应用看起来更合理。</p>
<p>从技术层面来说,你一样可以在 Redux 中使用多个 stores。没有人强迫你只能只用一个 store。 但那不是 Redux 建议的用法。因为那违反了最佳实践。在 Redux 中,你的单 store 通过 reducers 的全局事件来响应更新。</p>
<h4>如何使用?</h4>
<p>你需要跟随下面的代码学习使用 Redux,首先在全局 state 上新增一个 user 数组。你可以看到我通过<a href="https://link.segmentfault.com/?enc=LY5cgKT5DLWgxUNEXp2c4w%3D%3D.coc%2FcclsCvipwuVots48C2bX3SSqb6kYbWU3aykP1008ianU%2Blywg2pFE5pS8Q%2FABbN58DPMkYnsRoUuBQQWhg%3D%3D" rel="nofollow">对象扩展运算符</a>来返回一个新对象。你同样可以在 ES6(原文为 ES5,实际是应该是 ES6)中使用 <code>Object.assign()</code> 来操作不可变对象。</p>
<pre><code>const initialState = {
users: [
{
name: 'Dan'
},
{
name: 'Michel'
}
]
};
// reducer
function users(state = initialState, action) {
switch (action.type) {
case 'USER_ADD':
return { ...state, users: [ ...state.users, action.user ] };
default:
return state;
}
}
// action
{ type: 'USER_ADD', user: user };</code></pre>
<p>你必须使用 <code>dispatch({ type: 'USER_ADD', user: user });</code>来为全局 state 添加一个新 user 。</p>
<p>在 Mobx 中,一个 store 只管理一个子 state(就像 Redux 中管理子 state 的 reducer),但你可以直接修改 state 。<code>@observable</code> 让我们可以观察到 state 的变化。</p>
<pre><code>class UserStore {
@observable users = [
{
name: 'Dan'
},
{
name: 'Michel'
}
];
}</code></pre>
<p>现在我们就可以调用 store 实例的方法:<code>userStore.users.push(user);</code>。这是一种最佳实践,虽然使用 actions 去操作 state 的修改更加清楚明确。</p>
<pre><code>class UserStore {
@observable users = [
{
name: 'Dan'
},
{
name: 'Michel'
}
];
@action addUser = (user) => {
this.users.push(user);
}
}</code></pre>
<p>在 Mobx 中你可以加上 <code>useStrict()</code> 来强制使用 action。现在你可以调用 store 实例上的方法:<code>userStore.addUser(user);</code> 来修改你的 state 。</p>
<p>你已经看到如何在 Redux 和 Mobx 中更新 state 。它们是不同的,Redux 中 state 是只读的,你只能使用明确的 actions 来修改 state ,Mobx 则相反,state 是可读和写的,你可以不使用 actions 直接修改 state,但你可以 <code>useStrict()</code> 来使用明确的 actions 。</p>
<h3>React 状态管理的学习曲线</h3>
<p>React 应用广泛使用 Redux 和 Mobx 。但它们是独立的状态管理库,可以运用在除 React 的任何地方。它们的互操作库让我们能简单的连接React 组件。Redux + React 的 <a href="https://link.segmentfault.com/?enc=PgmKPK3gwMpMAO%2BQLygm6Q%3D%3D.uU1NFsVEt7rGHZMqAnSEgelTOXz2zdaxLTw%2FzmyPbylMft5oquoLwDtY7cOj6tAw" rel="nofollow">react-redux</a> 和 MobX + React 的 <a href="https://link.segmentfault.com/?enc=FegJuosyr24rI6sQY5ceGA%3D%3D.o9ZAVw%2BrmAre0mbVlBMnBNFLVYo%2F2%2FWgXMofybaOxpy6jT34t0WPhIAbUD9nfNXI" rel="nofollow">mobx-react</a> 。稍后我会说明它俩如何在 React 组件树中使用。</p>
<p>在最近的讨论中,人们在争论 Redux 的学习曲线。这通常发生在下面的情境中:想使用 Redux 做状态管理的 React 初学者。大部分人认为 React 和 Redux 本身都有颇高的学习曲线,两者结合的话会失控。一个替代的选择就是 Mobx ,因为它更适合初学者。</p>
<p>然而,我会建议 React 的初学者一个学习状态管理的新方法。先学习<br> React 组件内部的状态管理功能。在 React 应用,你首先会学到生命周期方法,而且你会用 <code>setState()</code> 和 <code>this.state</code> 解决本地的状态管理。我非常推荐上面的学习路径。不然你会在 React 的生态中迷失。在这条学习路径的最后,你会认识到组件内部管理状态难度越来越大。毕竟那是 <a href="https://link.segmentfault.com/?enc=bl5bty%2F8pNFINsaw7BZgwA%3D%3D.l7uh7dbcrMFbVcdPfuLwKfEhGN5NLYllhEKggvUK32M6ldCwKvMELspRbLZN%2BfA7tViOSHTLRKlY%2BAmJgrMhHg%3D%3D" rel="nofollow">The Road to learn React</a> 书里如何教授 React 状态管理的方法。</p>
<p>现在我们重点讨论 Redux 和 Mobx 为我们解决了什么问题?它俩都提供了在组件外部管理应用状态的方法。state 与组件相互解耦,组件可以读取 state ,修改 state ,有新 state 时更新。这个 state 是单一数据源。</p>
<p>现在你需要选择其中一个状态管理库。这肯定是要第一时间解决的问题。此外,在开发过相当大的应用之后,你应该能很自如使用 React 。</p>
<h4>初学者用 Redux 还是 Mobx ?</h4>
<p>一旦你对 React 组件和它内部的状态管理熟悉了,你就能选择出一个状态管理库来解决你的问题。在我两个库都用过后,我想说 Mobx 更适合初学者。我们刚才已经看到 Mobx 只要更少的代码,甚至它可以用一些我们现在还不知道的魔法注解。</p>
<p>用 Mobx 你不需要熟悉函数式编程。像“不可变”之类的术语对你可能依然陌生。函数式编程是不断上升的范式,但对于大部分 JavaScript 开发者来说是新奇的。虽然它有清晰的趋势,但并非所有人都有函数式编程的背景,有面向对象背景的开发者可能会更加容易适应 Mobx 的原则。</p>
<blockquote><p>注:<a href="https://link.segmentfault.com/?enc=8dcIhvyNIpShzJXfTEX21A%3D%3D.zL8q7wdxTvXoFzucOxc1ilzWf3vJEd6l0LIBMyK8I0IRti8YGGLPpIfK7eGUKNruqfH5pPms1%2BiNV%2BYXA7QhhY1APSq5vlKZv3K9e33qe%2BJNGsIsqumWyb6UJZcQLEMG" rel="nofollow">Mobx 可以很好的在 React 内部组件状态管理中代替 setState</a>,我还是建议继续使用 <code>setState()</code> 管理内部状态。但链接文章很清楚的说明了在 React 中用 Mobx 完成内部状态管理是很容易的。</p></blockquote>
<h4>规模持续增长的应用</h4>
<p>在 Mobx 中你改变注解过的对象,组件就会更新。Mobx 比 Redux 使用了更多的内部魔法实现,因此在刚开始的时候只要更少的代码。有 Angular 背景的会觉得跟双向绑定很像。你在一个地方保存 state ,通过注解观察 state ,一旦 state 修改组件会自动的更新。</p>
<p>Mobx 允许直接在组件树上直接修改 state 。</p>
<pre><code>// component
<button onClick={() => store.users.push(user)} /></code></pre>
<p>更好的方式是用 store 的 <code>@action</code> 。</p>
<pre><code>// component
<button onClick={() => store.addUser(user)} />
// store
@action addUser = (user) => {
this.users.push(user);
}</code></pre>
<p>用 actions 修改 state 更加明确。上面也提到过,有个小功能可以强制的使用 actions 修改 state 。</p>
<pre><code>// root file
import { useStrict } from 'mobx';
useStrict(true);</code></pre>
<p>这样的话第一个例子中直接修改 store 中的 state 就不再起作用了。前面的例子展示了怎样拥抱 Mobx 的最佳实践。此外,一旦你只用 actions ,你就已经使用了 Redux 的约束。</p>
<p>在快速启动一个项目时,我会推荐使用 Mobx ,一旦应用开始变得越来越大,越来越多的人开发时,遵循最佳实践就很有意义,如使用明确的 actions 。这是拥抱 Redux 的约束:你永远不能直接修改 state ,只能使用 actions 。</p>
<h4>迁移到 Redux</h4>
<p>一旦应用开始变得越来越大,越来越多的人开发时,你应该考虑使用 Redux 。它本身强制使用明确的 actions 修改 state 。action 有 type 和 payload 参数,reducer 可以用来修改 state 。这样的话,一个团队里的开发人员可以很简单的推断 state 的修改。</p>
<pre><code>// reducer
(state, action) => newState</code></pre>
<p>Redux 提供状态管理的整个架构,并有清晰的约束规则。这是<a href="https://link.segmentfault.com/?enc=pEcM7wkZJioYExF41PfnAg%3D%3D.CfRZR3pWMcBZb%2BKJjANQNPmqMi9MAEhuuaOsY6zl2%2BmrX8SADSB%2BDs1dQwwK5bXt" rel="nofollow"> Redux 的成功故事</a>。</p>
<p>另一个 Redux 的优势是在服务端使用。因为我们使用的是纯 JavaScript ,它可以在网络上传输 state 。序列化和反序列化一个 state 对象是直接可用的。当然 Mobx 也是一样可以的。</p>
<p>Mobx 是无主张的,但你可以通过 <code>useStrict()</code> 像 Redux 一样使用清晰的约束规则。这就是我为什么没说你不能在扩张的应用中使用 Mobx ,但 Redux 是有明确的使用方式的。而 Mobx 甚至在文档中说:“ Mobx 不会告诉你如何组织代码,哪里该存储 state 或 怎么处理事件。”所以开发团队首先要确定 state 的管理架构。</p>
<p>状态管理的学习曲线并不是很陡峭。我们总结下建议:React 初学者首先学习恰当的使用 <code>setState()</code> 和 <code>this.state</code> 。一段时间之后你将会意识到在 React 应用中仅仅使用 <code>setState()</code> 管理状态的问题。当你寻找解决方案时,你会在状态管理库 Mobx 或 Redux 的选择上犹豫。应该选哪个呢?由于 Mobx 是无主张的,使用上可以和 <code>setState()</code> 类似,我建议在小项目中尝试。一旦应用开始变得越来越大,越来越多的人开发时,你应该考虑在 Mobx 上实行更多的限制条件或尝试使用 Redux 。我使用两个库都很享受。即使你最后两个都没使用,了解到状态管理的另一种方式也是有意义的。</p>
<h3>尝试另一个状态管理方案?</h3>
<p>你可能已经使用了其中一个状态管理方案,但是想考虑另一个?你可以比较现实中的 <a href="https://link.segmentfault.com/?enc=wodNN%2Blm%2FGLHLrBlBZLF6Q%3D%3D.bUa39w7KIxc59lfEgM2eUKTSCRHV4cvrPoBQHxGkemCAyfZY9myujMbv36upw%2BZV" rel="nofollow">Mobx</a> 和 <a href="https://link.segmentfault.com/?enc=ftORoWU7r3vS4pYzHQjBDA%3D%3D.3VA7yNvCmFGDgWfbKtjF2Go%2FkgXM9tIsG2iN2Ka1gY34pWwZxbkCjaOltmVuerc3" rel="nofollow">Redux</a> 应用。我把所有的文件修改都提交到了一个 <a href="https://link.segmentfault.com/?enc=DrBwUT02Ds4l2x9OMfPk6w%3D%3D.7C3ghlBDQbL24FHeFCh0DyPxZmu7c54T7UX4%2BeCy8NpP3vhR3k953n7SCjIvzwcM%2FjmZR2m4XOwwElQLBl3Y6g%3D%3D" rel="nofollow">Pull Request</a> 。在这个 PR 里,项目从 Redux 重构成了 Mobx ,反之亦然,你可以自己实现。我不认为有必要和 Redux 或 Mobx 耦合,因为大部分的改变是和其他任何东西解耦的。</p>
<p>你主要需要将 Redux 的 Actions、Action Creator、 Action Types、Reducer、Global Store 替换成 Mobx 的 Stores 。另外将和 React 组件连接的接口 <a href="https://link.segmentfault.com/?enc=HYDEsOwhTjl3Kw2cPHCcyQ%3D%3D.P%2B5zihaOlfiUVoA%2B5lsKJhoZXKudXs%2BdMbxLph5EvBd%2FFxDypYI8CqcMP9jNoj6M" rel="nofollow">react-redux</a> 换成 <a href="https://link.segmentfault.com/?enc=Ukp06mTE1cdcYx%2B25OUxpg%3D%3D.nQOIfPlXRNc7yx6wPuE0xVS04cK8YmU7WBHhSlCWdea83M0%2F6x5q7bClERaY77OJ" rel="nofollow">mobx-react</a> 。<a href="https://link.segmentfault.com/?enc=zmJGUCDmTtI%2B9JpGrPACQw%3D%3D.GJNbWMrDu7B9x1432BTP%2FroEzsSl8e%2BaN3QVaSaxSUlTl70pKDYvEagiCABRduUQ5RsyGMXqr4YJAd1EHp6KiEQeY%2F57aE4zFDPHAAX0rvs%3D" rel="nofollow">presenter + container pattern</a> 依然可以执行。你仅仅还要重构容器组件。在 Mobx 中可以使用 <code>inject</code> 获得 store 依赖。然后 store 可以传递 substate 和 actions 给组件。Mobx 的 <code>observer</code> 确保组件在 store 中 <code>observable</code> 的属性变化时更新。</p>
<pre><code>import { observer, inject } from 'mobx-react';
...
const UserProfileContainer = inject(
'userStore'
)(observer(({
id,
userStore,
}) => {
return (
<UserProfile
user={userStore.getUser(id)}
onUpdateUser={userStore.updateUser}
/>
);
}));</code></pre>
<p>Redux 的话,你使用 <code>mapStateToProps</code> 和 <code>mapDispatchToProps</code> 传递 substate 和 actions 给组件。</p>
<pre><code>import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
...
function mapStateToProps(state, props) {
const { id } = props;
const user = state.users[id];
return {
user,
};
}
function mapDispatchToProps(dispatch) {
return {
onUpdateUser: bindActionCreators(actions.updateUser, dispatch),
};
}
const UserProfileContainer = connect(mapStateToProps, mapDispatchToProps)(UserProfile);</code></pre>
<p>这有一篇<a href="https://link.segmentfault.com/?enc=eHdVxbzIu%2BTD3TLYIamvWQ%3D%3D.%2FpSRJ8K44kxyY6bn%2B6xVXluo9cZnimLgo2BCirYZyMDLTkUOncE7uiMC4ukidpA8" rel="nofollow">怎样将 Redux 重构为 Mobx</a>指南。但就像我上面说过的,反过来一样也是可以的。一旦你选择了一个状态管理库,你会知道那并没有什么限制。它们基本上是和你的应用解耦的,所以是可以替换的。</p>
<h3>最后思考</h3>
<p>每当我看 Redux vs Mobx 争论下的评论时,总会有下面这条:“Redux 有太多的样板代码,你应该使用 Mobx,可以减少 xxx 行代码”。这条评论也许是对的,但没人考虑得失,<strong>Redux 比 Mobx 更多的样板代码,是因为特定的设计约束</strong>。它允许你推断应用状态即使应用规模很大。所以围绕 state 的仪式都是有原因的。</p>
<p>Redux 库非常小,大部分时间你都是在处理纯 JavaScript 对象和数组。它比 Mobx 更接近 vanilla JavaScript 。Mobx 通过包装对象和数组为可观察对象,从而隐藏了大部分的样板代码。它是建立在隐藏抽象之上的。感觉像是出现了魔法,但却很难理解其内在的机制。Redux 则可以简单通过纯 JavaScript 来推断。它使你的应用更简单的测试和调试。</p>
<p>另外,我们重新回到单页应用的最开始来考虑,一系列的单页应用框架和库面临着相同的状态管理问题,它最终被 flux 模式解决了。Redux 是这个模式的成功者。</p>
<p>Mobx 则又处在相反的方向。我们直接修改 state 而没有拥抱函数式编程的好处。对一些开发者来说,这让他们觉得像双向绑定。一段时间之后,由于没有引入类似 Redux 的状态管理库,他们可能又会陷入同样的问题。状态管理分散在各个组件,导致最后一团糟。</p>
<p>使用 Redux,你有一个既定的模式组织代码,而 Mobx 则无主张。但拥抱 Mobx 最佳实践会是明智的。 开发者需要知道如何组织状态管理从而更好的推断它。不然他们就会想要直接在组件中修改它。</p>
<p>两个库都非常棒。Redux 已经非常完善,Mobx 则逐渐成为一个有效的替代。</p>