- ๐ก (two) virtual Dom+Diff-vue source code to write series
- ๐ก (three) Complier template compilation – Vue source code to write series
1๏ธ. Preparation
The directory structure
โ โ โ public โ โ โ โ index. / / HTML template file โ โ โ the SRC โ โ โ โ index. The js / / test page โ โ โsourceโ โโ Vue // Vue Code โโ webPack.config.jsCopy the code
Configure the resolve
Make the project import Vue from ‘Vue’ point to the Vue in the source directory.
// webpack.config.js
module.exports = (env) = > {
return {
// ...
resolve: {
modules: [path.resolve(__dirname, 'source'), path.resolve(__dirname, 'node_modules')]}}}Copy the code
Entrance to the file
In the next section, we’ll implement initData, initComputed, initWatch, and $mount step by step.
function Vue(options) {
this._init(options)
}
Vue.prototype._init = function(options) {
let vm = this
vm.$options = options
initState(vm)
if (options.el) {
vm.$mount()
}
}
function initState (vm) {
const opts = vm.$options
if (opts.data) {
initData(vm)
}
if (opts.computed) {
initComputed(vm)
}
if (opts.watch) {
initWatch(vm)
}
}
Copy the code
2๏ธ. Observe objects and arrays
1๏ธ observation object
We’ll start this section by looking at defineProperty, which I won’t cover here.
When a Vue instance is initialized, the user configured data is passed into observe and all elements are iterated for defineReactive. When an object is encountered in the process, observe is recursively called and the entire data is redefined.
The reason for this is that we can customize getters and setters for properties, and we can define operations in them that depend on collection and view updates, which is the beginning of the reactive principle.
observe
export function observe(data) {
// If the object is not returned directly, no observation is required
if (typeofdata ! = ='object' || data === null) {
return data
}
return new Observer(data)
}
Copy the code
Observer
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = data[key]
defineReactive(data, key, value)
}
}
}
Copy the code
defineReactive
export function defineReactive(data, key, value) {
// If value is an object, you need to observe one more layer
observe(value)
Object.defineProperty(data, key, {
get() {
console.log('Get data')
return value
},
set(newValue) {
if (newValue === value) return
console.log('Update view')
// To clarify, the defineReactive execution is a closure, and when a new newValue comes in, the modified value can be shared in get.
value = newValue
}
})
}
Copy the code
In addition, data may be passed in as objects or functions that need to be handled as the data is passed in.
function initData(vm) {
let data = vm.$options.data
// Determine whether data is a function and assign data to vm._data
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
// Redefine the data inserted by the user with Object.definedProperty
observe(vm._data)
}
Copy the code
By the way, since vUE components can be reused, passing in an object can pollute multiple components referencing the same data. In practice, we pass in a function that generates a copy each time we initialize it.
After retrieving data from initData above, we attach the data to vm._data. The following operations are for this group of data.
๐งช test it out.
const vm = new Vue({
el: '#app',
data() {
return {
msg: 'hello'}}})console.log(vm._data.msg)
vm._data.msg = 'world'
Copy the code
This will print the console we wrote for accessing and modifying the MSG property.
2 ๏ธ โฃ agent vm. _data while forming
You may have noticed that we usually use VUE to retrieve data directly from the VM, rather than through vm._data as above.
So proxy data, in this case only the first layer of data can be proxy.
When accessing vm.obj.name, vm.obj (vm._data.obj) is first found, and all nested data is retrieved normally.
function proxy(vm, key, source) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key]
},
set(newValue) {
vm[source][key] = newValue
}
})
}
function initData(vm) {
// ...
// Map the _data attribute to the VM
for (const key in data) {
proxy(vm, key, '_data')}// ...
}
Copy the code
๐ Click here to see this section of code
3๏ธ observation array
Observe that all properties of the object have been accessed and modified by observe, but arrays have not been accessed. First we hijack methods that modify array data:
- push
- pop
- unshift
- shift
- sort
- reverse
- splice
To avoid contaminating the global array, we make a copy of the array prototype and modify the new prototype.
const arrayProto = Array.prototype
const newArrayProto = Object.create(arrayProto)
methods.forEach(method= > {
newArrayProto[method] = function (. args) {
// Call the original array method
const r = arrayProto[method].apply(this, args)
console.log('Called array method to set data')
return r
}
})
Copy the code
Set newArrayProto to the array passed in, and then walk through the array, observing all the elements in it.
class Observer {
constructor(data) {
if (Array.isArray(data)) {
// data.__proto__ = newArrayProto
/ / __proto__ is not recommended to use https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/proto
Object.setPrototypeOf(data, newArrayProto)
for (let i = 0; i < data.length; i++) {
observe(data[i])
}
} else {
this.walk(data)
}
}
// ...
}
Copy the code
We also need to look at a wave of new elements in the array.
methods.forEach(method= > {
newArrayProto[method] = function (. args) {
const r = arrayProto[method].apply(this, args)
// Observe the new element
let inserted
switch (method) {
case 'push':
case 'shift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
}
observeArray(inserted)
return r
}
})
function observeArray(arr) {
for (let i = 0; i < arr.length; i++) {
observe(arr[i])
}
}
Copy the code
๐งช Test it out
const vm = new Vue({
el: '#app',
data() {
return {
arr: [{ a:1 }, 1.2]
}
}
})
vm.arr[0].a = 2 // Test the nested objects in the array
vm.arr.push({ b: 1}) // Array methods hijack tests
vm.arr[3].b = 2 // Array methods add element tests
Copy the code
$set (arr[0] = 1); $set (arr[0] = 1); $set (arR [0] = 1);
๐ Click here to see this section of code
3 ๏ธ. $mount
1 ๏ธ โฃ rendering watcher
After initState, the $mount method is called if the user has configured the EL property.
function Vue (options) {
this._init(options)
}
Vue.prototype._init = function (options) {
vm.$options = options
// Initialize data Watch computed
initState(vm)
if (options.el) {
vm.$mount()
}
}
Copy the code
$Mount does two things:
- Get the el element and mount it on $el.
- Instantiate a Watcher to render the page.
function query(el) {
if (typeof el === 'string') {
return document.querySelector(el)
}
return el
}
Vue.prototype.$mount = function () {
const vm = this
let el = vm.$options.el
el = vm.$el = query(el)
// Render/update logic
const updateComponent = (a)= > {
vm._update()
}
new Watcher(vm, updateComponent)
}
Copy the code
The watcher here is called a rendered Watcher, and there are more watchers, such as computed Watcher.
You don’t need to know the concept of Watcher for now in this section. All you need to know is that New Watcher(VM, updateComponent) executes updateComponent once.
This is a brief declaration of the Watcher class, which will be extended and explained in more detail in later sections.
let id = 0 // Each watcher identifier
class Watcher {
/** ** @param {*} vm current Vue instance * @param {*} exprOrFn expression or function vm. Cb) such as' MSG '* @param {*} cb expression or function vm.$watch(' MSG ', cb) such as cb * @param {*} opts other parameters */
constructor(vm, exprOrFn, cb = () => {}, opts = {}) {
this.vm = vm
this.exprOrFn = exprOrFn
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
this.cb = cb
this.opts = opts
this.id = id++
this.get() // Get is called once by default when creating watcher
}
get() {
this.getter()
}
}
export default Watcher
Copy the code
2 ๏ธ โฃ _update
Ve2. X introduces the virtual Dom. It first parses the template into a Vdom, and then renders the Vdom into a real Dom. Generate a new Vdom, the old and new Vdom Diff, and then change the part that needs to be modified, the complete compilation process is more complex, here we do not introduce the virtual Dom, simple implementation, later will open a new article to sort out the virtual Dom and Diff.
Use createDocumentFragment to cut and paste all nodes into memory and compile the document fragment in memory.
Vue.prototype._update = function () {
const vm = this
const el = vm.$el
// Create a document fragment in memory, then operate on the document fragment, complete the replacement to the page, improve performance
const node = document.createDocumentFragment()
let firstChild
while (firstChild = el.firstChild) {
// appendChild will clip if the element exists
node.appendChild(firstChild)
}
complier(node, vm)
el.appendChild(node)
console.log('update')}Copy the code
Matches the {{}} text in the page, replacing it with the value of the real variable.
If it is an element node, continue to compile by calling complier.
/ / (? :.|\r? \n) Any character or carriage return
// Non-greedy mode '{{a}} {{b}}' ensures that two groups are identified instead of one
const defaultReg = / \ {\ {((? :.|\r? \n)+?) \}\}/g
const utils = {
getValue(vm, expr) {
const keys = expr.split('. ')
return keys.reduce((memo, current) = > {
return memo[current]
}, vm)
},
complierText(node, vm) {
// Add custom attributes to node to store the template for the first rendering
if(! node.expr) { node.expr = node.textContent }// Replace the expression in the template and update it to the node's textContentnode.textContent = node.expr.replace(defaultReg, (... args) => {return utils.getValue(vm, args[1])}}}export function complier(node, vm) {
const childNodes = node.childNodes;
// Class arrays are converted to arrays
[...childNodes].forEach(child= > {
if (child.nodeType === 1) { // Element node
complier(child, vm)
} else if (child.nodeType === 3) { // Text node
utils.complierText(child, vm)
}
})
}
Copy the code
๐งช Test that the variables on the page have been replaced correctly.
<div id="app">
{{msg}}
<div>
<div>
name: {{obj.name}}
</div>
<div>
age: {{obj.age}}
</div>
</div>
<div>
arr: {{arr}}
</div>
</div>
Copy the code
const vm = new Vue({
el: '#app',
data() {
return {
msg: 'hello, world'.obj: { name: 'forrest'.age: 11 },
arr: [1.2.3]}}})Copy the code
But if we change the attribute vm. MSG = ‘hello, sister’ now, the page does not update because there is no dependency collection at ๐
๐ Click here to see this section of code
4. Rely on collection
1๏ธ basic dependence on collection
Currently achieved:
- Hijacking objects and arrays, being able to customize what we want to do in getters and setters.
- Implements simple template parsing.
So how does Vue know if the page needs to be updated and if any changes to any set of data need to be re-rendered? Of course not, it’s only the data referenced by the page that needs to trigger a view update. And updates in vUE are component-level. You need to keep precise records of whether or not the data is referenced, and by whom, in order to determine whether or not to update, and by whom.
I think the whole dependency collection process is the most complex and core of the reactive principle, and I’ll start with a simple subscription publishing model.
An ๐ฐ :
class Dep {
constructor() {
// Store watcher observers/subscribers
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher= > watcher.update())
}
}
const dep = new Dep()
dep.addSub({
update() {
console.log('Subscriber 1')
}
})
dep.addSub({
update() {
console.log('Subscriber 2')
}
})
dep.notify()
Copy the code
There are two concepts:
dep
Publisher/subscription containerwatcher
Observer/subscriber
The above code instantiates a DEP and adds two watchers to the DEP. When dep.notify is executed, all watchers are broadcast and perform their own update method.
So the general idea of dependency collection is to declare a DEP for each property, call dep.addSub() in the getter of the property, collect dependencies when the page accesses the property, call dep.notify in the setter, and notify the view when the property is changed.
Now the question is what exactly to add when dep.addSub().
Flipping up, when we implement $mount we mention a render Watcher and declare a Watcher class.
Now modify Watcher slightly by adding an update method and attaching the current Watcher instance to dep.target before the getter is called.
import Dep from './dep'
let id = 0 // Each watcher identifier
class Watcher {
constructor(vm, exprOrFn, cb = () => {}, opts = {}) {
this.vm = vm
this.exprOrFn = exprOrFn
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
}
this.cb = cb
this.opts = opts
this.id = id++
this.get()
}
get() {
Dep.target = this PushTarget and popTarget will be covered later.
this.getter()
}
update() {
console.log('watcher update')
this.get()
}
}
Copy the code
Then modify the defineReactive method by adding addSub and dep.notify().
export function defineReactive(data, key, value) {
// ...
// Add a dep to each attribute
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
// Collect dependencies when fetching data
if (Dep.target) {
dep.addSub(Dep.target)
}
// ...
},
set(newValue) {
// Update the view when data changes
dep.notify()
// ...}})}Copy the code
๐งช test it and see that after 2 seconds the data is modified and the page is re-rendered.
<div id="app">
{{msg}}
</div>
Copy the code
const vm = new Vue({
el: '#app',
data() {
return {
msg: 'hello,world'
}
}
})
setTimeout((a)= > {
vm.msg = 'hello,guy'
}, 2000)
Copy the code
Here we comb through the entire code execution process:
new Vue()
After initializing the data, redefining the datagetter
.setter
.- Then call $mount to initialize a render watcher,
new Watcher(vm, updateComponent)
. - Watcher instantiation calls the get method, hangs the current render Watcher on dep. target, and executes the updateComponent method to render the template.
complier
Value when parsing the pagevm.msg
That triggers the propertygetter
And went tovm.msg
Add dep. target, also known as render Watcher, to deP.setTimeout
After 2 seconds, change the passwordvm.msg
, the property of the DEP is broadcast, triggeredRender the watcher
theupdate
Method, and the page is re-rendered.
2๏ธ collection optimization — DEP. Target
The most basic dependency collection is implemented at โก, but much needs to be optimized.
The get method in the Watcher class directly dep. target = this is problematic. Let’s look at the modified code first.
class Watcher {
get() {
// Add a target to the Dep that points to the current watcher
pushTarget(this)
this.getter()
// Remove the current watcher from dep.target after getter execution
popTarget()
}
}
Copy the code
const stack = []
export function pushTarget(watcher) {
Dep.target = watcher
stack.push(watcher)
}
export function popTarget() {
stack.pop()
Dep.target = stack[stack.length - 1]}Copy the code
Rendering and updating in Vue are component-level, component-by-component rendering Watcher, consider the following code.
<div id="app">
{{msg}}
<MyComponent />
{{msg2}}
</div>
Copy the code
The render function looks something like this.
renderRoot () {
...
renderMyComponent ()
...
}
Copy the code
According to our optimized code, the execution looks like this:
- Render the parent component at this time
Stack = [root render watcher]
, dep. target to root render Watcher. - When parsed to the MyComponent
Stack = [root render watcher, MyComponent render watcher]
, dep. target to MyComponent render Watcher. - After rendering MyComponent,
popTarget
Execute, at this pointStack = [root render watcher]
, dep. target to root render Watcher. - It then proceeds to render the other elements of the parent component.
Once you understand the entire rendering process, it becomes obvious that maintaining a Watcher stack ensures that the correct Watcher is collected by THE DEP for nested rendering.
๐ Click here to see this section of code
3๏ธ dependent collection optimization — filtration the same as watcher
โก To continue tuning, consider the following code:
<div id="app">
{{msg}}
{{msg}}
</div>
Copy the code
If MSG is evaluated twice, then the DEP of the MSG will store two identical render watchers, which will trigger two updates when the MSG changes.
In the above implementation, we added a unique identity ID to each DEP and watcher.
You can then have deP and Watcher remember each other, and while DEP collects watcher, Watcher keeps track of which DEPs it subscribed to.
class Dep {
// ...
depend() {
if (Dep.target) { // Call watcher's addDep method
Dep.target.addDep(this)}}}Copy the code
class Watcher {
constructor () {
// ...
this.depIds = new Set(a)this.deps = []
}
addDep(dep) {
const id = dep.id
if (!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
dep.addSub(this)}}}Copy the code
Then instead of adding watcher directly with dep.addSub(dep.target) in defineReactive, call dep.depend() and let Watcher decide whether to subscribe to the DEP.
export function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
if (Dep.target) {
dep.depend()
}
// ...
}
// ...
}
Copy the code
After this adjustment, a Watcher will be added to its OWN DEP when it gets the MSG value for the first time, and the ID of this DEP will be recorded in Watcher. When it gets the MSG for the second time, Watcher will not add the same Watcher to deP when it finds that it has subscribed to this DEP.
4๏ธ dependent collection optimization — array dependent collection
The dependency collection that โก handles so much doesn’t seem to work for arrays. We need to handle the dependency collection for arrays separately because we trigger updates in methods like arr.push and not in setters like normal properties.
We first add an __ob__ attribute to each observed object (including arrays), return the Observe instance itself, and add a DEP to each Observe instance that is specific to the array collection dependencies.
class Observe {
constructor(data) {
Object.defineProperty(data, '__ob__', {
get: (a)= > this
})
// This dep property is set specifically for arrays
this.dep = new Dep()
// ...}}Copy the code
Once added, we can retrieve the DEP in the array method.
methods.forEach(method= > {
newArrayProto[method] = function (. args) {
this.__ob__.dep.notify()
// ...}})Copy the code
Then we need to use this DEP to collect dependencies, looking at the code first.
export function defineReactive(data, key, value) {
// Only return an Observe instance if value is an array or an object
// This Observe instance is only an intermediary. The key is deP passing.
const obs = observe(value)
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
if (Dep.target) {
dep.depend()
// When value is an array, the instance has a deP property whose notify permission is granted to the array's methods
if (obs) {
obs.dep.depend()
}
}
return value
}
})
}
Copy the code
Let’s say our current data looks like this.
const new Vue({
data() {
return {
arr: [1.2.3]}}})Copy the code
The arR array defineReactive has two DEPs. The first is the DEP stored in the array and the second is the DEP declared for each attribute. When the page references arR, both dePs collect watcher. Arr.push (1), which triggers notify of the first DEP, updates the page. The assignment of arr = [0] triggers notify of the second DEP, which also updates the page.
๐ Click here to see this section of code
Finally, to solve the problem of nested array dependency collection, consider the following data.
const vm = new Vue({
data() {
return {
arr: [1.2.3[1.2]]}}})Copy the code
When we modify the data, vm.arr[3].push(3) does not update correctly because, like vm.arr[1] = 0, we do not look at the index of the array.
The nested arrays [1, 2] inside are not entered into defineReactive during observation.
A dependArray method is added to collect dependencies in the outer ARR as well as subarrays.
DependArray (deP) : dependArray (deP) : dependArray (deP) : dependArray (deP) : dependArray (deP) : dependArray (deP) : dependArray (deP) : dependArray (deP) : dependArray (deP) : dependArray (deP)
export function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
if (Dep.target) {
dep.depend()
if (obs) {
obs.dep.depend()
// Handle dependency collection of nested arrays
dependArray(value)
}
}
return value
}
})
}
Copy the code
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
const item = value[i]
item.__ob__ && item.__ob__.dep.depend()
if (Array.isArray(item)) {
dependArray(item)
}
}
}
Copy the code
Now that I’ve covered the basics of dependency collection, and the code is pretty much the same as the source code, I’ve got everything I need to know about computed and $watch, because they’re all based on the Watcher class, with some additional caching and callback functions.
๐ Click here to see this section of code
5. Batch updates
1๏ธ asynchronous renewal
Consider the following code: how many times does the page need to be rerendered after 1 second, and what is the value of MSG? We want to end up rendering only once, with the MSG value set to ‘fee’.
The result was that the page was re-rendered four times, even though the data was up to date.
const vm = new Vue({
data() {
return {
msg: 'hello, world'.obj: {
a: '123'
}
}
}
})
setTimeout((a)= > {
vm.msg = 'bar'
vm.msg = 'foo'
vm.msg = 'fee'
vm.obj.a = 'goo'
}, 1000)
Copy the code
Now you need to change the synchronous update to the asynchronous update and wait for the synchronized code to complete the update.
class Watcher {
update() {
console.log('update')
queueWatcher(this)
},
run() {
console.log('run')
this.get()
}
}
Copy the code
It’s not enough to simply update asynchronously, the number of updates is still the same, and we need to merge the same Watcher.
const queueIds = new Set(a)let queue = []
function flaskQueue() {
if(! queue.length)return
queue.forEach(watcher= > watcher.run())
queueIds.clear()
queue = []
}
function queueWatcher(watcher) {
const id = watcher.id
if(! queueIds.has(id)) { queueIds.add(id) queue.push(watcher)// TODO replace with nextTick
setTimeout(flaskQueue, 0)}}Copy the code
Each update notification will add an update task to the queue, and when the synchronized code is finished, the queue will be emptied. The final output on the page will be 4 updates and 1 run, as expected, and only one render.
๐ Click here to see this section of code
2 ๏ธ โฃ nextTick
Vue.$nextTick(cb) is used when we modify a set of data and want to do something after the view is updated.
vm.msg = 'hi'
vm.$nextTick((a)= > {
console.log('View updated')})console.log('I'm going to execute first because I'm synchronizing code.')
Copy the code
NextTick also maintains a queue of events that it empties when synchronous events are completed, just like queueWatcher, but with some compatibility and optimizations for browser API support.
In asynchronous queues, microtasks take higher priority, so Promise is preferred over setTimeout. There are also several asynchronous apis, in order of priority:
- Promise(Microtasks)
- MutationObserver(Microtasks)
- SetImmediate (Macro task)
- SetTimeout (macro task)
const callbacks = []
function flushCallbacks() {
callbacks.forEach(cb= > cb())
}
export default function nextTick(cb) {
callbacks.push(cb)
const timerFunc = (a)= > {
flushCallbacks()
}
if (Promise) {
return Promise.resolve().then(flushCallbacks)
}
if (MutationObserver) {
const observer = new MutationObserver(timerFunc)
const textNode = document.createTextNode('1')
observer.observe(textNode, { characterData: true })
textNode.textContent = '2'
return
}
if (setImmediate) {
return setImmediate(timerFunc)
}
setTimeout(timerFunc, 0)}Copy the code
The nextTick implementation replaces queueWatcher’s setTimeout as well.
function queueWatcher(watcher) {
const id = watcher.id
if(! queueIds.has(id)) { queueIds.add(id) queue.push(watcher) nextTick(flaskQueue) } }Copy the code
To review the original code, vm. MSG triggers the render Watcher update method, which adds a flaskQueue task to nextTick, and the user calls vm.$nextTick(cb), which adds another task to nextTick. So you end up rendering the page first and then printing the view when it’s updated.
vm.msg = 'hi'
vm.$nextTick((a)= > {
console.log('View updated')})Copy the code
๐ Click here to see this section of code
6 ๏ธ. $watch
1 two usages of ๏ธ watch
There are two ways to use watch, the first is to call vm.$watch directly, and the second is to configure the Watch property in the options.
const vm = new Vue({
data() {
return {
msg: 'hello'}},watch: {
msg(newVal, oldVal) {
console.log({ newVal, oldVal })
}
}
})
vm.$watch('msg'.function(newVal, oldVal) {
console.log({ newVal, oldVal })
})
Copy the code
In addition to configuring a handler function, you can also configure an object.
vm.$watch('msg', {
handler: function(newVal, oldVal) {
console.log({ newVal, oldVal })
},
immediate: true
})
Copy the code
You can also configure it as an array, so we’re not going to worry about arrays, we’re going to implement the core functions.
In fact, we only need to implement a vm.$watch, because the watch configured in the options also calls this method internally.
The $watch function does two things:
- UseDef separates handler and other parameters and is compatible with the configuration of functions and objects.
- New a Watcher, and increment
{ user: true }
Marked as user Watcher.
Vue.prototype.$watch = function (expr, useDef) {
const vm = this
let handler = useDef
const opts = { user: true }
if (useDef.handler) {
handler = useDef.handler
Object.assign(opts, useDef)
}
new Watcher(vm, expr, handler, opts)
}
Copy the code
2๏ธ internal principle of $watch
โก Let’s take a look inside Watcher.
We first convert the passed expression to a function, for example ‘MSG’ to utils.getValue(vm, ‘MSG ‘).
This step is critical because by default, new Watcher calls the get method once, and then executes the getter function. This process triggers the getter of the MSG, and causes the MSG DEP to add a user Watcher to complete the dependency collection.
if (typeof exprOrFn === 'function') {
// The updateComponent passed in before will go here
this.getter = exprOrFn
} else if (typeof exprOrFn === 'string') {
// Implementation $watch will go here
this.getter = function () {
return utils.getValue(vm, exprOrFn)
}
}
Copy the code
And then we want to return a new value, an old value in the callback function, so we need to record the value returned by the getter.
class Watcher {
constructor() {
// ...
this.value = this.get()
},
get() {
pushTarget(this)
const value = this.getter()
popTarget()
return value
}
}
Copy the code
After the dependency collection is complete, when MSG changes, the user watcher’s run method will be triggered, so we modify this method to execute the cb of the watcher.
class Watcher {
run() {
const newValue = this.get()
// Compare the old and new values and execute the user-added handler
if(newValue ! = =this.value) {
this.cb(newValue, this.value)
this.value = newValue
}
}
}
Copy the code
Finally, we’ll take a quick look at the immediate parameter, which is used to start cb execution once.
class Watcher {
constructor() {
// ...
if (this.immediate) {
this.cb(this.value)
}
}
}
Copy the code
After the $watch method is implemented, it iterates through the watch configuration in the options, calling vm.$watch one by one.
export function initState (vm) {
const opts = vm.$options
if (opts.data) {
initData(vm)
}
if (opts.watch) {
initWatch(vm)
}
}
Copy the code
function initWatch(vm) {
const watch = vm.$options.watch
for (const key in watch) {
const useDef = watch[key]
vm.$watch(key, useDef)
}
}
Copy the code
3๏ธ DEP and Watcher’s comb
$watcher ($watch, $watch, $dep, $watch, $watcher, $dep, $watcher)
<div id="app"> {{msg}} </div> const vm = new Vue({ data() { return { msg: 'hello' } }, watch: {MSG (newVal, oldVal) {console.log(' MSG monitor watcher1') console.log({newVal, oldVal})}}) vm. Function (newVal, oldVal) {console.log(' MSG monitor watcher2') console.log({newVal, oldVal})}) function(newVal, oldVal) {console.log(' MSG monitor watcher2')})Copy the code
At this point, when the value of MSG is updated, the page will be re-rendered and output MSG monitor watcher1, MSG monitor watcher2.
๐ Click here to see this section of code
๐ Click here to see this section of code
7 ๏ธ. Computed
1๏ธ one small target
Computed has the following characteristics:
- Every computed is a Watcher.
- Computed does not perform initially, but is referenced before the return value is computed.
- Computed returns the cached value if the dependency does not change.
- Computed needs to be defined on the VM.
We now have a small goal of rendering the following computed properly to the page.
// html
<div>{{fullName}}</div>
// js
const vm = new Vue({
data() {
return {
firstName: 'Forrest',
lastName: 'Lau'
}
},
computed() {
fullName() {
return this.firstName + this.lastName
}
}
})
Copy the code
First initialize a Watcher for each computed, and then define the attributes on the VM.
function initComputed(vm) {
const computed = vm.$options.computed
for (const key in computed) {
const watcher = new Watcher(vm, computed[key], () => {}, {})
// Define the calculated properties on the VM.
Object.defineProperty(vm, key, {
get() {
return watcher.value
}
})
}
}
Copy the code
Then we modify the get method in the Watcher so that the “this” in the getter is referred to the VM, so that the fullName method can execute normally, and the “firstName” and “lastName” results are attached to the value in the Watcher.
class Watcher {
get() {
pushTarget(this)
const value = this.getter.call(this.vm)
popTarget()
return value
}
}
Copy the code
ForrestLau is rendered on the ๐ page, next target.
2 ๏ธ โฃ lazy is calculated
Currently all computed computations perform computations at initialization, and we want to default to starting without computations and waiting for the page to reference them, so we add a lazy configuration that disables the getter by default, and then add an evaluate method to Watcher, Call evaluate when the page is evaluated.
function initComputed(vm) {
const computed = vm.$options.computed
for (const key in computed) {
const watcher = new Watcher(vm, computed[key], () => {}, { lazy: true })
Object.defineProperty(vm, key, {
get() {
watcher.evaluate()
return watcher.value
}
})
}
}
Copy the code
class Watcher {
constructor(opts) {
// If it is a calculated attribute, it will not be evaluated at first by default
this.value = this.lazy ? undefined : this.get()
}
evaluate() {
this.value = this.get()
}
}
Copy the code
3 ๏ธ โฃ computed cache
๐ To set up cache for computed data, first add a dirty attribute to Watcher to indicate whether the current computed Watcher needs to be recalculated.
Dirty defaults to true, there is no cache to evaluate, then becomes false after evaluate, and only becomes true again when relying on updates.
class Watcher {
constructor(opts) {
this.dirty = this.lazy
}
evaluate() {
this.value = this.get()
this.dirty = false
}
update() {
if (this.lazy) {
// Calculate the property watcher update by changing dirty to true
// Evaluate is re-evaluated when the evaluated attribute is obtained
this.dirty = true
} else {
queueWatcher(this)}}}Copy the code
Then modify the getter method that evaluates the property.
function initComputed(vm) {
const computed = vm.$options.computed
for (const key in computed) {
const watcher = new Watcher(vm, computed[key], () => {}, { lazy: true })
Object.defineProperty(vm, key, {
get () {
if (watcher) {
// Re-evaluate is needed only when dependencies change
if (watcher.dirty) {
watcher.evaluate()
}
}
return watcher.value
}
})
}
}
Copy the code
4๏ธ Dependent collection for computed retail
So far, the initialization and the value are fine, but when we try to change firstName or lastName, we find that the page is not updated because there is only a computed watcher in the DEP of these two attributes, and when firstName changes, The UPDATE method that triggers fullName computed Watcher simply changes dirty to true.
We need to add a render watcher for both firstName and lastName, so that when one of their properties changes, we will first set dirty to true, and then re-render. In the process, we will fetch the fullName value and find that dirty is true. It then calls evaluate to recalculate, and that’s what makes sense.
First we’ll add a Depend method to Watcher.
class Watcher {
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
Copy the code
Make a call in the getter for computed and find that all the above problems have been solved.
function initComputed(vm) {
const computed = vm.$options.computed
for (const key in computed) {
const watcher = new Watcher(vm, computed[key], () => {}, { lazy: true })
Object.defineProperty(vm, key, {
get () {
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
// Add this
if (Dep.target) {
watcher.depend()
}
}
return watcher.value
}
})
}
}
Copy the code
What happened to this process? ๐ค๏ธ before we write the dependency collection, we declared a stack to hold the watcher, let’s take a look at the various stages of the stack and the dep.target point.
The key is step 2 and step 3. When step 2 executes the evaluate method, it calls the get method for computed watcher, pushes target before evaluating, and adds a computed watcher to the stack. And have dep. target point to this computed watcher, get firstName and lastName, fire their setters, and add the current watcher to their Dep, That’s dp. Target fullName computed watcher.
Dep stores the watcher as follows:
- firstName dep:
[fullName computed watcher]
- lastName dep:
[fullName computed watcher]
In step 3, after evaluating, perform popTarget, remove computed Watcher from the stack, return the pointer to Dep. Target to render Watcher, and then perform the critical step: All OF the DEPs in fullName computed Watcher are computed and their own depend methods are called, and the dep. Target points to render Watcher. Successfully added a render Watcher for both firstName and lastName.
// Add this
if (Dep.target) {
watcher.depend()
}
Copy the code
Dep stores the watcher as follows:
- firstName dep:
[fullName Computed Watcher, render Watcher]
- lastName dep:
[fullName Computed Watcher, render Watcher]
๐น๐น has been fully implemented for computed here, and the whole principle of responsiveness has been completed, and here is the full code ๐น๐น