เรื่องราวการสอนด้วยตนเองของ JL #25

ซีรีส์ [เรื่องราวการสอนตนเองของ JL]

[Net Ninja] JS Regular Expressions
[Net Ninja] Vue JS2 (Part1, Part2, Part3)
[Net Ninja] Vuex
[Net Ninja] Python3 (Part1, Part2, Part3)
[Net Ninja] Django (Part1, Part2, Part3)
[Net Ninja] Sass (Part1, Part2)
[Sean Larkin] Webpack4 (Part1, Part2, Part3, Part4)
[Sarah Drasner] VueJS (Part1, Part2, Part3, Part4, 
                       Part5, Part6, Part7)
[Evan You] Advanced VueJS (Part1(current), Part2, Part3,
                           Part4, Part5, Part6, Part7)

😃 นี่เป็นส่วน ส่วนแรก ของบทสรุปของฉันเกี่ยวกับ “ฟีเจอร์ Advanced Vue.js จาก Ground Up” ของ Evan You ใน FrontendMasters

[1. Reactivity]
    1-1. Introducing Reactivity
    1-2. Challenge 1: Getters & Setters
    1-3. Challenge 2: Dependency Tracker
    1-4. Challenge 3: Mini Observer

[ 1–1. แนะนำปฏิกิริยา ]

Vue ติดตามการเปลี่ยนแปลงอย่างไร = การเปลี่ยนแปลงสถานะสะท้อนถึงการเปลี่ยนแปลงใน DOM อย่างไร

เริ่มต้นด้วยการเขียนโปรแกรมง่ายๆ เรามีตัวแปร a ซึ่งมีค่าเป็น 3 เจ้านายของคุณให้ข้อกำหนดแก่คุณ: ค่าของตัวแปร b ควรเป็น 10 เท่าของค่าของ a เสมอ

let a = 3;
let b = a * 10;
console.log(b) // 30

› ทุกอย่างดูดีไปหมด

a = 4;
console.log(b) // 30

› หลังจากนั้น ระบบจะขอให้คุณเปลี่ยน a เป็น 4 ตอนนี้สิ่งต่างๆ ไม่ตรงกัน เนื่องจากคุณลืมอัปเดต b! ('b' ไม่ได้รับการอัปเดตโดยอัตโนมัติ เนื่องจากเป็นขั้นตอน จึงไม่รักษาความสัมพันธ์ให้ตรงกัน)

b = a * 10;
console.log(b) // 40

> ที่ตายตัว. แต่ตอนนี้เรามีการทำซ้ำและแหล่งที่มาของข้อบกพร่องเมื่อใดก็ตามที่เราต้องการเปลี่ยนแปลง a

แทนที่จะต้องอัปเดต b ด้วยตนเองทุกครั้งที่คุณเปลี่ยน a เราต้องการให้ความสัมพันธ์นั้นได้รับการเปิดเผย ดังนั้นเราจึงตระหนักดีว่าสิ่งที่ผู้จัดการของคุณต้องการให้คุณสร้างจริงๆ คือสเปรดชีต

› ค่าของเซลล์ B1 จะแสดงออกมาอย่างชัดเจนด้วยสูตร ดังนั้นจึงอัปเดตโดยอัตโนมัติทุกครั้งที่คุณแก้ไขเซลล์ A1

เราจะแสดงสิ่งนั้นในแง่การเขียนโปรแกรมอย่างไร

onAChanged(() => {
  b = a * 10
})

› เพื่อให้เป็นไปตามข้อกำหนดของโปรแกรมของเราด้วยวิธีที่ง่ายที่สุด สมมติว่าเราใช้ฟังก์ชันมหัศจรรย์นี้: onAChanged คุณสามารถคิดได้ว่าเป็นผู้เฝ้าดู การติดต่อกลับเหตุการณ์ การสมัครรับข้อมูล ไม่สำคัญ สิ่งสำคัญในตอนนี้คือ b จะปฏิบัติตามข้อกำหนดที่เป็น 10 เท่าของ a เสมอ ตราบใดที่เราใช้ฟังก์ชันนี้อย่างถูกต้อง

ก่อนที่เราจะเจาะลึกถึงวิธีการใช้ฟังก์ชันนั้น เรามาแปลปัญหาเป็นสิ่งที่ใกล้เคียงกับการพัฒนาเว็บกันดีกว่า

<span class="cell b1"></span>
document
  .querySelector(‘.cell.b1’)
  .textContent = state.a * 10

› เราต้องการให้แน่ใจว่าเซลล์ 'b1' ได้รับการซิงค์อยู่เสมอ เช่นเดียวกับโค้ดความจำเป็นเริ่มต้นของเรา เราต้องเข้าถึง DOM และอัปเดตเนื้อหาข้อความของเซลล์โดยไม่จำเป็น

