当前位置: 首页>编程语言>正文

bios存储不存在 bios不会保存




bios存储不存在 bios不会保存,bios存储不存在 bios不会保存_sed,第1张


本文使用 Zhihu On VSCode 创作并发布

在 2019 年 11月 27日,Slate 发布了新版。底层从 immutable.js 迁移到 immer,基于 TypeScript 重构。

在之前的版本推荐阅读 编辑器初体验


长佑:编辑器初体验zhuanlan.zhihu.com

bios存储不存在 bios不会保存,bios存储不存在 bios不会保存_ide_02,第2张


笔者没有开发过编辑器,但对 Slate 的整体设计理念非常感兴趣。这里不会阐述太多编辑器相关的特定内容,而是分享从整体看 Slate 的重构后,那些令人耳目一新的设计以及思考。

在 Slate.js 里,我们几乎看不到 Class 了。所有的文件和暴露的模块基本都是最纯粹的对象。

我们来看,它的文件结构。


bios存储不存在 bios不会保存,bios存储不存在 bios不会保存_ide_03,第3张

文件结构

Slate 是框架无关的 Model,定义了所有的数据操作,提供 Command, Plugin 等机制。

Slate-React 是用于渲染编辑器的视图层处理,包括 Model 层和 React 中的绑定,以及一些基础编辑器文本内容渲染的组件。本文重点介绍的 Slate 和 Slate-history 这部分。

先从 createEditor 入口文件开始


import { Editor, Node, Transforms, Operation }  from 'slate'
const createEditor = () => {
	const editor = {
		children: [], // 初始化数据结构
		onChange: () => {},
	  apply:(op: Operation) => {
			Editor.transform(editor, op)
			editor.onChange()
		}
		insertNode(node:Node) {
			Transforms.insertNodes(editor, node)
		}
  }
	return editor;
}

const editor = createEditor();


这里涵盖了非常多的信息。该函数 return 了一个纯对象,并不是某个类的实例。该对象的属性上挂载了常用的编辑器调用方法。在处理的时候,以 insertNode 为例,由于传入的是当前的 editor,就能拿到当前 editor 的 children 属性。然后交由 Transforms 处理,其内部采用 immer 来进行 immutable 操作。


bios存储不存在 bios不会保存,bios存储不存在 bios不会保存_bios存储不存在_04,第4张

command 触发数据变更流程

Command (Transform) 设计

在 Slate.js 中,一个 Command 可能包含多个 Operation( 新版的 Slate 已称为 Transform,为便于描述,之后统称为 Command)。每个 Operation 都是原子操作,对数据的更改是最小化的。

比如,我们想要插入一个节点,那么除了给这个编辑器数据添加一个节点数据外,可能还需要更新光标、或者是选区等操作。

我们假设每个最小化的更改设计为原子操作。简单起见,我们把当前的 command 操作理解为 2个 operation。


const insertNode =() => {
 //insertnodedata  (operation1)
 //updateSelection (operation2)
}


这么做的好处是,在设计编辑器的 undo,redo 时,我们很容易根据这些 operations 回溯历史。

此外,也为开发调试带来便利。而更令人惊喜的是这种方式对未来文档协同编辑的支持将非常友好。

当Slate 内部调用 editor.apply 时,实际上就是对原子 Operation进行处理。

当 Operation 处理完之后,触发 editor 对象的 onChange。

值得一提的是:当我们把一个 Command 转换成多个 Operation 时,由于每次 Operation 都会触发onChange,那么比如插入一个节点就会收到多次数据的onChange 回调,这在 React 层面会导致多次 re-render。假设,我们希望一个 Command 执行一次,也只通知 onChange 一次,该怎么去实现呢?

有一种做法是为每个 Command 新建一个 Operation 调用栈,当所有原子命令操作执行完毕之后,清空栈,通过判断栈的长度,然后再决定是否触发 onChange。

而在 Slate 中,利用 microTask 优雅地解决该问题。


import { DIRTY_PATHS, FLUSHING } from './utils/weak-maps'
editor.apply = (op: Opeation) {
if (!FLUSHING.get(editor)) {
      FLUSHING.set(editor, true)
      Promise.resolve().then(() => {
        FLUSHING.set(editor, false)
        editor.onChange()
        editor.operations = []
      })
  }
}


虽然一个 Command 执行的时候会调用 apply 多次,但是当 Command 执行完毕后,我们使用 Promise 把回调放到 MicroTask 中,当 JavaScript 引擎没有其他任务运行时,就开始执行 MicroTask 中的任务。

CustomCommand

Slate 中编辑器所有操作都汇总到一个对象上,这是我们通过 createEditor 新建出来的。由于返回的只是普通的对象,我们可以非常简单得复写原有的 Command。比如,还是刚才的插入节点,我们想要在插入节点后,打印 log。


const nativeInsertNode = editor.insertNode;
editor.insertNode = (node: Node) => {
	console.log('log insertNode');
	nativeInsertNode(editor, node);
}


Plugin

Editor 对象是我们通过 createEditor 新建出来的。由于返回的只是普通的对象。 插件机制在新版的 Slate 中变得非常得简单。

