整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          JavaScript將執(zhí)行上下文、作用域、閉包串聯(lián)起

          JavaScript將執(zhí)行上下文、作用域、閉包串聯(lián)起來

          理清執(zhí)行上下文作用域閉包這三個(gè)關(guān)系之前,我們需要先理解一些概念。

          我們知道所學(xué)的高級語言分為兩類:1、編譯型 2、解釋型

          編譯型(compilation):將整個(gè)代碼立刻轉(zhuǎn)化為機(jī)器代碼,然后寫下機(jī)器代碼轉(zhuǎn)換為可在任何計(jì)算機(jī)上執(zhí)行的可移植文件,然后在進(jìn)行執(zhí)行。

          解釋:解釋器貫穿整個(gè)源代碼進(jìn)行一行一行的讀取執(zhí)行,但是還是會轉(zhuǎn)為機(jī)器代碼,但是發(fā)生在執(zhí)行前,而不是提前到和編譯一樣,整體進(jìn)行轉(zhuǎn)換為機(jī)器代碼。

          對于Javascript,我們了解到的是它是一個(gè)解釋型語言,但是隨著后續(xù)的發(fā)展,常用于web或者服務(wù)端的開發(fā),性能難以跟上。因此現(xiàn)代JavaScript是實(shí)時(shí)編譯(JIT),將匯編和解釋混合在一起,對整個(gè)源文件代碼進(jìn)行轉(zhuǎn)化為機(jī)器代碼,然后立刻執(zhí)行,這樣速度就更快些,省去了可移植文件,節(jié)省了時(shí)間。

          現(xiàn)代JavaScript在V8引擎的工作原理:

          因此JavaScript代碼的整個(gè)執(zhí)行過程,分為兩個(gè)階段:

          • 編譯階段:由編譯器完成,將代碼翻譯成可執(zhí)行代碼,這個(gè)階段作用域規(guī)則會確定。
          • 執(zhí)行階段:執(zhí)行階段由引擎完成,主要任務(wù)是執(zhí)行可執(zhí)行代碼,執(zhí)行上下文在這個(gè)階段創(chuàng)建。


          明白這些之后,我們繼續(xù)說執(zhí)行上下文和作用域。

          執(zhí)行上下文

          1、執(zhí)行上下文一句話解釋

          執(zhí)行上下文是一段JavaScript代碼執(zhí)行的環(huán)境,包含了所有必要的信息以供其執(zhí)行。

          2、執(zhí)行上下文的出現(xiàn)時(shí)機(jī)

          上面畫過一張V8引擎運(yùn)行圖,當(dāng)時(shí)所講的詞法環(huán)境是在 編譯階段產(chǎn)生,現(xiàn)在講的執(zhí)行上下文是在引擎執(zhí)行階段進(jìn)行。

          當(dāng)一段js代碼出現(xiàn)的時(shí)候,js引擎對整體代碼進(jìn)行編譯完成轉(zhuǎn)為機(jī)器代碼后便可進(jìn)行執(zhí)行,此時(shí)會為頂級代碼立刻創(chuàng)建一個(gè)全局上下文(頂級代碼一定不在任何函數(shù)內(nèi),只有函數(shù)外的代碼會被首先執(zhí)行,函數(shù)內(nèi)的只會在調(diào)用的時(shí)候執(zhí)行),創(chuàng)建完成后將其推入js引擎的堆棧當(dāng)中。接著進(jìn)入執(zhí)行階段,執(zhí)行可執(zhí)行代碼,該賦值賦值,遇到函數(shù),就創(chuàng)建一個(gè)函數(shù)執(zhí)行上下文,并往調(diào)用棧中壓入該函數(shù)的執(zhí)行上下文;反復(fù)循環(huán),到最后調(diào)用棧中只剩一個(gè)全局執(zhí)行上下文,除非你關(guān)閉瀏覽器,不然全局執(zhí)行上下文不會彈出。

          3、執(zhí)行上下文的生命周期

          根據(jù)第二點(diǎn),執(zhí)行上下文被分為兩個(gè)階段:

          • 創(chuàng)建階段:
            • this 值的確定,即我們所熟知的 this 綁定
            • 創(chuàng)建詞法環(huán)境組件(LexicalEnvironment component )
            • 創(chuàng)建變量環(huán)境組件(VariableEnvironment component )
          • 執(zhí)行階段:就是將執(zhí)行上下文不斷的壓棧出棧進(jìn)行執(zhí)行

          如果看視頻或者上下文的文章,我們了解到執(zhí)行上下文在創(chuàng)建階段所含的內(nèi)容不同,如下圖所示,目前的ES6執(zhí)行上下文也又發(fā)生了變化。但本質(zhì)還是一樣的


          那詞法環(huán)境組件和變量環(huán)境組件是什么呢?

          • 詞法環(huán)境組件:包括環(huán)境記錄器和 outer,詞法環(huán)境的環(huán)境記錄器收集 let、const、class 等變量
          • 變量環(huán)境組件:包括環(huán)境記錄器和 outer,變量環(huán)境的環(huán)境記錄器收集 var、function 等變量

          詞法環(huán)境組件、變量環(huán)境組件和詞法環(huán)境的區(qū)別是什么呢?

          我的理解:本質(zhì)上這兩個(gè)是一個(gè)東西,只是由于執(zhí)行上下文它是一個(gè)JavaScript代碼的環(huán)境,記錄一些執(zhí)行所需要的信息,因此創(chuàng)建一個(gè)變量來存儲前面已經(jīng)有的詞法環(huán)境(作用域),這樣來保持運(yùn)行。相當(dāng)于在執(zhí)行上下文創(chuàng)建的時(shí)候,除了 this,像變量環(huán)境、詞法環(huán)境在編譯階段就已經(jīng)確定了,其中變量環(huán)境的變量 var、function 會進(jìn)行變量、函數(shù)提升,并初始化,而詞法環(huán)境中的變量雖然提升了,但不會被初始化;而兩者的 outer 則相同,它們都指向父作用域。這個(gè)時(shí)候就會確定我們的作用域鏈,變量提升也在編譯的時(shí)候完成了。

          作用域與作用域鏈

          1、作用域到底是什么

          作用域是表示在哪里可以訪問到變量,其本質(zhì)是一套規(guī)則,而這個(gè)規(guī)則的底層遵循的就是詞法作用域模型,即在詞法分析時(shí)生成的作用域,詞法分析階段,也可以理解為代碼書寫階段,當(dāng)你把函數(shù)(塊級作用域同理)書寫到某個(gè)位置,不用執(zhí)行,它的作用域就已經(jīng)確定了。簡單來說,“詞法作用域”就是作用域的成因。

          從語言的層面來說,作用域模型分兩種:

          • 詞法作用域:也稱靜態(tài)作用域,是最為普遍的一種作用域模型
          • 動態(tài)作用域:相對“冷門”,bash腳本、Perl等語言采納的是動態(tài)作用域

          總結(jié)一下。詞法作用域和動態(tài)作用域最根本區(qū)別在于生成作用域的時(shí)機(jī)

          • 詞法作用域:在代碼書寫時(shí)完成劃分,作用域沿著它定義的位置往外延伸
          • 動態(tài)作用域:在代碼運(yùn)行時(shí)完成劃分,作用域鏈沿著他的調(diào)用棧往外延伸

          JavaScript擁有詞法作用域,因此可以訪問到變量的規(guī)則是基于代碼中函數(shù)和塊被寫在哪里,也就是它是在編譯階段就確定了。

          2、作用域分類

          在JS中分為作用域分三種:

          • 全局作用域
          • 函數(shù)作用域
          • 塊作用域

          3、作用域鏈

          作用域鏈?zhǔn)前炎饔糜驅(qū)訉忧短祝?dāng)查找一個(gè)變量在內(nèi)部作用域中未找到,向上父作用域去查找,找不到接著找,一直找到全局作用域這樣的一個(gè)關(guān)系叫做作用域鏈。

          變量提升的出現(xiàn)

          變量提升的表象:使某些類型的變量在實(shí)際聲明之前就可以在代碼中訪問。“變量被提升到其作用域的頂部”。

          其深層的原因:在執(zhí)行之前,掃描代碼以查找變量聲明,并在變量環(huán)境對象中為每個(gè)變量創(chuàng)建一個(gè)新屬性。這都是發(fā)生在執(zhí)行上下文的創(chuàng)建階段。


          是否變量提升

          初始值

          作用域

          函數(shù)聲明

          ?是

          實(shí)際函數(shù)

          嚴(yán)格模式下:塊級作用域;非嚴(yán)格模式:函數(shù)作用域

          var聲明的變量

          ?是

          undefined

          函數(shù)作用域

          let、const聲明的變量

          ?否

          <uninitialized>(未初始化)、TDZ(暫時(shí)性死區(qū))

          塊級作用域

          函數(shù)表達(dá)式、箭頭函數(shù)

          取決于使用var、const、let哪一個(gè)關(guān)鍵字



          理論上let和const關(guān)鍵字也會變量提升,但是它的初始值為未初始化,因此沒有任何用處,可以說是這些聲明的變量被置于暫時(shí)性死區(qū)(TDZ),這樣使得我們在作用域范圍最初到聲明變量的位置之間是無法訪問變量的,因此在聲明之前訪問會進(jìn)行報(bào)錯(cuò),但在之后就可以正常訪問了。

          為什么會有暫時(shí)性死區(qū)?

          • 更加容易去避免bug和捕捉bug
          • 讓const關(guān)鍵字能夠正常使用:由于常量不能夠改變,因此不能進(jìn)行初始化之后再來重新賦值,因此只在執(zhí)行時(shí)分配值

          為什么會存在變量提升?

          • 在實(shí)際聲明之前可以調(diào)用函數(shù),好處有:相互函數(shù)遞歸等
          • var關(guān)鍵字的變量提升是個(gè)副產(chǎn)品

          閉包內(nèi)部

          1、閉包是什么

          在說閉包背后原理之前,我們先看看閉包是什么。

          閉包是內(nèi)層函數(shù)能夠訪問外層函數(shù)聲明的變量,并且內(nèi)層函數(shù)在全局環(huán)境下可訪問。

          這個(gè)解釋其實(shí)僅僅對閉包的現(xiàn)象進(jìn)行解釋了一遍。

          真正的實(shí)際閉包,它背后是可以訪問附加到函數(shù)身上的變量環(huán)境,這樣的一個(gè)關(guān)系被稱之為閉包。

          任何函數(shù)都可以訪問到創(chuàng)建它的執(zhí)行上下文當(dāng)中的變量環(huán)境(詞法環(huán)境組件和變量環(huán)境組件),所有函數(shù)都有個(gè)[[Environment]] 的隱藏屬性,該屬性保存了對創(chuàng)建該函數(shù)的詞法環(huán)境的引用,[[Environment]] 引用在函數(shù)創(chuàng)建時(shí)被設(shè)置并永久保存。

          許多大神對閉包的定義都是去描述上方這句話。

          接下來我們看一下閉包的例子:

          2、舉例說明

          function foo() {
              var a=1;
              var b=2;
              return function bar() {
                  console.log(a++);
              };
          }
          var baz=foo();
          baz();
          • 在任何代碼執(zhí)行之前,先創(chuàng)建全局執(zhí)行上下文,并往調(diào)用棧中壓棧
          • 接著創(chuàng)建詞法環(huán)境,登記函數(shù)聲明 foo 和變量聲明 baz
          • 由于全局詞法環(huán)境沒有外部引用,所以箭頭指向了 null


          • 代碼開始執(zhí)行,執(zhí)行 foo(),創(chuàng)建 foo() 的函數(shù)執(zhí)行上下文,并往調(diào)用棧中壓棧
          • 在開始執(zhí)行 foo 函數(shù)內(nèi)代碼前,創(chuàng)建 foo 的詞法環(huán)境,登記函數(shù)聲明 bar 和變量聲明 a、b。它的 outer 指向父作用域——全局作用域



          代碼執(zhí)行至 function bar 時(shí),創(chuàng)建 bar 的詞法環(huán)境,它沒有變量,outer 指向父作用域 foo

          • 所有函數(shù)在“誕生”時(shí)都會記住創(chuàng)建它們的詞法環(huán)境
          • 所有函數(shù)都有個(gè)[[Environment]] 的隱藏屬性,該屬性保存了對創(chuàng)建該函數(shù)的詞法環(huán)境的引用
          • 我們說過作用域與它創(chuàng)建于哪里相關(guān),與在哪兒調(diào)用無


        1. 調(diào)用完函數(shù) foo(),彈出調(diào)用棧,foo 中的函數(shù) bar、變量 b 隨著 foo 出棧而被釋放
        2. 由于函數(shù) bar 的結(jié)果賦值給了全局變量 baz,baz 相當(dāng)于多個(gè)了隱藏屬性 [[Environment]],它指向父作用域 foo,而 bar 又引用了 foo 作用域下的變量 a,所以變量 a 無法被釋放
          • 因此,baz.[[Environment]] 有對 {a: 0} 詞法環(huán)境的引用
          • [[Environment]] 引用在函數(shù)創(chuàng)建時(shí)被設(shè)置并永久保存


          調(diào)用函數(shù) baz(),創(chuàng)建 baz() 執(zhí)行上下文,并將其壓入調(diào)用棧中

        3. 并在執(zhí)行代碼前,創(chuàng)建一個(gè)新的詞法環(huán)境,并且它的 outer 指向 baz.[[Environment]] ,即父作用域 foo
        4. 當(dāng)它查找變量 a 時(shí),先在自己的詞法環(huán)境中找,找不到,沿著 outer 往它的父作用域找,在 foo 詞法環(huán)境中找到了變量 a,并在變量所在的詞法環(huán)境中更新變量

        5. 如此,調(diào)用完 baz,因?yàn)?baz 一直存在全局詞法環(huán)境中,它的隱藏屬性[[Environment]] 一直引用著 foo 函數(shù)中的 a 變量(即使 foo 函數(shù)已經(jīng)被銷毀了)

          當(dāng)再次調(diào)用 baz 時(shí),就會再往調(diào)用棧中壓入baz(),并生成一個(gè)新的 bar 的詞法環(huán)境,它的 outer 還是引用 baz.[[Environment]],即上圖中的 foo 詞法環(huán)境。

          這里我們可以通過運(yùn)行這段閉包代碼,打印baz函數(shù),可以看見它含有的內(nèi)部屬性[[scope]]里含有閉包(closures),保存引用。查看閉包的一個(gè)優(yōu)先級大于作用域鏈

          總結(jié)

          到此,執(zhí)行上下文作用域閉包三者的聯(lián)系串聯(lián)起來給大家講清楚,也分別介紹了三者是什么。

          最后用簡短的一句話分別描述一下這三個(gè)是什么:

          • 執(zhí)行上下文:執(zhí)行一段JavaScript代碼的環(huán)境
          • 作用域:在哪里可以訪問到變量
          • 閉包:內(nèi)層函數(shù)能訪問外層函數(shù)變量環(huán)境

          這是我根據(jù)自行閱讀文章和視頻總結(jié)出來的個(gè)人理解,如有不正確的地方歡迎大家進(jìn)行指正。

          先要明白的是,javascript和python都是解釋型語言,它們的運(yùn)行是需要具體的runtime的。

          • Python: 我們最常安裝的Python其實(shí)是cpython,就是基于C來運(yùn)行的。除此之外還有像pypy這樣的自己寫了解釋器的,transcrypt這種轉(zhuǎn)成js之后再利用js的runtime的。基本上,不使用cpython作為python的runtime的最大問題就是通過pypi安裝的那些外來包,甚至有一些cpython自己的原生包(像 collections 這種)都用不了。
          • JavaScript: 常見的運(yùn)行引擎有g(shù)oogle的V8,Mozilla的SpiderMonkey等等,這些引擎會把JavaScript代碼轉(zhuǎn)換成機(jī)器碼執(zhí)行。基于這些基礎(chǔ)的運(yùn)行引擎,我們可以開發(fā)支持JS的瀏覽器(比如Chrome的JS運(yùn)行引擎就是V8);也可以開發(fā)功能更多的JS運(yùn)行環(huán)境,比如Node.js,相當(dāng)于我們不需要一個(gè)瀏覽器,也可以跑JS代碼。有了Node.js,JS包管理也變得方便許多,如果我們想把開發(fā)好的Node.js包再給瀏覽器用,就需要把基于Node.js的源代碼編譯成瀏覽器支持的JS代碼。

          在本文敘述中,假定:

          • 主語言: 最終的主程序所用的語言
          • 副語言: 不是主語言的另一種語言

          例如,python調(diào)用js,python就是主語言,js是副語言

          TL; DR

          適用于:

          1. python和javascript的runtime(基本特指cpython[不是cython!]和Node.js)都裝好了
          2. 副語言用了一些復(fù)雜的包(例如python用了numpy、javascript用了一點(diǎn)Node.js的C++擴(kuò)展等)
          3. 對運(yùn)行效率有要求的話:
          4. python與javascript之間的交互不能太多,傳遞的對象不要太大、太復(fù)雜,最好都是可序列化的對象
          5. javascript占的比重不過小。否則,python調(diào)js的話,啟動Node.js子進(jìn)程比實(shí)際跑程序還慢;js調(diào)python的話,因?yàn)閖s跑得快,要花很多時(shí)間在等python上。
          6. 因?yàn)镮PC大概率會用線程同步輸入輸出,主語言少整啥多進(jìn)程多、線程之類的并發(fā)編程

          有庫!有庫!有庫!

          python調(diào)javascript

          • JSPyBridge : pip install javascript優(yōu)點(diǎn):作者還在維護(hù),回issue和更新蠻快的。支持比較新的python和node版本,安裝簡單基本支持互調(diào)用,包括綁定或者傳回調(diào)函數(shù)之類的。缺點(diǎn) :沒有合理的銷毀機(jī)制, import javascript 即視作連接JS端,會初始化所有要用的線程多線程。如果python主程序想重啟對JS的連接,或者主程序用了多進(jìn)程,想在每個(gè)進(jìn)程都連接一次JS,都很難做到,會容易出錯(cuò)。
          • PyExecJS : pip install PyExecJS ,比較老的技術(shù)文章都推的這個(gè)包優(yōu)點(diǎn): 支持除了Node.js以外的runtime,例如PhantomJS之類的缺點(diǎn): End of Life,作者停止維護(hù)了

          javascript調(diào)python

          (因?yàn)榕c我的項(xiàng)目需求不太符合,所以了解的不太多)

          • JSPyBridge : npm i pythonia
          • node-python-bridge : npm install python-bridge
          • python-shell : npm install python-shell

          原理

          首先,該方法的前提是兩種語言都要有安裝好的runtime,且能通過命令行調(diào)用runtime運(yùn)行文件或一串字符腳本。例如,裝好cpython后我們可以通過 python a.py 來運(yùn)行python程序,裝好Node.js之后我們可以通過 node a.js 或者 node -e "some script" 等來運(yùn)行JS程序。

          當(dāng)然,最簡單的情況下,如果我們只需要調(diào)用一次副語言,也沒有啥交互(或者最多只有一次交互),那直接找個(gè)方法調(diào)用CLI就OK了。把給副語言的輸入用stdin或者命令行參數(shù)傳遞,讀取命令的輸出當(dāng)作副語言的輸出。

          例如,python可以用 subprocess.Popensubprocess.callsubprocess.check_output 或者 os.system 之類的,Node.js可以用 child_process 里的方法, exec 或者 fork 之類的。 需要注意的是,如果需要引用其他包,Node.js需要注意在 node_modules 所在的目錄下運(yùn)行指令,python需要注意設(shè)置好PYTHONPATH環(huán)境變量。

          # Need to set the working directory to the directory where `node_modules` resides if necessary
          >>> import subprocess
          >>> a, b=1, 2
          >>> print(subprocess.check_output(["node", "-e", f"console.log({a}+{b})"]))
          b'3\n'
          >>> print(subprocess.check_output(["node", "-e", f"console.log({a}+{b})"]).decode('utf-8'))
          3
          // Need to set PYTHONPATH in advance if necessary
          const a=1;
          const b=2;
          const { execSync }=require("child_process");
          console.log(execSync(`python -c "print(${a}+${b})"`));
          //<Buffer 33 0a>
          console.log(execSync(`python -c "print(${a}+${b})"`).toString());
          //3
          //

          如果有復(fù)雜的交互,要傳遞復(fù)雜的對象,有的倒還可以序列化,有的根本不能序列化,咋辦?

          這基本要利用 進(jìn)程間通信(IPC) ,通常情況下是用 管道(Pipe) 。在 stdinstdoutstderr 三者之中至少挑一個(gè)建立管道。

          假設(shè)我用 stdin 從python向js傳數(shù)據(jù),用 stderr 接收數(shù)據(jù),模式大約會是這樣的:

          (以下偽代碼僅為示意,沒有嚴(yán)格測試過,實(shí)際使用建議直接用庫)

          1. 新建一個(gè)副語言(假設(shè)為JS)文件 python-bridge.js :該文件不斷讀取 stdin 并根據(jù)發(fā)來的信息不同,進(jìn)行不同的處理;同時(shí)如果需要打印信息或者傳遞object給主語言,將它們適當(dāng)序列化后寫入 stdout 或者 stderrprocess.stdin.on('data', data=> { data.split('\n').forEach(line=> { // Deal with each line // write message process.stdout.write(message + "\n"); // deliver object, "$j2p" can be any prefix predefined and agreed upon with the Python side // just to tell python side that this is an object needs parsing process.stderr.write("$j2p sendObj "+JSON.stringify(obj)+"\n); }); } process.on('exit', ()=> { console.debug('** Node exiting'); });
          2. 在python中,用Popen異步打開一個(gè)子進(jìn)程,并將子進(jìn)程的之中的至少一個(gè),用管道連接。大概類似于:cmd=["node", "--trace-uncaught", f"{os.path.dirname(__file__)}/python-bridge.js"] kwargs=dict( stdin=subprocess.PIPE, stdout=sys.stdout, stderr=subprocess.PIPE, ) if os.name=='nt': kwargs['creationflags']=subprocess.CREATE_NO_WINDOW subproc=subprocess.Popen(cmd, **kwargs)
          3. 在需要調(diào)用JS,或者需要給JS傳遞數(shù)據(jù)的時(shí)候,往 subproc 寫入序列化好的信息,寫入后需要 flush ,不然可能會先寫入緩沖區(qū):subproc.stdin.write(f"$p2j call funcName {json.dumps([arg1, arg2])}".encode()) subproc.stdin.flush() # write immediately, not writing to the buffer of the stream
          4. 對管道化的 stdout / stderr ,新建一個(gè)線程,專門負(fù)責(zé)讀取傳來的數(shù)據(jù)并進(jìn)行處理。是對象的重新轉(zhuǎn)換成對象,是普通信息的直接打印回主進(jìn)程的 stderr 或者 stdoutdef read_stderr(): while subproc.poll() is None: # when the subprocess is still alive, keep reading line=self.subproc.stderr.readline().decode('utf-8') if line.startswith('$j2p'): # receive special information _, cmd, line=line.split(' ', maxsplit=2) if cmd=='sendObj': # For example, received an object obj=json.loads(line) else: # otherwise, write to stderr as it is sys.stderr.write(line) stderr_thread=threading.Thread(target=read_stderr, args=(), daemon=True) stderr_thread.start()這里由于我們的 stdout 沒有建立管道,所以node那邊往 stdout 里打印的東西會直接打印到python的 sys.stdout 里,不用自己處理。
          5. 由于線程是異步進(jìn)行的,什么時(shí)候知道一個(gè)函數(shù)返回的對象到了呢?答案是用線程同步手段,信號量(Semaphore)、條件(Condition),事件(Event)等等,都可以。以 python的條件 為例:func_name_cv=threading.Condition() # use a flag and a result object in case some function has no result func_name_result_returned=False func_name_result=None def func_name_wrapper(arg1, arg2): # send arguments subproc.stdin.write(f"$p2j call funcName {json.dumps([arg1, arg2])}".encode()) subproc.stdin.flush() # wait for the result with func_name_cv: if not func_name_result_returned: func_name_cv.wait(timeout=10000) # when result finally returned, reset the flag func_name_result_returned=False return func_name_result同時(shí),需要在讀stderr的線程 read_stderr 里解除對這個(gè)返回值的阻塞。需要注意的是,如果JS端因?yàn)橐馔舛顺隽耍?subproc 也會死掉, 這時(shí)候也要記得取消主線程中的阻塞def read_stderr(): while subproc.poll() is None: # when the subprocess is still alive, keep reading # Deal with a line line=self.subproc.stderr.readline().decode('utf-8') if line.startswith('$j2p'): # receive special information _, cmd, line=line.split(' ', maxsplit=2) if cmd=='sendObj': # acquire lock here to ensure the editing of func_name_result is mutex with func_name_cv: # For example, received an object func_name_result=json.loads(line) func_name_result_returned=True # unblock func_name_wrapper when receiving the result func_name_cv.notify() else: # otherwise, write to stderr as it is sys.stderr.write(line) # If subproc is terminated (mainly due to error), still need to unblock func_name_wrapper func_name_cv.notify()當(dāng)然這是比較簡單的版本,由于對JS的調(diào)用基本都是線性的,所以可以知道只要得到一個(gè)object的返回,那就一定是 func_name_wrapper 對應(yīng)的結(jié)果。如果函數(shù)多起來的話,情況會更復(fù)雜。
          6. 如果想 取消對JS的連接 ,首先應(yīng)該先關(guān)閉子進(jìn)程,然后等待讀 stdout / stderr 的線程自己自然退出,最后 一定不要忘記關(guān)閉管道 。并且 這三步的順序不能換 ,如果先關(guān)了管道,讀線程會因?yàn)?stdout / stderr 已經(jīng)關(guān)了而出錯(cuò)。subproc.terminate() stderr_thread.join() subproc.stdin.close() subproc.stderr.close()

          如果是通過這種原理javascript調(diào)用python,方法也差不多,javascript方是Node.js的話,用的是 child_process 里的指令。

          優(yōu)點(diǎn)

          1. 只需要正常裝好兩方的runtime就能實(shí)現(xiàn)交互,運(yùn)行環(huán)境相對比較好配。
          2. 只要python方和javascript方在各自的runtime里正常運(yùn)行沒問題,那么連上之后運(yùn)行也基本不會有問題。(除非涉及并發(fā))
          3. 對兩種語言的所有可用的擴(kuò)展包基本都能支持。

          缺點(diǎn)

          1. 當(dāng)python與JavaScript交互頻繁,且交互的信息都很大的時(shí)候,可能會很影響程序效率。因?yàn)閮H僅通過最多3個(gè)管道混合處理普通要打印的信息、python與js交互的對象、函數(shù)調(diào)用等,通信開銷很大。
          2. 要另起一個(gè)子進(jìn)程運(yùn)行副語言的runtime,會花一定時(shí)間和空間開銷。

          文將比較全面細(xì)致的梳理一下 CSS 動畫的方方面面,針對每個(gè)屬性用法的講解及進(jìn)階用法的示意,希望能成為一個(gè)比較好的從入門到進(jìn)階的教程。

          CSS 動畫介紹及語法

          首先,我們來簡單介紹一下 CSS 動畫。

          最新版本的 CSS 動畫由規(guī)范 -- CSS Animations Level 1 定義。

          CSS 動畫用于實(shí)現(xiàn)元素從一個(gè) CSS 樣式配置轉(zhuǎn)換到另一個(gè) CSS 樣式配置。

          動畫包括兩個(gè)部分: 描述動畫的樣式規(guī)則和用于指定動畫開始、結(jié)束以及中間點(diǎn)樣式的關(guān)鍵幀。

          簡單來說,看下面的例子:

          div {
              animation: change 3s;
          }
          
          @keyframes change {
              0% {
                  color: #f00;
              }
              100% {
                  color: #000;
              }
          }
          
          1. animation: move 1s 部分就是動畫的第一部分,用于描述動畫的各個(gè)規(guī)則;
          2. @keyframes move {} 部分就是動畫的第二部分,用于指定動畫開始、結(jié)束以及中間點(diǎn)樣式的關(guān)鍵幀;

          一個(gè) CSS 動畫一定要由上述兩部分組成。

          CSS 動畫的語法

          接下來,我們簡單看看 CSS 動畫的語法。

          創(chuàng)建動畫序列,需要使用 animation 屬性或其子屬性,該屬性允許配置動畫時(shí)間、時(shí)長以及其他動畫細(xì)節(jié),但該屬性不能配置動畫的實(shí)際表現(xiàn),動畫的實(shí)際表現(xiàn)是由 @keyframes 規(guī)則實(shí)現(xiàn)。

          animation 的子屬性有:

          • animation-name:指定由 @keyframes 描述的關(guān)鍵幀名稱。
          • animation-duration:設(shè)置動畫一個(gè)周期的時(shí)長。
          • animation-delay:設(shè)置延時(shí),即從元素加載完成之后到動畫序列開始執(zhí)行的這段時(shí)間。
          • animation-direction:設(shè)置動畫在每次運(yùn)行完后是反向運(yùn)行還是重新回到開始位置重復(fù)運(yùn)行。
          • animation-iteration-count:設(shè)置動畫重復(fù)次數(shù), 可以指定 infinite 無限次重復(fù)動畫
          • animation-play-state:允許暫停和恢復(fù)動畫。
          • animation-timing-function:設(shè)置動畫速度, 即通過建立加速度曲線,設(shè)置動畫在關(guān)鍵幀之間是如何變化。
          • animation-fill-mode:指定動畫執(zhí)行前后如何為目標(biāo)元素應(yīng)用樣式
          • @keyframes 規(guī)則,當(dāng)然,一個(gè)動畫想要運(yùn)行,還應(yīng)該包括 @keyframes 規(guī)則,在內(nèi)部設(shè)定動畫關(guān)鍵幀

          其中,對于一個(gè)動畫:

          • 必須項(xiàng)animation-nameanimation-duration@keyframes規(guī)則
          • 非必須項(xiàng)animation-delayanimation-directionanimation-iteration-countanimation-play-stateanimation-timing-functionanimation-fill-mode,當(dāng)然不是說它們不重要,只是不設(shè)置時(shí),它們都有默認(rèn)值

          上面已經(jīng)給了一個(gè)簡單的 DEMO, 就用上述的 DEMO,看看結(jié)果:

          這就是一個(gè)最基本的 CSS 動畫,本文將從 animation 的各個(gè)子屬性入手,探究 CSS 動畫的方方面面。

          animation-name / animation-duration 詳解

          整體而言,單個(gè)的 animation-nameanimation-duration 沒有太多的技巧,非常好理解,放在一起。

          首先介紹一下 animation-name,通過 animation-name,CSS 引擎將會找到對應(yīng)的 @keyframes 規(guī)則。

          當(dāng)然,它和 CSS 規(guī)則命名一樣,也存在一些騷操作。譬如,他是支持 emoji 表情的,所以代碼中的 animation-name 命名也可以這樣寫:

          div {
              animation:  3s;
          }
          
          @keyframes  {
              0% {
                  color: #f00;
              }
              100% {
                  color: #000;
              }
          }
          

          animation-duration 設(shè)置動畫一個(gè)周期的時(shí)長,上述 DEMO 中,就是設(shè)定動畫整體持續(xù) 3s,這個(gè)也非常好理解。

          animation-delay 詳解

          animation-delay 就比較有意思了,它可以設(shè)置動畫延時(shí),即從元素加載完成之后到動畫序列開始執(zhí)行的這段時(shí)間。

          簡單的一個(gè) DEMO:

          <div></div>
          <div></div>
          
          div {
              width: 100px;
              height: 100px;
              background: #000;
              animation-name: move;
              animation-duration: 2s;
          }
          
          div:nth-child(2) {
              animation-delay: 1s;
          }
          @keyframes move {
              0% {
                  transform: translate(0);
              }
              100% {
                  transform: translate(200px);
              }
          }
          

          比較下列兩個(gè)動畫,一個(gè)添加了 animation-delay,一個(gè)沒有,非常直觀:

          上述第二個(gè) div,關(guān)于 animation 屬性,也可以簡寫為 animation: move 2s 1s,第一個(gè)時(shí)間值表示持續(xù)時(shí)間,第二個(gè)時(shí)間值表示延遲時(shí)間。

          animation-delay 可以為負(fù)值

          關(guān)于 animation-delay,最有意思的技巧在于,它可以是負(fù)數(shù)。也就是說,雖然屬性名是動畫延遲時(shí)間,但是運(yùn)用了負(fù)數(shù)之后,動畫可以提前進(jìn)行

          假設(shè)我們要實(shí)現(xiàn)這樣一個(gè) loading 動畫效果:

          有幾種思路:

          1. 初始 3 個(gè)球的位置就是間隔 120°,同時(shí)開始旋轉(zhuǎn),但是這樣代碼量會稍微多一點(diǎn)
          2. 另外一種思路,同一個(gè)動畫,3 個(gè)元素的其中兩個(gè)延遲整個(gè)動畫的 1/3,2/3 時(shí)間出發(fā)

          方案 2 的核心偽代碼如下:

          .item:nth-child(1) {
              animation: rotate 3s infinite linear;
          }
          .item:nth-child(2) {
              animation: rotate 3s infinite 1s linear;
          }
          .item:nth-child(3) {
              animation: rotate 3s infinite 2s linear;
          }
          

          但是,在動畫的前 2s,另外兩個(gè)元素是不會動的,只有 2s 過后,整個(gè)動畫才是我們想要的:

          此時(shí),我們可以讓第 2、3 個(gè)元素的延遲時(shí)間,改為負(fù)值,這樣可以讓動畫延遲進(jìn)行 -1s-2s,也就是提前進(jìn)行 1s2s

          .item:nth-child(1) {
              animation: rotate 3s infinite linear;
          }
          .item:nth-child(2) {
              animation: rotate 3s infinite -1s linear;
          }
          .item:nth-child(3) {
              animation: rotate 3s infinite -2s linear;
          }
          

          這樣,每個(gè)元素都無需等待,直接就是運(yùn)動狀態(tài)中的,并且元素間隔位置是我們想要的結(jié)果:

          利用 animation-duration 和 animation-delay 構(gòu)建隨機(jī)效果

          還有一個(gè)有意思的小技巧。

          同一個(gè)動畫,我們利用一定范圍內(nèi)隨機(jī)的 animation-duration 和一定范圍內(nèi)隨機(jī)的 animation-delay,可以有效的構(gòu)建更為隨機(jī)的動畫效果,讓動畫更加的自然。

          我在下述兩個(gè)純 CSS 動畫中,都使用了這樣的技巧:

          1. 純 CSS 實(shí)現(xiàn)華為充電動畫:
          1. 純 CSS 實(shí)現(xiàn)火焰動畫:

          純 CSS 實(shí)現(xiàn)華為充電動畫為例子,簡單講解一下。

          仔細(xì)觀察這一部分,上升的一個(gè)一個(gè)圓球,拋去這里的一些融合效果,只關(guān)注不斷上升的圓球,看著像是沒有什么規(guī)律可言:

          我們來模擬一下,如果是使用 10 個(gè) animation-durationanimation-delay 都一致的圓的話,核心偽代碼:

          <ul>
              <li></li>
              <!--共 10 個(gè)...--> 
              <li></li>
          </ul>
          
          ul {
              display: flex;
              flex-wrap: nowrap;
              gap: 5px;
          }
          li {
              background: #000;
              animation: move 3s infinite 1s linear;
          }
          @keyframes move {
              0% {
                  transform: translate(0, 0);
              }
              100% {
                  transform: translate(0, -100px);
              }
          }
          

          這樣,小球的運(yùn)動會是這樣的整齊劃一:

          要讓小球的運(yùn)動顯得非常的隨機(jī),只需要讓 animation-durationanimation-delay 都在一定范圍內(nèi)浮動即可,改造下 CSS:

          @for $i from 1 to 11 {
              li:nth-child(#{$i}) {
                  animation-duration: #{random(2000)/1000 + 2}s;
                  animation-delay: #{random(1000)/1000 + 1}s;
              }
          }
          

          我們利用 SASS 的循環(huán)和 random() 函數(shù),讓 animation-duration 在 2-4 秒范圍內(nèi)隨機(jī),讓 animation-delay 在 1-2 秒范圍內(nèi)隨機(jī),這樣,我們就可以得到非常自然且不同的上升動畫效果,基本不會出現(xiàn)重復(fù)的畫面,很好的模擬了隨機(jī)效果:

          CodePen Demo -- 利用范圍隨機(jī) animation-duration 和 animation-delay 實(shí)現(xiàn)隨機(jī)動畫效果

          animation-timing-function 緩動函數(shù)

          緩動函數(shù)在動畫中非常重要,它定義了動畫在每一動畫周期中執(zhí)行的節(jié)奏。

          緩動主要分為兩類:

          1. cubic-bezier-timing-function 三次貝塞爾曲線緩動函數(shù)
          2. step-timing-function 步驟緩動函數(shù)(這個(gè)翻譯是我自己翻的,可能有點(diǎn)奇怪)

          三次貝塞爾曲線緩動函數(shù)

          首先先看看三次貝塞爾曲線緩動函數(shù)。在 CSS 中,支持一些緩動函數(shù)關(guān)鍵字。

          /* Keyword values */
          animation-timing-function: ease;  // 動畫以低速開始,然后加快,在結(jié)束前變慢
          animation-timing-function: ease-in;  // 動畫以低速開始
          animation-timing-function: ease-out; // 動畫以低速結(jié)束
          animation-timing-function: ease-in-out; // 動畫以低速開始和結(jié)束
          animation-timing-function: linear; // 勻速,動畫從頭到尾的速度是相同的
          

          關(guān)于它們之間的效果對比:

          除了 CSS 支持的這 5 個(gè)關(guān)鍵字,我們還可以使用 cubic-bezier() 方法自定義三次貝塞爾曲線:

          animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);
          

          這里有個(gè)非常好用的網(wǎng)站 -- cubic-bezier 用于創(chuàng)建和調(diào)試生成不同的貝塞爾曲線參數(shù)。

          三次貝塞爾曲線緩動對動畫的影響

          關(guān)于緩動函數(shù)對動畫的影響,這里有一個(gè)非常好的示例。這里我們使用了純 CSS 實(shí)現(xiàn)了一個(gè)鐘的效果,對于其中的動畫的運(yùn)動,如果是 animation-timing-function: linear,效果如下:

          而如果我們我把緩動函數(shù)替換一下,變成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29),它的曲線對應(yīng)如下:

          整個(gè)鐘的動畫律動效果將變成這樣,完全不一樣的感覺:

          CodePen Demo - 緩動不同效果不同

          對于許多精益求精的動畫,在設(shè)計(jì)中其實(shí)都考慮到了緩動函數(shù)。我很久之前看到過一篇《基于物理學(xué)的動畫用戶體驗(yàn)設(shè)計(jì)》,可惜如今已經(jīng)無法找到原文。其中傳達(dá)出的一些概念是,動畫的設(shè)計(jì)依據(jù)實(shí)際在生活中的表現(xiàn)去考量。

          譬如 linear 這個(gè)緩動,實(shí)際應(yīng)用于某些動畫中會顯得很不自然,因?yàn)橛捎诳諝庾枇Φ拇嬖冢绦蚰M的勻速直線運(yùn)動在現(xiàn)實(shí)生活中是很難實(shí)現(xiàn)的。因此對于這樣一個(gè)用戶平時(shí)很少感知到的運(yùn)動是很難建立信任感的。這樣的勻速直線運(yùn)動也是我們在進(jìn)行動效設(shè)計(jì)時(shí)需要極力避免的。

          步驟緩動函數(shù)

          接下來再講講步驟緩動函數(shù)。在 CSS 的 animation-timing-function 中,它有如下幾種表現(xiàn)形態(tài):

          {
              /* Keyword values */
              animation-timing-function: step-start;
              animation-timing-function: step-end;
          
              /* Function values */
              animation-timing-function: steps(6, start)
              animation-timing-function: steps(4, end);
          }
          

          在 CSS 中,使用步驟緩動函數(shù)最多的,就是利用其來實(shí)現(xiàn)逐幀動畫。假設(shè)我們有這樣一張圖(圖片大小為 1536 x 256,圖片來源于網(wǎng)絡(luò)):

          可以發(fā)現(xiàn)它其實(shí)是一個(gè)人物行進(jìn)過程中的 6 種狀態(tài),或者可以為 6 幀,我們利用 animation-timing-function: steps(6) 可以將其用一個(gè) CSS 動畫串聯(lián)起來,代碼非常的簡單:

          <div class="box"></div>
          
          .box {
            width: 256px;
            height: 256px;
            background: url('https://github.com/iamalperen/playground/blob/main/SpriteSheetAnimation/sprite.png?raw=true');
            animation: sprite .6s steps(6, end) infinite;
          }
          @keyframes sprite {
            0% { 
              background-position: 0 0;
            }
            100% { 
              background-position: -1536px 0;
            }
          }
          

          簡單解釋一下上述代碼,首先要知道,剛好 256 x 6=1536,所以上述圖片其實(shí)可以剛好均分為 6 段:

          1. 我們設(shè)定了一個(gè)大小都為 256px 的 div,給這個(gè) div 賦予了一個(gè) animation: sprite .6s steps(6) infinite 動畫;
          2. 其中 steps(6) 的意思就是將設(shè)定的 @keyframes 動畫分為 6 次(6幀)執(zhí)行,而整體的動畫時(shí)間是 0.6s,所以每一幀的停頓時(shí)長為 0.1s
          3. 動畫效果是由 background-position: 0 0background-position: -1536px 0,由于上述的 CSS 代碼沒有設(shè)置 background-repeat,所以其實(shí) background-position: 0 0 是等價(jià)于 background-position: -1536px 0,就是圖片在整個(gè)動畫過程中推進(jìn)了一輪,只不過每一幀停在了特點(diǎn)的地方,一共 6 幀;

          將上述 1、2、3,3 個(gè)步驟畫在圖上簡單示意:

          從上圖可知,其實(shí)在動畫過程中,background-position 的取值其實(shí)只有 background-position: 0 0background-position: -256px 0background-position: -512px 0 依次類推一直到 background-position: -1536px 0,由于背景的 repeat 的特性,其實(shí)剛好回到原點(diǎn),由此又重新開始新一輪同樣的動畫。

          所以,整個(gè)動畫就會是這樣,每一幀停留 0.1s 后切換到下一幀(注意這里是個(gè)無限循環(huán)動畫),:

          完整的代碼你可以戳這里 -- CodePen Demo -- Sprite Animation with steps()

          animation-duration 動畫長短對動畫的影響

          在這里再插入一個(gè)小章節(jié),animation-duration 動畫長短對動畫的影響也是非常明顯的。

          在上述代碼的基礎(chǔ)上,我們再修改 animation-duration,縮短每一幀的時(shí)間就可以讓步行的效果變成跑步的效果,同理,也可以增加每一幀的停留時(shí)間。讓每一步變得緩慢,就像是在步行一樣。

          需要提出的是,上文說的每一幀,和瀏覽器渲染過程中的 FPS 的每一幀不是同一個(gè)概念。

          看看效果,設(shè)置不同的 animation-duration 的效果(這里是 0.6s -> 0.2s),GIF 錄屏丟失了一些關(guān)鍵幀,實(shí)際效果會更好點(diǎn):

          當(dāng)然,在 steps() 中,還有 steps(6, start)steps(6, end) 的差異,也就是其中關(guān)鍵字 startend 的差異。對于上述的無限動畫而言,其實(shí)基本是可以忽略不計(jì)的,它主要是控制動畫第一幀的開始和持續(xù)時(shí)長,比較小的一個(gè)知識點(diǎn)但是想講明白需要比較長的篇幅,限于本文的內(nèi)容,在這里不做展開,讀者可以自行了解。

          同個(gè)動畫效果的補(bǔ)間動畫和逐幀動畫演繹對比

          上述的三次貝塞爾曲線緩動和步驟緩動,其實(shí)就是對應(yīng)的補(bǔ)間動畫和逐幀動畫。

          對于同個(gè)動畫而言,有的時(shí)候兩種緩動都是適用的。我們在具體使用的時(shí)候需要具體分析選取。

          假設(shè)我們用 CSS 實(shí)現(xiàn)了這樣一個(gè)圖形:

          現(xiàn)在想利用這個(gè)圖形制作一個(gè) Loading 效果,如果利用補(bǔ)間動畫,也就是三次貝塞爾曲線緩動的話,讓它旋轉(zhuǎn)起來,得到的效果非常的一般:

          .g-container{
              animation: rotate 2s linear infinite;
          }
          @keyframes rotate {
              0% {
                  transform: rotate(0);
              }
              100% {
                  transform: rotate(360deg);
              }
          }
          

          動畫效果如下:

          但是如果這里,我們將補(bǔ)間動畫換成逐幀動畫,因?yàn)橛?20 個(gè)點(diǎn),所以設(shè)置成 steps(20),再看看效果,會得到完全不一樣的感覺:

          .g-container{
              animation: rotate 2s steps(20) infinite;
          }
          @keyframes rotate {
              0% {
                  transform: rotate(0);
              }
              100% {
                  transform: rotate(360deg);
              }
          }
          

          動畫效果如下:

          整個(gè) loading 的圈圈看上去好像也在旋轉(zhuǎn),實(shí)際上只是 20 幀關(guān)鍵幀在切換,整體的效果感覺更適合 Loading 的效果。

          因此,兩種動畫效果都是很有必要掌握的,在實(shí)際使用的時(shí)候靈活嘗試,選擇更適合的。

          上述 DEMO 效果完整的代碼:CodePen Demo -- Scale Loading steps vs linear

          animation-play-state

          接下來,我們講講 animation-play-state,顧名思義,它可以控制動畫的狀態(tài) -- 運(yùn)行或者暫停。類似于視頻播放器的開始和暫停。是 CSS 動畫中有限的控制動畫狀態(tài)的手段之一。

          它的取值只有兩個(gè)(默認(rèn)為 running):

          {
              animation-play-state: paused | running;
          }
          

          使用起來也非常簡單,看下面這個(gè)例子,我們在 hover 按鈕的時(shí)候,實(shí)現(xiàn)動畫的暫停:

          <div class="btn stop">stop</div>
          <div class="animation"></div>
          
          .animation {
              width: 100px;
              height: 100px;
              background: deeppink;
              animation: move 2s linear infinite alternate;
          }
          
          @keyframes move {
              100% {
                  transform: translate(100px, 0);
              }
          }
          
          .stop:hover ~ .animation {
              animation-play-state: paused;
          }
          

          一個(gè)簡單的 CSS 動畫,但是當(dāng)我們 hover 按鈕的時(shí)候,給動畫元素添加上 animation-play-state: paused

          animation-play-state 小技巧,默認(rèn)暫停,點(diǎn)擊運(yùn)行

          正常而言,按照正常思路使用 animation-play-state: paused 是非常簡單的。

          但是,如果我們想創(chuàng)造一些有意思的 CSS 動畫效果,不如反其道而行之。

          我們都知道,正常情況下,動畫應(yīng)該是運(yùn)行狀態(tài),那如果我們將一些動畫的默認(rèn)狀態(tài)設(shè)置為暫停,只有當(dāng)鼠標(biāo)點(diǎn)擊或者 hover 的時(shí)候,才設(shè)置其 animation-play-state: running,這樣就可以得到很多有趣的 CSS 效果。

          看個(gè)倒酒的例子,這是一個(gè)純 CSS 動畫,但是默認(rèn)狀態(tài)下,動畫處于 animation-play-state: paused,也就是暫停狀態(tài),只有當(dāng)鼠標(biāo)點(diǎn)擊杯子的時(shí),才設(shè)置 animation-play-state: running,讓酒倒下,利用 animation-play-state 實(shí)現(xiàn)了一個(gè)非常有意思的交互效果:

          完整的 DEMO 你可以戳這里:CodePen Demo -- CSS Beer!

          在非常多 Web 創(chuàng)意交互動畫我們都可以看到這個(gè)技巧的身影。

          1. 頁面 render 后,無任何操作,動畫不會開始。只有當(dāng)鼠標(biāo)對元素進(jìn)行 click ,通過觸發(fā)元素的 :active 偽類效果的時(shí)候,賦予動畫 animation-play-state: running,動畫才開始進(jìn)行;
          2. 動畫進(jìn)行到任意時(shí)刻,鼠標(biāo)停止點(diǎn)擊,偽類消失,則動畫停止;

          animation-fill-mode 控制元素在各個(gè)階段的狀態(tài)

          下一個(gè)屬性 animation-fill-mode,很多人會誤認(rèn)為它只是用于控制元素在動畫結(jié)束后是否復(fù)位。這個(gè)其實(shí)是不準(zhǔn)確的,不全面的。

          看看它的取值:

          {
              // 默認(rèn)值,當(dāng)動畫未執(zhí)行時(shí),動畫將不會將任何樣式應(yīng)用于目標(biāo),而是使用賦予給該元素的 CSS 規(guī)則來顯示該元素的狀態(tài)
              animation-fill-mode: none;
              // 動畫將在應(yīng)用于目標(biāo)時(shí)立即應(yīng)用第一個(gè)關(guān)鍵幀中定義的值,并在 `animation-delay` 期間保留此值,
              animation-fill-mode: backwards; 
              // 目標(biāo)將保留由執(zhí)行期間遇到的最后一個(gè)關(guān)鍵幀計(jì)算值。 最后一個(gè)關(guān)鍵幀取決于 `animation-direction` 和 `animation-iteration-count`
              animation-fill-mode: forwards;    
              // 動畫將遵循 `forwards` 和 `backwards` 的規(guī)則,從而在兩個(gè)方向上擴(kuò)展動畫屬性
              animation-fill-mode: both; 
          }
          

          對于 animation-fill-mode 的解讀,我在 Segment Fault 上的一個(gè)問答中(SF - 如何理解 animation-fill-mode)看到了 4 副很好的解讀圖,這里借用一下:

          假設(shè) HTML 如下:

          <div class="box"></div>
          

          CSS如下:

          .box{
              transform: translateY(0);
          }
          .box.on{
              animation: move 1s;
          }
          
          @keyframes move{
              from{transform: translateY(-50px)}
              to  {transform: translateY( 50px)}
          }
          

          使用圖片來表示 translateY 的值與 時(shí)間 的關(guān)系:

          • 橫軸為表示 時(shí)間,為 0 時(shí)表示動畫開始的時(shí)間,也就是向 box 加上 on 類名的時(shí)間,橫軸一格表示 0.5s
          • 縱軸表示 translateY 的值,為 0 時(shí)表示 translateY 的值為 0,縱軸一格表示 50px
          1. animation-fill-mode: none 表現(xiàn)如圖:

          一句話總結(jié),元素在動畫時(shí)間之外,樣式只受到它的 CSS 規(guī)則限制,與 @keyframes 內(nèi)的關(guān)鍵幀定義無關(guān)。

          1. animation-fill-mode: backwards 表現(xiàn)如圖:

          一句話總結(jié),元素在動畫開始之前(包含未觸發(fā)動畫階段及 animation-delay 期間)的樣式為動畫運(yùn)行時(shí)的第一幀,而動畫結(jié)束后的樣式則恢復(fù)為 CSS 規(guī)則設(shè)定的樣式。

          1. animation-fill-mode: forwards 表現(xiàn)如圖:

          一句話總結(jié),元素在動畫開始之前的樣式為 CSS 規(guī)則設(shè)定的樣式,而動畫結(jié)束后的樣式則表現(xiàn)為由執(zhí)行期間遇到的最后一個(gè)關(guān)鍵幀計(jì)算值(也就是停在最后一幀)。

          1. animation-fill-mode: both 表現(xiàn)如圖:

          一句話總結(jié),綜合了 animation-fill-mode: backwardsanimation-fill-mode: forwards 的設(shè)定。動畫開始前的樣式為動畫運(yùn)行時(shí)的第一幀,動畫結(jié)束后停在最后一幀。

          animation-iteration-count/animation-direction 動畫循環(huán)次數(shù)和方向

          講到了 animation-fill-mode,我們就可以順帶講講這個(gè)兩個(gè)比較好理解的屬性 -- animation-iteration-countanimation-direction

          • animation-iteration-count 控制動畫運(yùn)行的次數(shù),可以是數(shù)字或者 infinite,注意,數(shù)字可以是小數(shù)
          • animation-direction 控制動畫的方向,正向、反向、正向交替與反向交替

          在上面講述 animation-fill-mode 時(shí),我使用了動畫運(yùn)行時(shí)的第一幀替代了@keyframes 中定義的第一幀這種說法,因?yàn)閯赢嬤\(yùn)行的第一幀和最后一幀的實(shí)際狀態(tài)還會受到動畫運(yùn)行方向 animation-directionanimation-iteration-count 的影響。

          在 CSS 動畫中,由 animation-iteration-countanimation-direction 共同決定動畫運(yùn)行時(shí)的第一幀和最后一幀的狀態(tài)。

          1. 動畫運(yùn)行的第一幀由 animation-direction 決定
          2. 動畫運(yùn)行的最后一幀由 animation-iteration-countanimation-direction 決定

          動畫的最后一幀,也就是動畫運(yùn)行的最終狀態(tài),并且我們可以利用 animation-fill-mode: forwards 讓動畫在結(jié)束后停留在這一幀,這個(gè)還是比較好理解的,但是 animation-fill-mode: backwardsanimation-direction 的關(guān)系很容易弄不清楚,這里簡答講解下。

          設(shè)置一個(gè) 100px x 100px 的滑塊,在一個(gè) 400px x 100px 的容器中,其代碼如下:

          <div class="g-father">
              <div class="g-box"></div>
          </div>
          
          .g-father {
              width: 400px;
              height: 100px;
              border: 1px solid #000;
          }
          .g-box {
              width: 100px;
              height: 100px;
              background: #333;
          }
          

          表現(xiàn)如下:

          那么,加入 animation 之后,在不同的 animation-iteration-countanimation-direction 作用下,動畫的初始和結(jié)束狀態(tài)都不一樣。

          如果設(shè)置了 animation-fill-mode: backwards,則元素在動畫未開始前的狀態(tài)由 animation-direction 決定:

          .g-box {
              ...
              animation: move 4s linear;
              animation-play-state: paused;
              transform: translate(0, 0);
          }
          @keyframes move {
              0% {
                  transform: translate(100px, 0);
              }
              100% {
                  transform: translate(300px, 0);
              }
          }
          

          注意這里 CSS 規(guī)則中,元素沒有設(shè)置位移 transform: translate(0, 0),而在動畫中,第一個(gè)關(guān)鍵幀和最后一個(gè)關(guān)鍵的 translateX 分別是 100px300px,配合不同的 animation-direction 初始狀態(tài)如下。

          下圖假設(shè)我們設(shè)置了動畫默認(rèn)是暫停的 -- animation-play-state: paused,那么動畫在開始前的狀態(tài)為:

          動畫的分治與復(fù)用

          講完了每一個(gè)屬性,我們再來看看一些動畫使用過程中的細(xì)節(jié)。

          看這樣一個(gè)動畫:

          <div></div>
          
          div {
              width: 100px;
              height: 100px;
              background: #000;
              animation: combine 2s;
          }
          @keyframes combine {
              100% {
                  transform: translate(0, 150px);
                  opacity: 0;
              }
          }
          

          這里我們實(shí)現(xiàn)了一個(gè) div 塊下落動畫,下落的同時(shí)產(chǎn)生透明度的變化:

          對于這樣一個(gè)多個(gè)屬性變化的動畫,它其實(shí)等價(jià)于:

          div {
              animation: falldown 2s, fadeIn 2s;
          }
          
          @keyframes falldown {
              100% {
                  transform: translate(0, 150px);
              }
          }
          @keyframes fadeIn {
              100% {
                  opacity: 0;
              }
          }
          

          在 CSS 動畫規(guī)則中,animation 是可以接收多個(gè)動畫的,這樣做的目的不僅僅只是為了復(fù)用,同時(shí)也是為了分治,我們對每一個(gè)屬性層面的動畫能夠有著更為精確的控制。

          keyframes 規(guī)則的設(shè)定

          我們經(jīng)常能夠在各種不同的 CSS 代碼見到如下兩種 CSS @keyframes 的設(shè)定:

          1. 使用百分比
          @keyframes fadeIn {
              0% {
                  opacity: 1;
              }
              100% {
                  opacity: 0;
              }
          }
          
          1. 使用 fromto
          @keyframes fadeIn {
              from {
                  opacity: 1;
              }
              to {
                  opacity: 0;
              }
          }
          

          在 CSS 動畫 @keyframes 的定義中,from 等同于 0%,而 to 等同于 100%

          當(dāng)然,當(dāng)我們的關(guān)鍵幀不止 2 幀的時(shí),更推薦使用百分比定義的方式。

          除此之外,當(dāng)動畫的起始幀等同于 CSS 規(guī)則中賦予的值并且沒有設(shè)定 animation-fill-mode0%from 這一幀是可以刪除的。

          動畫狀態(tài)的高優(yōu)先級性

          我曾經(jīng)在這篇文章中 -- 深入理解 CSS(Cascading Style Sheets)中的層疊(Cascading) 講過一個(gè)很有意思的 CSS 現(xiàn)象。

          這也是很多人對 CSS 優(yōu)先級的一個(gè)認(rèn)知誤區(qū),在 CSS 中,優(yōu)先級還需要考慮選擇器的層疊(級聯(lián))順序

          只有在層疊順序相等時(shí),使用哪個(gè)值才取決于樣式的優(yōu)先級。

          那什么是層疊順序呢?

          根據(jù) CSS Cascading 4 最新標(biāo)準(zhǔn):

          CSS Cascading and Inheritance Level 5(Current Work)

          定義的當(dāng)前規(guī)范下申明的層疊順序優(yōu)先級如下(越往下的優(yōu)先級越高,下面的規(guī)則按升序排列):

          • Normal user agent declarations
          • Normal user declarations
          • Normal author declarations
          • Animation declarations
          • Important author declarations
          • Important user declarations
          • Important user agent declarations
          • Transition declarations

          簡單翻譯一下:

          按照上述算法,大概是這樣:

          過渡動畫過程中每一幀的樣式 > 用戶代理、用戶、頁面作者設(shè)置的!important樣式 > 動畫過程中每一幀的樣式優(yōu)先級 > 頁面作者、用戶、用戶代理普通樣式。

          然而,經(jīng)過多個(gè)瀏覽器的測試,實(shí)際上并不是這樣。(尷尬了)

          舉個(gè)例子,我們可以通過這個(gè)特性,覆蓋掉行內(nèi)樣式中的 !important 樣式:

          <p class="txt" style="color:red!important">123456789</p>
          
          .txt {
              animation: colorGreen 2s infinite;
          }
          @keyframes colorGreen {
              0%,
              100% {
                  color: green;
              }
          }
          

          在 Safari 瀏覽器下,上述 DEMO 文本的顏色為綠色,也就是說,處于動畫狀態(tài)中的樣式,能夠覆蓋掉行內(nèi)樣式中的 !important 樣式,屬于最最高優(yōu)先級的一種樣式,我們可以通過無限動畫、或者 animation-fill-mode: forwards,利用這個(gè)技巧,覆蓋掉本來應(yīng)該是優(yōu)先級非常非常高的行內(nèi)樣式中的 !important 樣式。

          我在早兩年的 Chrome 中也能得到同樣的結(jié)果,但是到今天(2022-01-10),最新版的 Chrome 已經(jīng)不支持動畫過程中關(guān)鍵幀樣式優(yōu)先級覆蓋行內(nèi)樣式 !important 的特性。

          對于不同瀏覽器,感興趣的同學(xué)可以利用我這個(gè) DEMO 自行嘗試,CodePen Demo - the priority of CSS Animation

          CSS 動畫的優(yōu)化

          這也是非常多人非常關(guān)心的一個(gè)重點(diǎn)。

          我的 CSS 動畫很卡,我應(yīng)該如何去優(yōu)化它?

          動畫元素生成獨(dú)立的 GraphicsLayer,強(qiáng)制開始 GPU 加速

          CSS 動畫很卡,其實(shí)是一個(gè)現(xiàn)象描述,它的本質(zhì)其實(shí)是在動畫過程中,瀏覽器刷新渲染頁面的幀率過低。通常而言,目前大多數(shù)瀏覽器刷新率為 60 次/秒,所以通常來講 FPS 為 60 frame/s 時(shí)動畫效果較好,也就是每幀的消耗時(shí)間為 16.67ms。

          頁面處于動畫變化時(shí),當(dāng)幀率低于一定數(shù)值時(shí),我們就感覺到頁面的卡頓。

          而造成幀率低的原因就是瀏覽器在一幀之間處理的事情太多了,超過了 16.67ms,要優(yōu)化每一幀的時(shí)間,又需要完整地知道瀏覽器在每一幀干了什么,這個(gè)就又涉及到了老生常談的瀏覽器渲染頁面。

          到今天,雖然不同瀏覽器的渲染過程不完全相同,但是基本上大同小異,基本上都是:

          簡化一下也就是這個(gè)圖:

          這兩張圖,你可以在非常多不同的文章中看到。

          回歸本文的重點(diǎn),Web 動畫很大一部分開銷在于層的重繪,以層為基礎(chǔ)的復(fù)合模型對渲染性能有著深遠(yuǎn)的影響。當(dāng)不需要繪制時(shí),復(fù)合操作的開銷可以忽略不計(jì),因此在試著調(diào)試渲染性能問題時(shí),首要目標(biāo)就是要避免層的重繪。那么這就給動畫的性能優(yōu)化提供了方向,減少元素的重繪與回流

          這其中,如何減少頁面的回流與重繪呢,這里就會運(yùn)用到我們常說的** GPU 加速**。

          GPU 加速的本質(zhì)其實(shí)是減少瀏覽器渲染頁面每一幀過程中的 reflow 和 repaint,其根本,就是讓需要進(jìn)行動畫的元素,生成自己的 GraphicsLayer

          瀏覽器渲染一個(gè)頁面時(shí),它使用了許多沒有暴露給開發(fā)者的中間表現(xiàn)形式,其中最重要的結(jié)構(gòu)便是層(layer)。

          在 Chrome 中,存在有不同類型的層: RenderLayer(負(fù)責(zé) DOM 子樹),GraphicsLayer(負(fù)責(zé) RenderLayer 的子樹)。

          GraphicsLayer ,它對于我們的 Web 動畫而言非常重要,通常,Chrome 會將一個(gè)層的內(nèi)容在作為紋理上傳到 GPU 前先繪制(paint)進(jìn)一個(gè)位圖中。如果內(nèi)容不會改變,那么就沒有必要重繪(repaint)層。

          而當(dāng)元素生成了自己的 GraphicsLayer 之后,在動畫過程中,Chrome 并不會始終重繪整個(gè)層,它會嘗試智能地去重繪 DOM 中失效的部分,也就是發(fā)生動畫的部分,在 Composite 之前,頁面是處于一種分層狀態(tài),借助 GPU,瀏覽器僅僅在每一幀對生成了自己獨(dú)立 GraphicsLayer 元素層進(jìn)行重繪,如此,大大的降低了整個(gè)頁面重排重繪的開銷,提升了頁面渲染的效率。

          因此,CSS 動畫(Web 動畫同理)優(yōu)化的第一條準(zhǔn)則就是讓需要動畫的元素生成了自己獨(dú)立的 GraphicsLayer,強(qiáng)制開始 GPU 加速,而我們需要知道是,GPU 加速的本質(zhì)是利用讓元素生成了自己獨(dú)立的 GraphicsLayer,降低了頁面在渲染過程中重繪重排的開銷。

          當(dāng)然,生成自己的獨(dú)立的 GraphicsLayer,不僅僅只有 transform3d api,還有非常多的方式。對于上述一大段非常繞的內(nèi)容,你可以再看看這幾篇文章:

          • 【W(wǎng)eb動畫】CSS3 3D 行星運(yùn)轉(zhuǎn) && 瀏覽器渲染原理
          • Accelerated Rendering in Chrome

          除了上述準(zhǔn)則之外,還有一些提升 CSS 動畫性能的建議:

          減少使用耗性能樣式

          不同樣式在消耗性能方面是不同的,改變一些屬性的開銷比改變其他屬性要多,因此更可能使動畫卡頓。

          例如,與改變元素的文本顏色相比,改變元素的 box-shadow 將需要開銷大很多的繪圖操作。box-shadow 屬性,從渲染角度來講十分耗性能,原因就是與其他樣式相比,它們的繪制代碼執(zhí)行時(shí)間過長。這就是說,如果一個(gè)耗性能嚴(yán)重的樣式經(jīng)常需要重繪,那么你就會遇到性能問題。

          類似的還有 CSS 3D 變換、mix-blend-modefilter,這些樣式相比其他一些簡單的操作,會更加的消耗性能。我們應(yīng)該盡可能的在動畫過程中降低其使用的頻率或者尋找替代方案。

          當(dāng)然,沒有不變的事情,在今天性能很差的樣式,可能明天就被優(yōu)化,并且瀏覽器之間也存在差異。

          因此關(guān)鍵在于,我們需要針對每一起卡頓的例子,借助開發(fā)工具來分辨出性能瓶頸所在,然后設(shè)法減少瀏覽器的工作量。學(xué)會 Chrome 開發(fā)者工具的 Performance 面板及其他渲染相關(guān)的面板非常重要,當(dāng)然這不是本文的重點(diǎn)。大家可以自行探索。

          使用 will-change 提高頁面滾動、動畫等渲染性能

          will-change 為 Web 開發(fā)者提供了一種告知瀏覽器該元素會有哪些變化的方法,這樣瀏覽器可以在元素屬性真正發(fā)生變化之前提前做好對應(yīng)的優(yōu)化準(zhǔn)備工作。 這種優(yōu)化可以將一部分復(fù)雜的計(jì)算工作提前準(zhǔn)備好,使頁面的反應(yīng)更為快速靈敏。

          值得注意的是,用好這個(gè)屬性并不是很容易:

          • 不要將 will-change 應(yīng)用到太多元素上:瀏覽器已經(jīng)盡力嘗試去優(yōu)化一切可以優(yōu)化的東西了。有一些更強(qiáng)力的優(yōu)化,如果與 will-change 結(jié)合在一起的話,有可能會消耗很多機(jī)器資源,如果過度使用的話,可能導(dǎo)致頁面響應(yīng)緩慢或者消耗非常多的資源。
          • 有節(jié)制地使用:通常,當(dāng)元素恢復(fù)到初始狀態(tài)時(shí),瀏覽器會丟棄掉之前做的優(yōu)化工作。但是如果直接在樣式表中顯式聲明了 will-change 屬性,則表示目標(biāo)元素可能會經(jīng)常變化,瀏覽器會將優(yōu)化工作保存得比之前更久。所以最佳實(shí)踐是當(dāng)元素變化之前和之后通過腳本來切換 will-change 的值。
          • 不要過早應(yīng)用 will-change 優(yōu)化:如果你的頁面在性能方面沒什么問題,則不要添加 will-change 屬性來榨取一丁點(diǎn)的速度。 will-change 的設(shè)計(jì)初衷是作為最后的優(yōu)化手段,用來嘗試解決現(xiàn)有的性能問題。它不應(yīng)該被用來預(yù)防性能問題。過度使用 will-change 會導(dǎo)致大量的內(nèi)存占用,并會導(dǎo)致更復(fù)雜的渲染過程,因?yàn)闉g覽器會試圖準(zhǔn)備可能存在的變化過程。這會導(dǎo)致更嚴(yán)重的性能問題。
          • 給它足夠的工作時(shí)間:這個(gè)屬性是用來讓頁面開發(fā)者告知瀏覽器哪些屬性可能會變化的。然后瀏覽器可以選擇在變化發(fā)生前提前去做一些優(yōu)化工作。所以給瀏覽器一點(diǎn)時(shí)間去真正做這些優(yōu)化工作是非常重要的。使用時(shí)需要嘗試去找到一些方法提前一定時(shí)間獲知元素可能發(fā)生的變化,然后為它加上 will-change 屬性。

          有人說 will-change 是良藥,也有人說是毒藥,在具體使用的時(shí)候,可以多測試一下。

          最后

          好了,本文從多個(gè)方面,由淺入深地描述了 CSS 動畫我認(rèn)為的一些比較重要、值得一講、需要注意的點(diǎn)。當(dāng)然很多地方點(diǎn)到即止,或者限于篇幅沒有完全展開,很多細(xì)節(jié)還需要讀者進(jìn)一步閱讀規(guī)范或者自行嘗試驗(yàn)證,實(shí)踐出真知,紙上得來終覺淺。

          OK,本文到此結(jié)束,希望本文對你有所幫助 :)


          主站蜘蛛池模板: 久久国产香蕉一区精品| 亚欧在线精品免费观看一区| 精品视频一区二区三区| 国产福利电影一区二区三区| 国产精品一区12p| 久久精品国产一区二区三区不卡| 一区二区三区免费视频播放器| 精品人妻一区二区三区四区| 亚洲日本中文字幕一区二区三区| 精品国产亚洲一区二区三区| 无码一区二区三区老色鬼| 乱色精品无码一区二区国产盗| 久久久久人妻一区精品性色av| 日韩最新视频一区二区三| 国内精品视频一区二区三区八戒| 亚洲国产精品一区二区第四页| 91国在线啪精品一区| 亚洲Av高清一区二区三区| 国产a久久精品一区二区三区| 农村人乱弄一区二区| 麻豆亚洲av熟女国产一区二| 国产福利日本一区二区三区| 中文字幕永久一区二区三区在线观看| 日韩内射美女人妻一区二区三区| 蜜芽亚洲av无码一区二区三区| 国产伦精品一区二区三区视频猫咪 | 精品欧洲av无码一区二区| 国产成人AV区一区二区三| 无码毛片视频一区二区本码| 午夜一区二区在线观看| 美女一区二区三区| 日本高清天码一区在线播放| 看电影来5566一区.二区| 国产精品va一区二区三区| 鲁丝片一区二区三区免费| 视频精品一区二区三区| 久久久精品一区二区三区| 日本一区二区三区日本免费| 亚洲日本一区二区三区| 亚洲一区二区三区国产精品| 亚洲av综合av一区二区三区|