<span class="cell b1"></span>
onStateChanged(() => {
  document
    .querySelector(‘.cell.b1’)
    .textContent = state.a * 10
})

› เพื่อให้มันชัดเจน เพียงแค่ห่อมันไว้ในฟังก์ชั่นเวทย์มนตร์ของเรา!

<span class="cell b1">
  {{ state.a * 10 }}
</span>
onStateChanged(() => {
  view = render(state)
})

ด้วยการแยกออกไปไกลออกไป เราจึงได้สร้างไลบรารี Vue ขึ้นมา

  • ยิ่งไปกว่านั้น เรามาเปลี่ยนมาร์กอัปให้เป็นเทมเพลตกันดีกว่า โดยปกติเทมเพลตจะคอมไพล์เป็นฟังก์ชันการเรนเดอร์ ดังนั้นโค้ด JavaScript ของเราจึงสามารถทำให้ง่ายขึ้นได้
  • เทมเพลต (หรือฟังก์ชันการเรนเดอร์ JSX) คือโครงสร้างที่ช่วยให้เราสามารถประกาศความสัมพันธ์ระหว่างสถานะและมุมมอง กล่าวอีกนัยหนึ่ง เทมเพลตจะเทียบเท่ากับสูตรในสเปรดชีต
onStateChanged(() => {
  view = render(state)
})

การแสดงภายใน “view = render(state)” นี้เป็นการแสดงนามธรรมระดับที่สูงมากว่าระบบการเรนเดอร์ Vue ทั้งหมดทำงานอย่างไร

การมอบหมายให้ view นี้สามารถตีความได้แตกต่างกันไปขึ้นอยู่กับว่าคุณมองอย่างไร ในบริบท DOM เสมือน เราสามารถคิดได้ว่า view เป็นแผนผัง DOM เสมือนใหม่ แต่โดยทั่วไปแล้ว ให้ข้าม DOM เสมือนและคิดว่ามันเป็นการใช้ผลข้างเคียงของการกลายพันธุ์ DOM

พูดตามตรง นี่เป็นเพียงครึ่งเดียวของภาพ: ยังมีคำถามว่าจะแมปอินพุตของผู้ใช้กับการเปลี่ยนแปลงสถานะอย่างเปิดเผยได้อย่างไร ในแง่ Cycle นั่นคือครึ่งหนึ่งที่เชื่อมโยงความตั้งใจของผู้ใช้กับสถานะ นั่นคือโดเมนที่ Rx เก่งมาก แต่เมื่อพิจารณาจากความยาวของการพูดคุยนี้ Evan You จึงมุ่งเน้นไปที่ส่วน state -> view

ตอนนี้คำถามคือ “แอปพลิเคชันรู้ได้อย่างไรว่าเมื่อใดควรเรียกใช้ฟังก์ชันอัปเดตนี้อีกครั้ง”

onStateChanged(() => {
  view = render(state)
})

มาหาคำตอบกัน! นี่เป็นเวอร์ชันที่สร้างสรรค์อย่างยิ่งเกี่ยวกับวิธีการทำงาน:

let update, state
const onStateChanged = _update => {
  update = _update
}

const setState = newState => {
  state = newState
  update()
}

› แทนที่จะอนุญาตให้ผู้ใช้จัดการสถานะโดยพลการ เราต้องการให้ผู้ใช้เรียกใช้ฟังก์ชันเพื่อจัดการสถานะเสมอ ฟังก์ชันนี้เรียกว่า
setState

...และว้าว เราได้ใช้งาน React แล้ว! (แม้ว่าจะเป็นเวอร์ชันที่เรียบง่ายมากเพียงเพื่อให้เข้าใจประเด็น)

onStateChanged(() => {
  view = render(state)
})

setState({ a: 5 })

ตอบสนอง บังคับให้คุณทริกเกอร์การเปลี่ยนแปลงสถานะของ setState ของคุณ

onStateChanged(() => {
  view = render(state)
})
state.a = 5

› เราสามารถจัดการสถานะได้โดยตรง โดยไม่ต้อง โดยไม่ต้องเรียก setState ใน AngularJS จะใช้การตรวจสอบสกปรกที่สกัดกั้นเหตุการณ์ เช่น การคลิก เพื่อดำเนินการรอบสรุปและตรวจสอบทุกสิ่งเพื่อดูว่ามีอะไรเปลี่ยนแปลงหรือไม่ ใน VueJS จะแปลงออบเจ็กต์สถานะให้เป็น reactive ด้วยการใช้ ES5 object.defineProperty API Vue จะแปลงคุณสมบัติเหล่านี้เป็น getters และ setters ในกรณีของ state.a Vue จะแปลง a เป็น getter และ setter