插件也只是一个简单的函数,Slate 提供 withHistory library 来记录 undo,redo。


import { createEditor } from 'slate'
import { withHistory } from 'slate-history'

const editor = withHistory(createEditor())


这一切看起来更加得函数式了。

而 withHistory 本质上也只是在复写 editor.apply 函数,当 Slate 触发 apply 来执行 Operation 时, withHistory 内部保存了 apply 中 Operation 属性,然后继续执行 editor 原有的 apply 操作。


const withHistory = editor => {
	const e = editor;
	const { apply } = e
  // 新增 history 属性
	e.history = {undos:[], redos:[]}
  // 新增 undo 方法
	e.undo = () => {
		const { history } = e
		const { undos } = history
		const op = undos[undos.length - 1];
		e.apply(op);
		history.undos.pop();
		history.redos.push(op);
		
	}

  // 新增 redo 方法;
	e.redo = () => {
		const { history } = e
		const { redos } = history
		const op = undos[redos.length - 1];
		e.apply(op);
		history.undos.push(op);
		history.redos.pop();
	}

	e.apply = (op: Operation) => {
		e.history.undos.push(op); // 增加 undo		
		apply(op)
	}
	return  e;
}


一切变得更加得函数式,我们不需要构建类,更不需要 Decoration。只需一个纯对象,就可以利用函数组合来实现同等效果。

得益于强大的插件机制,Slate 保证内核非常纯粹,而定制化的处理则通过自定义插件来实现。

Normalize

在新版 Slate 中移除了 Schema。Schema 原先是用于校验文档数据类型。实际在 Slate 里面通过 normalizeNode 来实现校验数据的功能,当传入的数据和预期接受的类型不一致的时候,我们可以借助它来修正数据。

在 Slate 内部提供了许多内置对数据结构的约束。具体可以参考 https://docs.slatejs.org/concepts/10-normalizing

开发者也可以通过插件的形式增加自己的约束。

Slate 在 React 中的数据共享

是一个类似 Provider 的 Wrapper ,用来处理 onChange 事件。其内部包裹了一层 Context 实现 Slate 的数据共享。使得其余子组件可以通过 useSlate 来获得 Slate。


export const SlateContext = createContext<[ReactEditor] | null>(null)
export const useSlate = () => {
  const context = useContext(SlateContext)

  if (!context) {
    throw new Error(
      `The `useSlate` hook must be used inside the <SlateProvider> component's context.`
    )
  }

  const [editor] = context
  return editor
}

const Slate = (props: {
  editor: ReactEditor
  value: Node[]
  children: React.ReactNode
  onChange: (value: Node[]) => void) => {
	return <SlateContext.Provider value={context}>
      <EditorContext.Provider value={editor}>
        <FocusedContext.Provider value={ReactEditor.isFocused(editor)}>
          {children}
        </FocusedContext.Provider>
      </EditorContext.Provider>
    </SlateContext.Provider>
}


React 自定义渲染


const App = () => {
  const editor = useMemo(() => withReact(createEditor()), [])
  const [value, setValue] = useState([
    {
      type: 'paragraph',
      children: [{ text: 'A line of text in a paragraph.' }],
    },
  ])

  // Define a rendering function based on the element passed to `props`. We use
  // `useCallback` here to memoize the function for subsequent renders.
  const renderElement = useCallback(props => {
    switch (props.element.type) {
      case 'code':
        return <CodeElement {...props} />
      default:
        return <DefaultElement {...props} />
    }
  }, [])

  return (
    <Slate editor={editor} value={value} onChange={value => setValue(value)}>
      <Editable
        // Pass in the `renderElement` function.
        renderElement={renderElement}
        onKeyDown={event => {
          if (event.key === '&') {
            event.preventDefault()
            editor.insertText("and")
          }
        }}
      />
    </Slate>
  )
}


Slate-React 提供自定义渲染机制,即在组件渲染时检测是否有注入 renderElement 的 props,如果存在,则执行该方法。不过前提也需要一个字段来表明是哪种类型的组件,来决定最终的渲染。


const Element = (props) = {
const {
    element,
    renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
  } = props
	return <SelectedContext.Provider value={!!selection}>
      {renderElement({ attributes, children, element })}
    </SelectedContext.Provider>
}


总结

很多时候,对于复杂的应用我们可能第一时间考虑会用类来组织各个模块,这是个成熟的选择,业界也总结了一套非常完善的设计模式。与此类型的应用,我还专门研究过百度脑图的源码,它整体通过类来构建应用。

对比而言,我更喜欢 Slate ,Slate 的 Model 层面整体设计非常优雅,对于复杂应用而言,测试覆盖率是非常关键的,而越是纯函数,对编写单元测试就越友好。整体非常灵活,代码阅读上去感官很好。而使用类的方式则显得较为笨重。

但要强调的是这种给人简单的感觉往往背后是非常精心的设计。否则的话,很容易写成面向过程的代码。


https://www.xamrdz.com/lan/59r1963822.html

相关文章: