"Vue.js Design and Implementation" reading notes

start

"Vue.js Design and Implementation" is a new book published in 2022. The price is relatively expensive, but it is worth the money, and it is very detailed and transparent

The title is "Design and Implementation". This book is all about the principles of Vue3, how to design, and how to implement the most basic functions through code.

The main module of Vue3

From the table of contents of this book, it can be seen that

  • Response system: monitor variable data and trigger a callback function when the data changes
  • Renderer: mount or update VDOM to real DOM, which involves diff algorithm
  • Componentization: Support splitting a large system into several components to form a component tree
  • Compiler: Compile Vue templates into JS code (corresponding to JSX in React)

Knowledge point record

Imperative and Declarative

  • Imperative - focus on process, e.g. DOM manipulation with jQuery
  • Declarative - focus on results, such as binding events in Vue templates, using interpolation and directives

In front-end development , whether it is jQuery Vue or React, it is actually a combination of the two: use declarative style to write UI configuration, and use imperative style to do business logic processing.

But Vue tends to be more declarative, mainly due to its responsiveness. After defining the data and template, you can directly modify the value of the data attribute without executing any special commands. In contrast, React's setState is imperative, and of course JSX is declarative.

In principle, the performance of the imperative method will be better, because you can directly operate the basic API, which is simple and rude. But the declarative style is easier to expand and maintain, and the performance is not bad.

Why use VDOM

 In the same way, the performance of VDOM will not be better than that of directly manipulating DOM, and the more basic and lower-level API, the better the performance.

However, VDOM combined with the diff algorithm can gain advantages in a large number of DOM updates, because at this time, direct DOM operations will bring extremely high complexity, development is very difficult, and maintenance costs are also high.

Responsive basic design ideas

  • Use Proxy to monitor get set of data attributes
  • When getting, record effectFn to a WeakMap (recorded separately by attribute) - so, if you want to implement responsiveness, you must first execute effectFn once, that is, touch
  • When set, find all effectFn in the Map, and then trigger them respectively
bucket: WeakMap {
    target1: Map {
        key1: Set[
            fn1,
            fn2,
            fn3
        ],
        key1: Set[ ... ]
    },
    target2: Map { ... }
} 

 There are many other situations, such as: ternary expressions, nested effectFn, circular calls, etc., which are explained in detail in the book.

computed fundamentals

Responsive supports self-configuration of the "execution scheduling" function, which can be passed in  lazy so that the effectFn will not be triggered immediately, but needs to be executed manually.

effect(
 () => { console.log(obj.foo) },
 {
     lazy: true // obj.foo 被修改时,函数不会被触发
 }
) 

Computed in Vue is also passively triggered in this way , not actively executed. computed can be defined like this

function computed(getter) {
    const effectFn = effect(getter, { lazy: true })
    
    const obj = {
        get value() {
            return effectFn() // 当读取 .value 时再执行 effctFn
        }
    }
    return obj
} 

Another important function of computed is to cache calculation results, which can be combined with  scheduler the scheduling function to achieve caching, which is very simple

function computed(getter) {
    let value
    let dirty = true // 默认为缓存失效

    const effectFn = effect(getter, {
        lazy: true, // 修改数据不会触发 getter
        scheduler() {
            dirty = true // 修改数据会触发 scheduler ,让之前的缓存失效
        }
    })
    
    const obj = {
        get value() {
            if (dirty) {
                value = effectFn() // 重新计算,并记录缓存
                return value
            }
            return value // 缓存未失效
        }
    }
    return obj
} 

fundamentals of watch

watch can   be implemented using the lazy and  dispatch functions. schedulerwatch and computed are related in internal implementation.

function watch(source, cb) {
    let newValue, oldValue

    const effectFn = effect(
        () => source.foo, // 要监听的数据
        {
            lazy: true, // 用于下面的被动调用,获取 newValue
            scheduler() {
                newValue = effectFn()
                cb(newValue, oldValue) // 执行 watch 回调函数
                oldValue = newValue // 更新旧值
            }
        }
    )
    oldValue = effectFn()
} 

What the Reflect API does

Execute the following code, see the comment for the returned result