autorun(() => {
  console.log(state.count)
})

› หากเราเปลี่ยนชื่อฟังก์ชัน onStateChanged เป็นฟังก์ชัน การทำงานอัตโนมัติ นี่เป็นรูปแบบพื้นฐานของระบบการติดตามการพึ่งพาที่แชร์กันทั่วไปใน Knockout.js, Meteor Tracker , Vue.js และ MobX (รูปแบบการจัดการสถานะสำหรับ React)

[ 1–2. ความท้าทายที่ 1: Getters & Setters ]

คำแนะนำ:

ใช้ฟังก์ชัน convert ที่:

  • ใช้ Object เป็นอาร์กิวเมนต์
  • แปลงคุณสมบัติของวัตถุในตำแหน่งเป็น getter/setters โดยใช้ Object.defineProperty
  • วัตถุที่แปลงแล้วควรคงลักษณะการทำงานดั้งเดิมไว้ แต่ในขณะเดียวกันก็ บันทึก การดำเนินการ get/set ทั้งหมด

ที่คาดหวัง:

let realValue
Object.defineProperty(obj, 'foo', {
  get(){
    return 'bar'
  },
  set(newValue){
  
  }
})
const obj = { foo: 123 }
convert(obj)
obj.foo // should log: 'getting key "foo": 123'
obj.foo = 234 // should log: 'setting key "foo" to 234'
obj.foo // should log: 'getting key "foo": 234'

แม่แบบ:

<script>
function convert(obj){
  // Implement this!
</script>

สารละลาย:

[ 1–3. ความท้าทายที่ 2: ตัวติดตามการพึ่งพา ]

คำแนะนำ:

งานคือการเชื่อมโยงอินสแตนซ์ของการขึ้นต่อกันกับการคำนวณ

  • สร้างคลาส Dep ด้วยสองวิธี: depend และ notify

›› เชื่อมโยง การคำนวณ กับ การพึ่งพา การคำนวณ ควรได้รับการพิจารณาว่าเป็นสมาชิกของ การพึ่งพา

  • สร้างฟังก์ชัน autorun ที่ใช้ฟังก์ชันอัปเดต (การคำนวณ)

›› เมื่อคุณเข้าสู่ฟังก์ชั่น ทุกอย่างจะพิเศษขึ้น เราอยู่ในโซน ปฏิกิริยา

  • ภายในฟังก์ชันตัวอัปเดต คุณสามารถพึ่งพาอินสแตนซ์ของ Dep ได้อย่างชัดเจนโดยการเรียก dep.depend()

›› เมื่อคุณอยู่ในโซน ปฏิกิริยา คุณสามารถลงทะเบียน การขึ้นต่อกัน ได้

  • หลังจากนั้น คุณสามารถทริกเกอร์ฟังก์ชันตัวอัปเดตให้ทำงานอีกครั้งได้โดยโทร dep.notify()

ที่คาดหวัง:

const dep = new Dep()
autorun(() => {
  dep.depend()
  console.log('updated')
})
// should log: "updated"
dep.notify()
// should log: "updated"

แม่แบบ:

<script>
// a class representing a dependency
// exposing it on window is necessary for testing
window.Dep = class Dep {
  // Implement this!
}
function autorun (update) {
  // Implement this!
}
</script>

เทมเพลตพร้อมคำแนะนำ:

  • JavaScript เป็นแบบเธรดเดียว ในเวลาใดก็ตาม สามารถดำเนินการได้เพียงฟังก์ชันเดียวเท่านั้น ดังนั้นการสร้างฟังก์ชันจะทำเครื่องหมายตัวเองว่าเป็นฟังก์ชันที่กำลังดำเนินการอยู่ จากนั้นเราจะทราบได้ตลอดเวลาว่าฟังก์ชันนี้กำลังทำงานอยู่หรือไม่ (=ว่าเราอยู่ในฟังก์ชันนี้หรือไม่)
  • เมื่อใดก็ตามที่เราเรียก wrappedUpdate() โค้ดภายในฟังก์ชันจะทำงาน รับประกันว่ารหัสจะสามารถเข้าถึงรหัสภายนอกฟังก์ชันผ่านตัวแปร global activeUpdate ดังนั้นคลาสการพึ่งพาของเราสามารถเข้าถึง activeUpdate ได้
  • ออบเจ็กต์ Set ช่วยให้คุณจัดเก็บค่าที่ไม่ซ้ำกันทุกประเภท ไม่ว่าจะเป็น "ค่าดั้งเดิม" หรือการอ้างอิงออบเจ็กต์

สารละลาย:

  • เราลงทะเบียน wrappedUpdate() เป็นการอัปเดตที่ใช้งานอยู่ ดังนั้น เมื่อการขึ้นต่อกันของเราเปลี่ยนแปลงและฟังก์ชันอัปเดตถูกเรียกอีกครั้ง เราจะเรียก wrappedUpdate() อีกครั้ง ดังนั้นเคล็ดลับการติดตามการพึ่งพาของเราจะยังคงใช้ได้กับการทำซ้ำในอนาคต ดังนั้นจึงรวบรวมการพึ่งพาอย่างต่อเนื่อง

›› นี่เป็นสิ่งสำคัญ เนื่องจากฟังก์ชันการอัปเดตของเราอาจมีเงื่อนไขในบางกรณี ดังนั้นเมื่อตัวแปรตัวหนึ่งเป็นจริง ฟังก์ชันอาจรวบรวมการขึ้นต่อกันบางอย่าง และเมื่อตัวแปรอื่นถูกพลิก ตัวแปรนั้นอาจรวบรวมการขึ้นต่อกันอื่นๆ ดังนั้น ระบบรวบรวมการขึ้นต่อกันของเราต้อง แบบไดนามิก ปรับสมดุล การขึ้นต่อกัน เพื่อให้แน่ใจว่ารายการ การขึ้นต่อกัน เป็นข้อมูลล่าสุดอยู่เสมอ

››› แม้ว่าเครื่องมือติดตามการพึ่งพาของเราจะใช้งานได้กับกรณีทดสอบของเรา แต่ก็พลาดกรณี Edge บางส่วนไป (เช่น คุณจะจัดการกับอาร์เรย์อย่างไร แล้วคุณสมบัติที่เพิ่มเข้ามาใหม่ล่ะ)

[ 1–4. ความท้าทายที่ 3: ผู้สังเกตการณ์จิ๋ว ]

คำแนะนำ:

ตอนนี้เป็นเวลาที่จะรวบรวมความท้าทายทั้งสองก่อนหน้านี้เข้าด้วยกัน รวมสองฟังก์ชันก่อนหน้านี้ เปลี่ยนชื่อ convert() (จาก Challenge 1) เป็น observe() และคง autorun() (จาก Challenge 2):

  • observe() แปลงคุณสมบัติใน วัตถุ ที่ได้รับ และทำให้มัน มีปฏิกิริยา สำหรับคุณสมบัติ ที่แปลงแล้ว แต่ละรายการ ได้รับการกำหนดอินสแตนซ์ Dep ซึ่งติดตามรายการฟังก์ชัน อัปเดต ที่สมัครรับข้อมูล และทริกเกอร์ให้ทำงานอีกครั้งเมื่อ ตัวตั้งค่า ถูกเรียกใช้
  • autorun() รับฟังก์ชัน อัปเดต และเรียกใช้อีกครั้งเมื่อคุณสมบัติที่ฟังก์ชัน อัปเดต สมัครรับข้อมูลมีการเปลี่ยนแปลง ฟังก์ชัน อัปเดต เรียกว่า "สมัครสมาชิก" กับพร็อพเพอร์ตี้หากอาศัยพร็อพเพอร์ตี้นั้นในระหว่างการประเมิน

คำแนะนำ (ในคำอื่น ๆ ):

  • ใช้ Dep คลาส & autorun() จาก Challenge 2
  • คัดลอกและแก้ไข convert() จาก Challenge 1 เพื่อเชื่อมต่อ dep.depend() เพื่อให้ dep.notify() โทรไปที่ getters และ setters
  • เมื่อเชื่อมต่อ autorun() และ convert()(เปลี่ยนชื่อเป็น observe()) เราสามารถเข้าถึงคุณสมบัติที่รวบรวมการอ้างอิงโดยการเรียก dep.depend() การเปลี่ยนแปลงคุณสมบัติทำให้ dep.notify() ทริกเกอร์การเปลี่ยนแปลง

ที่คาดหวัง:

const state = {
  count: 0
}
observe(state)
autorun(() => {
  console.log(state.count)
  // should call dep.depend() "getter"
})
// should immediately log "count is: 0"
state.count++
// should log "count is: 1"
// should call dep.notify() "setter"

แม่แบบ:

<script>
function observe (obj) {
  // Implement this!
}
function autorun (update) {
  // Implement this!
}
</script>

สารละลาย:

ผู้สอนประจำชั้นเรียน “Evan You” เป็นผู้สร้างและหัวหน้าโครงการของ Vue.js

สามารถพบได้ที่ เว็บไซต์ส่วนตัวของเขา, GitHub, LinkedIn, Twitter, Medium(Evan You) และ Frontend Masters

ขอบคุณที่อ่าน! 💕 ถ้าคุณชอบโพสต์บนบล็อกนี้ กรุณาปรบมือ👏