const obj = {
    _name: 'xxx',
    get name() {
        console.log('this', this) // obj 对象
        return this._name
    }
}
const p = new Proxy(obj, {
    get(target, key, receiver) {
        console.log('key', key) // 'name' (没有 '_name')
        return target[key] // 【注意】这里不用 Reflect.get
    }
})
p.name 

Simply modify the above code and use it  return Reflect.get(target, key, receiver) , the result will be different

const obj = {
    _name: 'xxx',
    get name() {
        console.log('this', this) // p 对象
        return this._name
    }
}
const p = new Proxy(obj, {
    get(target, key, receiver) {
        console.log('key', key) // 'name' 和 '_name'
        return return Reflect.get(target, key, receiver) // 【注意】使用了 Reflect.get
    }
})
p.name 

What is the difference between the two?

  • In the first case,  _name the attribute get cannot be monitored, which does not meet expectations
  • In the second case, you can monitor  _name the attribute get, which meets expectations

The role of the Reflect API is: to change the object getter  this.

Note that this is only for objects  getter , if you  get name() replace it with an ordinary function,  getName() you won't have this problem.

const obj = {
    _name: 'xxx',
    getName() {
        console.log(this) // p 对象
        return this._name
    }
}

const p = new Proxy(obj, {
    get(target, key, receiver) {
        console.log('key', key) // 'name' 和 '_name'
        
        // 以下两个,打印的效果一样
        return target[key]
        // return Reflect.get(target, key, receiver)
    }
}) 

the nature of ref

// ref 的本质就是 reactive

function ref(val) {
    const wrapper = {
        value: val
    }
    
    // 定义一个 ref 的标记。在模板中可直接使用 ref 而不用 value ,就根据这个标记判断 (这跟响应式无关)
    // 【注意】使用 defineProperty 定义属性,只定义一个 value ,其他的(configurable, enumerable, writable)都默认是 false
    Object.defineProperty(wrapper, '__v_isRef', { value: true })
    
    return reactive(wrapper) // 使用 reactive 做响应式
} 

toRef is the same

function toRef(obj, key) {
    // obj 本身是 reactive() 封装过的
    const wrapper = {
        get value() {
            return obj[key]
        }
        set value(v) {
            obj[key] = v
        }
    }
    Object.defineProperty(wrapper, '__v_isRef', { value: true }) // 标记为 ref
    return wrapper
} 

toRefs is to traverse the attributes and execute toRef one by one

Efficiently switch DOM events when rendering

Events are bound in the Vue template, so rendering to real DOM also needs to bind DOM events. If the event is updated, the general idea is to first removeEventListener and then addEventListener, that is, two DOM operations - DOM operations are expensive.

Vue optimizes this, greatly reducing DOM operations. it's actually really easy:

invoker = { value: someFn }

elem.addEventListener(type, invoker.value)

// 如果事件更新,只修改 invoker.value 即可,不用进行 DOM 操作 

Diff algorithm

Vue2 uses the diff algorithm of double-ended comparison, referring to snabbdom.js.

Vue3 uses a fast diff algorithm, referring to ivi and inferno. The idea is:

  • Do a paired-end comparison first
  • The remaining part calculates the longest increasing subsequence (a very common algorithm) to find nodes that do not need to be rebuilt and moved
  • Finally deal with the remainder

asynchronous update

The responsive type is originally synchronous, that is, after the data attribute changes, the effectFn will trigger the execution synchronously.

However, if the data attribute is modified multiple times, it will trigger multiple executions of effectFn synchronously, and it will waste performance if it is used to render DOM.

Therefore, Vue has been optimized on this basis and changed to asynchronous rendering, that is, modifying the data attribute multiple times will only trigger the execution of effectFn at the last time, and will not trigger continuously in the middle.

const queue = new Set() // 任务队列。Set 可自动去重,这很重要,否则重复添加 fn 将导致重复执行
let isFlushing = false // 标记是否正在刷新
const p = new Promise()

function queueJob(job) {
    queue.add(job) // 添加任务
    
    // 如果还没有开始刷新,则启动
    if (!isFlushing) {
        isFlushing = true  // 标记为刷新中
        p.then(() => {
            try {
                queue.forEach(job => job())
            } finally {
                isFlushing = false // 标记为刷新完成
                queue.length = 0 // 清空任务队列
            }
        })
    }
} 

 The implementation method is as the above code, first cache the effectFn in a task queue (remove it), and then trigger a promise then callback, which is only  isFlushing triggered once by marking. Finally,  queueJob trigger the execution of effectFn through the function.

How does the Composition API get component instances

If  onMounted it can be used inside the component, it can also be used outside the component (but must be  setup triggered in). When it's used outside of a component, how do you know which component it's currently in?

Vue defines a global variable  currentInstance that stores the currently executing  setup component instance and clears it after execution.

let currentInstance = null // 全局变量,存储当前正在 setup 的组件实例

// 挂载组件
function mountComponent(vnode, container, anchor) {
    const instance = { ... } // 当前组件实例
    
    currentInstance = instance // 存储到全局变量
    
    // 执行组件的 setup 函数,其中可能会调用 onMounted
    
    currentInstance = null // 清空全局变量
    
}

function onMounted(fn) {
    if (currentInstance == null) throw new Error('找不到组件实例') // 说明 onMounted 没有在 setup 中触发

    // 把 fn 记录到当前组件的 mounted 函数列表中,等待 mounted 之后被触发
    currentInstance.mounted.push(fn)
} 

At this point, it is clear why  onMounted it must be  setup triggered internally.

About Vue Function Components

Vue function components are stateless components, only props, no data and life cycle. The Composition API is still used for normal components and cannot be used for functional components. This is different from React Hooks.

Vue2 function components perform better than normal components. And Vue3's ordinary components are initialized very quickly, so the use of function components is mainly for simplicity and has no performance advantage.

keep-alive cache principle

Components and DOM elem inside keep-alive will be cached, and only activate and deactivate life cycles will be triggered when switching, and will not be created repeatedly.

The cache cannot be expanded without limit, and a clipping mechanism is required. Vue clips through the LRU algorithm. max include exclude You can control caching yourself with  .

Vue3 compilation optimization

Compilation optimization is something that all compilers will do. For example,  const a = 10; const b = a + 10; after compiling and compiling  in JS code const b = 20; , it will be the simplest optimization.

Vue3 has also done a lot of optimizations during compilation: first, improve the efficiency of executing the render function to generate vnode; second, improve the execution efficiency of the diff algorithm.

  • patchFlag Patch mark - to distinguish between static nodes and dynamic nodes, and only dynamic nodes can be compared when diffing
  • Static promotion - promote the generation of static nodes to the outside of the render function, so that it can only be executed once, instead of every time it is rendered
  • Cache inline events - Cache template inline events without regenerating events every time you render

Misconceptions about SSR

Strictly speaking, it should be called isomorphism, not real SSR 

When rendering for the first time, the server returns: 1. A purely static page; 2. Packaged JS and CSS codes. The browser will directly display the static page, and then load the JS and CSS codes. After the loading and execution of the JS is completed, the webpage is truly usable.

Therefore, isomorphic rendering can only solve the problem of the white screen of the web page when it is rendered for the first time, and is more friendly to SEO. But it can't improve the time to interact (TTI), because the JS needs to be downloaded and executed before the webpage can be truly interactive. This time is almost the same as CSR.

Life cycle of SSR components

SSR generation is a "snapshot" of the current component, a purely static HTML code, which is returned to the client immediately after generation.

The server cannot render the DOM, so there is no beforeMount and mounted, and there is no beforeDestroy and destroyed for the same reason. The server does not need to bind the DOM event, only the client can execute the event. The server does not need to monitor the response, so there is no beforeUpdate and updated.

Therefore, the SSR component life cycle only has beforeCreate and created, and nothing else

 Activation of SSR components on the client side

SSR returns component snapshots, pure static HTML code, no DOM events, and is rendered as real DOM in the browser. At this time, the hydrate needs to be activated on the client side, and the webpage will not be available until activated. Mainly two things:

Associate real DOM and VDOM, that is, vnode.el = elem
binds DOM events
In addition, since the real DOM has already been rendered, Vue will not re-render the DOM at this time, just activate it.

Guess you like

Origin blog.csdn.net/dabaooooq/article/details/129785013